公司每隔一段时间都有一些规范性的内容的考试,略头疼,有一点还行的是事先都提供了题库,于是本着自己动手丰衣足食的精神,用React写了个题库搜题的小Web应用

搜题小应用倒是好写,这个省去不说。但是题库的录入就有点费事了,从一开始写搜题Web应用到最终这个脚本诞生,中间经历了几个阶段

1.手动录入题目,费事费力,是个重体力活

2.手机给题目截图,用图片的方式展示搜题结果,工作量依旧很大,一开始设想的是图片保存后接一个ORC服务,来实现关键文字信息提取,但是接入ORC服务这依旧是一个费力的过程,还不说ORC服务是否需要付费,和手机上截图的背景对准确性产生的影响

3.在经历了前面两次考试之后,刷题背题的过程中才发现是页面文字是可以复制出来的,于是这一代的就开始了一题题的复制题目,存在手机备忘录,然后再在电脑上取得一整个所有题目的大又长的字符串,再做一些字符串解析的工作,解析之后就可以使用了。看起来不错,不过一题题的复制,依旧还是一个不轻松的工作,而且后面的字符串解析也实现起来有点点麻烦,各个字段,题目之间的分割特征不是很明显

4.抓包软件的应用,在实现3的时候其实想到了可以使用抓包软件来实现,不过最终没有走上这一步,而且如果要实现这一步的话,最终每次接口请求的数据也都需要人工收集

5.经过观察确定,这个题库本质其实是个web页面,那么既然是web页面了,必然还是可以在电脑上用浏览器打开的吧,假如服务端没有做一些特别的限制的话,经过几次尝试果然,确实可以打开,那么我们的油猴脚本就有大展身手的机会了

基本思路:

实现一个自己的XMLHttpRequest方法,替换掉window上原本的XMLHttpRequest方法,并对特定的事件进行记录,那么我们需要的数据也就水到渠成的可以截取下来。中间还用到了CustomEvent 。具体的就可以看下面代码了,从一开始的初始版本可以截取信息,到最终用起来顺手,还是经过了几个版本的迭代的

// ==UserScript==
// @name         题库小柯基(科技)
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  try to take over the world!
// @author       You
// @match        https://xxxxxx.com?*
// @grant        none
// ==/UserScript==

//需要存下来的数据
window.list = [];
//题库问题唯一编码列表.用来去重以防重复存储
window.subjectCodeList = []
//中断执行标记
window.ifInterrupt = true
//总页数
let totalCount = () => document.getElementsByClassName("examNumbers")[0].children[3].innerText
//当前页码
let currentNum = () => document.getElementsByClassName("examNumbers")[0].children[1].innerText
let isEnd = () => (currentNum() - 0) >= (totalCount() - 0)
let clearList = () => {
    window.list = []
    window.subjectCodeList = []
}
let goNext = () => {
    window.ifInterrupt = false
    !isEnd() && document.getElementsByClassName('nextBtnClass')[0].click()
}

/**
 * 在页面上创建一个按钮,用来触发实现对应需要的功能
 * @param func
 * @param btnText
 * @param top
 */
let createButton = (func, btnText, top) => {
    let btn = document.createElement("button")
    btn.style.position = "fixed"
    btn.style.right = 0
    btn.style.top = top
    btn.style.padding = "10px"
    btn.style.zIndex = 99999
    btn.innerText = btnText
    btn.addEventListener("click", func)
    document.body.append(btn)
}

createButton(clearList, "清除缓存", "120px")
createButton(goNext, "开始抓取", "80px")
createButton(() => window.ifInterrupt = true, "暂停", "160px")

;(function () {
    if (typeof window.CustomEvent === "function") return false;

    function CustomEvent(event, params) {
        params = params || {bubbles: false, cancelable: false, detail: undefined};
        let evt = document.createEvent('CustomEvent');
        evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
        return evt;
    }

    CustomEvent.prototype = window.Event.prototype;
    window.CustomEvent = CustomEvent;
})();
;(function () {
    function ajaxEventTrigger(event) {
        let ajaxEvent = new CustomEvent(event, {detail: this});
        window.dispatchEvent(ajaxEvent);
    }

    let oldXHR = window.XMLHttpRequest;

    function newXHR() {
        let realXHR = new oldXHR();
        realXHR.addEventListener('abort', function () {
            ajaxEventTrigger.call(this, 'ajaxAbort');
        }, false);
        realXHR.addEventListener('error', function () {
            ajaxEventTrigger.call(this, 'ajaxError');
        }, false);
        realXHR.addEventListener('load', function () {
            ajaxEventTrigger.call(this, 'ajaxLoad');
        }, false);
        realXHR.addEventListener('loadstart', function () {
            ajaxEventTrigger.call(this, 'ajaxLoadStart');
        }, false);
        realXHR.addEventListener('progress', function () {
            ajaxEventTrigger.call(this, 'ajaxProgress');
        }, false);
        realXHR.addEventListener('timeout', function () {
            ajaxEventTrigger.call(this, 'ajaxTimeout');
        }, false);
        realXHR.addEventListener('loadend', function () {
            ajaxEventTrigger.call(this, 'ajaxLoadEnd');
        }, false);
        realXHR.addEventListener('readystatechange', function () {
            ajaxEventTrigger.call(this, 'ajaxReadyStateChange');
        }, false);
        let send = realXHR.send;
        realXHR.send = function (...arg) {
            send.apply(realXHR, arg);
            realXHR.body = arg[0];
            ajaxEventTrigger.call(realXHR, 'ajaxSend');
        }
        let open = realXHR.open;
        realXHR.open = function (...arg) {
            open.apply(realXHR, arg)
            realXHR.method = arg[0];
            realXHR.orignUrl = arg[1];
            realXHR.async = arg[2];
            ajaxEventTrigger.call(realXHR, 'ajaxOpen');
        }
        let setRequestHeader = realXHR.setRequestHeader;
        realXHR.requestHeader = {};
        realXHR.setRequestHeader = function (name, value) {
            realXHR.requestHeader[name] = value;
            setRequestHeader.call(realXHR, name, value)
        }
        return realXHR;
    }

    window.XMLHttpRequest = newXHR;

})();
window.addEventListener("ajaxReadyStateChange", function (e) {
    let xhr = e.detail;
    if (xhr.readyState == 4 && xhr.status == 200) {
        // xhr.getAllResponseHeaders()  响应头信息
        // xhr.requestHeader            请求头信息
        // xhr.responseURL              请求的地址
        // xhr.responseText             响应内容
        // xhr.orignUrl                 请求的原始参数地址
        // xhr.body                     post参数,(get参数在url上面)
        let url = xhr.orignUrl
        //只需关注我们需要的url,其他忽略
        if ("/url-witch-need-to-be-listen.json" == url) {
            //最终监听到的接口返回的信息
            let json = JSON.parse(xhr.responseText);
            let {subjectCode} = json
            if (window.subjectCodeList.includes(subjectCode)){
                return
            }
            window.list.push(createQuestionItem(json))
            window.subjectCodeList.push(subjectCode)
            console.log(window.list)
            //判断是否到了最后一页,以及是否需要中断本次执行
            if (!window.ifInterrupt && !isEnd()) {
                //随机一个5秒内的时间,点击下一页按钮的操作。触发下一次请求
                setTimeout(() => {
                    document.getElementsByClassName('nextBtnClass').length &&
                    document.getElementsByClassName('nextBtnClass')[0].click()
                }, Math.random() * 5000)
            }
            if (isEnd()) {
                console.warn("结束啦")
                //最终创建一个新的window展示抓取下来的内容,方便后续录入题库操作
                let newWindow = window.open('', '获取结果', 'height=300,width=400,top=0,left=0,toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,status=no')
                newWindow.document.body.innerText = JSON.stringify(window.list)
            }
        }
    }
});

let createQuestionItem = ( questionInfo ) => {
    let type, question, answerArr, answer, desc;
    return { type, question, answerArr, answer, desc, questionInfo }
}