跳转至

浏览器渲染

需要:

pip install "netspy[render]"
playwright install chromium

停止时排队中的请求会失败,而不是挂住

渲染是串行的(pool_size 个浏览器轮流处理),停止爬虫时队列里往往还排着请求。 这些请求会被当场收尾成失败,调用方拿到 RequestError —— 重试用尽后落进 failed_requests.jsonlnetspy retry --requests 只是探活(重新下载看状态码,不跑回调、不入库);要把数据真正抓回来,开 RETRY_FAILED_ON_START 重跑一次爬虫。

早先的版本不收尾它们:submit() 的等待没有上限,而 close() 不排空队列, 于是那些调用线程永远醒不来。实测 1 个在渲染、4 个排队时关闭, 4 个线程永久挂起 —— stop() 它不看、join() 它不动,优雅停止就此失效。

用法

yield mw.Request(
    "https://spa.example.com/list",
    render=True,
    wait_for="ul.items li",          # 等这个选择器出现再取内容
    render_time=1.0,                 # 之后再等 1 秒
    render_script=lambda page: page.click("button.load-more"),  # 在浏览器线程执行
    callback=self.parse,
)

response.text 是渲染后的完整 DOM,.xpath / .css 照常用。

配置

setting.py

WEBDRIVER = dict(
    pool_size=2,          # 并发浏览器数
    browser="chromium",   # chromium | firefox | webkit
    headless=True,
    load_images=False,    # 拦截图片 / 字体 / 媒体,加速
    timeout=30,           # 秒
    wait_until="domcontentloaded",
    proxy=None,
    stealth=True,         # 注入基础反检测脚本
    viewport=[1920, 1080],
    channel=None,         # 如 "chrome":用系统里真实安装的 Chrome 而非 Playwright 自带的 Chromium
    locale=None,          # 如 "zh-CN"
    timezone_id=None,     # 如 "Asia/Shanghai"
    engine="playwright",  # playwright | patchright
    user_data_dir=None,   # 如 "./.netspy_profiles":设置后启用持久化 profile
)

channel / locale / timezone_id 都是 Playwright new_context() 原生支持的参数,不需要额外依赖。 未设置时行为跟以前完全一样。channel="chrome" 需要本机装了对应版本的 Chrome(playwright install chrome 或系统自带),UA 版本号、TLS/HTTP2 指纹会更贴近目标站预期的「真实 Chrome」画像,而不是 Playwright 打包的那份 Chromium;启动时也默认带上 --disable-blink-features=AutomationControlled, 关掉最直接的自动化标记(这条无条件生效,没有开关)。

engine="patchright"

pip install "netspy[render-patchright]"

patchright 是打了 CDP 层探测点补丁的 Playwright fork:避免调用 Runtime.enable、去掉默认的 --enable-automation 等参数,堵的是比 playwright-stealth(JS 注入层)更底层的探测面。两者可以同时开——stealth 补的是 navigator.* 这类 JS 可读属性,engine="patchright" 补的是 CDP 协议层面的痕迹,不冲突。

只支持 chromium:配 browser="firefox""webkit" 会在 PlaywrightDownloader 构造期直接抛 ConfigError,不会等到真正渲染时才发现配错了。

不要在同一个进程里混用两种引擎

实测撞到的真冲突:如果同一个 Python 进程里,一个 vanilla Playwright 浏览器和一个 patchright 浏览器同时存活(比如测试里前一个用例的浏览器还没关,下一个用例又用另一个引擎开了一个), page.goto() 会稳定卡满超时。两边各自维护一条到浏览器驱动进程的 asyncio 连接,冲突在两个 独立维护的三方库内部,不是 Netspy 的问题,但会影响你自己写的测试或脚本——一个进程认定用哪个 引擎,不要中途切换。生产部署本来就只会有一个 WEBDRIVER["engine"],不会撞上这个问题。

user_data_dir(长期登录态)

设置后浏览器带着上次的 cookies / localStorage / 历史重新启动,而不是每次都是全新指纹—— 全新的自动化 profile 没有任何历史状态本身也是一种信号,长期跑同一批"账号"时尤其明显。

WEBDRIVER = dict(user_data_dir="./.netspy_profiles")

pool_size 个渲染线程各用一个子目录(user_data_dir/0user_data_dir/1 ……)——Chrome 不允许多个实例共享同一份 profile,共用会互相打架。只有Max-Age / Expires 的持久 cookie才会跨重启存活,会话 cookie(不带这两个属性)在真实浏览器里本来就不会挺过一次完整 重启,user_data_dir 救不了它——这是浏览器自己的行为,跟有没有持久化 profile 无关。

模拟真人交互

from netspy.network.downloader._humanize import human_click, human_scroll, human_type

def script(page):
    human_scroll(page)
    human_click(page, "button.load-more")
    human_type(page, "input#q", "关键词")

yield mw.Request(url, render=True, render_script=script, callback=self.parse)

human_move / human_click / human_type / human_scroll 四个函数:鼠标分步移动 (带抖动,不是单帧瞬移)、逐字符输入(字符间随机延迟,不是 fill() 瞬间灌入)、分段滚动 (带停顿,不是一次性跳到底)。不会自动生效——这是刻意的,自动应用会给所有请求默认加上 不确定的延迟,且用哪个动作、作用在哪个元素上,只有你自己的 render_script 知道。

这几个函数诚实地说不能做到什么:Playwright 读不到鼠标当前位置,human_move 只能假设一个 起点(默认视口中心),不是逐像素复刻真人轨迹的方案。做的是把"单帧瞬移、零间隔批量输入、 一次性跳转滚动"这几个最基础的信号去掉,聊胜于无,不是万能药。

架构

pool_size 个独立渲染线程,每个持有一个 chromium(Playwright 的 sync API 不能跨线程)。 工作线程把渲染任务投进队列并阻塞等结果 —— 所以 pool_size 就是真正的并发浏览器上限, 小于 SPIDER_THREAD_COUNT 时天然形成背压。

导航超时 / 连接失败 → RequestError → 走正常重试逻辑。爬虫结束时自动关闭所有浏览器。