Playwright03-CDP/WebSocket/PlayWright

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 写法;它更短、更安全、也不会在任务结束后留下僵尸进程。


相关推荐
森叶1 小时前
手搓一个 Windows 注册表清理器:从开发到 EXE 打包全流程
windows·python
骚戴1 小时前
大语言模型(LLM)进阶:从闭源大模型 API 到开源大模型本地部署,四种接入路径全解析
java·人工智能·python·语言模型·自然语言处理·llm·开源大模型
柒壹漆1 小时前
用Python制作一个USB Hid设备数据收发测试工具
开发语言·git·python
东哥很忙XH1 小时前
python使用PyQt5开发桌面端串口通信
开发语言·驱动开发·python·qt
Dxy12393102162 小时前
Python的正则表达式入门:从小白到能手
服务器·python·正则表达式
艾上编程2 小时前
第三章——爬虫工具场景之Python爬虫实战:行业资讯爬取与存储,抢占信息先机
开发语言·爬虫·python
Pyeako2 小时前
网络爬虫相关操作--selenium库(超详细版)
爬虫·python·selenium
dagouaofei2 小时前
全面整理6款文档生成PPT工具,PDF转PPT不再难
python·pdf·powerpoint
β添砖java2 小时前
python第一阶段第10章
开发语言·python
伊玛目的门徒2 小时前
HTTP SSE 流式响应处理:调用腾讯 智能应用开发平台ADP智能体的 API
python·网络协议·http·腾讯智能体·adp·智能应用开发平台