- 网页爬虫
- 浏览器控制
- 测试
【免费下载链接】puppeteer-extra
💯 Teach puppeteer new tricks through plugins.
本文以 packages/puppeteer-extra-plugin-stealth/evasions/_template 目录下的官方模板为起点,讲解如何在 puppeteer-extra 体系中从零编写一个可独立运行的"反检测 evasion 插件"。读完本文,你将掌握最小插件骨架的每一行代码、PuppeteerExtraPlugin基类的生命周期钩子、共享工具库_utils的正确用法,并能参照navigator.webdriver、chrome.app等真实 evasion 的实现,快速复制模板目录并开发出自己的新反检测方案。
模板的本质:一个最小的反检测插件骨架
_template是puppeteer-extra-plugin-stealth官方提供的最小可运行插件模板,官方对其定位非常直白(见 模板 readme):
Minimal stealth plugin template, not being used. :-) Feel free to copy this folder as the basis for additional detection evasion plugins.
翻译过来就是:这个模板本身并不参与实际反检测("not being used"),它的用途是作为"复制即用"的起点——当你发现一个新的 headless 可探测特征、想为 stealth 插件贡献一条新 evasion 时,直接复制整个_template文件夹,改一改类名、插件名和onPageCreated里的注入逻辑即可。这也是 stealth 主插件官方建议的贡献路径:主插件 index.js 的注释明确写着"if you want to add a new evasion technique I suggest you look at the template to kickstart things"。
一个完整的 evasion 插件目录由三部分构成:
| 文件 | 作用 |
|---|---|
index.js | 插件本体,继承PuppeteerExtraPlugin并实现生命周期钩子 |
readme.md | 由documentation.js自动生成的 API 文档(npm run docs会更新它) |
package.json | 声明main: index.js与private: true(见 模板 package.json) |
目录结构可参照真实的navigator.pluginsevasion(navigator.plugins 目录):除了index.js,还按需附带mimeTypes.js、plugins.js、magicArray.js、data.json等拆分文件与配套测试。
模板源码逐行拆解
模板的完整实现位于 evasions/_template/index.js,全文件仅 28 行,结构如下:
'use strict' const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') /** * Minimal stealth plugin template, not being used. :-) * * Feel free to copy this folder as the basis for additional detection evasion plugins. */ class Plugin extends PuppeteerExtraPlugin { constructor(opts = {}) { super(opts) } get name() { return 'stealth/evasions/_template' } async onPageCreated(page) { await page.evaluateOnNewDocument(() => { console.debug('hello world') }) } } module.exports = function(pluginConfig) { return new Plugin(pluginConfig) }第一行:严格模式
'use strict'是 stealth 插件家族所有实现(navigator.webdriver、chrome.app、navigator.plugins等)的统一开头。由于这些代码最终会通过page.evaluateOnNewDocument注入浏览器页面上下文,保持严格模式能避免一些隐蔽的静默错误。
类的骨架与 opts 传递
class Plugin extends PuppeteerExtraPlugin { constructor(opts = {}) { super(opts) }PuppeteerExtraPlugin来自独立包puppeteer-extra-plugin(stealth 的依赖声明见 package.json)。它的构造函数会做一件关键的事:把插件定义的defaults与用户传入的opts做深合并,合并结果保存在this.opts中,插件运行期通过this.opts.xxx读取配置(见 src/index.ts 的_opts = merge(this.defaults, opts || {}))。
name:插件的全局唯一标识
get name() { return 'stealth/evasions/_template' }name是PuppeteerExtraPlugin基类要求每个插件必须覆写的属性(基类默认实现会直接抛错:Plugin must override "name",见 src/index.ts)。它同时承担三个职责:
- 调试命名空间:
this.debug()的日志会以puppeteer-extra-plugin:${name}为前缀输出,可用DEBUG=puppeteer-extra-plugin:<插件名>环境变量开关(见 src/index.ts); - 依赖解析:stealth 主插件通过
dependenciesgetter 把启用列表映射为stealth/evasions/<名字>形式的依赖名(见 stealth index.js),puppeteer-extra会自动require()缺失的依赖(见 src/index.ts); - 事件绑定去重:基类用
_hasChildClassMember检查子类是否真的实现了某个钩子,再决定是否注册对应的事件监听(见 src/index.ts)。
命名惯例上,真实 evasion 采用stealth/evasions/<目录名>格式,例如stealth/evasions/navigator.webdriver、stealth/evasions/chrome.app。复制模板后第一件事就是把这里改成你的新 evasion 名称。
onPageCreated:反检测逻辑的注入点
async onPageCreated(page) { await page.evaluateOnNewDocument(() => { console.debug('hello world') }) }这是模板的灵魂所在。onPageCreated是基类提供的生命周期钩子之一:当浏览器创建新页面(包括window.open、browser.newPage、隐身上下文中的页面)时被调用,且已由基类预先过滤为page类型 target(见 src/index.ts)。基类通过监听 puppeteer 的targetcreated事件触发它。
page.evaluateOnNewDocument保证注入脚本在页面任何脚本执行之前运行——这正是 stealth 反检测"先下手为强"的关键:在页面自身的检测代码运行前,先把navigator.webdriver、window.chrome等特征改写掉。真实插件对这一点利用得淋漓尽致,例如:
- navigator.webdriver/index.js 在
onPageCreated中根据 Chrome 版本删除原型上的webdriver属性; - chrome.app/index.js 在 headless 下完整 mock
window.chrome.app; - window.outerdimensions/index.js 补上 headless 缺失的
window.outerWidth/outerHeight。
模块导出:工厂函数模式
module.exports = function(pluginConfig) { return new Plugin(pluginConfig) }模板采用工厂函数而不是直接导出类实例,这是 stealth 插件家族的统一约定(所有 evasion 的index.js末尾都是这个模式)。这样做的原因在于opts的延迟注入:调用方写成require('...')({ userAgent: '...' }),参数在实例化时才传入并参与defaults合并。默认参数opts = {}在构造函数签名中已给出(文档中标注"opts (optional, default{})"),确保无参调用也能工作。
生命周期钩子:一个 evasion 能用到哪些时机
PuppeteerExtraPlugin基类为插件提供了完整的一套钩子,覆盖浏览器从启动到关闭的全过程(全部为可选实现,见 src/index.ts):
| 钩子 | 触发时机 | 典型用途 |
|---|---|---|
beforeLaunch(options) | 新浏览器启动前 | 修改 puppeteer 启动参数(如追加--disable-blink-features=AutomationControlled) |
afterLaunch(browser, opts) | 浏览器启动后 | 获取浏览器实例引用 |
beforeConnect(options) | puppeteer.connect()前 | 修改连接参数 |
afterConnect(browser, opts) | 连接建立后 | 处理已连接浏览器 |
onBrowser(browser, opts) | launch/connect 任一方式拿到浏览器时 | 统一处理两种来源 |
onTargetCreated(target) | 新 target 创建 | 处理任意类型 target |
onPageCreated(page) | 新页面创建 | 注入反检测脚本(模板核心) |
onTargetChanged(target)/onTargetDestroyed(target) | target 变化/销毁 | 状态清理 |
onDisconnected() | 与浏览器断开 | 清理资源 |
onPluginRegistered() | puppeteer.use()注册后 | 初始化 |
配套还有三个配置类 getter(见 src/index.ts):
defaults:插件默认配置,与用户 opts 深合并;requirements:声明launch(不支持 connect)、headful(不支持 headless)、dataFromPlugins、runLast等需求,供基类向用户告警或调整执行顺序;dependencies:声明依赖的其他插件,缺失时由puppeteer-extra自动加载。
模板只用了onPageCreated这一个钩子,但真实 evasion 常常组合多个。最典型的例子是 user-agent-override/index.js:它同时实现了onPageCreated(通过 CDP 的Network.setUserAgentOverride设置 UA 与平台)、beforeLaunch(记录是否 headless)、beforeConnect(把 connect 方式视为 headless)以及data(向user-preferences插件提供语言偏好数据),还声明了dependencies: new Set(['user-preferences'])。
使用共享工具库_utils:让 mock 不留痕迹
模板为了保持"最小",直接用了裸page.evaluateOnNewDocument。但 stealth 家族约 16 个 evasion(完整清单见 stealth/index.js 的availableEvasions)中,大部分改写在技术上有一个共性痛点:直接Object.defineProperty或new Proxy会留下可被反检测的蛛丝马迹,例如:
- Proxy 会出现在错误堆栈里(
at Reflect.get、at Object.get等行); - 被替换函数的
toString()结果不再是function xxx() { [native code] }; - 新增属性会出现在
Object.getOwnPropertyNames枚举中。
puppeteer-extra-plugin-stealth/evasions/_utils目录正是为消除这些痕迹而生的共享工具箱:
- _utils/index.js:核心工具函数集合,包括
stripProxyFromErrors(剥离 Proxy 错误堆栈)、replaceProperty(基于原有 descriptor 安全替换属性)、patchToString/redirectToString(伪造原生toString结果)、replaceWithProxy/replaceGetterWithProxy/replaceGetterSetter/mockWithProxy/createProxy(带反检测的 Proxy 包装)、preloadCache/makeNativeString(缓存原生toString字符串以防被二次嗅探)、stringifyFns/materializeFns(函数在 Node 与浏览器上下文间传递)、memoize等; - _utils/withUtils.js:对 page 的轻量封装,提供
page.evaluate和page.evaluateOnNewDocument的"带工具版",把_utils里的函数序列化后注入页面上下文,页面内通过utils参数直接调用。
一个使用withUtils的典型实现是 chrome.app/index.js:
async onPageCreated(page) { await withUtils(page).evaluateOnNewDocument(utils => { // headless 下 window.chrome 不存在,按 headful 的真实 descriptor 定义 Object.defineProperty(window, 'chrome', { writable: true, enumerable: true, configurable: false, value: {} }) // ... 构造 chrome.app 的静态数据与方法 mock utils.patchToStringNested(window.chrome.app) // 让所有 mock 函数看起来是原生代码 }) }navigator.pluginsevasion(navigator.plugins/index.js)则展示了"函数与数据分离"的进阶写法:把generateMimeTypeArray、generatePluginArray等生成函数用utils.stringifyFns序列化后随data(来自data.json)一起作为evaluateOnNewDocument的参数传入,页面内再用utils.materializeFns还原——既保持了代码结构清晰,又绕过了page.evaluate无法直接传函数的序列化限制。
开发建议:如果你的新 evasion 需要改写原生 API(而不是像模板那样仅仅打印日志),请务必复制_template后改用withUtils(page).evaluateOnNewDocument(...)模式,并复用_utils中已有的工具,避免"能用但会被检测出来"的半成品实现。
实战:如何把模板变成你自己的 evasion
参照官方在 evasions/readme.md 中给出的指引("If you want to add a new evasion technique I suggest you look at the template to kickstart things"),完整开发流程如下:
第 1 步:复制模板目录
把_template整个复制为evasions/<你的新名字>,并同步修改package.json与readme.md。
第 2 步:改写插件名
get name() { return 'stealth/evasions/<你的新名字>' }第 3 步:实现反检测逻辑
在onPageCreated中实现你的检测点修复。逻辑分两种形态:
- 直接修补类(适合简单场景),如 navigator.webdriver/index.js:
async onPageCreated(page) { await page.evaluateOnNewDocument(() => { if (navigator.webdriver === false) { // Post Chrome 89.0.4339.0 and already good } else if (navigator.webdriver === undefined) { // Pre Chrome 89.0.4339.0 and already good } else { // Pre Chrome 88.0.4291.0 and needs patching delete Object.getPrototypeOf(navigator).webdriver } }) }该插件还在
beforeLaunch中向启动参数追加--disable-blink-features=AutomationControlled(navigator.webdriver/index.js),并注释了 Linux 下去掉 Chrome 警告 infobar 的 Chrome 策略配置方法。 - 工具库辅助类(适合 Proxy mock 场景),如 media.codecs/index.js:用
utils.replaceWithProxy(HTMLMediaElement.prototype, 'canPlayType', canPlayType)拦截canPlayType调用,对 Chromium 缺失的video/mp4; codecs="avc1.42E01E"、audio/x-m4a、audio/aac返回'probably'/'maybe'。
第 4 步:编写测试(强烈建议)
stealth 插件的测试体系由 ava 驱动(测试脚本见 package.json),并提供了一组指纹采集工具 test/util.js:getVanillaFingerPrint用原生 puppeteer 采集指纹、getStealthFingerPrint用addExtra(vanillaPuppeteer).use(Plugin())采集加了你的 evasion 之后的指纹,两者对比即可验证效果。典型写法可参考 navigator.webdriver/index.test.js:先断言 vanilla 下navigator.webdriver === true,再断言 stealth 下等于false(Chrome ≥ 89.0.4339.0)或undefined(更早版本),并附带回归测试确认不会误伤其他navigator方法。
第 5 步:独立使用或并入 stealth 主插件
新 evasion 本身就是一个标准的puppeteer-extra插件,可以独立使用:
const puppeteer = require('puppeteer-extra') puppeteer.use(require('puppeteer-extra-plugin-stealth/evasions/<你的新名字>')())如果想并入 stealth 全家桶,则需要两步(见 stealth/index.js):把新名字加入defaults中的availableEvasionsSet(第 82-99 行),enabledEvasions默认复制全部可用项(第 103 行),这样dependencies就会自动生成对应的依赖名并由puppeteer-extra动态加载(第 112-116 行)。
常见问题与注意事项
- 不要用
page.setUserAgent()与 stealth 的 UA 处理混用:user-agent-override插件注释明确警告,常规的page.setUserAgent()会重置插件设置的语言与平台值(见 user-agent-override/index.js)。同理,开发新 evasion 时要意识到 stealth 各插件之间是协同关系,避免互相覆盖。 evaluateOnNewDocument里不能直接用 Node 侧闭包变量:需要通过withUtils的参数机制或stringifyFns序列化传递,这是_utils存在的重要原因。- 保持模块化与最小依赖:stealth 主插件的设计哲学是"只加载启用的 evasion"(见 stealth/index.js 的 Modularity 说明)。你的新插件应尽量自包含,必要时才声明
dependencies。 documentation.js注释驱动文档:readme.md由npm run docs(内部执行documentation readme --shallow,见 package.json)从index.js的 JSDoc 注释自动生成。给类和方法写规范的 JSDoc,readme.md的 API 段才会完整。- 版本与场景边界:本仓库
puppeteer-extra-plugin-stealth版本为 2.11.2,引擎要求 Node >= 8(见 package.json);各 evasion 的实现细节(如navigator.webdriver对不同 Chrome 版本的分支处理)以源码注释为准,实际效果随 Chrome 版本演进可能变化。
总结
_template虽只有 28 行代码,却完整定义了 stealth evasion 插件的标准形态:继承PuppeteerExtraPlugin、覆写name、在onPageCreated中通过evaluateOnNewDocument抢先注入补丁、以工厂函数导出。从它出发,你可以对照_utils工具箱与navigator.webdriver、chrome.app、navigator.plugins、media.codecs等真实实现,快速产出一个既模块化、又经得起指纹检测对比测试的新反检测插件——这正是 puppeteer-extra 社区持续扩展 stealth 能力的标准路径。
- 网页爬虫
- 浏览器控制
- 测试
【免费下载链接】puppeteer-extra
💯 Teach puppeteer new tricks through plugins.
相关推荐
puppeteer-extra-plugin-stealth 反检测插件解析:navigator.hardwareConcurrency 硬件并发数伪装原理与实战
puppeteer extra plugin stealth 反检测插件解析:navigator.hardwareConcurrency 硬件并发数伪装原理与实
网页爬虫浏览器控制测试puppeteer-extra-plugin-stealth 的 navigator.permissions 反检测插件:原理、源码与测试全解析
puppeteer extra plugin stealth 的 navigator.permissions 反检测插件:原理、源码与测试全解析 在 puppe
网页爬虫浏览器控制测试puppeteer-extra 插件解析:chrome.loadTimes 反检测插件原理与实战
puppeteer extra 插件解析:chrome.loadTimes 反检测插件原理与实战 chrome.loadTimes 是 Chrome 专属的浏览
网页爬虫浏览器控制测试
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考