Nowcoder Quiz Snatcher

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

Ekde 2023/03/10. Vidu La ĝisdata versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
        }
    }
})();