zoukankan      html  css  js  c++  java
  • [爬虫] b站滑动验证码图片的获取

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

  • 相关阅读:
    android监听屏幕打开关闭广播无响应的情况
    2020/4/9
    2020/4/8
    2020/4/7
    conda镜像
    2020/4/3
    2020/4/2
    2020/4/1
    EYELIKE源代码解读
    bzoj3162 独钓寒江雪
  • 原文地址:https://www.cnblogs.com/justaman/p/11503805.html
Copyright © 2011-2022 走看看