Playwright理解与封装

前言

之前对 Playwright 一直是懵懵懂懂的状态,用起来总觉得隔着一层纱。最近深入学习后,基本掌握了其主要概念,同时也基于自己的理解做了一个封装库。本文将分享我对 Playwright 的理解,以及如何将其封装成一个可以像普通浏览器一样使用的自动化工具。


一、Playwright 核心概念:一个生动的比喻

很多人初次接触 Playwright 时,会被 PlaywrightBrowserContextPage 这些概念搞晕。让我用一个生动的比喻来解释:

🎯 核心比喻

概念 比喻 说明
Playwright 司机 整个自动化过程的控制者,负责启动和管理整个流程
Browser 车辆 实际的浏览器实例,可以是 Chrome、Firefox 或 WebKit
Context 车厢 独立的浏览器会话,拥有自己的 cookies、storage 和上下文
Page 窗户 每个标签页或窗口,用户通过它与网页交互

关键理解

  • 一列火车可以有多节车厢(多个 Context)
  • 每节车厢可以有多个窗户(多个 Page)
  • 只有司机(Playwright)能启动和停止整列火车

二、资源管理:必须关闭 Browser

这是很多初学者容易踩的坑:如果不关闭 Browser,会有进程残留!

⚠️ 正确关闭顺序

python 复制代码
# ✅ 正确做法:按顺序关闭
page.close()      # 关闭页面(可选)
context.close()   # 关闭上下文
browser.close()   # 关闭浏览器 ← 必须!
playwright.stop()  # 停止 Playwright

💡 特殊情况:永久 Context

如果使用永久上下文(Persistent Context),情况会简单一些:

python 复制代码
# 永久上下文会自动管理,只需要关闭它即可
context = playwright.chromium.launch_persistent_context(...)
# 使用完后:
context.close()  # 关闭后,browser 也会自动关闭

这是因为永久上下文本质上是一个完整的浏览器实例,关闭它就相当于关闭了整个浏览器。


三、我对 Playwright 的封装

🎯 封装目的

使用 requests 库时,有些网站需要运行 JavaScript 才能正常获取内容。传统的 Playwright 使用方式需要频繁创建和销毁浏览器,体验不够流畅。

我封装了一个 单例模式 的 Playwright 工具,实现:

  • ✅ 永久上下文,像普通浏览器一样使用
  • ✅ 自动管理下载
  • ✅ 单例模式,全局唯一实例
  • ✅ 自动释放资源,防止进程残留

📦 完整代码

python 复制代码
# myplaywright.py
import platform
import atexit
from pathlib import Path
from typing import Optional
from playwright.sync_api import sync_playwright, Playwright, BrowserContext, Page, Download

class MyPlaywright:
    '''自定义Playwright单例类'''
    _instance: Optional['MyPlaywright'] = None
    _playwright: Optional[Playwright] = None
    _context: Optional[BrowserContext] = None
    _page: Optional[Page] = None
    _is_initialized: bool = False

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        if self._is_initialized: return
        self._is_initialized = True
        
        self._base_dir = Path.cwd()
        self._chromium_data_dir = self._base_dir / "chromium_data"
        self._downloads_dir = self._base_dir / "downloads"
        self._chromium_data_dir.mkdir(exist_ok=True)
        self._downloads_dir.mkdir(exist_ok=True)
        self._is_windows = platform.system() == "Windows"
        
        atexit.register(self.release)  # 注册退出时释放资源

    def _get_headless(self) -> bool:
        '''Windows 显示浏览器,Linux 无头模式'''
        return not self._is_windows

    @property
    def playwright(self) -> Playwright:
        if self._playwright is None:
            self._playwright = sync_playwright().start()
        return self._playwright

    @property
    def context(self) -> BrowserContext:
        if self._context is None:
            self.reset_context()
        return self._context

    def reset_context(self, headless: Optional[bool] = None) -> BrowserContext:
        '''重置浏览器上下文'''
        if self._context:
            self._context.close()
        
        is_headless = headless if headless is not None else self._get_headless()
        
        self._context = self.playwright.chromium.launch_persistent_context(
            user_data_dir=self._chromium_data_dir,
            headless=is_headless,
            bypass_csp=True,  # 跳过内容安全策略
            args=['--start-maximized'],
            no_viewport=True,
            accept_downloads=True,
            downloads_path=self._downloads_dir,
        )
        return self._context

    @property
    def page(self) -> Page:
        '''获取页面单实例'''
        if self._page is None:
            self._page = self.context.pages[0]
            self._page.on('download', self._on_download)
        return self._page

    def _on_download(self, download: Download):
        '''下载事件处理'''
        print(f"开始下载 {download.suggested_filename}...")
        download_path = self._downloads_dir / download.suggested_filename
        download.save_as(download_path)
        print(f"下载完成: {download_path}")

    def release(self):
        '''释放资源'''
        try:
            if self._context:
                self._context.close()
            if self._playwright:
                self._playwright.stop()
            self._context = None
            self._playwright = None
        except Exception:
            pass

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.release()

🚀 使用示例

python 复制代码
# 方式1:上下文管理器(推荐)
with MyPlaywright() as pw:
    pw.page.goto("https://www.baidu.com")
    print(pw.page.title())
    # 退出时自动释放资源

# 方式2:直接使用
pw = MyPlaywright()
pw.page.goto("https://example.com")
# 程序退出时自动释放资源

✨ 封装亮点

特性 说明
单例模式 全局只有一个实例,避免重复启动浏览器
永久上下文 浏览器状态持久化,cookies 不会丢失
自动下载 监听下载事件,自动保存文件
跨平台 Windows 显示浏览器,Linux 自动无头
资源托管 使用 atexit 确保程序退出时释放资源

四、总结

🎯 学习要点

  1. 理解层次关系:Playwright → Browser → Context → Page
  2. 比喻记忆:司机、火车、车厢、窗户
  3. 资源管理:必须关闭 Browser,永久 Context 只需关闭自身
  4. 实际应用:封装成单例模式,打造永久化的自动化浏览器

📚 参考资料


本文为本人原创,首发于掘金,同步发布于 CSDN、知乎等平台。
如果你有任何问题或想法,欢迎在评论区交流!

相关推荐
zhangchaoxies1 小时前
MySQL触发器能否监控特定用户操作_结合审计功能实现分析
jvm·数据库·python
qq_413502022 小时前
如何解决ORA-12518监听程序无法分配进程_内存耗尽与PGA溢出
jvm·数据库·python
zhangrelay2 小时前
三分钟云课实践速通--大学物理--python 版
linux·开发语言·python·学习·ubuntu·lubuntu
djjdjdjdjjdj2 小时前
如何用参数解构在函数入口处直接提取对象属性
jvm·数据库·python
forEverPlume3 小时前
mysql如何批量增加表的字段_脚本化DDL操作实践
jvm·数据库·python
asdzx673 小时前
使用 Python 读取 PDF: 提取文本和图片
开发语言·python·pdf
m0_596406373 小时前
CSS如何高效引入样式表_对比link标签与import指令的性能差异
jvm·数据库·python
南宫萧幕3 小时前
HEV 智能能量管理实战:从 MPC/PPO 理论解析到 Python-Simulink 联合仿真闭环全流程
开发语言·python·算法·matlab·控制
码农的神经元3 小时前
Python 实现县域变电站智能巡检与抢修调度:地图、路径规划与恢复策略
开发语言·python