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、知乎等平台。
如果你有任何问题或想法,欢迎在评论区交流!

相关推荐
天衍四九-4 小时前
第一章:从 LLM 到 Agent —— DeepSeek Harness 入门
网络·数据库·人工智能·python
529宝宝起名网4 小时前
用 Python 实现名字寓意评分算法:基于 NLP 语义分析的名字内涵深度评估
python·算法·自然语言处理
2601_962298674 小时前
Python:对象缓存优化机制(CPython)
python·性能优化·内存优化·cpython·对象缓存
枫彩5 小时前
A股数据源怎么选?用 Python 验收 AI 复盘的日期与明细
python·ai agent·股票数据·mcp
2601_962382435 小时前
Python 代码不可不知的函数式编程技术
python·函数式编程·高阶函数·嵌套函数·头等函数
2601_962078035 小时前
Python办公自动化与数据分析实战 培训班2021年
python·数据分析·办公自动化·自动化办公·实战演练
用户938515635076 小时前
Text2SQL 实战:用 DeepSeek 大模型实现自然语言查询 SQLite 数据库
python·sqlite
2601_962099686 小时前
Python环境下中文分词实现与应用探索
python·自然语言处理·中文分词·jieba·开源社区
2601_966949656 小时前
批量获取多只股票五档盘口的极简方案:QuantDash Python SDK 实战指南
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
用户019027581616 小时前
如何用 Python 监控 A 股涨停跌停与封板/炸板?
python