深⼊浅出CDP(ChromeDevToolsProtocol)
深⼊浅出 CDP (Chrome DevTools Protocol)
14 Jan 2020
Table of Contents
背景
⾃从 Chrome 59 发布⽀持 –headless 启动参数以后 (Windows 上是 60 版本), 轻量级浏览器内核就不再是 webdriver ⼀家独⼤, 甚
⾄ phantomjs 作者也发⽂表⽰不再维护该项⽬, 国外也有越来越多的⽂章推荐使⽤ headless Chrome 代替过去 selenium + webdriver 的⽅式进⾏ Web 测试或者爬⾍相关⼯作. ⽬前国内实际上使⽤ headless Chrome 的并不少, 只不过⽬前⼤量营销号的存在, 导致为了点击量频繁刷⽂, 进⽽把早年间 selenium ⽤作爬⾍的旧⽂章重新翻到读者眼前, 所以遇到各种稀奇古怪的问题, 初学者使⽤体验较差. selenium 作为⽼
牌 Web 测试⼿段闻名已久, 在⾼级功能 API 层⾯⾮常成熟, 后来也加强了对 Chrome headless 模式下 CDP 的⽀持, ⽬前依然拥有⼤量⽤户在使⽤.这⾥, 简单提⼀下 selenium + webdriver ⽅式的⼀些不⾜:
1. 默认参数启动时很容易被服务端发现
2. 性能与 Chrome headless 相⽐, 较差
3. 存在了⽆数年的内存泄漏问题
4. 不像 Chrome 有⼤⼚在背后⽀撑, 上千 issues 解决不完
5. ⽆法作为完整浏览器使⽤和调试
简⽽⾔之, 都 2020 年了, 不要再抱着 selenium 不放了
概述
CDP
交流⽅式
通过 HTTP, WebSocket 两种⽅式, 对添加了远程调试接⼝参数( --remote-debugging-port=9222 )的浏览器进⾏远程调试, ⼤部分功能其实与浏览器⼿机打开的 devtools ⼀致
1. HTTP 负责总览当前 Tabs 信息
2. 每个 Tab 的对话使⽤ WebSocket 建⽴连接, 并接收已开启功能 (enabled domain) 的事件消息.
Headless Browser
俗称的⽆头浏览器, 实际上就是没有图形界⾯的浏览器, 因为省去了视觉渲染的⼯作, 性能和开销有较⼤优化, 粗略估计, 原本只能启动⼗个浏览器的内存, 使⽤ Headless 模式可以⾄少启动三倍的数量
常见⽤途
1. 主要还是 Web 测试
2. 少数情况会⽤来做爬⾍, 所见即所得的调试体验⾮常容易上⼿
3. 有⼀些 Web ⾃动化的⼯作, 可以替代⾃⼰写扩展或者 tampermonkey JS 脚本, 毕竟权限更⾼更全⾯, GUI 模式调试完以后, ⽆⼈参与操
作的多数情况, 则可以⽆痛改成 –Headless 模式来提⾼性能
常见问题
1. Chrome 浏览器有⼀个并发连接数的限制. 即对同⼀个⽹站, 只允许建⽴最多 6 个连接(纯静态情况下,
可以看作是 6 个
同 domain 的 Tabs). 如果真的遇到超过 6 个连接的需求, 可以通过新开⼀个浏览器实例来解决.
2. 对于 Linux 来说, ⼦进程处理不正确会导致出现僵⼫进程/孤⼉进程, 导致⽩⽩浪费资源, 时间长了整台服务器的内存都会垮掉. 常见解决
⽅案有 3 种
1. 将 Chrome 守护进程 (Daemon) 与业务代码隔离, 随需要启动对应数量的 Chrome 实例
2. 就 Python subprocess 这个内置模块来说, 确定每次关闭的时候执⾏正确的姿势
1. 调⽤ Browser.close 功能 gracefully 地关闭浏览器
2. 然后 terminate ⼦进程后, 记得 wait ⼀下消息
3. 最后保险起见可以再加个 kill, 虽然实际没什么⽤
3. 最简单的其实是到 chrome 实例的进程 ID 来杀, 毕竟杀死以后, subprocess 那边⽴刻就结束了
3. 神奇的是, 除了 chrome 实例有僵⼫进程, 连 tab 也会存在⼀些看不见 ( /json ⾥那些⾮ “page” 类型的就是了)或关不掉(僵⼫标签页)
的 tab 页
1. ⽬前这种 tab 不确定会不会⾃⼰关闭, 访问 B 站遇到过
2. 以前我处理这种 tab 的⽅式是给每个 tab 设定⼀个 lifespan, 异步⼀个循环, 扫描并关闭那些⾮ page 类型或者寿命超时了的 tab
3. 然⽽ tab 数量多了以后, 反⽽会出现很多⽆法关闭的僵⼫ tab, 通过 /json/close 或者发送 Page.close 事件都⽆效, 暂时只好重
启 chrome 实例来清理
4. 拿来做爬⾍还有⼏个问题没解决
1. chronium 开发团队本着 “你并不是真的特别需要” 原则, 没有动态挂代理的开发意向, 毕竟⼈家也不太希望⼈们拿它来做爬⾍, 只能
指望不同代理 IP 启动多个 chrome 实例来解决
2. 在 “⾮ headless” 情况下, 可以通过代理扩展, 或者 pac ⽂件, 来搞定动态代理的问题
3. 在 headless 的模式, 那就只好从 upstream ⾓度搞了, 甚⾄挂上 mitmproxy 也⾏吧tampermonkey
4. ⾄于动态修改 UA, 暂时可以⽤扩展来搞, 不过如果喜欢钻研, 可以发现 CDP ⾥⽀持动态修改 Request 的各项属性, 在这⾥
改 headers 是有效的
⽂档
常⽤功能
Chrome DevTools Protocol ⽂档的使⽤, 主要还是使⽤⾥⾯的检索功能, 不过最常⽤的还是以下⼏个领域
1. Page
1. 简单地理解, 可以把⼀个 Page 看成⼀个 Page 类型的 Tab
2. 对 Tab 的刷新, 跳转, 停⽌, 激活, 截图等功能都可以到
3. 也会有很多有⽤的事件需要 enable Page 以后才能监听到, ⽐如 loadEventFired
4. 多个⽹站的任务, 可以在同⼀个浏览器⾥打开多个 Tab 进⾏操作, 通过不同的 Websocket 地址进⾏连接, 相对隔离, 并且托异步模
型的福, Chrome 多个标签操作的抗压能⼒还不错
5. 然⽽并发操作多个 Tab 的时候, 可能会出现⼀点⼩问题需要注意: 同⼀个浏览器实例, 对⼀个域名只能建⽴ 6 个连接, 这个不太好
改; 过快⽣成⼤量 Tab, 可能会导致有的 Tab ⽆法正常关闭(zombie tabs)
2. Network
1. 和产⽣⽹络流量有关系的⼤都在这个 Domain
2. ⽐如 setExtraHTTPHeaders / setUserAgentOverride 对当前标签页的所有请求修改原是参数
3. ⽐如对 cookie 的各种操作
4. 通过 responseReceived + getResponseBody 来监听流量, 只⽤前者就能嗅探到 mp4 这种特殊类型
的 url 了, ⽽后者可以把流量⾥
已经 base64 化的数据进⾏其他操作, ⽐如验证码图⽚的处理
3. 其他功能也基本和 devtools ⼀致
常规姿势
1. 和某个 Tab 建⽴连接
2. 通过 send 发送你想使⽤的 methods
3. 通过 recv 监听你发送 methods 产⽣的事件, 或者其他 enable 的事件, 并执⾏对应回调
实践
准备⼯作
1. 安装 chrome 浏览器
2. 安装 Python
3.7
pip install ichrome -U
ichrome 库是可选的, 主要是为了演⽰通过 HTTP / Websocket client 与 chrome 实例实现通信
ichrome 库除了协程实现, 也有⼀个同步实现, 观察它的源码⽐协程版本的更直观⼀点, 也易于学习
启动调试模式下的 chrome
from ichrome import ChromeDaemon
def launch_chrome():
with ChromeDaemon(host="127.0.0.1", port=9222, max_deaths=1) as chromed:
chromed.run_forever()
if __name__ == "__main__":
launch_chrome()
以上代码负责启动 chrome 调试模式的守护进程, 具体参数如下:
1. **chrome_path: **表⽰ chrome 的可执⾏路径 / 命令, 默认为 None 的时候, 会⾃动根据操作系统去尝试寻 chrome 路径, 如 linux 下
的 google-chrome 和 google-chrome-stable, macOS 下的 /Applications/Google Chrome.app/Contents/MacOS/Google Chrome, 或者Windows 下的
1. C:/Program Files (x86)/Google/Chrome/
2. C:/Program Files/Google/Chrome/
3. “%s\AppData\Local\Google\Chrome\” % os.getenv(“USERPROFILE”)
2. **host: ** 默认为 127.0.0.1, 之所以不⽤ localhost, 是因为很多 Windows / macOS 的 etc/hosts ⽂件⾥被强制绑定到了 ipv6 地址上
3. **port: ** 默认为 9222
4. **headless: ** 常见参数 –headless, –hide-scrollbars, 放在初始化参数⾥了
5. **user_agent: ** 常见参数 –user-agent
6. **proxy: ** 常见参数 –proxy-server
7. **user_data_dir: ** 避免 chrome 到处乱放 user data, 所以默认会放到 user ⽬录下的 ichrome_user_data ⽂件夹下, 命名按端⼝号
chrome_9222
8. **disable_image: ** 常⽤参数 –blink-settings=imagesEnabled=false, 从 blink 层⾯禁⽤, ⽐其他禁⽌图⽚加载的⽅式要靠谱
9. **max_deaths: ** ⽤来⾃动重启, max_deaths=2 表⽰快速杀死 chrome 实例 2 次才能避免再次⾃动重启, 所以默认为 1
10. **extra_config: ** 就是添加其他更多 chrome 启动的参数, 参数类型为 list
启动带图形界⾯的 chrome 之后, 可以⼿动尝试下通过 http 请求和 chrome 实例通信了
1. 访问  , 会拿到⼀个列出当前 tabs 信息的 json
2. 其他操作参考  (HTTP Endpoints 部分)
[
{
"description": "",
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/E6826ED4A0365605F3234B2A441B1D03",
"id": "E6826ED4A0365605F3234B2A441B1D03",
"title": "about:blank",
"type": "page",
"url": "about:blank",
"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/E6826ED4A0365605F3234B2A441B1D03"
}
]
操作 Tab
1. 建⽴到 webSocketDebuggerUrl 的 Websocket 连接, 然后监听事件
2. ⼤部分功能 ichrome 已经打包好了
from ichrome import AsyncChrome
import asyncio
async def async_operate_tab():
chrome = AsyncChrome(host='127.0.0.1', port=9222)
if not t():
raise RuntimeError
tab = (await chrome.tabs)[0]
async with tab():
# 跳转到 httpbin, 3 秒 loading 超时的话则 stop loading
await tab.set_url('', timeout=3)
# 注⼊ js, 并查看返回结果
result = await tab.js("document.title")
title = result['result']['result']['value']
# 打印 title
print(title)
#
# 通过 js 修改 title
await tab.js("document.title = 'New Title'")
# click ⼀个 css 选择器的位置, 跳转到了 Github
await tab.click('body > a:first-child')
# 等待加载完成
await tab.wait_loading(3)
async def callback_function(request):
if request:
# 监听到经过过滤的流量, 等待它加载⼀会⽐较保险
for _ in range(3):
result = _response(request)
('error'):
await tab.wait_loading(1)
continue
# 拿到整个 html
body = result['result']['body']
print(body)
def filter_func(r):
url = r['params']['response']['url']
print('received:', url)
return url == 'github/'
# 监听流量, 需要异步处理, 则使⽤ sure_future 即可
# 监听 10 秒
task = sure_future(
tab.wait_response(
filter_function=filter_func,
callback_function=callback_function,
timeout=10),
loop=tab.loop)
# 点击⼀下左上⾓的⼩章鱼则会触发流量
await tab.click('[href="github/"]')
# 等待监听流量
await task
if __name__ == "__main__":
asyncio.run(async_operate_tab())
总结
CDP 单单⼊门的话, 其实没想象中那么复杂, chrome 59 刚出的时候, puppeteer 都没的⽤, 更别说 pyppeteer 之类的包装, 看了⼏个早期项⽬的源码, 发现简单使⽤的话, 其实主要就是:
1. HTTP
2. Websocket
3. Javascript
4. Protocol
pyppeteer 诞⽣之初曾体验了⼀下, 第⼀步就因为⼀些不可抗⼒导致下载 chromium 失败, 所以之后只能阅读⼀下⾥⾯⼀些有意思的源码, 主要看了下如何从 puppeteer 原⽣事件驱动转为 Python ⾓度的事件, pyee 的使⽤也让⼈眼前⼀亮
之后⾃⼰摸索过程中也碰到了各种各样问题, 除了上⾯提到的, 其实还遇到 Websocket 粘包(粘包本⾝
就是个因为理解不⾜导致的伪命题), Chrome Headless 阉割掉了很多基础功能也使开发过程中总是⽆理由地调试失败, 甚⾄关闭 user-dir 使⽤匿名模式导致⼀系列不知名故障也是费⼼费⼒, 不过总体来说收获颇⼤⽤ Python 来操作 chrome 能做的事情挺多, 尤其是各路签到爬⾍, 或者索取公众平台⼤概 20 ⼩时有效期的 cookie / token 给后台爬⾍使⽤, 采集视频, ⾃动化测试时候截图, 启动 Headless 模式以后节省了很多⼿动操作的时间, 甚⾄可以丢到⽆ GUI 的 linux server 上去. 要知道以前指望的还是 tampermonkey 或者⼿写扩展, 很多 Python 的功能只能转 js 再⽤, 劳⼼劳⼒.

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。