—-update—–

有更简单的方式,不用这么复杂的,自行百度

 

本文仅是获取验证码图片,python+selenium实现

图片的处理,算出偏移位置网上都有现成的;而由于b站的更新,图片的获取则与之前完全不同,不能直接从html中拿到

 

过程比较曲折所以记录一下,可能比较长

 

从分析的过程来展开,刚开始的分析最终发现有些问题,虽然可以拿到图片但与当前的验证码图片不一致;

经过前面的经历,找到了后面的方法,可以成功获取到当前图片

一、(可直接看二,测试可行的)

分析结果:两个参数challenge/gt,其中gt是固定的b6cc0fc51ec7995d8fd3c637af690de3,而challenge每次请求都不一样,所以关键在于challenge

1.故事的开始,combine接口,没有请求参数,返回challenge字符串

image

image

 

2.get.php接口,请求参数很多,但有用的只有challenge;返回了最重要的验证码图片地址

image

image

 

3.然鹅,看起来虽然通过combine接口→→get接口即可获得图片地址

但实际上get进行第二次请求时不返回数据,而返回了错误信息,错误信息是旧的参数,难道需要用新的challenge参数请求?可是新的在哪呢

image

 

4.就在被卡住时,验证码刷新了,发起了reset和refresh请求,而其中refresh与get一样,返回的是图片地址,所以出现了转机

image

image

refresh的请求参数正是get返回的challenge,最重要的是refresh接口可以重复请求,获得图片地址(故事在这里埋下了伏笔)

既然是用新的challenge,那么用refresh返回的试一下,结果是不行,依然显示old_challenge

那么换一下思路,用get的challenge参数,而接口用refresh,是不是就能返回get接口的图片的地址了,实际上还真获取到了

image

所以就以为图片实际的获取接口是refresh,现在只要拿到get接口的challenge就可以了,而这个之前就已经开始实现了

然后就是码代码了。。

coding。。。

完成

 

测试一下吧,图片确实保存在本地,过程也都没什么了问题了

然后就是点开看一下图片

咦!?不对啊,图片和页面上的不一样啊

然后才意识到是之前梳理的逻辑出问题了

分析ing。。。

测试发现,带着同一个challenge参数的refresh每次返回的图片地址都不一样,所以后台应该是随机返回图片,而且后台也不保存每次生成的图片,图片都是临时的,当然每次地址都不一样了;这样的话,图片的接口根本就不是refresh,之前的get和refresh本质是一样的,都是随机返回图片;

(至于为什么只能请求一次而且返回的错误信息是old_challeng就不知道了;其实这里面还有一条线,就是reset接口,伴随refresh出现,第一次带着combine返回的参数请求,返回新的challenge和s,推测是js根据这两个参数生成新的challenge,即new challenge,带着这个参数才能请求到数据)(c也很可疑,验证码被分成了52份,这些会不会和顺序有关系?)

image

 

所以这条路就走不通了,只能试试其他方法

 

 

二、直接通过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获取响应

启发:selenium 获取请求返回内容的解决方案

他在文中提到了:

// 获取请求返回内容 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 result
    The 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 response


def 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的进一步理解,包括代理模式,远程连接等

版权声明:本文为justaman原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/justaman/p/11503805.html