一.实现分析
1.分析页面题目
首先我们看到题目
通过调试控制台可以发现,我们看到的题目是一张图片 ,这也就说明了,我们无法通过dom直接拿到题目具体内容。但是我们发现
- 这个题目图片的路径好像有点特别
- 每一个题目都是在一个含有类名‘sec’的div下,题目的img标签在pre标签中
2.分析“题目解析页面”,获得答案来源
首先,我们答完题目后,点击页面中的"查看解析"按钮,进入题库自身提供的答案查看页面
打开浏览器调试控制台,我们和答题界面一样,来分析一下题目
我们可以分析出以下信息
- 和答题页面一样,题目是一张图片
- 和答题页面一样,题目所处div都含有类名"sec"
- 同样的题目,在答题界面题目图片的src 值 和 答案的 src 值不一致
3.总结分析,得出原理
我们重点关注两个页面中的题目src值
//答题页面题目src
https://tiku.kgc.cn/testing/cdn/getImage?relativePath=0020000/1656301516418/9eb6aa00ef33cc9b8e3cf532006d7c80.jpg&imageType=2
//答案页面题目src
https://tiku.kgc.cn/testing/cdn/getImage?relativePath=0020000/1656300959654/id_1656300959654.png&imageType=0
通过分析,我们可以发现
- 这两个src都是请求了同一个地址https://tiku.kgc.cn/testing/cdn/getImage
- 参数relativePath的内容不一致,但是很接近,似乎存在着某种映射关系
这个时候,我们就要引入一个外部变量,这个变量是解决整个问题的关键钥匙
https://tiku.kgc.cn/testing/index/556764/f30f18fe3e7d4ad3aa735f48d780d948
这样看或许发现不了什么,但是,当我们把答题页面和答案页面中的题目relativePath参数提取一下,我们把"="到第二个"/"之间的内容拿出来,同时,我们将引入的关键钥匙也提取一下,拿到"index/"到"/f30..."之间的数据可以得到以下数据
//答题页面提取
1656301516418
//答案页面提取
1656300959654
//关键钥匙提取
556764
这时候,我们有了一个惊人的发现,那就是答案页面提取出来的内容减去关键钥匙提取出来的内容等于答题页面提取的内容,也就是
1656301516418 - 556764 = 1656300959654
这个时候将会有以下疑问
- 关键钥匙从哪里来的?
- 这个关系的成立意味着什么?
- 那我们能做什么?
1.关键钥匙来自题库首页的地址栏的内容(前提是你登入了)。
2.我们可以大胆猜测一下,我们在答题时拿到的题目图片地址不是原地址,并且,假设在同一时刻,有许多人都在刷题,并且碰巧都是一样的题目,但是,因为账号的区别,所以尽管题目一样,但是题目图片的URl就是不一样。而通过上面的计算,我们可以确定原来图片地址Id(也有可能他的本意不是这样,在这里我们就叫他题目地址Id)。
3.根据以上的分析总结,我们能做的就是:
- 编写一个脚本,负责在答题界面时,获取并解析页面中的题目图片地址,并还原地址为原地址在此之后,将解析的内容提交到服务器获取答案,并且在答题完毕后进入"查看解析"页面,自动分析页面中的题目地址数据以及答案,将这些数据发送到我们自己的服务器保存记录,这样,在下次遇到改题目的时候我们就可以获取正确答案。
- 编写一个服务端API,用来处理前端的各种请求,比如:添加数据,解析脚本传来的数据,解析返回正确答案。
二.说干就干,上代码
首先,根据我们上面的结论,我们可以写出以下代码
js脚本代码(从服务器获取答案)
//待提交到服务器的数据
let submitData = "";
//获取页面中所有题目的div
let allElement = document.getElementsByClassName("sec");
//循环allElement,获取img标签
for (let i = 0; i < allElement.length; i++) {
let pre = allElement[i].getElementsByTagName("pre");
let img = pre[0].getElementsByTagName("img");
if (img.length == 1) {
let params = queryURLparamsRegEs5(img[0].src)["relativePath"];
//到这里,我们拿到了加密的题目Id
let encryptionId= getID(params);
//这是我们分析过程的关键钥匙,在实际使用中,可以在首页时读取该值,保存到cookie,然后再读取
let userID = 556764;
//获取真实ID
let realID = getID(encryptionId) + "/" + (getIDNum(encryptionId) - userID);
submitData += iD+ ";";
}
}
//提交数据
function sendData(submitData){
let data = new FormData();
data.append("id",submitData);
//创建异步对象
let xhr = new XMLHttpRequest();
//设置请求的类型及url
xhr.open('post', '你的后端API地址');
//发送请求
xhr.send(data);
xhr.onreadystatechange = function () {
// 这步为判断服务器是否正确响应
if (xhr.readyState == 4 && xhr.status == 200) {
//拿到服务器的数据
let result = xhr.responseText;
//解析数据,将数据填写到页面中,实现自动答题.............
//此处省略
}
};
}
//获得参数
function queryURLparamsRegEs5(url) {
let obj = {}
let reg = /([^?=&]+)=([^?=&]+)/g
url.replace(reg, function () {
obj[arguments[1]] = arguments[2]
})
return obj
}
function getID(parameter) {
var index = parameter.lastIndexOf("/");
var ID = parameter.substr(0, index);
return ID;
}
function getIDNum(encryptionId) {
var index = encryptionId.lastIndexOf("/");
var IDNum = encryptionId.substr(index + 1, encryptionId.length);
return IDNum;
}
js脚本代码(添加题目数据)
let allElement = document.getElementsByClassName("sec");
let submitData = "";
for (let i = 0; i < allElement .length; i++) {
let thisze = allElement [i].getElementsByTagName("img");
if (thisze.length > 0) {
let params = queryURLparamsRegEs5(thisze[0].src)["relativePath"];
let id = getID(params);
//拿到了题目编号,我们还需要获取题目答案
let answer = allElement [i].getElementsByTagName("label")[0];
//题目答案
let correctOption = answer.firstElementChild.innerHTML.replace(/\s*/g, "");
//提交数据
submitData = submitData + id + "|" + correctOption + ";";
}
}
sendData(submitData);
function sendData(submitData) {
//创建异步对象
let xhr = new XMLHttpRequest();
//设置请求的类型及url
xhr.open('post', '你的API地址');
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
//发送请求
xhr.send('id=' + submitData);
xhr.onreadystatechange = function () {
// 这步为判断服务器是否正确响应
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
};
}
function queryURLparamsRegEs5(url) {
let obj = {}
let reg = /([^?=&]+)=([^?=&]+)/g
url.replace(reg, function () {
obj[arguments[1]] = arguments[2]
})
return obj
}
function getID(parameter) {
let index = parameter.lastIndexOf("/");
let ID = parameter.substr(0, index);
return ID;
}
后端API代码(响应请求,返回答案)
//解析传入的参数
$arrID = explode(";", $id);
//返回结果
$dataResult = "";
foreach ($arrID as $id) {
if (!($id == "" || $id == " ")) {
// 在数据库中拿数据作比较
$sql = "SELECT * FROM topiccollection WHERE QuestionNo like '$id'";
$result = mysqli_query($con, $sql);
if ($info = mysqli_fetch_array($result)) {
$answer = $info["Answer"];
$dataResult .= $answer;
} else {
$dataResult .= "-1";
}
$dataResult .= ";";
}
}
echo $dataResult;
后端API代码(向数据库中添加题目)
//允许跨站请求
header("Access-Control-Allow-Origin:*");
$id = $_POST['id'];
//导入数据库链接
include("conn.php");
//解析传入的参数
$arrID = explode(";",$id);
foreach($arrID as $temp){
$tempArr = explode("|", $temp);
$questionNo = $tempArr[0];
$answer = $tempArr[1];
if(!($questionNo == "" || $answer == "")){
$sql_1 = "SELECT * FROM topiccollection WHERE QuestionNo like '$questionNo'";
$result = mysqli_query($con, $sql_1);
if(!($info = mysqli_fetch_array($result))){
if(strlen($answer) > 2){
$type = 1;
}else{
$type = -1;
}
$sql = "insert into topiccollection(QuestionNo,Answer,Type) values ('$questionNo','$answer','$type');";
$con->query($sql);
}
}
}
数据库表结构
三.一些废话
1.代码相关
嗯,由于一些原因(可能是没时间,也有可能是因为没人看吧),上面提供的代码都只是一部分,但我认为这应该是最核心的内容了,如果需要完整代码或可用程序可以再文章末尾下载成品,然后自己折腾。
2.下一代产品的开发
截止到发文时间,我已经有了下一代的开发思路,可以不用依赖数据库和服务器来完成自动刷题。 大概提一嘴,就是利用了答题页面中收藏按钮的逻辑设计缺陷。如果有人想学习或研究可以私信或评论吧,我看到会尽力回复的
3.关于本地服务的部署
嗯,首先上面也说了,我提供的版本是需要收费的,但是,你也可以根据我提供的代码,或者下载的版本,自己搭建服务端到本地电脑,也可以不用租外网服务器。相关教程嗯,等我整理一下,有空就发出来
4.这个东西能不能用来考试
首先肯定是可以的,在我提供的版本中已经提供了相关代码,可以自行下载回来分析,折腾。但是我并不太推荐太依赖这玩意 。不过可惜的是,下一代产品只能在答题时不依赖服务器以及数据库,如果要用来考试还是要用到服务器和数据库的。还是比较可惜。如果各位大佬有什么思路可以在考试时也不依赖服务器和数据库,还望分享一下。
5.提供免费版本
本着节约时间的观念,我并不希望任何一个学习IT的人员把时间浪费在无意义的事情上,我决定最新版本免费,您下载后可以通过联系我们页面或邮箱联系又或者站内留言等形式告知联系我们,我们免费为您开通激活该服务