作为主流浏览器Chrome,其自动化一直备受关注,各种解决方案层出不穷。selenium、Puppeteer等自动化工具自然不必说,各种开发插件也是漫天飞,今天的主角PyChromeDevTools
是针对Chrome Devtool Protocol
的一款Python库分析,其实类似的开源库很多,github上有人总结了一些,可以参考一下:https://github.com/ChromeDevTools/awesome-chrome-devtools。其中Python相关的库有下面几个(居然没有本文的主角,-_-||):
为啥分析PyChromeDevTools呢?
155
行,去掉空行和注释,有效代码只有130行左右功能强大
,没错,就是强大,这百行代码写了个框架,需要用什么只需要自己拼就好了__getattr__
、__setattr__
软件名 | 版本号 | 描述 |
---|---|---|
操作系统 | Win10-1607 | |
Python(venv) | Python3.8.6(virtualenv) | |
Google Chrome | 96.0.4664.110 (正式版本) (64 位) (cohort: 97_Win_99) |
Chrome Devtool Protocol(下面简称 CDP)是一个非常强大工具,简单来说,它可以揭开束缚 Chrome 的各种封印,从浏览器角度深入页面(及其它领域,包括 worker),完成一些平日里难以完成的操作。
Chrome 提供了 websocket 调试接口用于对当前 Tab
内页面的 DOM、网络、性能、存储 等等进行调试,我们常用的开发者工具就是基于此接口。
常用命令:
无头模式,就是无界面模式运行,启动chrome时候添加参数--headless
就可以了。
在该模式下,系统缺少了显示设备、键盘或鼠标。
一般来说,服务器(如提供Web服务的主机)往往可能缺少前述设备,但又需要使用他们提供的功能,生成相应的数据,以提供给应用程序,无头模式就有了用武之地。
无头模式只是不显示出来,图片、视频等资源都会正常下载!!!
在Chrome96.0.4664.110上,添加--remote-debugging-port=9991
并不生效,需要增加--headless
才可以正常运行,具体命令如下:
"C:\Program Files\Google\Chrome\Application\chrome.exe" "https://www.baidu.com" --remote-debugging-port=9991 --headless
通过浏览器打开网页http://localhost:9991/
,就可以访问无头浏览器了。
此时,任意页面访问http://127.0.0.1:9991/json/new?http://www.csdn.com
,再次刷新http://localhost:9991/
页面,可以看到已经创建了新的Tab页面。
先上一张源码结构图,就两个类ChromeInterface
和GenericElement
。
git clone https://github.com/marty90/PyChromeDevTools
pip install PyChromeDevTools
def test_PyChromeDevTools(): import PyChromeDevTools # 初始化cdp(默认连接第一个tab页面) chrome = PyChromeDevTools.ChromeInterface(port=9991) # 打印所有tab页面的url和标题 for tab in chrome.tabs: print(tab['url'], tab['title']) # 这里以CSDN主页为例,分析Page中的Frame # result是cdp协议解析返回的json字符串解析后的dict对象 result, messages = chrome.Page.getFrameTree() print(f'指令ID: {result.get("id")}') frameTree = result.get('result').get('frameTree') # 顶部frame frame_top = frameTree.get('frame') print('id: {}, parentId: {}, url: {}'.format( frame_top.get('id', ''), frame_top.get('parentId', ''), frame_top.get('url', '') )) # 子frame列表信息 frame_children = frameTree.get('childFrames') for child in frame_children: child = child.get('frame') print('\tid: {}, parentId: {}, url: {}'.format( child.get('id', ''), child.get('parentId', ''), child.get('url', '') ))
运行结果:
PyChromeDevTools通过构造函数ChromeInterface创建对象即可完成接口对象初始化。
chrome = PyChromeDevTools.ChromeInterface(port=9991)
其调用堆栈过程如下:
- 构造函数
所有参数都有默认值,默认情况会连接第0个tab页面(第0个对应的是最后创建的Tab页面)。
构造函数中只是对各个参数赋值给对象自身,然后调用connect方法:self.connect(tab=tab)
。
- connect方法分析
def connect(self, tab=0, update_tabs=True): # 调用get_tabs,初始化self.tabs if update_tabs or self.tabs is None: self.get_tabs() # 获取第tab个标签页面的webSocket调试URL # /devtools/inspector.html?ws=localhost:9991/devtools/page/4873CA17C4E484B54405D71AAE7BDC84 wsurl = self.tabs[tab]['webSocketDebuggerUrl'] # 关闭之前的连接 self.close() # 连接新的websocket self.ws = websocket.create_connection(wsurl) self.ws.settimeout(self.timeout)
- get_tabs方法分析
def get_tabs(self): # 其实就是通过requests库,请求了接口`http://localhost:9991/json` response = requests.get(f'http://{self.host}:{self.port}/json') self.tabs = json.loads(response.text)
chrome.Page
方法分析
class ChromeInterface(object): # 当访问object不存在的属性时会调用该方法 def __getattr__(self, attr): genericelement = GenericElement(attr, self) # 将genericelement设置为对象属性 self.__setattr__(attr, genericelement) return genericelement
chrome.Page.getFrameTree
方法分析
class GenericElement(object): def __init__(self, name, parent): self.name = name self.parent = parent def __getattr__(self, attr): func_name = '{}.{}'.format(self.name, attr) def generic_function(**args): # 清除所有message self.parent.pop_messages() # 消息ID递增 self.parent.message_counter += 1 message_id = self.parent.message_counter # 消息ID递增 call_obj = {'id': message_id, 'method': func_name, 'params': args} self.parent.ws.send(json.dumps(call_obj)) # 解析cdp结果并返回数据 result, messages = self.parent.wait_result(message_id) return result, messages # 返回一个函数 return generic_function
- 关于元素是否有某成员的思考
一个类定义了__getattr__
后就能通过.
进行对象的访问了,本项目中,在__getattr__
方法中执行了__setattr__
方法,使得获取元素过程中就将元素设置到了对象上面。
其实本项目每次都会执行大量的对象赋值操作,完全没必要。那么怎么解决呢?
getattr
获取对象属性的时候,每次都返回True。已经无法满足我们的需求了dir
和in
检测是否有元素,对于本项目,就是执行'Page' in dir(chrome)
语句判断是否存在元素Page。ps: 当访问object不存在的属性时会调用
__getattr__
方法,也就是说,调用执行chrome.Page
50次,也只调用一次__getattr__
方法
http://127.0.0.1:9991/json
获取所有页面及websocket的URLmethod
:
GenericElement
这个类GenericElement
这个类的属性__getattr__
**ps:**文章中内容仅用于技术交流,请勿用于违规违法行为。