[爬虫] b站滑动验证码图片的获取
—-update—–
有更简单的方式,不用这么复杂的,自行百度
本文仅是获取验证码图片,python+selenium实现
图片的处理,算出偏移位置网上都有现成的;而由于b站的更新,图片的获取则与之前完全不同,不能直接从html中拿到
过程比较曲折所以记录一下,可能比较长
从分析的过程来展开,刚开始的分析最终发现有些问题,虽然可以拿到图片但与当前的验证码图片不一致;
经过前面的经历,找到了后面的方法,可以成功获取到当前图片
一、(可直接看二,测试可行的)
分析结果:两个参数challenge/gt,其中gt是固定的b6cc0fc51ec7995d8fd3c637af690de3,而challenge每次请求都不一样,所以关键在于challenge
1.故事的开始,combine接口,没有请求参数,返回challenge字符串
2.get.php接口,请求参数很多,但有用的只有challenge;返回了最重要的验证码图片地址;
3.然鹅,看起来虽然通过combine接口→→get接口即可获得图片地址
但实际上get进行第二次请求时不返回数据,而返回了错误信息,错误信息是旧的参数,难道需要用新的challenge参数请求?可是新的在哪呢
4.就在被卡住时,验证码刷新了,发起了reset和refresh请求,而其中refresh与get一样,返回的是图片地址,所以出现了转机
refresh的请求参数正是get返回的challenge,最重要的是refresh接口可以重复请求,获得图片地址(故事在这里埋下了伏笔)
既然是用新的challenge,那么用refresh返回的试一下,结果是不行,依然显示old_challenge
那么换一下思路,用get的challenge参数,而接口用refresh,是不是就能返回get接口的图片的地址了,实际上还真获取到了
所以就以为图片实际的获取接口是refresh,现在只要拿到get接口的challenge就可以了,而这个之前就已经开始实现了
然后就是码代码了。。
coding。。。
完成
测试一下吧,图片确实保存在本地,过程也都没什么了问题了
然后就是点开看一下图片
咦!?不对啊,图片和页面上的不一样啊
然后才意识到是之前梳理的逻辑出问题了
分析ing。。。
测试发现,带着同一个challenge参数的refresh每次返回的图片地址都不一样,所以后台应该是随机返回图片,而且后台也不保存每次生成的图片,图片都是临时的,当然每次地址都不一样了;这样的话,图片的接口根本就不是refresh,之前的get和refresh本质是一样的,都是随机返回图片;
(至于为什么只能请求一次而且返回的错误信息是old_challeng就不知道了;其实这里面还有一条线,就是reset接口,伴随refresh出现,第一次带着combine返回的参数请求,返回新的challenge和s,推测是js根据这两个参数生成新的challenge,即new challenge,带着这个参数才能请求到数据)(c也很可疑,验证码被分成了52份,这些会不会和顺序有关系?)
所以这条路就走不通了,只能试试其他方法
二、直接通过selenium获取请求的响应
0.browsermob-proxy
先试了browsermob-proxy,即通过代理获取浏览器请求信息,但https无法请求成功,查了下,因为是由java写的,所以对Java实现的比较好,应该可以解决,但python查了很多资料都没有解决方法
但如果没有https问题,其实browsermob-proxy挺好用的,可以通过proxy.new_har(“”)创建har文件,一种json格式文件,之后请求的所有信息都会保存在proxy.har中,可以直接写入文件中,数据很也直观;
实现获取http请求信息:
from browsermobproxy import Server
from selenium import webdriver
import json# browsermob-proxy.bat的路径
server = Server(r"xxx.\browsermob-proxy\bin\browsermob-proxy.bat")
server.start()
proxy = server.create_proxy()# 创建har
proxy.new_har("google")
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument("--proxy-server={0}".format(proxy.proxy))
driver = webdriver.Chrome(options=chrome_options)
driver.get("http://www.xxx.com/")
proxy.wait_for_traffic_to_stop(1, 60)# 保存har中的信息到本地
with open(\'1.har\', \'w\') as outfile:
json.dump(proxy.har, outfile,indent=2,ensure_ascii=False)
请求信息,全部在entries的列表中,每个请求保存为一个字典,其中的键主要有:request/response/timings
{
"log": {
"entries": [
{
"cache": {},
"time": 569,
"startedDateTime": "2019-09-10T16:32:33.342+0000",
"request": {
"method": "GET",
"url": "http://www.yiguo.com/",
"httpVersion": "HTTP",
"headersSize": 0,
"headers": [],
"queryString": [],
"cookies": [],
"bodySize": 0
},
"response": {
"content": {
"size": 10693,
"mimeType": "text/html; charset=utf-8"
},
"httpVersion": "HTTP",
"headersSize": 0,
"redirectURL": "",
"statusText": "OK",
"headers": [],
"status": 200,
"cookies": [
{
"name": "CityCSS",
"value": "UnitId=1&AreaId=7bc089fd-9d27-4e5f-a2e1-65907c5a5399&UnitName=%e4%b8%8a%e6%b5%b7",
"path": "/",
"domain": "yiguo.com",
"expires": "2020-09-10T16:32:35.000+0000"
}
],
"bodySize": 10693
},
"timings": {
"dns": 211,
"receive": 3,
"connect": 82,
"send": 0,
"blocked": 0,
"wait": 273
},
"serverIPAddress": "150.242.239.211",
"pageref": "baidu"
},
最终方案:通过webdriver自带的API
1.获取请求信息
参考:Browser performance tests through selenium—stackoverflow
第一版:
from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities caps = DesiredCapabilities.CHROME
# 必须有这一句,才能在后面获取到performance caps[\'loggingPrefs\'] = {\'performance\': \'ALL\'} driver = webdriver.Chrome(desired_capabilities=caps) driver.get(\'https://stackoverflow.com\')
# 重要:获取浏览器请求的信息,包括了每一个请求的请求方法/请求头,requestId等信息 logs = [json.loads(log[\'message\'])[\'message\'] for log in driver.get_log(\'performance\')] with open(\'devtools.json\', \'wb\') as f: json.dump(logs, f) driver.close()
但实际会报错:invalid argument: log type \’performance\’ not found,即get_log(\’performance\’)]出错
解决:Selenium Chrome can\’t see browser logs InvalidArgumentException
第二版:加上chrome_options.add_experimental_option(\’w3c\’, False)
from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
chrome_options = webdriver.ChromeOptions()
chrome_options.add_experimental_option(\'w3c\', False)
caps = DesiredCapabilities.CHROME
caps[\'loggingPrefs\'] = {\'performance\': \'ALL\'}
driver = webdriver.Chrome(desired_capabilities=caps,options=chrome_options)
driver.get(\'https://stackoverflow.com\')# 重要:获取浏览器请求的信息,包括了每一个请求的请求方法/请求头,requestId等信息 logs = [json.loads(log[\'message\'])[\'message\'] for log in driver.get_log(\'performance\')] with open(\'devtools.json\', \'wb\') as f: json.dump(logs, f) driver.close()
到这里就获取到了请求的的信息,包含着各个请求的url,和后面用到的requestId
(get_log(\’performance\’)的返回数据,参考:selenium 如何抓取请求信息)
2.根据请求信息中的requestId获取响应
他在文中提到了:
// 获取请求返回内容 session.getCommand().getNetwork().getResponseBody(“requestIdxxxxx”);
但没有获取到响应内容,最终发现可以通过ExecuteSendCommandAndGetResult来实现,只要传 cmd 与 params 命令就可以调用这个接口,最后自己通过代码实现,不过是Java的也看不太懂
但可以直接去python中的selenium源码看,是否有类似的接口,结果还真给找到了
selenium/webdriver/chrom/webdriver/下的WebDriver类的一个方法:execute_cdp_cmd()就实现了这样的功能;
而我们一般用的webdriver.Chrom(),返回的就是WebDriver的实例对象
源码如下:
def execute_cdp_cmd(self, cmd, cmd_args):"""
Execute Chrome Devtools Protocol command and get returned resultThe command and command args should follow chrome devtools protocol domains/commands, refer to link
https://chromedevtools.github.io/devtools-protocol/
:Args:
- cmd: A str, command name
- cmd_args: A dict, command args. empty dict {} if there is no command args
:Usage:
driver.execute_cdp_cmd(\'Network.getResponseBody\', {\'requestId\': requestId})
:Returns:
A dict, empty dict {} if there is no result to return.
For example to getResponseBody:
{\'base64Encoded\': False, \'body\': \'response body string\'}
"""return self.execute("executeCdpCommand", {\'cmd\': cmd, \'params\': cmd_args})[\'value\']
def execute(self, driver_command, params=None):"""
Sends a command to be executed by a command.CommandExecutor.:Returns:
The command\'s JSON response loaded into a dictionary object.
"""response = self.command_executor.execute(driver_command, params)return responsedef execute(self, command, params):return self._request(command_info[0], url, body=data)def _request(self, method, url, body=None):"""Send an HTTP request to the remote server.:Returns:
A dictionary with the server\'s parsed JSON response.
"""# 太长只看其中的逻辑部分resp = self._conn.request(method, url, body=body, headers=headers)data = resp.data.decode(\'UTF-8\')return data
思路:
1.通过正则在前面拿到的请求信息中,匹配到想要获取的请求所对应的的requestId
2.然后直接调用execute_cdp_cmd()接口,传入requestId
pat = r"""https://api\.geetest\.com/get\.php\?is_next.*?\".*?\"requestId\": \"(\d+?\.\d+?)\","""
requestId = re.findall(pat, browser_log, re.S)[0]response_dict = driver.execute_cdp_cmd(\'Network.getResponseBody\', {\'requestId\': requestId})# body即为之前提到的get接口返回的json数据,其中包含了验证码图片的地址body = response_dict["body"]
3.拿到验证码url,即可用requests模块请求,最终保存在本地
最后附上完整代码:
import json
import requests
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
import time
import re
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"
}
chrome_options = webdriver.ChromeOptions()
chrome_options.add_experimental_option(\'w3c\', False)
caps = DesiredCapabilities.CHROME
caps[\'loggingPrefs\'] = {\'performance\': \'ALL\'}
driver = webdriver.Chrome(desired_capabilities=caps,options=chrome_options)
driver.get(\'https://passport.bilibili.com/login\')
def input_click_01():
input_name = driver.find_element_by_xpath("//input[@id=\'login-username\']")
input_pwd = driver.find_element_by_xpath("//input[@id=\'login-passwd\']")
input_name.send_keys("username")
input_pwd.send_keys("passport")
time.sleep(3)
login_btn = driver.find_element_by_class_name("btn-login")
login_btn.click()
time.sleep(5)
def browser_log_02():
browser_log_list = driver.get_log("performance")
# 先保存到文件,利于测试,和后面的正则匹配
logs = [json.loads(log[\'message\'])[\'message\'] for log in browser_log_list]
with open(\'devtools.json\', \'w\') as f:
json.dump(logs, f, indent=4, ensure_ascii=False)
with open(\'devtools.json\', \'r\') as f:
browser_log = f.read()
print("浏览器日志获取完成")
return browser_log
def get_response_img_url_03(browser_log):
# 获取requestId
# 获取到的有两种,取前者,暂时没出错,出现异常再进行筛选
pat = r"""https://api\.geetest\.com/get\.php\?is_next.*?\".*?\"requestId\": \"(\d+?\.\d+?)\","""
requestId = re.findall(pat, browser_log, re.S)[0]
# print(requestId)
# 最重要的一步:调用接口,通过requestId获取请求的响应
response_dict = driver.execute_cdp_cmd(\'Network.getResponseBody\', {\'requestId\': requestId})
body = response_dict["body"]
# print(body)
# 从响应中获取图片链接
fullbg = re.findall(r"fullbg\":.\"(.*?)\",",body)
bg = re.findall(r"\"bg\":.\"(.*?)\",",body)
fullbg_url = "https://static.geetest.com/" + fullbg[0]
bg_url = "https://static.geetest.com/" + bg[0]
return fullbg_url,bg_url
def get_img_04(fullbg_url,bg_url):
# 请求
origin_img_data = requests.get(fullbg_url, headers=headers).content
fix_img_data = requests.get(bg_url, headers=headers).content
# 先保存图片
with open("原图.jpg", "wb") as f:
f.write(origin_img_data)
with open("缺口图.png", "wb") as f:
f.write(fix_img_data)
print("保存图片完成")
def main():
input_click_01()
log_data = browser_log_02()
url_tuple = get_response_img_url_03(log_data)
get_img_04(*url_tuple)
driver.close()
if __name__ == \'__main__\':
main()
最后,也不知道是不是绕远了,有更简洁的方法可以获取到验证码url;不过也确实是有些收获的;还有对于selenium的进一步理解,包括代理模式,远程连接等