Patchright 深度解析:让自动化浏览器"隐身"的秘密武器

3 阅读10分钟

一、先搞清楚问题:为什么普通 Playwright 会被检测到?

在讲 Patchright 之前,必须先理解它要解决的核心问题。

当你用 Playwright(或 Selenium)控制浏览器时,网站的反爬虫系统有很多方式识别出"这不是真人在操作"。这些识别点业界称为自动化特征(Automation Leaks)

浏览器自动化的"原罪"

正常用户浏览器                   Playwright 控制的浏览器
─────────────────                ───────────────────────────
navigator.webdriver = undefined  navigator.webdriver = true  ← 直接暴露!
无 CDP 协议通信                  存在 Runtime.enable 调用    ← 协议层泄露
正常命令行参数                   --enable-automation          ← Flag 泄露
正常 console 行为                Console.enable 激活          ← 协议泄露
正常 Shadow DOM 访问             特殊方式访问 Shadow Root     ← 行为泄露

以最典型的 navigator.webdriver 为例,在浏览器控制台运行一行 JS 就能知道当前浏览器是否被自动化控制:

// 正常浏览器:undefined
// Playwright 控制的浏览器:true
console.log(navigator.webdriver)

各大平台的风控系统正是利用这些特征来识别并封锁自动化脚本。


二、技术演进史:从 Selenium 到 Patchright

时间线

├── 早期  Selenium + ChromeDriver
         └─ webdriver 特征明显,极易被检测

├── 2020  Playwright 发布(微软)
         └─ 更强大,但同样有自动化特征

├── 2021  puppeteer-extra + stealth 插件(JS 生态)
         └─ 通过注入 JS 脚本绕过部分检测
         └─ stealth.min.js(social-auto-upload 也在用这个)

├── 2022  undetected-chromedriver
         └─ 针对 Selenium,修改 Chrome 二进制

├── 2023  playwright-stealth(Python 社区方案)
         └─ 注入 stealth JS,治标不治本

└── 2024  Patchright 诞生 
          └─ 在源码层面 patch Playwright 本身
          └─ 根治协议层泄露,而非表面打补丁

Patchright 的不同之处在于:它不是在 Playwright 外面套壳,而是直接修改 Playwright 的内部实现,从根本上消除自动化特征。


三、Patchright 核心补丁原理详解

补丁 1:消除 Runtime.enable 泄露(最关键)

这是 Patchright 最核心、最有价值的补丁。

背景知识: Playwright 通过 Chrome DevTools Protocol(CDP)控制浏览器。执行 JavaScript 时,Playwright 默认会调用 Runtime.enable 来启用 JavaScript 运行时,而这个调用本身就是可被检测到的特征。

普通 Playwright 执行 JS 的流程:
─────────────────────────────────────
CDP → Runtime.enable()        ← 网站可以检测到这个调用!
CDP → Runtime.evaluate(...)   ← 执行 JS
CDP → Runtime.disable()

Patchright 执行 JS 的流程:
─────────────────────────────────────
CDP → 在隔离的 ExecutionContext 中执行 JS   ← 无需 Runtime.enable
                                              ← 检测不到任何异常调用

用一个比喻来理解:这就像你去某个地方,普通做法是走正门(会被门卫记录),Patchright 的做法是从一个合法但不被监控的侧门进入。

补丁 2:消除 Console.enable 泄露

Playwright 默认会启用浏览器 Console API 来捕获 console 输出,而 Console.enable 这个 CDP 调用同样是可检测特征。

Patchright 的解决方案:直接禁用 Console API

代价:patchright 控制的浏览器中,console.log() 不会输出任何内容
好处:Console.enable 特征消失,更难被检测

如果你需要调试输出,改用:
const log = (msg) => fetch('/log?msg=' + encodeURIComponent(msg))
// 或者通过 Page.route 捕获网络请求来传递调试信息

补丁 3:修复命令行 Flag 泄露

Playwright 启动 Chrome 时会加入很多"自动化专用"的命令行参数,这些参数本身就是特征。

Playwright 默认添加/保留的危险 Flag:
──────────────────────────────────────
--enable-automation              ← 直接告诉网站"我是自动化"
(缺失)--disable-blink-features=AutomationControlled
                                 ← 没有这个 navigator.webdriver 就是 true

Patchright 的处理:
──────────────────────────────────────
✅ 移除  --enable-automation
✅ 移除  --disable-popup-blocking
✅ 移除  --disable-component-update    ← 这个会暴露是"隐身驱动"
✅ 移除  --disable-default-apps
✅ 移除  --disable-extensions          ← 移除后可正常使用扩展
✅ 添加  --disable-blink-features=AutomationControlled

补丁 4:支持 Closed Shadow Root 访问

现代 Web 组件大量使用 Shadow DOM 隔离内部结构,有些组件使用 mode: 'closed' 完全封闭,普通 Playwright 无法直接操作。

Patchright 对此进行了特殊处理,使得普通的 page.locator() 就能穿透 Closed Shadow Root 访问内部元素,无需任何特殊操作。

# 普通 Playwright 无法操作 closed shadow root 内部的元素
# Patchright 可以透明处理,用法完全一样:
button = page.locator('custom-component >> button.submit')
await button.click()  # 即使 button 在 closed shadow root 里也能用

四、Patchright vs Playwright:对比一览

┌─────────────────────────┬──────────────────────┬──────────────────────┐
│ 特性                    │ Playwright            │ Patchright           │
├─────────────────────────┼──────────────────────┼──────────────────────┤
│ Runtime.enable 调用     │ 有(可被检测)         │ 无(已消除)          │
│ Console.enable 调用     │ 有(可被检测)         │ 无(已禁用)          │
│ navigator.webdriver     │ true(暴露)           │ undefined(隐藏)     │
│ --enable-automation     │ 存在(暴露)           │ 已移除                │
│ Closed Shadow Root      │ 无法直接访问           │ 透明支持              │
│ API 兼容性              │ 原版                  │ 完全兼容(drop-in)   │
│ 支持的浏览器            │ Chromium/Firefox/WebKit│ 仅 Chromium           │
│ console.log() 输出      │ 正常工作               │ 不工作(已禁用)      │
│ 通过 Cloudflare 检测    │ ❌                    │ ✅                    │
│ 通过 fingerprint.com    │ ❌                    │ ✅                    │
│ 通过 CreepJS            │ ❌                    │ ✅(配合正确设置)     │
└─────────────────────────┴──────────────────────┴──────────────────────┘

五、Patchright 在 social-auto-upload 中的使用方式

5.1 导入方式

social-auto-upload 把 Patchright 当作 Playwright 的直接替代品使用,import 时换个包名即可,其余 API 完全一致:

# 旧版(使用 Playwright)
from playwright.async_api import async_playwright, Page, Playwright

# 新版(使用 Patchright,API 完全相同)
from patchright.async_api import async_playwright, Page, Playwright

5.2 浏览器启动配置

以抖音上传器为例,核心启动代码:

# uploader/douyin_uploader/main.py(简化展示)
from patchright.async_api import async_playwright
from utils.base_social_media import set_init_script

async def douyin_cookie_gen(account_file, headless=True):
    async with async_playwright() as playwright:
        # 关键:使用 channel="chrome",指向本地真实 Chrome
        # 而不是 Playwright 内置的 Chromium
        browser = await playwright.chromium.launch(
            headless=headless,
            channel="chrome"
        )
        context = await browser.new_context()
        
        # 额外注入 stealth.min.js(双重保险)
        context = await set_init_script(context)
        
        page = await context.new_page()
        await page.goto("https://creator.douyin.com/")
        # ... 后续登录逻辑

5.3 stealth.min.js 的作用(双重防护)

项目在 utils/ 目录下还保留了 stealth.min.js,通过 set_init_script() 在每个新页面加载前注入:

# utils/base_social_media.py
from pathlib import Path
from conf import BASE_DIR

async def set_init_script(context):
    stealth_js_path = Path(BASE_DIR / "utils/stealth.min.js")
    await context.add_init_script(path=stealth_js_path)
    return context

这是一个 JS 层面的补充防护,与 Patchright 的协议层补丁形成双层防御:

防御体系
──────────────────────────────────────────
层 1(协议层):Patchright
  └─ 消除 CDP 协议特征(Runtime/Console.enable 等)
  └─ 修复命令行 Flag 泄露
  └─ navigator.webdriver = undefined

层 2(JS 注入层):stealth.min.js
  └─ 伪造 navigator.plugins(插件列表)
  └─ 伪造 navigator.languages
  └─ 修复 chrome.runtime 对象
  └─ 修复 WebGL 渲染器信息
  └─ 其他浏览器指纹细节修复

六、实际代码示例:从 Playwright 迁移到 Patchright

基础用法(与 Playwright 完全相同)

import asyncio
from patchright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        # 启动浏览器(推荐使用真实 Chrome,而非 Chromium)
        browser = await p.chromium.launch(
            headless=True,
            channel="chrome"  # 使用本地安装的 Google Chrome
        )
        page = await browser.new_page()
        await page.goto("https://bot.sannysoft.com/")  # 自动化检测网站
        
        # 检测 webdriver 特征
        webdriver = await page.evaluate("navigator.webdriver")
        print(f"navigator.webdriver = {webdriver}")  # 输出: None(而非 true)
        
        await page.screenshot(path="result.png")
        await browser.close()

asyncio.run(main())

最佳实践:使用持久化 Context(最隐蔽)

根据 Patchright 官方推荐,使用 launch_persistent_context 配合真实 Chrome 是目前最接近真人浏览器的方式:

import asyncio
from patchright.async_api import async_playwright
from pathlib import Path

async def stealth_browser_example():
    """
    最佳实践配置:
    1. 使用持久化 Context(保留浏览历史、Cookie 等,更像真人)
    2. channel="chrome" 使用真实 Chrome
    3. no_viewport=True 不固定窗口尺寸(固定尺寸是一个指纹特征)
    4. 不添加自定义 headers 或 user_agent(会引入新特征)
    """
    async with async_playwright() as p:
        context = await p.chromium.launch_persistent_context(
            user_data_dir=Path("./user_data"),  # 持久化用户数据目录
            channel="chrome",
            headless=False,   # 无头模式会有额外特征,有条件建议有头
            no_viewport=True, # 不要固定视口大小!
            # ⚠️ 不要加 user_agent!
            # ⚠️ 不要加自定义 headers!
            args=[
                "--disable-blink-features=AutomationControlled",
            ]
        )
        
        page = await context.new_page()
        await page.goto("https://fingerprintjs.github.io/fingerprintjs/")
        
        # 等待页面加载并获取指纹
        await page.wait_for_timeout(3000)
        result = await page.evaluate("window.fpPromise.then(fp => fp.get())")
        print(f"浏览器指纹 visitorId: {result['visitorId']}")
        
        await context.close()

asyncio.run(stealth_browser_example())

在自动化任务中使用 Cookie 持久化(social-auto-upload 的做法)

import asyncio
import json
from pathlib import Path
from patchright.async_api import async_playwright

async def save_cookies(page, cookie_file: str):
    """保存当前页面的 Cookie 到文件"""
    cookies = await page.context.cookies()
    Path(cookie_file).parent.mkdir(exist_ok=True)
    with open(cookie_file, "w", encoding="utf-8") as f:
        json.dump(cookies, f, ensure_ascii=False, indent=2)
    print(f"✅ Cookie 已保存:{cookie_file}")

async def load_cookies(context, cookie_file: str) -> bool:
    """从文件加载 Cookie 到浏览器 Context"""
    if not Path(cookie_file).exists():
        return False
    with open(cookie_file, "r", encoding="utf-8") as f:
        cookies = json.load(f)
    await context.add_cookies(cookies)
    return True

async def upload_with_saved_cookie(video_path: str, cookie_file: str):
    """使用已保存的 Cookie 上传,无需重复登录"""
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True, channel="chrome")
        context = await browser.new_context()
        
        # 加载已保存的 Cookie(跳过登录)
        cookie_loaded = await load_cookies(context, cookie_file)
        if not cookie_loaded:
            raise RuntimeError("Cookie 文件不存在,请先登录")
        
        page = await context.new_page()
        
        # 注入 stealth 脚本(额外防护)
        await context.add_init_script(path="utils/stealth.min.js")
        
        # 打开创作者平台
        await page.goto("https://creator.douyin.com/")
        
        # 检查是否已登录
        await page.wait_for_timeout(2000)
        if "login" in page.url:
            raise RuntimeError("Cookie 已失效,需要重新登录")
        
        print("✅ Cookie 有效,已登录")
        # ... 后续上传操作
        
        await browser.close()

七、检测能力对比(通过/失败)

以下是 Patchright 在各主流反爬虫检测站点上的表现:

检测站点                  普通 Playwright    Patchright(正确配置)
─────────────────────────────────────────────────────────────────
Cloudflare Bot管理        ❌ 被拦截          ✅ 通过
Kasada(Nike等电商用)    ❌ 被拦截          ✅ 通过
Akamai                    ❌ 被拦截          ✅ 通过
Fingerprint.com           ❌ 被识别          ✅ 通过
CreepJS(综合指纹检测)   ❌ 被识别          ✅ 通过(需CDP补丁)
Sannysoft(基础检测)     ❌ webdriver暴露   ✅ 通过
BrowserScan               ❌ 被识别          ✅ 通过
PixelScan                 ❌ 被识别          ✅ 通过
Datadome(法国反爬)      ❌ 被拦截          ✅ 通过

对于 social-auto-upload 来说,这意味着:抖音、小红书、快手等平台的反爬虫系统更难识别出这是自动化脚本,登录状态更稳定,上传成功率更高。


八、安装与常见问题

安装

# 安装 patchright Python 包
pip install patchright
# 或
uv add patchright

# 安装 Chromium 驱动
patchright install chromium

# 推荐:安装真实 Chrome(更隐蔽)
patchright install chrome

常见问题

Q: 为什么推荐用 channel="chrome" 而不是默认 Chromium?

真实的 Google Chrome 有完整的证书、插件等浏览器特征,而 Patchright 内置的 Chromium 是一个精简版,缺少很多"正常浏览器"应有的特征,反而更容易被识别。

# 不推荐(内置 Chromium,特征更少)
browser = await p.chromium.launch(headless=True)

# 推荐(使用本地真实 Chrome)
browser = await p.chromium.launch(headless=True, channel="chrome")

# 最推荐(持久化 Context + 真实 Chrome)
context = await p.chromium.launch_persistent_context(
    user_data_dir="./profile",
    channel="chrome",
    headless=False,
    no_viewport=True,
)

Q: console.log 为什么不工作了?

这是 Patchright 禁用 Console API 的副作用,是有意为之的设计(Console.enable 是一个检测点)。调试时可以用网络请求代替:

# 在 page.evaluate 里用 fetch 传递调试信息
await page.evaluate("""
    fetch('/debug?msg=' + encodeURIComponent('hello from browser'))
""")

Q: 无头模式(headless=True)下还会被检测到吗?

无头模式本身也有一些额外特征(比如 navigator.userAgent 里不含 HeadlessChrome,但字体渲染、GPU 信息等有差异)。Patchright 尽可能消除这些差异,但 Patchright 官方也建议:条件允许的情况下优先使用有头模式。social-auto-upload 的 --headed 选项就是为此设计的。

Q: Patchright 会一直有效吗?

这是一场猫和老鼠的游戏。随着各平台不断升级反爬虫策略,Patchright 也会持续更新补丁。Patchright 的版本号与 Playwright 保持一致(比如 1.58.2),每次 Playwright 发布新版本,Patchright 都会跟进打补丁。social-auto-upload 的 pyproject.toml 中固定了 patchright==1.58.2,升级时需要留意兼容性。


九、一图总结:Patchright 在整个系统中的位置

┌──────────────────────────────────────────────────────────────────┐
│                     social-auto-upload 防检测体系                 │
└──────────────────────────────────────────────────────────────────┘

你的代码(sau CLI / Python API)
          │
          ▼
┌─────────────────────┐
│   DouYinVideo 等     │  ← 业务逻辑层
│   上传器类           │
└──────────┬──────────┘
           │ 调用
           ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Patchright(协议层防护)                     │
│                                                                 │
│  ✅ Runtime.enable 已消除   ✅ Console.enable 已禁用             │
│  ✅ navigator.webdriver 隐藏  ✅ 危险 Flag 已清理               │
│  ✅ Closed Shadow Root 支持   ✅ 通过 Cloudflare/Kasada/Akamai  │
└──────────────────────────────┬──────────────────────────────────┘
                               │ 注入
                               ▼
                  ┌────────────────────────┐
                  │   stealth.min.js        │  ← JS 层补充防护
                  │   修复浏览器指纹细节    │
                  └────────────┬───────────┘
                               │
                               ▼
                  ┌────────────────────────┐
                  │   Chrome 浏览器         │  ← channel="chrome"
                  │   (真实 Chrome 内核)  │    使用本地安装版本
                  └────────────┬───────────┘
                               │ HTTPS
                               ▼
                  ┌────────────────────────┐
                  │   抖音 / 小红书 / 快手   │  ← 平台风控系统
                  │   等平台服务器          │    已"看不出"是机器人
                  └────────────────────────┘

总结一句话:Patchright = Playwright 的隐身版,它从 CDP 协议层、启动参数层、JS 运行时层三个维度同时入手,让自动化浏览器在各大平台的风控眼中尽可能接近一个真实的人类用户。这也是 social-auto-upload 从老版本 Playwright 迁移到 Patchright 的根本原因——让视频上传任务跑得更稳、更持久、更不容易被封号。