公司每隔一段时间都有一些规范性的内容的考试,略头疼,有一点还行的是事先都提供了题库,于是本着自己动手丰衣足食的精神,用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 }
}