Python cdp ( Chrome DevTools Protocol ) 爬虫

devtools-protocol:https://github.com/ChromeDevTools/devtools-protocol
Chrome DevTools Protocol:https://chromedevtools.github.io/devtools-protocol/
Awesome Chrome DevTools :https://github.com/ChromeDevTools/awesome-chrome-devtools#chrome-devtools-protocol

1、关于 Chrome Debugging Protocol

Chrome 提供了 websocket 调试接口用于对当前 Tab 内页面的 DOM、网络、性能、存储 等等进行调试,我们常用的开发者工具就是基于此接口,这个接口也支持远程调用,在启动参数中加上 --remote-debugging-port=9222 即可。

远程调试接口是一个 WebSocket 的接口,Chrome 提供的开发者工具是一种客户端,自己写代码调用也是一种客户端。DevTools 可以 attach 到一个远程运行的 Chrome 实例,来进行 debug,

  • 启动一个 host Chrome 实例:chrome.exe --remote-debugging-port=9222
  • 指定一个用户的 profile 启动一个 client 客户端 Chrome 实例:chrome.exe --user-data-dir=<some directory>

在客户端中输入 http://localhost:9222,你会看到当前 client 端的 DevTools 就像内嵌在电脑中Chrome 的 devtools 一样,然后你就可以通过 client 操作他了。

  • 当用 client 连接到 9222 的时候,DevTools 前端 会被 host Chrome 实例 serve 为一个远程服务端的 web application,他会通过 HTTP 协议 fetch HTML,JavaScript,CSS,一旦加载,DevTools 会和 host 建立一个 ws 链接,并开始交换 JSON 信息

也可以用自己的实现来替代 DevTools 前端,除了 localhost:9222

远程调试还提供了一个JSON接口,用于管理浏览器的 Tab 页面。( localhost:9222/json 来获取到 ws 通信的 JSONobject,并修改使用它们, HTTP Endpoints 章节中有详细内容。 )

2、实战 Chrome Headless 数据抓取

实战 Chrome Headless 数据抓取(上):https://blog.csdn.net/chixulu6723/article/details/100730003

启动 Chrome

启动之前,需要关闭所有 Chrome 窗口

Linux 中的 screen 命令使用:https://blog.csdn.net/han0373/article/details/81352663

如果在远程服务器上建议在 screen 里运行,一个小工具防止网络突然中断:$ screen -S chrome

然后会打开一个新的 shell,可以用 Ctrl + A + D 切出来,或者断开SSH 直接切出来。再进去只需要执行:$ screen -r chrome

然后在 screen 里面的 shell 执行( 本机 Windows 调试把 google-chrome-unstable 换成chrome.exe ):$ google-chrome-unstable --headless --remote-debugging-port=9222 --user-data-dir='/home/luke/chrome-data/baidu' --user-agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3080.5 Safari/537.36'

解释下参数:

  • --headless:无头模式,就是无界面模式运行
  • --remote-debugging-port:开启远程调试,端口9222和我们之前转发出来的端口一致
  • --user-data-dir:设置独立的文件保存目录,建议一个网站一个目录
  • --user-agent:伪装浏览器,默认User-Agent里的浏览器叫HeadlessChrome,很容易被发现

连接调试端口

启动之前,需要关闭所有 Chrome 窗口,然后在启动,

远程调试接口是一个 WebSocket 的接口,Chrome 提供的开发者工具是一种客户端,我们自己写代码调用也是一种客户端。这里先用开发者工具测试,后面会写代码来实现。 在本地浏览器开:http://服务器IP:9222/json ,本机测试的话就是 http://127.0.0.1:9222/json 。我已经用 SSH 把服务器的 9222 转发到本机的 9222 了,这是效果:

这里列出了当前远程浏览器内打开的Tab,每个页面一个UUID用以识别。已知接口:

抓取百度

远程打开,新开一个 Tab 打开百度首页,然后刷新 http://127.0.0.1:9222/json ,可以看到百度已经打开了:

 注意到,有一个devtoolsFrontendUrl,那就是开发者工具的前端地址,就是一个html应用,url里面传过去WebSocket调试地址。打开这个地址就可以看到熟悉的开发者工具了!注意:这个窗口调试的是远程chrome上的页面。

 如果你想看看页面在远程服务器的 Chrome 里渲染的结果,在开发者工具里切换到 Performance,勾选 Screenshots,点刷新图标,重新加载完成就可以看到逐帧加载的截图。

远程操作 DOM

思路:我们在 Elements 里面找到输入框的 ID,使用 JQuery 操作。百度首页已经有 JQuery 了,其他网站我们可以在 Console 里执行 JS,加载一个。 我们切换到 Console 里直接用 JS 操作DOM,执行:

$("input[name='wd']").val('测试');
$("form.fm").submit();

相当于在百度输入框里输入了测试并点击了“百度一下”按钮。

现在再打开 http://127.0.0.1:9222/json ,可以看到原来的页面标题已经变成了 “测试_百度搜索”,也就说明成功完成了搜索。 

获取搜索结果

依然是在 Elements 里面找到结果列表的 ID,然后用 JS 获取内容。 获得结果数量:

console.log($("#container").find(".nums").text());

获得所有结果标题:

$("#container").find(".c-container").each(function(){console.log($(this).find("h3.t").text())});

常用指令

Chrome DevTools Protocol的指令分为三十多个大类,每类又有若干个指令,这里不能一一介绍,只选择几个简单而常用的指令介绍一下:

  1. 跳转到指定页面:Page.navigate

  2. 执行JS函数:Runtime.evaluate

  3. 获取资源树:Page.getResourceTree

  4. 获取资源:Page.getResourceContent

其中 Page.navigate 是必备指令,用于跳转页面。而 Runtime.evaluate 的效果等同于在 Develop Tools 的 Console 控制台执行指令,基本可以执行任何js指令,模拟输入,输出渲染后的 html 用它都可以轻松搞定,可以说是大杀器了。

 获取资源树和获取资源指令则用于获取浏览器当前原始请求的数据,可以用它来构建Develop Tools的Source树。

可以说,利用 Develop Tools 实现的功能我们都可以通过 Chrome DevTools Protocol 实现,Chrome 自己也内置了一个官方的实现,用 Chrome 直接访问页面信息的 devtoolsFrontendUrl 即可看到,和按 F12 调用出来的 Develop Tools 基本一模一样。 

3、chrome headless 爬虫抓取 websoket 数据

From:https://brucedone.com/archives/1201

要分析的网站:https://datacenter.jin10.com/price

Python 连接 websocket 代码:

# coding:utf-8

from websocket import create_connection
from websocket import ABNF

api = "wss://sshibikfdn.jin10.com:9084/socket.io/?EIO=3&transport=websocket&sid=VsJvZikGdc8spBaPAAMO"

headers = {
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    'Cookie': 'UM_distinctid=16614315bf179b-0354a40c6714ff-34677908-232800-16614315bf2acb; XSRF-TOKEN=eyJpdiI6IkNZRU9uSmM1ZnY2M0VqNUttK1pxRGc9PSIsInZhbHVlIjoiRlJpNlRuekIxTDJZeVd3bHpvXC9OUEZGamw4VndZNEdXTEVsRjRMaFFyOEIxUHRtNDdTc1JaQ042eG4xdjlFeWJjWGlkcWFaeWl6NTRVUUlQMThaZmJ3PT0iLCJtYWMiOiJkYWU1MzQ2NjEyM2U3OTk0MzY5NWNjZTdhZmNlZjE0YTViMjc2YzBiYWM4YjhiMjNhZmRjMzU3YzliNDg3ZGIzIn0%3D; laravel_session=eyJpdiI6IjBRS3h0Y29XcGRBRlFIc0xIeWFiZGc9PSIsInZhbHVlIjoibVRLblpNTDJJa1JIN1ZJc0s5c2xrSkYzckNadDB6aGp0REd5SVJQTlkxNVAzajhvdXY5ZElSQ3VTcGVicjNiSXZ3NE9pZDZOdHJUM1d6WG1KQjZXNkE9PSIsIm1hYyI6Ijg3MWVkZDVlMDFjZDM2NDRjZmI2ZDhkNDJmZGI5MjNhMzk3MTViNmI1YTNmMDRmYWJjNzQ4ZGU2YWZhNzNhNzUifQ%3D%3D; io=VsJvZikGdc8spBaPAAMO',
    'Host': 'sshibikfdn.jin10.com:9084',
    'Connection': 'Upgrade',
    'Origin': 'https://datacenter.jin10.com',
    'Pragma': 'no-cache',
    'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
    'Sec-WebSocket-Key': 'g4UA3smEJ0eGufMkyz7AOw==',
    'Sec-WebSocket-Version': '13',
    'Upgrade': 'websocket',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
}


def get_web_socket():
    start_message = "2probe"
    ws = create_connection(
        api,
        header=headers,
        cookie=headers['Cookie'],
        #origin=headers['Origin'],
        #host=headers['Host']
    )
    frame = ABNF.create_frame("2probe", ABNF.OPCODE_TEXT)
    ws.send_frame(frame)

    data = ws.recv_frame()
    print(data)


if __name__ == '__main__':
    get_web_socket()

使用 websocket 和 chrome header less 进行交互

import json
import time

import requests
import websocket

request_id = 0
target_url = 'https://datacenter.jin10.com/price'


def get_websocket_connection():
    r = requests.get('http://10.10.2.42:9222/json') #这是开启docker chrome headless的机器地址
    if r.status_code != 200:
        raise ValueError("can not get the api ,please check if docker is ready")

    conn_api = r.json()[0].get('webSocketDebuggerUrl')

    return websocket.create_connection(conn_api)


def run_command(conn, method, **kwargs):
    global request_id
    request_id += 1
    command = {'method': method,
               'id': request_id,
               'params': kwargs}
    conn.send(json.dumps(command))
    #while True:
    msg = json.loads(conn.recv())
    if msg.get('id') == request_id:
        return msg


def get_element():
    conn = get_websocket_connection()
    msg = run_command(conn, 'Page.navigate', url=target_url)
    time.sleep(5)
    js = "var p = document.querySelector('.jin-pricewall_list-item_b').innerText ; p ;"

    for _ in range(20):
        time.sleep(1)
        msg = run_command(conn, 'Runtime.evaluate', expression=js)
        print(msg.get('result')['result']['value'])


if __name__ == '__main__':
    get_element()

整体逻辑非常简单,打开指定页面,等待页面数据刷新,然后直接偷懒拿数据渲染之后的页面值,运行效果如下:

这里使用 chrome-headless 的相关渲染环境来解决了抓取数据的问题,并且使用 websocket api 来进一步操作,其实 google 官方有 sdk 进行操作,https://github.com/GoogleChrome/puppeteer ,渲染的终究不是高效的做法,但是对于这种单页面目的性很强的数据,可以尝试渲染大法

Pyppeteer 防止检测

4、chrome devtools protocol

chrome devtools protocol 允许第三方对基于 chrome 的 web 应用程序进行调试、分析等,它基于 WebSocket,利用 WebSocke t建立连接 DevTools 和浏览器内核的快速数据通道。一句话,有了这个协议就可以自己开发工具获取 chrome 的数据

协议详细内容看这里 chrome devtools protocolhttps://chromedevtools.github.io/devtools-protocol/

目前已经有很多大神针对这个协议封装出不同语言(nodejs,python,java...)的库。

详细信息看这里 awesome-chrome-devtoolshttps://github.com/ChromeDevTools/awesome-chrome-devtools#chrome-devtools-protocol

Protocol Driver Libraries

PyChromeDevTools 爬虫 示例

PyChromeDevTools:https://github.com/marty90/PyChromeDevTools

PyChromeDevTools 是一个 Python 模块,允许在 Python 脚本中使用 Chrome DevTools 协议与 Google Chrome 交互。

页面加载时间

import PyChromeDevTools
import time

chrome = PyChromeDevTools.ChromeInterface()
chrome.Network.enable()
chrome.Page.enable()

start_time=time.time()
chrome.Page.navigate(url="http://www.google.com/")
chrome.wait_event("Page.loadEventFired", timeout=60)
end_time=time.time()

print ("Page Loading Time:", end_time-start_time)

打印所有安装的 cookies

import PyChromeDevTools
import time

chrome = PyChromeDevTools.ChromeInterface()
chrome.Network.enable()
chrome.Page.enable()

chrome.Page.navigate(url="http://www.nytimes.com/")
chrome.wait_event("Page.frameStoppedLoading", timeout=60)

#Wait last objects to load
time.sleep(5)

cookies,messages = chrome.Network.getCookies()
for cookie in cookies["result"]["cookies"]:
    print ("Cookie:")
    print ("\tDomain:", cookie["domain"])
    print ("\tKey:", cookie["name"])
    print ("\tValue:", cookie["value"])
    print ("\n")

打印页面的所有对象 URL

import PyChromeDevTools

chrome = PyChromeDevTools.ChromeInterface()
chrome.Network.enable()
chrome.Page.enable()

chrome.Page.navigate(url="http://www.facebook.com")
event,messages=chrome.wait_event("Page.frameStoppedLoading", timeout=60)

for m in messages:
    if "method" in m and m["method"] == "Network.responseReceived":
        try:
            url=m["params"]["response"]["url"]
            print (url)
        except:
            pass

pychrome 爬虫 示例

Pychrome:能跟 chrome 开发者工具交流的 Python 包,查看 github 代码中 examples 文件夹,查看更多例子。pychrome github地址,使用方法很简单,直接看 github上它的 Demo

这个库依赖 websocket-client

python - pychrome 页面抓取测试:https://blog.csdn.net/max229max/article/details/91972429

使用 Chrome-headless 抓取页面内容,使用 python 的 pychrome 包。

要先开启浏览器,然后通过pychrome调用chrome dev protocol

# -*- coding: utf-8 -*-
# @Author  : 
# @Date    : 2021/8/19
# @File    : test.py
# @description : XXX


import pychrome
from concurrent.futures import ThreadPoolExecutor


def request_will_be_sent(**kwargs):
    print("loading: %s" % kwargs.get('request').get('url'))


def start_chrome(chrome_url='http://127.0.0.1:9222'):
    browser = pychrome.Browser(url=chrome_url)
    tab = browser.new_tab()
    tab.set_listener("Network.requestWillBeSent", request_will_be_sent)

    tab.start()
    tab.call_method("Network.enable")
    tab.call_method("Page.navigate", url="https://detail.tmall.com/item.htm?id=612274990981", _timeout=10)

    tab.wait(20)
    tab.stop()

    browser.close_tab(tab)


if __name__ == '__main__':

    """
    chrome.exe --remote-debugging-port=9222 --user-data-dir=c:/user_data_dir_9222
    chrome.exe --remote-debugging-port=9223 --user-data-dir=c:/user_data_dir_9223
    """

    tem_list = ('http://127.0.0.1:9222', 'http://127.0.0.1:9223')

    with ThreadPoolExecutor() as tp_executor:
        tp_executor.submit(start_chrome, 'http://127.0.0.1:9222')
        tp_executor.submit(start_chrome, 'http://127.0.0.1:9223')
    # start_chrome()
    pass

Web 性能自动化

chrome devtools protocol --- Web 性能自动化实践介绍 :https://testerhome.com/topics/15817

在测试 Web 页面加载时间时,可能会是这样的:

  1. 打开 chrome 浏览器。
  2. 按 F12 打开开发者工具。
  3. 在浏览器上打开要测试的页面
  4. 查看开发者工具中 Network 面板的页面性能数据并记录
  5. 或者 开发者 Console 面板运行 performance.timing 和 performance.getEntries() 收集数据

performance 相关信息看这里 PerformanceTiming

几十上百个页面,每个版本都这样来,估计疯了,所以就想怎么把它做成自动化呢?

获取 performance api 数据

这里使用 Runtime Domain 中运行 JavaScript 脚本的 API Runtime.evaluate

# 开始前先启动chrome,启动chrome必须带上参数`--remote-debugging-port=9222`开启远程调试否则无法与chrome交互
browser = pychrome.Browser('http://127.0.0.1:%d' % 9222)
tab = browser.new_tab()
tab.start()
tab.Runtime.enable()
tab.Page.navigate(url={你的页面地址})
# 设置等待页面加载完成的时间
tab.wait(10)
# 运行js脚本
timing_remote_object = tab.Runtime.evaluate(
            expression='performance.timing'
        )
# 获取performance.timing结果数据
timing_properties = tab.Runtime.getProperties(
            objectId=timing_remote_object.get('result').get('objectId')
        )
timing = {}
for item in timing_properties.get('result'):
            if item.get('value', {}).get('type') == 'number':
                    timing[item.get('name')] = item.get('value').get('value')
# 获取performance.getEntries()数据
entries_remote_object = tab.Runtime.evaluate(
            expression='performance.getEntries()'
        )
entries_properties = tab.Runtime.getProperties(
            objectId=entries_remote_object.get('result').get('objectId')
        )
entries_values = []
for item in entries_properties.get('result'):
  if item.get('name').isdigit():
    url_timing_properties = tab.Runtime.getProperties(
                    objectId=item.get('value').get('objectId')
                )
     entries_value = {}
     for son_item in url_timing_properties.get('result'):
                    if (son_item.get('value', {}).get('type') == 'number'or
                            son_item.get('value', {}).get('type') == 'string'):
                        entries_value[son_item.get('name')] = son_item.get('value').get('value')
                entries_values.append(entries_value)

获取 Network 数据

实际上 performance.getEntries() 不会记录 404 的请求信息,另外当前页面通过 js 触发新 html 页面请求时它只会记录第一个页面的请求,在这些情况下就需要通过 Network Domain 的 API 来收集所有请求信息,先介绍用到的 API :

  • Network.requestWillBeSent  每个 http 请求发送前回调
  • Network.responseReceived  首次接送到 http 响应时回调
  • Network.loadingFinished  请求加载完成时回调
  • Network.loadingFailed  请求加载失败时回调
# 封装上面4个事件对应的回调方法
class NetworkAPIImplemention(object):

def __init__(self):
    self.request_dict = {}
    # 首个请求开始时间
    self.start = None

def request_will_be_sent(self, **kwargs):
    if self.start is None:
        self.start = time.time()
    dict_http = {
        'url':kwargs.get('request').get('url'),
        'start':kwargs.get('timestamp')
    }
    self.request_dict[kwargs.get('requestId')]=dict_http
    #print "loading:%s" % kwargs.get('request').get('url')

def loading_finished(self, **kwargs):
    # 服务器返回code 例如404也是finished
    self.request_dict[kwargs.get('requestId')]['end'] = kwargs.get('timestamp')
    self.request_dict[kwargs.get('requestId')]['size'] = kwargs.get('encodedDataLength')

def response_received(self, **kwargs):
    self.request_dict[kwargs.get('requestId')]['type'] = kwargs.get('type')
    self.request_dict[kwargs.get('requestId')]['response'] = kwargs.get('response')

def loading_failed(self, **kwargs):
    self.request_dict[kwargs.get('requestId')]['end'] = kwargs.get('timestamp')
    self.request_dict[kwargs.get('requestId')]['error_text'] = kwargs.get('errorText')
network_api = NetworkAPIImplemention()
browser = pychrome.Browser('http://127.0.0.1:%d' % 9222)
tab = browser.new_tab()
# 绑定回调函数
tab.Network.requestWillBeSent = network_api.request_will_be_sent
tab.Network.responseReceived = network_api.response_received
tab.Network.loadingFinished = network_api.loading_finished
tab.Network.loadingFailed = network_api.loading_failed
tab.start()
tab.Network.enable()
tab.Runtime.enable()
# 是否禁用缓存
if disable_cache:
tab.Network.setCacheDisabled(cacheDisabled=True)
tab.Page.navigate(url={你的页面地址})
tab.wait(10)
tab.stop()
self.browser.close_tab(tab)
# 获取的所有url详细信息
print network_api.request_dict

监听 页面 事件

有时候特别是一些复杂的页面,页面依赖 js 和 后端资源数据,并不是通常意义上页 loadEventEnd 事件触发完就表示页面加载完成,这种情况可能需要依赖开发打点。

这里以开发设计了一个 Loaded 事件为例:

# 具体事件注册方式和注册时机询问开发,所谓注册时机即要求在js对象生成后注册,我们项目中page是在一个js文件中声明的,需要等这个js文件请求完成后再注册
# 这边使用Promise方式,这种方式awaitPromise参数必须是True
js = """
    new Promise((resolve, reject) => {
        page.getController().getPageEvent().addEventListener("Loaded",
                function(){
                    resolve(new Date().getTime());
                });
        });
   """
custom_result = tab.Runtime.evaluate(
    expression=js,
    awaitPromise=True,
    timeout=timeout * 1000
)
print custom_result.get('result').get('value')

有个坑 peformance.now()获取与 chrome 开发者工具协议一样类型的时间时,这个时间不准确,只好用 new Date().getTime()

写在最后

一开始是使用 nodejs 的 chrome-remote-interface,但是发现 Page.loadEventFired 回调后不会再记录请求,事实上有些页面仍然有请求没有完成,不懂是不是我使用姿势不对。。。

附赠 W3C 的一幅图

猜你喜欢

转载自blog.csdn.net/freeking101/article/details/110213782