Playwright03-CDP/WebSocket/PlayWright
playwright自动化开发记录,学习BrowserUse的时候涉及到playwright知识点

1-CDP/WebSocket/PlayWright对比
我在看源码的时候,最开始使用简单demo进行学习的时候还好,但是一复杂起来,我就理不清CDP/WebSocket/PlayWright这三者之间的关系,其实底层还是我不知道,我只是在照猫画虎,我不理解这件事情的内核
总结:
1)browser_playwright = p.chromium.launch
2)browser_ws = p.chromium.connect_over_cdp
1-PlayWright简单示例
1-PlayWright打开网址
总结:【sync_playwright()】代表真正的【浏览器操作对象】,这个【浏览器操作对象】,使用了【chromium】浏览器进行打开,并访问了【baidu网址】
1)再递进一步理解:如果我不使用【sync_playwright()】,还可以打开浏览器吗?--->OfCourseNot!2)再递进一步理解:我直接使用webSocket进行链接chrome可以直接访问吗?--->OfCourseNot!浏览器都没有打开,哪来的webSocket的地址,你又怎么连接上去?
3)再递进一步理解:那就是说即使我用webSocket去链接,背后也必须有一个playwright在运行!
python
import time
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# 1. 启动浏览器(默认 headless=True,想看画面可传 headless=False)
browser_playwright = p.chromium.launch(headless=False)
# 2. 新建标签页
page = browser_playwright.new_page()
# 3. 打开目标网址
web_url = "https://www.baidu.com/"
page.goto(web_url, timeout=60000)
print("浏览器 成功打开浏览器:", web_url)
# 4. 简单等待,方便肉眼观察
time.sleep(3)
# 5. 关闭浏览器
browser.close()
2-webSocket通讯-Http版本
和webSocket的版本返回的结果是一样的
python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# 使用Http方式连接上Chrome进行调试
browser_ws = p.chromium.connect_over_cdp("http://localhost:9222")
browser_contexts = browser_ws.contexts[0]
print("=======browser_contexts响应数据结构========")
print(browser_contexts)
print("=======browser_contexts响应数据结构========\n")
default_ctx_page = browser_contexts.pages[0] # 默认上下文里已有的页面
print("默认页面标题:", default_ctx_page.title())
-
通讯结果
=======browser_contexts响应数据结构========
<BrowserContext browser=<Browser type=<BrowserType name=chromium executable_path=/Users/rong/Library/Caches/ms-playwright/chromium-1200/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing> version=143.0.7499.4>>
=======browser_contexts响应数据结构========默认页面标题: 百度一下,你就知道
3-webSocket通讯-WS版本
和http的版本返回的结果是一样的
python
import requests
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# 1-获取 WebSocket URL
resp = requests.get("http://localhost:9222/json/version")
ws_url = resp.json()["webSocketDebuggerUrl"]
# 2-使用WebSocket方式连接上Chrome进行调试
browser_info_ws = p.chromium.connect_over_cdp(ws_url)
browser_contexts = browser_info_ws.contexts[0]
print("=======browser_contexts响应数据结构========")
print(browser_contexts)
print("=======browser_contexts响应数据结构========\n")
default_ctx_page = browser_contexts.pages[0] # 默认上下文里已有的页面
print("默认页面标题:", default_ctx_page.title())
- 响应结果
python
=======browser_contexts响应数据结构========
<BrowserContext browser=<Browser type=<BrowserType name=chromium executable_path=/Users/rong/Library/Caches/ms-playwright/chromium-1200/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing> version=143.0.7499.4>>
=======browser_contexts响应数据结构========
默认页面标题: 百度一下,你就知道
2-WebSocket简单示例
1-playWright浏览器操作
为了把【playWright浏览器操作】和【webSocket通讯】拆开,我开两个进程进行演示
打开debug模式,并让浏览器打开网页后就一直不关闭,浏览器始终不关闭,这样就可以用另一个进程去测试webSocket的连接通讯
python
import time
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# 1. 启动浏览器,并强制开启远程调试端口
browser_playwright = p.chromium.launch(
headless=False,
args=["--remote-debugging-port=9222"] # 开启 CDP 端口
)
# 2. 新建标签页
page = browser_playwright.new_page()
# 3. 打开目标网址
web_url = "https://www.baidu.com/"
page.goto(web_url, timeout=60000)
print("浏览器 成功打开浏览器:", web_url)
# 4. 简单等待,方便肉眼观察
time.sleep(30000000)
# 5. 关闭浏览器
# browser.close()
2-webSocket纯血版-通讯
此时使用的就是纯血版本的webSocket的工具类,和chrome进行通讯,操作都是基于命令行的那种
python
# 1. 获取 WebSocket URL
import requests
from playwright_websocket_util import PlaywrightWebSocketUtil
resp = requests.get("http://localhost:9222/json/version")
ws_url = resp.json()["webSocketDebuggerUrl"]
# 2. 创建 WebSocket 工具实例
ws_util = PlaywrightWebSocketUtil(ws_url)
# 3. 连接到浏览器
if ws_util.connect():
# 4. 发送 CDP 命令-视图对象(命令行操作)
browser_info_ws = ws_util.get_browser_info()
print(browser_info_ws)
# 5. 获取所有目标-视图对象(命令行操作)
targets = ws_util.get_targets()
print(targets)
# 6. 断开连接
ws_util.disconnect()
3-webSocket纯血版-连接代码
使用原生的webSocket连接,以后就没有办法返回【Broswer】或者【Pages】这种可视化的数据对象了,只能通用命令行返回请求的通讯对象
python
async def _connect_and_listen(self):
"""
连接并持续监听消息
"""
try:
# 注意:这是使用的是原生的websocket进行网络连接
self.websocket = await websockets.connect(self.ws_url)
self.connected = True
print(f"成功连接到 WebSocket: {self.ws_url}")
# 持续监听消息
await self._listen_for_messages()
except Exception as e:
print(f"连接 WebSocket 失败: {e}")
self.connected = False
4-能不能全都要
可以,那就是我封装一个工具类
1)即可以使用使用webSocket进行连接->绕过一些验证码之类的操作
2)又可以直接操作浏览器,并返回Browser和Page等可视化视图对象

1-使用websocket进行链接
可以【web_util】进行命令行操作
python
import json
from playwright_websocket_util import PlaywrightCDPConnector
# 使用上下文管理器
with PlaywrightCDPConnector(debug_port=9222) as cdp_util:
# 通过 WebSocket 连接
if cdp_util.connect_via_websocket():
browser_ws = cdp_util.get_browser_info_via_websocket()
print(browser_ws)
# 2-websocket(命令行操作)
ws_util = cdp_util.ws_util
# 获取浏览器信息
browser_info_ws = ws_util.get_browser_info()
print("浏览器信息:", json.dumps(browser_info_ws.get('result', browser_info_ws), indent=2, ensure_ascii=False))
# 获取所有目标
targets = ws_util.get_targets()
print("可用目标数量:", len(targets.get('result', {}).get('targetInfos', [])))
# 创建新页面
new_target = ws_util.create_target("https://www.baidu.com")
print("创建新页面结果:", json.dumps(new_target, indent=2, ensure_ascii=False))
2-使用playwright进行链接
可以拿到【Browser对象】对象
python
with PlaywrightCDPConnector(debug_port=9222) as cdp_util:
# 1-通过 PlayWright 连接(同时具备websocket和playwright两种能力)
if cdp_util.connect_via_playwright():
# 1.1-playwright直接获取视图对象(对象操作)->会创建一个新的Chrome浏览器
browser_playwright = cdp_util.browser
print(browser_playwright)
# 1.2-playwright直接获取视图对象(对象操作)
new_page = browser_playwright.new_page()
new_page.goto("https://www.json.cn/", timeout=60000)
3-工具类代码
python
import asyncio
import json
import websockets
from typing import Optional, Dict, Any, Callable
import threading
import time
import queue
from playwright.sync_api import sync_playwright, Browser, BrowserContext, Page
class PlaywrightWebSocketUtil:
"""
Playwright WebSocket 工具类,用于通过 WebSocket 直接连接和操作浏览器
"""
def __init__(self, ws_url: str):
self.ws_url = ws_url
self.websocket: Optional[websockets.WebSocketClientProtocol] = None
self.message_id = 0
self.loop = None
self.thread = None
self.connected = False
self.response_futures = {}
self.event_handlers = {}
def connect(self) -> bool:
"""
在新线程中连接到 WebSocket 服务器
"""
def run_loop():
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.loop.run_until_complete(self._connect_and_listen())
self.thread = threading.Thread(target=run_loop, daemon=True)
self.thread.start()
# 等待连接完成
for _ in range(50): # 最多等待5秒
if self.connected:
return True
time.sleep(0.1)
return False
async def _connect_and_listen(self):
"""
连接并持续监听消息
"""
try:
# 这是使用的是原生的websocket进行网络连接
self.websocket = await websockets.connect(self.ws_url)
self.connected = True
print(f"成功连接到 WebSocket: {self.ws_url}")
# 持续监听消息
await self._listen_for_messages()
except Exception as e:
print(f"连接 WebSocket 失败: {e}")
self.connected = False
async def _listen_for_messages(self):
"""
持续监听 WebSocket 消息
"""
try:
while True:
response = await self.websocket.recv()
response_data = json.loads(response)
# 检查是否是某个请求的响应
msg_id = response_data.get("id")
if msg_id and msg_id in self.response_futures:
future = self.response_futures.pop(msg_id)
self.loop.call_soon_threadsafe(future.set_result, response_data)
else:
# 处理事件消息
await self._handle_event_message(response_data)
except websockets.exceptions.ConnectionClosed:
print("WebSocket 连接已关闭")
self.connected = False
except Exception as e:
print(f"监听消息时出错: {e}")
self.connected = False
async def _handle_event_message(self, message: dict):
"""
处理事件消息
Args:
message: 收到的消息
"""
method = message.get("method")
if method and method in self.event_handlers:
handler = self.event_handlers[method]
if asyncio.iscoroutinefunction(handler):
await handler(message)
else:
handler(message)
def register_event_handler(self, method: str, handler: Callable):
"""
注册事件处理器
Args:
method: 事件方法名
handler: 处理函数
"""
self.event_handlers[method] = handler
def disconnect(self):
"""
断开 WebSocket 连接
"""
if self.websocket and self.loop and self.connected:
asyncio.run_coroutine_threadsafe(self.websocket.close(), self.loop)
self.connected = False
def send_command(self, method: str, params: Dict[str, Any] = None, session_id: str = None, timeout: float = 5.0) -> Dict[str, Any]:
"""
发送命令到 WebSocket 服务器
Args:
method: CDP 方法名
params: 参数字典
session_id: 会话ID(用于特定目标的命令)
timeout: 超时时间(秒)
Returns:
响应结果
"""
if not self.connected or not self.websocket or not self.loop:
raise Exception("WebSocket未连接")
self.message_id += 1
message = {
"id": self.message_id,
"method": method
}
if params:
message["params"] = params
if session_id:
message["sessionId"] = session_id
# 创建 Future 对象用于接收响应
future = self.loop.create_future()
self.response_futures[self.message_id] = future
# 发送消息
asyncio.run_coroutine_threadsafe(self.websocket.send(json.dumps(message)), self.loop)
# 等待响应
try:
# 使用 asyncio.wait_for 来处理超时
result = asyncio.run_coroutine_threadsafe(
asyncio.wait_for(future, timeout),
self.loop
)
return result.result()
except Exception as e:
print(f"等待响应失败: {e}")
if self.message_id in self.response_futures:
del self.response_futures[self.message_id]
return {"error": str(e)}
def get_browser_info(self) -> Dict[str, Any]:
"""
获取浏览器信息
Returns:
浏览器版本信息
"""
return self.send_command("Browser.getVersion")
def get_targets(self) -> Dict[str, Any]:
"""
获取所有目标(页面、iframe等)
Returns:
目标列表
"""
return self.send_command("Target.getTargets")
def create_target(self, url: str = "about:blank") -> Dict[str, Any]:
"""
创建新目标(新页面)
Args:
url: 页面URL
Returns:
创建的目标信息
"""
return self.send_command("Target.createTarget", {"url": url})
def attach_to_target(self, target_id: str) -> Dict[str, Any]:
"""
附加到目标
Args:
target_id: 目标ID
Returns:
附加结果
"""
return self.send_command("Target.attachToTarget", {"targetId": target_id, "flatten": True})
def navigate_to_url(self, target_id: str, url: str) -> Dict[str, Any]:
"""
导航到指定URL
Args:
target_id: 目标ID
url: 目标URL
Returns:
导航结果
"""
# 首先需要启用Page域
self.send_command("Page.enable")
# 然后导航到指定URL
return self.send_command("Page.navigate", {"url": url})
def close_target(self, target_id: str) -> Dict[str, Any]:
"""
关闭目标
Args:
target_id: 目标ID
Returns:
关闭结果
"""
return self.send_command("Target.closeTarget", {"targetId": target_id})
class PlaywrightCDPConnector:
"""
Playwright CDP 连接器,结合 Playwright 和 WebSocket 工具
"""
def __init__(self, debug_port: int = 9222):
self.debug_port = debug_port
self.ws_url = None
self.playwright = None
self.browser = None
self.ws_util = None
def get_websocket_url(self) -> str:
"""
获取 WebSocket 调试 URL
Returns:
WebSocket URL
"""
import requests
try:
resp = requests.get(f"http://localhost:{self.debug_port}/json/version", timeout=5)
self.ws_url = resp.json()["webSocketDebuggerUrl"]
return self.ws_url
except Exception as e:
raise Exception(f"无法获取 WebSocket URL: {e}")
def connect_via_websocket(self) -> bool:
"""
通过 WebSocket 连接到浏览器
Returns:
连接是否成功
"""
if not self.ws_url:
self.get_websocket_url()
# 使用原生的WebSocket进行连接
self.ws_util = PlaywrightWebSocketUtil(self.ws_url)
return self.ws_util.connect()
def connect_via_playwright(self) -> Browser:
"""
通过 Playwright 连接到浏览器
Returns:
Playwright Browser 实例
"""
if not self.ws_url:
self.get_websocket_url()
# 使用playWright的【playwright.chromium.connect_over_cdp(ws_url)】进行链接
if not self.playwright:
self.playwright = sync_playwright().start()
self.browser = self.playwright.chromium.connect_over_cdp(self.ws_url)
return self.browser
def get_browser_info_via_websocket(self) -> Dict[str, Any]:
"""
通过 WebSocket 获取浏览器信息
Returns:
浏览器信息
"""
if not self.ws_util:
raise Exception("WebSocket 未连接")
return self.ws_util.get_browser_info()
def get_targets_via_websocket(self) -> Dict[str, Any]:
"""
通过 WebSocket 获取所有目标
Returns:
目标列表
"""
if not self.ws_util:
raise Exception("WebSocket 未连接")
return self.ws_util.get_targets()
def create_target_via_websocket(self, url: str = "about:blank") -> Dict[str, Any]:
"""
通过 WebSocket 创建新目标
Args:
url: 页面URL
Returns:
创建的目标信息
"""
if not self.ws_util:
raise Exception("WebSocket 未连接")
return self.ws_util.create_target(url)
def attach_to_target_via_websocket(self, target_id: str) -> Dict[str, Any]:
"""
通过 WebSocket 附加到目标
Args:
target_id: 目标ID
Returns:
附加结果
"""
if not self.ws_util:
raise Exception("WebSocket 未连接")
return self.ws_util.attach_to_target(target_id)
def navigate_to_url_via_websocket(self, target_id: str, url: str) -> Dict[str, Any]:
"""
通过 WebSocket 导航到指定URL
Args:
target_id: 目标ID
url: 目标URL
Returns:
导航结果
"""
if not self.ws_util:
raise Exception("WebSocket 未连接")
return self.ws_util.navigate_to_url(target_id, url)
def close_target_via_websocket(self, target_id: str) -> Dict[str, Any]:
"""
通过 WebSocket 关闭目标
Args:
target_id: 目标ID
Returns:
关闭结果
"""
if not self.ws_util:
raise Exception("WebSocket 未连接")
return self.ws_util.close_target(target_id)
def disconnect_all(self):
"""
断开所有连接
"""
if self.ws_util:
self.ws_util.disconnect()
if self.browser:
self.browser.close()
if self.playwright:
self.playwright.stop()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.disconnect_all()
2-with资源初始化对比
- 对比以下两种书写方式
python
# 调用方式一:使用with进行资源初始化
with PlaywrightCDPConnector(debug_port=9222) as cdp_util:
# 通过 PlayWright 连接
if cdp_util.connect_via_playwright():
browser_info = cdp_util.browser
print(browser_info)
default_page = browser_info.contexts[0].pages[0]
print("默认页面标题:", default_page.title())
- 对比下面的写法
python
# 调用方式2:直接new对象进行使用
cdp_util = PlaywrightCDPConnector(debug_port=9222)
# 通过 PlayWright 连接
if cdp_util.connect_via_playwright():
browser_info = cdp_util.browser
print(browser_info)
default_page = browser_info.contexts[0].pages[0]
print("默认页面标题:", default_page.title())
两种写法在"能不能跑通"层面没有区别,最终都能拿到同一颗浏览器实例。
真正的差异只有一点:第一种写法把 PlaywrightCDPConnector 放进了 with 语句,第二种写法没有。
1. 资源释放
- with 写法 :
__exit__会被自动调用,Playwright 的浏览器进程、CDP 连接、临时文件等会被干净地关闭,即使中间抛异常也能保证回收。 - 手动写法 :不会自动关闭浏览器。脚本跑完后进程还挂在系统里,长期运行会吃光内存;如果中间抛异常,资源泄漏更明显。
想避免泄漏就得自己再包一层try/finally手动调cdp_util.close(),代码量反而更多。
2. 可读性 / 习惯
with是 Python 推荐的"资源获取即初始化"模式,一眼就能看出"这段代码结束就把连接器关掉"。- 手动写法容易忘记收尾,尤其多人协作或脚本里穿插其他逻辑时,维护成本更高。
3. 异常安全
with块里任何地方 raise 出来,都会先执行__exit__,浏览器不会留下僵尸。- 手动写法一旦异常跳出
if分支,后面的close()根本没机会执行。
4. 复用场景
如果你故意 想让浏览器常驻(比如调试时人工反复操作),第二种写法反而更方便------对象一直存活,你可以在后面继续调它的方法。但生产代码里这种"故意不关闭"的情况极少,真需要常驻也最好显式写成全局单例并自己管理生命周期,而不是靠"忘写 close()"来达成。
结论
除非你有明确理由要"保持浏览器一直开着",否则优先用第一种 with 写法;它更短、更安全、也不会在任务结束后留下僵尸进程。