Nowcoder Quiz Snatcher

自动化抓取牛客网上一些免费题库,并以CSV格式导出下载

目前为 2023-03-10 提交的版本。查看 最新版本

// ==UserScript==
// @name         Nowcoder Quiz Snatcher
// @namespace    https://www.nowcoder.com/
// @version      0.1
// @description  自动化抓取牛客网上一些免费题库,并以CSV格式导出下载
// @author       Yifei Zhow
// @match        https://www.nowcoder.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @grant        none
// @license MIT
// ==/UserScript==

// 以此页面举例:https://www.nowcoder.com/exam/intelligent?questionJobId=2&tagId=21029
const gKeywords = ['用户研究','原型设计','数据分析','产品常识','产品设计','产品规划','需求分析','竞品研究','文档撰写'];
// 针对某一题库,每次点击出题具有随机性,抓取到达一定的阈值后停止
const gThresholdLvl = 85;

/* JSON Object looks like:
 {"用户研究":{"claim_to_have": 56, "already_grabbed": 30, content: [{
   "id": 1261504920, "type": "单选题", "info": "题干", "options": ["A.甲甲甲","B.乙乙乙","C.丙丙丙","D.丁丁丁"]
 }, ..., {...}]}}
 */
const kNowWorkingKey = 'TM_NOWCODER_WORKING_KEY';
const kQuestionDictKey = 'TM_NOWCODER_QUESTION_DICT_KEY';
const kClaimToHave = 'claim_to_have';
const kAlreadyGrabbed = 'already_grabbed';


// Utilities
function getLocation(href) {
    var match = href.match(/^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/);
    return match && {
        href: href,
        protocol: match[1],
        host: match[2],
        hostname: match[3],
        port: match[4],
        pathname: match[5],
        search: match[6],
        hash: match[7]
    }
}

function hashCode(s) {
    for(var i = 0, h = 0; i < s.length; i++)
        h = Math.imul(31, h) + s.charCodeAt(i) | 0;
    return h;
}

function exportToCsv(filename, rows) {
    var processRow = function (row) {
        var finalVal = '';
        if (!Array.isArray(row)) {
            // Mapping
            const mappedRow = [row['id'], row['type'], row['info']];
            Array.prototype.forEach.call(row['options'], function(el) {
                mappedRow.push(el);
            });
            row = mappedRow; // Deep copy preferred
        }
        for (var j = 0; j < row.length; j++) {
            var innerValue = row[j] === null ? '' : row[j].toString();
            if (row[j] instanceof Date) {
                innerValue = row[j].toLocaleString();
            };
            var result = innerValue.replace(/"/g, '""');
            if (result.search(/("|,|\n)/g) >= 0)
                result = '"' + result + '"';
            if (j > 0)
                finalVal += ',';
            finalVal += result;
        }
        return finalVal + '\n';
    };

    var csvFile = '';
    for (var i = 0; i < rows.length; i++) {
        csvFile += processRow(rows[i]);
    }

    var blob = new Blob([csvFile], { type: 'text/csv;charset=utf-8;' });
    if (navigator.msSaveBlob) { // IE 10+
        navigator.msSaveBlob(blob, filename);
    } else {
        var link = document.createElement("a");
        if (link.download !== undefined) { // feature detection
            // Browsers that support HTML5 download attribute
            var url = URL.createObjectURL(blob);
            link.setAttribute("href", url);
            link.setAttribute("download", filename);
            link.style.visibility = 'hidden';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }
    }
}

// Sub-process
function storeQuestionIfNecessary(key, id, type, info, options = []) {
    let dict;
    try {
        dict = JSON.parse(window.localStorage.getItem(kQuestionDictKey) || '{}');
    } catch (error) {
        // JSON parse error!
        dict = {};
    }
    const keyDict = dict[key] || {};
    const keyContent = keyDict['content'] || [];
    for (let c of keyContent) {
        if (c['id'] == id) { return false; }
    }
    keyContent.push({
        id,
        type,
        info,
        options
    });
    // Write-back
    keyDict['content'] = keyContent;
    keyDict[kAlreadyGrabbed] = (keyDict[kAlreadyGrabbed] || 0) + 1;
    dict[key] = keyDict;
    window.localStorage.setItem(kQuestionDictKey, JSON.stringify(dict));
    return true;
}

function willJumpToNextWords(workingKey) {
    let dict;
    try {
        dict = JSON.parse(window.localStorage.getItem(kQuestionDictKey) || '{}');
    } catch (error) {
        return { 'next': false, 'final-word': false };
    }
    const keyDict = dict[workingKey] || {};
    const total = keyDict[kClaimToHave] || 0;
    const portion = keyDict[kAlreadyGrabbed] || 0;
    if (total == 0) {
        console.err('Now working percent: ', 'Unable to calculate.');
        alert('Total count missing!');
        return { 'next': false, 'final-word': false };
    } else {
        console.log('Now working percent: ', ((portion/total) * 100).toFixed(2) + '%');
    }

    if ((portion/total) * 100 > gThresholdLvl) {
        // Reach Threshold Level
        var idx = gKeywords.indexOf(workingKey);
        idx += 1;
        if (idx < gKeywords.length) {
            // Has next word
            workingKey = gKeywords[idx];
            window.localStorage.setItem(kNowWorkingKey, workingKey);
            return { 'next': true, 'final-word': false };
        } else {
            // Current is final word
            return { 'next': false, 'final-word': true };
        }
    } else {
        // Not reached
        return { 'next': false, 'final-word': false };
    }
}

function init() {
    let key = window.localStorage.getItem(kNowWorkingKey);
    if (!key) {
        key = gKeywords[0];
    }
    window.localStorage.setItem(kNowWorkingKey, key);
}

function reset() {
    window.localStorage.removeItem(kNowWorkingKey);
    window.localStorage.removeItem(kQuestionDictKey);
}

function storeClaim(workingKey, claim) {
    let dict;
    try {
        dict = JSON.parse(window.localStorage.getItem(kQuestionDictKey) || '{}');
    } catch (error) {
        return false;
    }
    const keyDict = dict[workingKey] || {};
    const total = keyDict[kClaimToHave];
    if (!total || total < 0 || total < claim) {
        // Write-back
        keyDict[kClaimToHave] = claim;
        dict[workingKey] = keyDict;
        window.localStorage.setItem(kQuestionDictKey, JSON.stringify(dict));
    }
}
// Main
(function() {
    'use strict';
    init();

    const h = getLocation(window.location.href);
    const workingKey = window.localStorage.getItem(kNowWorkingKey);

    let els;
    if (!h || h.hostname != "www.nowcoder.com") return;
    if (h.pathname == "/exam/intelligent") {
        // Index Page
        els = document.getElementsByClassName("exercise-card") || [];
        Array.prototype.forEach.call(els, function(el) {
            const title = el.querySelector(".exercises-card-title").innerText;
            const score = el.querySelector(".tw-text-gray-500").innerText;
            if (title.indexOf(workingKey) != -1) {
                // Extract kClaimToHave and Save
                const mth = score.match(/已做(\d+)\/(\d+)题/);
                if (mth && mth[2]) {
                    storeClaim(workingKey, parseInt(mth[2], 10));
                    setTimeout(function() {
                        el.click();
                    }, 2000);
                }
            }
        });
    } else if (h.pathname.indexOf('/exam/test/') != -1) {
        els = document.querySelectorAll(".test-paper .paper-question");
        Array.prototype.forEach.call(els, function(el) {
            const qType = el.querySelector(".question-desc-header").innerText;
            const qInfo = el.querySelector(".question-info").innerText;
            let qOptions = [];
            Array.prototype.forEach.call(el.querySelectorAll(".question-select .option-item"), function(e) { qOptions.push(e.innerText); });
            const qID = hashCode(qInfo);
            const isStored = storeQuestionIfNecessary(workingKey, qID, qType, qInfo, qOptions);
            console.log('Is Stored: ', isStored, qType);
        });
        // Exam Page
        let result = willJumpToNextWords(workingKey);
        if (result['final-word']) {
            if (confirm("Scnatched completed, export as CSV?")) {
                 var dict = JSON.parse(window.localStorage.getItem(kQuestionDictKey));
                Array.prototype.forEach.call(gKeywords, function(k) {
                    exportToCsv(k+'.csv', dict[k]['content']);
                });
                // reset();
            }
        } else {
             setTimeout(function() {
                 history.back();
             }, 2000);
        }
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址