本文章内容禁供学习和交流切勿用作非法用途!
目标页面的URLhttp://dati.mwr.gov.cn/dxsgltl/templates/dxsgltl/answer.jsp?answer=1
在浏览器的开发者调试工具
的网络(NewWork)
这个页面下可以查看到在加载这个页面时,页面中的的内容的加载信息,通过对响应内容的大小和名称的观察,可以看到存在一个接口**http://dati.mwr.gov.cn/dxsgltl/handle/dt/loadRandomQuestion
**
不出意外的话就是这个接口的响应值完成了页面中题目信息的加载了
查看接口的返回值,通过查看接口的返回值,我们很容易的发现这个接口的返回值设计也很直观
这不就是传说中的谜底就在谜面上
按照这个接口中的answer
对应的选项尝试着对题目进行了答题,果不其然,100分
✅
也就是说只要获取到这个接口中的响应值,然后在根据响应值的内容进行答题就可以完成满分答题
自动答题的逻辑是捋清楚了,但是这个页面需要通过完成登录才可以到达
所以在解决这个问题之前还要解决登录的问题
自动登录我们可以借助selenium
驱动chromeDrive
来完成
http://xxxx/xxxxx/xxxxx/xxxxx/login.jsp
通过找到姓名
和身份证
两个input
框对应的Xpath
通过selenium
中的find_element_by_xpath(xpath).send_keys(key)
就可以完成d对应信息的输入
但是 还有一个数字验证码,解决这个验证码的方式有主要有两种
具体怎么解决,自己选择
从获得接口的响应的数据
上面我们提到了,谜底就谜面上只要我们获得了接口的响应值,我们就相当于获得了题目的答案
通过查阅了一些资料,我发现可以利用chromeDrive
通过network
来获取response.body
从而获得那个接口的响应数据,进而解析出接口中的答案,具体的实现过程见下面的代码
因为selenium
中没有提供对查询接口数据的函数,但是可以通过webdriver
提供的API进行查询
使用的函数是Network.getResponseBody
Network.getResponseBody
的参数是requestid
requestid
是webdriver
在执行每个请求过程中自动生成的唯一ID
拿到requestid
就能拿到对应的接口的返回数据
requestid
的获取
创建webdriver
对象时配置相关设置,即可获取每个请求的日志信息
对日志信息的检索可以找到对应的requestid
# 配置webdriver的相关参数
caps = {
'browserName': 'chrome',
'loggingPrefs': {
'browser': 'ALL',
'driver': 'ALL',
'performance': 'ALL',
},
'goog:chromeOptions': {
'perfLoggingPrefs': {
'enableNetwork': True,
},
'w3c': False,
},
}
# executable_path 可以指定驱动文件 chromedriver.exe 的路径
driver = webdriver.Chrome(desired_capabilities=caps,executable_path="chromedriver.exe")
# 设置窗口最大化
driver.maximize_window()
# 设置隐式等待(10秒)
driver.implicitly_wait(10)
姓名
和身份证号
通过浏览器定位到具体input
输入框的xpath
利用selenium
中的find_element_by_xpath()
函数定位到元素后根据不同的事件调用不同的函数
点击.click()
填写文本.send_keys(key)
访问页面,输入姓名和身份证号环节代码如下
driver.get(login_url)
print('正在执行自动化登陆...')
# 用户名
driver.find_element_by_xpath('//*[@id="userName"]').send_keys(user_name)
# 身份证号
driver.find_element_by_xpath('//*[@id="idCard"]').send_keys(id_card_num)
验证码的获取可通过多种方式,我所使用的方式是调用baidu图像识别接口
完成验证码的识别
调用baidu图像识别的接口的方式本文不进行详细说明,有空在写
建议手动输入完成验证码
验证码输入完成后就可以来到下一个页面
直接点击开始答题按钮即可
# 开始答题
click_by_xpath('/html/body/div/div[1]/div/div/div[2]/form/table/tbody/tr[7]/td/div/button')
如果已经完成过了一次答题的记录,在下一个页面上会出现证书下载
和再答一次
进入答题页面后需要**等待当前所有被发出的请求都得到响应
**才能获取日志
# 这里需要等待一下,等所有的接口都收到响应,才可以抓取到数据
time.sleep(5)
# 得到页面的所有的接口的log数据
log_list = driver.get_log('performance')
此时得到的log_list
是一个list对象
其中每一个元素的格式如下
{
"level": "INFO",
"message": "{\"message\":{\"method\":\"Network.responseReceived\",\"params\":{\"frameId\":\"7F38488422C362C23121B0032A63211B\",\"hasExtraInfo\":true,\"loaderId\":\"053A0A4B77B5104685F03154CFEE68DF\",\"requestId\":\"12360.179\",\"response\":{\"connectionId\":31,\"connectionReused\":true,\"encodedDataLength\":191,\"fromDiskCache\":false,\"fromPrefetchCache\":false,\"fromServiceWorker\":false,\"headers\":{\"Connection\":\"Keep-Alive\",\"Content-Length\":\"46668\",\"Content-Type\":\"application/json;charset=UTF-8\",\"Date\":\"Tue, 22 Feb 2022 07:29:49 GMT\",\"Keep-Alive\":\"timeout=5, max=90\",\"Server\":\"iis\"},\"mimeType\":\"application/json\",\"protocol\":\"http/1.1\",\"remoteIPAddress\":\"219.142.62.58\",\"remotePort\":80,\"responseTime\":1.645515025854593e+12,\"securityState\":\"insecure\",\"status\":200,\"statusText\":\"\",\"timing\":{\"connectEnd\":-1,\"connectStart\":-1,\"dnsEnd\":-1,\"dnsStart\":-1,\"proxyEnd\":-1,\"proxyStart\":-1,\"pushEnd\":0,\"pushStart\":0,\"receiveHeadersEnd\":167.98,\"requestTime\":2379.742195,\"sendEnd\":0.369,\"sendStart\":0.201,\"sslEnd\":-1,\"sslStart\":-1,\"workerFetchStart\":-1,\"workerReady\":-1,\"workerRespondWithSettled\":-1,\"workerStart\":-1},\"url\":\"http://dati.mwr.gov.cn/dxsgltl/handle/dt/loadRandomQuestion\"},\"timestamp\":2379.910729,\"type\":\"XHR\"}},\"webview\":\"7F38488422C362C23121B0032A63211B\"}",
"timestamp": 1645515030579
}
其中message
字段对应的是另一个字符串格式的json
对象
取出message
对应的值进行转义后可得到如下格式的json
对象
{
"message": {
"method": "Network.responseReceived",
"params": {
"frameId": "7F38488422C362C23121B0032A63211B",
"hasExtraInfo": true,
"loaderId": "053A0A4B77B5104685F03154CFEE68DF",
"requestId": "12360.179",
"response": {
"connectionId": 31,
"connectionReused": true,
"encodedDataLength": 191,
"fromDiskCache": false,
"fromPrefetchCache": false,
"fromServiceWorker": false,
"headers": {
"Connection": "Keep-Alive",
"Content-Length": "46668",
"Content-Type": "application/json;charset=UTF-8",
"Date": "Tue, 22 Feb 2022 07:29:49 GMT",
"Keep-Alive": "timeout=5, max=90",
"Server": "iis"
},
"mimeType": "application/json",
"protocol": "http/1.1",
"remoteIPAddress": "219.142.62.58",
"remotePort": 80,
"responseTime": 1.645515025854593e+12,
"securityState": "insecure",
"status": 200,
"statusText": "",
"timing": {
"connectEnd": -1,
"connectStart": -1,
"dnsEnd": -1,
"dnsStart": -1,
"proxyEnd": -1,
"proxyStart": -1,
"pushEnd": 0,
"pushStart": 0,
"receiveHeadersEnd": 167.98,
"requestTime": 2379.742195,
"sendEnd": 0.369,
"sendStart": 0.201,
"sslEnd": -1,
"sslStart": -1,
"workerFetchStart": -1,
"workerReady": -1,
"workerRespondWithSettled": -1,
"workerStart": -1
},
"url": "http://dati.mwr.gov.cn/dxsgltl/handle/dt/loadRandomQuestion"
},
"timestamp": 2379.910729,
"type": "XHR"
}
},
"webview": "7F38488422C362C23121B0032A63211B"
}
对象中的message.method
的值存在多种类型例如Network.requestWillBeSent
和Network.responseReceived
其中只有menthd
的值为Network.responseReceived
的对象才是对应的是接口响应数据的对象
根据在最开始的分析,我们可以确定我们要找的是请求路径为http://dati.mwr.gov.cn/dxsgltl/handle/dt/loadRandomQuestion
的接口的响应数据
结合log对象中的message字段对应的对象
的结构,可以找到message.params.response.url
记录的数据就是接口的请求URL
根据以上的分析对log_list
数据进行过滤即可,代码如下:
request_url = 'http://xxxxx/xxxxx/xxxxx/xxxxx/loadRandomQuestion'
# 遍历列表中的对象,寻找需要解析的接口数据
for item in log_list:
# message同时也是一个json对象
message = item['message']
# 解析message
message_dict = json.loads(message)['message']
# 得到params对象
params_dict = message_dict['params']
network_method = message_dict.get('method')
# 判断params中是否存在response
if network_method == 'Network.responseReceived':
response_dict = params_dict.get('response')
url_info = response_dict['url']
# 判断url是不是我们要找的题目的url
if request_url == url_info:
request_id = params_dict['requestId']
print('题目数据接口的请求ID = '+request_id)
此时我们已经得到我们需要的接口对应的requestid
requestid
从日志中获取接口响应的数据# 传入request_id 获得响应的数据
content = driver.execute_cdp_cmd('Network.getResponseBody', {'requestId': request_id})
# 获取body对应的数据
response_body = content['body']
print("接口数据解析成功!")
截止目前我们已经获得到了http://xxxxx/xxxxx/xxxxx/xxxxx/loadRandomQuestion
的响应数据
下一步需要将返回值的进行处理,结合selenium
完成自动对正确选项的勾选
接口的相应数据格式如下 (数据经过简化)
{
"message": "ok",
"result": {
"answer": {
},
"questions": [
{
"answer": "D",
"createTime": "2022-01-05 15:56:57",
"createTimeStr": "2022-01-05 15:56:57",
"difficulty": 3,
"id": "5dbfb524d3c34019bd61d4057b15e3dc",
"knowledgePoint": 12,
"knowledgePointName": "",
"name": "()的地下水,是指与大气降水和地表水体没有密切水力联系,无法补给或者补给非常缓慢的地下水。",
"options": [
{
"content": "浅层以下",
"createTime": "2022-01-05 15:56:58",
"id": "0eca09b3c5c748efbf6a368318f9444f",
"question": null,
"sortNo": "A",
"status": 0,
"updateTime": "2022-01-05 15:56:58"
},
{
"content": "深层",
"createTime": "2022-01-05 15:56:58",
"id": "4e7cb8e3e3b4458390e65f939cb1476e",
"question": null,
"sortNo": "B",
"status": 0,
"updateTime": "2022-01-05 15:56:58"
},
{
"content": "缓慢更新",
"createTime": "2022-01-05 15:56:58",
"id": "864a10198cfb4978aa8e9e177df3df33",
"question": null,
"sortNo": "C",
"status": 0,
"updateTime": "2022-01-05 15:56:58"
},
{
"content": "难以更新",
"createTime": "2022-01-05 15:56:58",
"id": "5bc380f665c64936ad246df5023dc34f",
"question": null,
"sortNo": "D",
"status": 0,
"updateTime": "2022-01-05 15:56:58"
}
],
"quType": 1,
"sortNo": 0,
"status": 0,
"updateTime": "2022-01-05 15:56:57"
},
{
"answer": "A|B|C|D",
"createTime": "2022-01-05 15:56:57",
"createTimeStr": "2022-01-05 15:56:57",
"difficulty": 3,
"id": "f359a081c4704598a2f011ae9357803f",
"knowledgePoint": 12,
"knowledgePointName": "",
"name": "《xxxxx》中明确,坚持节水优先,统筹()、()、()、(),优化水资源配置格局,提升配置效率。",
"options": [
{
"content": "地表水与地下水",
"createTime": "2022-01-05 15:56:58",
"id": "f191dd0bb2e34a0fa9d0e71eabff8ea4",
"question": null,
"sortNo": "A",
"status": 0,
"updateTime": "2022-01-05 15:56:58"
},
{
"content": "天然水与再生水",
"createTime": "2022-01-05 15:56:58",
"id": "5bde2b80d17f4d24890c7050a10fcd5c",
"question": null,
"sortNo": "B",
"status": 0,
"updateTime": "2022-01-05 15:56:58"
},
{
"content": "当地水与外调水",
"createTime": "2022-01-05 15:56:58",
"id": "96d5e55b102840dda36cb3ddf85a1258",
"question": null,
"sortNo": "C",
"status": 0,
"updateTime": "2022-01-05 15:56:58"
},
{
"content": "常规水与非常规水",
"createTime": "2022-01-05 15:56:58",
"id": "89d422ae2a2f446d83612eb3e3d5c2e2",
"question": null,
"sortNo": "D",
"status": 0,
"updateTime": "2022-01-05 15:56:58"
}
],
"quType": 2,
"sortNo": 0,
"status": 0,
"updateTime": "2022-01-05 15:56:57"
},
{
"answer": "A",
"createTime": "2022-01-05 15:56:57",
"createTimeStr": "2022-01-05 15:56:57",
"difficulty": 3,
"id": "0b64195829b34442976acdbfc5268a5e",
"knowledgePoint": 12,
"knowledgePointName": "",
"name": "xxxxx》提出,开展地下水超采综合治理行动,加大中下游地下水超采漏斗治理力度,逐步实现重点区域地下水采补平衡。",
"options": [
{
"content": "正确",
"createTime": "2022-01-05 15:56:58",
"id": "125e797508564f1b978d48c20e337240",
"question": null,
"sortNo": "A",
"status": 0,
"updateTime": "2022-01-05 15:56:58"
},
{
"content": "错误",
"createTime": "2022-01-05 15:56:58",
"id": "b522fbef81254f64906027534c4e6f8c",
"question": null,
"sortNo": "B",
"status": 0,
"updateTime": "2022-01-05 15:56:58"
}
],
"quType": 3,
"sortNo": 0,
"status": 0,
"updateTime": "2022-01-05 15:56:57"
}
]
},
"returncode": 0
}
其中result.questions
字段的值为包含题目中的数据列表
该对象中answer
字段为正确选项的结果
options
为题目中的选项
quType
题目的类型
其中不同的题目类型对应的其他数据的值也不同
题目类型 | quType |
answer 的值 |
options 数量(选项数量) |
---|---|---|---|
单选 | 1 | A | 4 |
多选 | 2 | A | B | C | D | 4 |
判断 | 3 | A | 2 |
题目中全部选项的数量 =
questions
中每个对象的options
的长度的累加
为了后续可以更加方便的用selenium
对页面中的DOM元素
数据进行遍历
先构造一个二位数组对每个答案进行封装
如果在单选中答案为A
则结果为 [1,0,0,0]
如果多选答案为 A B C
则结果为 [1,1,1,0]
如果判断题答案为 对
则结果为 [1,0]
为了让后续遍历跟家的顺利,将二维数组转换成一维数组
数组的长度 =
页面中全部的题目的数量
如果某个选项是正确选项
则对应数组中值为1
反之数组中的值为0
代码如下
response_body = json.loads(response_body)
# 得到result
result_dict = response_body['result']
# 题目列表
question_list = result_dict['questions']
# 遍历题目列表得到答案
answer_result = []
for item in question_list:
# 得到答案
answer_str = item['answer']
# 对答案进行拆分
answer_list = str(answer_str).split("|")
# 得到题目的类型
question_type = item['quType']
# 得到选项
question_option = item['options']
# 构造选项结果
item_answer_list = []
# 遍历选项列表
for option_index in range(len(question_option)):
# 跳出循环的条件
flag = True
# 得到选项对象
option_item = question_option[option_index]['sortNo']
# 遍历答案列表
for answer_index in range(len(answer_list)):
# 得到答案对象
answer_item = answer_list[answer_index]
# 判断两个元素是否一致
if option_item == answer_item:
item_answer_list.append(1)
flag = False
# 判断跳出循环的情况
if flag:
item_answer_list.append(0)
answer_result.append(item_answer_list)
# 将二维数组变成一维数组
answer_result_list = []
for item_list in answer_result:
for item in item_list:
answer_result_list.append(item)
return answer_result_list
通过上面的操作已经得到了每个每个选项是否是正确答案的结果集
得到页面上的全部的选项的对象后就可开始对数据的遍历操作
代码如下
# 得到页面上所有的选项
check_list = driver.find_elements_by_tag_name("input")
"""
此时页面上的列表中的元素的个数
和answer_result_list的个数是一样的
所以answer_result_list的下标进行遍历即可
"""
for item_index in range(len(answer_result_list)):
# 得到每个元素的值
answer_item = answer_list[item_index]
# 点击选项
if answer_item:
check_list[item_index].click()
代码运行结束后,页面中的所有的正确答案已经被选中
后续再次定位需要点击的页面元素
调用selenium
的点击事件即可完成剩余操作
selenium调用chromedriver接口获得响应数据的相关引用自文章
https://blog.csdn.net/mxdzchallpp/article/details/106475193
感谢博主分享