三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

绕过无限Debugger反爬:Puppeteer实战与浏览器自动化对抗策略

绕过无限Debugger反爬:Puppeteer实战与浏览器自动化对抗策略

1. 项目概述:当爬虫遇上无限Debugger

做爬虫的朋友,估计都遇到过这种让人头疼的场面:打开目标网站,按下F12,准备分析网络请求和页面结构,结果浏览器瞬间卡住,开发者工具里一个红色的“暂停”图标亮起,光标停在了一行debugger;语句上。你点一下“继续执行”,它立马又跳到了下一个debugger;,如此循环往复,就像进入了一个无限循环的迷宫,让你根本无法正常查看页面加载的资源、分析接口参数,更别提写爬虫脚本了。这就是所谓的“无限Debugger”反爬机制。

这玩意儿本质上是一种基于浏览器开发者工具的“反调试”策略。网站通过在JavaScript代码中插入大量的、或经过混淆的debugger语句,或者利用setInterval定时触发调试器,来干扰和阻止自动化脚本(包括爬虫和手工分析)的正常运行。它的目的很明确:提高你分析网站的成本,让你知难而退。对于依赖浏览器环境执行JavaScript来渲染页面的现代网站(即SPA单页应用,或大量使用Ajax加载数据的页面),这招尤其有效,因为你的爬虫往往需要先模拟浏览器执行JS,才能拿到最终的数据。

那么,“绕过”就成了我们必须掌握的技能。这里的“绕过”不是指去破解网站的JavaScript代码(那属于逆向工程,难度和风险都高),而是指让我们的爬虫程序或分析环境,能够无视这些调试器陷阱,顺畅地执行下去。这涉及到对浏览器开发者工具原理、浏览器自动化工具(如Selenium、Puppeteer、Playwright)的深度配置,以及对反爬策略的针对性处理。接下来,我就结合自己趟过的坑,详细拆解几种行之有效的绕过思路和实操方案。

2. 无限Debugger的实现原理与常见形式

要绕过它,首先得知道它是怎么工作的。debugger是JavaScript语言中的一个关键字,当代码执行到它,并且当前运行环境开启了调试功能(比如浏览器打开了开发者工具),执行就会暂停,进入调试模式。网站利用这个特性,主要玩以下几种花样:

2.1 简单粗暴的无限循环Debugger

这是最原始的形式,通常出现在一些防护意识初级的网站上。

// 示例1: 循环中的debugger while (true) { debugger; } // 示例2: 定时器触发debugger setInterval(function() { debugger; }, 100);

这种代码一旦在开发者工具打开时执行,就会导致脚本不断暂停。对于人肉分析,你可以通过条件断点或者禁用所有断点来跳过。但对于爬虫使用的无头浏览器(Headless Browser)来说,如果浏览器启动时默认开启了调试端口或某些标志,同样会触发。

2.2 基于开发者工具检测的Debugger

这种就稍微高级一些,网站会尝试检测浏览器是否打开了开发者工具,一旦检测到,再触发debugger或进行其他干扰。

// 一种常见的检测方式:比较窗口内外尺寸 var threshold = 160; // 一个经验值 var widthThreshold = window.outerWidth - window.innerWidth > threshold; var heightThreshold = window.outerHeight - window.innerHeight > threshold; if (widthThreshold || heightThreshold) { // 认为开发者工具已打开 debugger; // 或者执行其他破坏性代码,如无限循环、内存消耗等 }

其原理是,当打开开发者工具(尤其是停靠在侧面或底部时),浏览器窗口的内外宽度或高度差会发生变化。更复杂的检测还可能包括检查console.log函数的引用、debugger关键字是否被重定义、甚至监测调试器的性能特征。

2.3 经过混淆和加密的Debugger

在高防护等级的网站(如一些大型电商、社交平台)上,你很难在源代码里直接搜到debugger这个单词。它们会被编码、拆分、或通过函数动态生成。

// 示例:eval执行经过编码的debugger语句 eval("debug" + "ger"); // 或更复杂的 Function("de" + "bu" + "gger" + "()")();

这种形式让简单的关键词搜索失效,增加了静态分析的难度。反爬系统可能会将检测逻辑和触发逻辑分散在多个脚本文件中,动态加载和执行。

2.4 结合其他反爬手段的复合型干扰

单纯的debugger暂停有时容易被绕过,所以常与其他手段结合:

  1. 无限循环+内存增长:在触发debugger的同时,创建大量对象或进行密集计算,试图耗尽浏览器内存或导致标签页崩溃。
  2. 时间差反调:检测代码执行的时间间隔,如果发现两次执行间有异常的暂停(疑似在手动断点查看),则触发反制措施,如跳转到错误页、清空关键数据。
  3. 干扰控制台:重写console.logconsole.error等方法,输出大量垃圾信息干扰视线,或者监测控制台的使用。

理解这些形式,有助于我们选择正确的绕过策略。核心思路是:让目标网站“感知”不到它正运行在一个被调试或自动化的环境中

3. 核心绕过策略与方案选型

面对无限Debugger,我们不能硬碰硬,而是要“欺骗”它。根据不同的应用场景和技术栈,主要有以下几套方案:

3.1 方案一:禁用浏览器调试功能(最直接)

这是最根本的解决方法。既然debugger只在调试功能开启时生效,那我们直接关掉它就好了。

  • 适用场景:使用Puppeteer、Playwright、Selenium等浏览器自动化工具进行爬虫开发。
  • 核心原理:在启动浏览器实例时,通过添加特定的启动参数(Chrome DevTools Protocol 参数),告诉浏览器不要启用JavaScript调试器,或者忽略debugger语句。
  • 优点:从根源上解决问题,一劳永逸。对上述所有形式的debugger都有效。
  • 缺点:如果你在爬虫开发过程中,确实需要用到开发者工具进行调试(比如分析某个阶段的DOM结构),这个方法会让你自己也无法调试。通常用于生产环境的爬虫。

3.2 方案二:重写或Hook关键函数(针对性拦截)

如果方案一因为某些原因不适用(例如,某些云服务环境对启动参数限制严格),或者你需要保留调试能力,可以采用“拦截”策略。

  • 适用场景:对页面JavaScript环境有控制权时,如使用Puppeteer/Playwright的evaluateOnNewDocument方法在页面加载前注入脚本。
  • 核心原理:赶在网站的反爬脚本执行之前,通过重写Function构造函数、evalsetInterval等关键函数,或者直接重定义debugger关键字,使其失效或变成一个空操作(noop)。
  • 优点:灵活性高,可以精确控制。可以在绕过反爬的同时,保留浏览器其他正常的调试功能(如果需要)。
  • 缺点:需要针对不同的反爬实现进行适配,如果对方检测了这些函数是否被重写,可能会引发新的对抗。属于“魔高一尺,道高一丈”的博弈。

3.3 方案三:使用无头浏览器且隐藏自动化特征

很多现代反爬系统不仅检测debugger,还会检测浏览器是否由自动化工具控制(如检测navigator.webdriver属性)。因此,绕过debugger常常需要结合反反爬措施。

  • 适用场景:所有使用自动化浏览器进行爬取的场景,尤其是对付那些综合防护的网站。
  • 核心原理:通过启动参数和页面脚本注入,最大限度地让自动化浏览器看起来像一个真实的、由人类操作的普通浏览器。
  • 优点:能应对更全面的检测,提升爬虫的稳定性和成功率。
  • 缺点:配置相对复杂,需要持续维护以应对网站更新检测手段。

3.4 方案四:脱离浏览器环境的静态分析(终极绕行)

对于高手而言,终极方案是彻底不运行对方的JavaScript。通过抓取网页源代码,直接分析网络请求(XHR/Fetch),找到数据接口,然后用爬虫直接模拟请求接口。

  • 适用场景:数据通过清晰的API接口返回,且接口参数易于逆向或固定不变。
  • 核心原理:完全避开前端渲染和JS执行环节。使用抓包工具(如Charles、Fiddler、或浏览器开发者工具的Network面板,在触发debugger前快速记录)分析出数据请求的URL、Headers、Body参数。
  • 优点:效率极高,资源消耗极低,稳定性最好。一旦成功,爬取速度和质量远超浏览器模拟方案。
  • 缺点:技术门槛高,需要对网络协议、加密参数逆向有深入理解。对于参数动态生成、加密复杂或依赖浏览器环境生成Token(如canvas指纹)的接口,此方法难度极大。

对于大多数爬虫工程师,方案一和方案三的组合是最实用、最通用的起点。接下来,我将以最流行的Puppeteer(Node.js)和Playwright(支持多语言)为例,展示详细的实操步骤。

4. 基于Puppeteer/Playwright的详细实操

这里我以Puppeteer为例,Playwright的API非常相似,概念完全通用。

4.1 环境准备与基础启动

首先,确保你的项目已安装Puppeteer。

npm install puppeteer

一个最基础的、会触发debugger的爬虫脚本可能是这样的:

const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ headless: false, // 方便观察,设为非无头模式 }); const page = await browser.newPage(); await page.goto('https://你的目标网站.com'); // ... 后续操作 await browser.close(); })();

运行这个脚本,如果目标网站有无限debugger,页面打开后很快就会在开发者工具里暂停,脚本也就卡住了。

4.2 关键步骤:启动参数配置(方案一实现)

我们需要在puppeteer.launch()的参数中,添加关键的args选项。

const browser = await puppeteer.launch({ headless: 'new', // 推荐使用新的Headless模式,更稳定 args: [ '--disable-blink-features=AutomationControlled', // 隐藏自动化特征 '--disable-dev-shm-usage', // 解决某些Linux环境下的内存问题 '--no-sandbox', // 非可信环境可能需要,但有安全风险 '--disable-web-security', // 禁用同源策略,有时用于调试,生产环境慎用 // 以下是绕过debugger的核心参数 '--disable-javascript-harmony-shipping', '--disable-features=IsolateOrigins,site-per-process', // 某些情况下需要 ], });

注意--no-sandbox--disable-web-security参数会降低浏览器安全性,仅在你知道潜在风险且必要时使用。在Docker容器等受限环境中,--no-sandbox常是必需的。

仅仅这些可能还不够。最关键的参数是直接告诉Chrome忽略调试器:

args: [ // ... 其他参数 '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', // 核心:禁用调试相关功能 '--remote-debugging-port=0', // 关闭远程调试端口 '--no-default-browser-check', '--disable-sync', '--disable-translate', '--disable-default-apps', '--disable-extensions', // 尝试禁用V8的调试功能 '--js-flags="--noexpose_wasm --max-old-space-size=8192"', ],

其中,--remote-debugging-port=0关闭了远程调试,这对阻止外部调试器附加很有帮助。但针对页面内部的debugger语句,我们还需要下一步。

4.3 核心步骤:页面脚本注入(方案二实现)

在页面加载任何其他脚本之前,我们先注入自己的脚本,将debugger关键字“废掉”。

await page.evaluateOnNewDocument(() => { // 1. 重写debugger关键字(简单粗暴,可能被检测) // Object.defineProperty(window, 'debugger', { // get: function() {}, // set: function() {} // }); // 2. 更隐蔽的方式:重写Function构造函数和eval const originalFunction = Function; window.Function = function(...args) { const body = args[args.length - 1]; // 检查函数体内是否包含debugger if (typeof body === 'string' && body.includes('debugger')) { // 替换或移除debugger语句 const newBody = body.replace(/debugger;?/g, '; // debugger removed'); args[args.length - 1] = newBody; } return originalFunction.apply(this, args); }; // 复制原型链属性,使其更难被检测 Object.setPrototypeOf(window.Function, originalFunction.prototype); for (const key in originalFunction) { if (originalFunction.hasOwnProperty(key)) { window.Function[key] = originalFunction[key]; } } // 3. 重写setInterval/SetTimeout,清除其中的debugger定时任务 const originalSetInterval = setInterval; window.setInterval = function(callback, delay, ...args) { // 尝试检查callback的toString是否包含debugger(不完美,但有用) if (callback && callback.toString().includes('debugger')) { console.warn('Blocked setInterval containing debugger'); return 0; // 返回一个无效的ID } return originalSetInterval.call(this, callback, delay, ...args); }; // 对setTimeout做类似处理 // 4. 覆盖console.debug,防止某些通过console触发的调试 console.debug = function() {}; });

这段代码需要在page.goto()之前执行。evaluateOnNewDocument确保它在页面内任何脚本执行前生效。我们优先选择重写Functioneval的方式,因为很多混淆的debugger是通过它们动态执行的。直接重定义window.debugger太过明显,容易被反检测。

4.4 进阶步骤:隐藏自动化特征(方案三实现)

仅仅绕过debugger,你的爬虫可能还会被识别为“机器人”。需要进一步伪装。

await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); // 注入脚本,隐藏webdriver属性 await page.evaluateOnNewDocument(() => { Object.defineProperty(navigator, 'webdriver', { get: () => undefined, }); // 覆盖plugins和languages属性,使其更真实 Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5], }); Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en'], }); }); // 设置视口和窗口大小,避免尺寸检测 await page.setViewport({ width: 1920, height: 1080 }); await page.setJavaScriptEnabled(true); // 确保JS执行(默认就是true)

4.5 完整示例代码

将以上所有步骤整合,一个具备较强绕过能力的爬虫启动模板如下:

const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ headless: 'new', // 或 true 用于生产 args: [ '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--no-sandbox', // 根据环境需要 '--disable-web-security', // 慎用 '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--remote-debugging-port=0', '--disable-features=IsolateOrigins,site-per-process', '--window-size=1920,1080', ], }); const page = await browser.newPage(); // 1. 隐藏自动化特征 await page.setUserAgent('你的真实User-Agent字符串'); await page.evaluateOnNewDocument(() => { Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); // 更多伪装... }); // 2. 注入反debugger脚本(核心) await page.evaluateOnNewDocument(() => { // 这里放入上面提到的重写Function/eval/setInterval的代码 const originalFunction = Function; window.Function = function(...args) { const body = args[args.length - 1]; if (typeof body === 'string' && body.includes('debugger')) { const newBody = body.replace(/debugger;?/g, '; // debugger removed'); args[args.length - 1] = newBody; } return originalFunction.apply(this, args); }; Object.setPrototypeOf(window.Function, originalFunction.prototype); // ... 其他重写 }); // 3. 导航到目标页面 try { await page.goto('https://你的目标网站.com', { waitUntil: 'networkidle2', // 等待网络基本空闲 timeout: 60000 // 超时时间设长一点 }); console.log('页面加载成功,debugger应已被绕过。'); // 4. 进行你的爬取操作,例如截图、提取数据 // await page.screenshot({ path: 'page.png' }); // const data = await page.evaluate(() => { // return document.querySelector('...').innerText; // }); } catch (error) { console.error('页面加载或操作失败:', error); } finally { // 5. 关闭浏览器 await browser.close(); } })();

5. 常见问题排查与实战技巧

即使按照上面的步骤做了,在实际操作中你可能还是会遇到各种问题。下面是一些常见的坑和解决思路。

5.1 Debugger仍然触发

  • 现象:配置了所有参数和脚本,打开页面后依然在开发者工具里暂停。
  • 排查
    1. 注入时机问题:确保page.evaluateOnNewDocumentpage.goto之前调用。有时反爬脚本执行得非常早。
    2. 脚本覆盖不全:对方可能使用了更冷门的触发方式,比如通过Proxy代理对象、Generator函数或在Web Worker中执行debugger。你的注入脚本只运行在主页面上下文,对Worker无效。
    3. 参数未生效:某些Chrome启动参数在新版本中可能已改名或失效。检查你使用的Puppeteer/Chromium版本对应的参数文档。
  • 解决
    • 更早注入:尝试使用puppeteer.launch中的ignoreDefaultArgsdefaultViewport等选项进行更底层的控制,或者寻找能否在浏览器启动时通过插件方式加载脚本。
    • 动态应对:先让页面加载,在触发debugger暂停的瞬间,通过Puppeteer的page.evaluate执行一段代码来删除或覆盖触发debugger的定时器。这需要你先手动操作一次,找到触发源。
    • 终极方案:如果网站防护极强,考虑使用方案四(静态分析接口),或者使用更底层的CDP(Chrome DevTools Protocol)协议直接发送命令,在debugger暂停时强制恢复执行。

5.2 页面卡死或无响应

  • 现象:页面能打开,但很快变得非常卡顿,甚至浏览器崩溃。
  • 排查:这通常是遇到了“内存消耗型”反爬。无限debugger循环中可能伴随着大量的DOM操作或对象创建。
  • 解决
    • 限制页面资源:在page.goto前,使用page.setRequestInterception(true)拦截并阻止不必要的资源加载(如图片、样式表、媒体文件、特定脚本)。
    await page.setRequestInterception(true); page.on('request', (req) => { const resourceType = req.resourceType(); // 只允许文档和XHR/Fetch请求通过 if (['document', 'xhr', 'fetch'].includes(resourceType)) { req.continue(); } else { req.abort(); } });
    • 设置超时与超时处理:为所有可能长时间运行的操作(如goto,waitForSelector,evaluate)设置合理的超时时间,并在超时后执行清理或重试逻辑。
    • 使用更强大的硬件:在内存充足的服务器上运行。

5.3 被网站识别为爬虫并封禁

  • 现象:IP被封,返回验证码页面,或数据返回为空。
  • 排查:你虽然绕过了debugger,但其他特征(如IP请求频率、鼠标移动轨迹、浏览器指纹)暴露了你。
  • 解决
    • IP代理池:这是必须的。使用高质量的住宅IP代理,并合理设置请求间隔(page.waitForTimeout模拟人工延迟)。
    • 模拟人类行为:使用page.mouse.move(),page.mouse.click()模拟更真实的鼠标移动和点击轨迹,而不是直接调用page.click()
    • 管理Cookie和会话:妥善保存和复用登录后的Cookie,避免频繁登录。
    • 轮换User-Agent和浏览器指纹:每次启动浏览器时,从预定义的列表中随机选择一套指纹(包括UA、屏幕分辨率、时区、语言等)。

5.4 Puppeteer/Playwright 特定问题

  • page.evaluate中无法使用外部变量:这是设计如此。你需要通过参数传递。
    const externalData = 'hello'; const result = await page.evaluate((data) => { // 在浏览器环境中,只能使用传入的data return data + ' world'; }, externalData); // 将变量作为参数传入
  • Headless模式下的差异:有些反爬在无头(Headless)模式下行为不同。如果headless: false能成功但true失败,尝试使用headless: 'new'(新的Headless模式),或者添加--headless=new启动参数。也可以尝试非无头模式配合xvfb在服务器上运行。

6. 高级对抗与指纹伪装

当基础绕过手段失效时,说明网站可能采用了更先进的浏览器指纹检测。除了navigator.webdriver,它们还会检查:

  • WebGL Vendor/Renderer
  • Canvas 指纹
  • AudioContext 指纹
  • 字体列表
  • 硬件并发数
  • 插件列表(mimeTypes, plugins)

要对抗这个,需要使用专门的指纹伪装库,或者进行极其细致的环境模拟。一个相对简单的方法是使用像puppeteer-extra和它的插件puppeteer-extra-plugin-stealth

npm install puppeteer-extra puppeteer-extra-plugin-stealth
const puppeteer = require('puppeteer-extra'); const StealthPlugin = require('puppeteer-extra-plugin-stealth'); puppeteer.use(StealthPlugin()); (async () => { const browser = await puppeteer.launch({ headless: 'new' }); const page = await browser.newPage(); // Stealth插件会自动处理很多常见的反爬特征 await page.goto('https://目标网站'); // ... })();

stealth插件会帮你自动处理很多常见的指纹漏洞,但对于高度定制化的检测,可能仍需手动调整。

7. 非浏览器方案(方案四)的探索

如果目标网站的数据并非完全由前端JavaScript动态渲染,那么直接抓包分析接口永远是最高效、最稳定的方法。

  1. 使用专业抓包工具:在浏览器(可以先用上述方法临时禁用debugger完成抓包)或手机App中正常操作一遍,用Charles/Fiddler记录下所有网络请求。
  2. 筛选数据接口:在抓包记录中,寻找返回JSON或纯文本数据的请求(通常是XHR或Fetch类型)。观察其URL规律、请求头(Headers)和请求体(Body)。
  3. 逆向参数:重点分析请求中看起来是动态生成的参数,如token,sign,_t,nonce等。它们可能由之前的某个响应生成,或由页面中的固定JavaScript算法计算得出。
  4. 模拟请求:使用requests(Python)、axios(Node.js) 等HTTP库,完全复现该请求。这可能需要你模拟整个会话(Session/Cookie),并正确实现参数生成算法。

这个过程的难点在于参数逆向。你需要有一定的JavaScript代码阅读和调试能力,可能还需要用到AST(抽象语法树)分析工具来解混淆代码。这属于爬虫工程师的进阶技能了。

绕过无限debugger只是爬虫与反爬虫对抗中的一道常见关卡。它考验的是你对浏览器运行机制和前端技术的理解深度。没有一成不变的银弹,最有效的方法往往是多种策略的组合:正确的启动参数 + 提前注入的拦截脚本 + 良好的指纹伪装 + 合理的请求节奏。在实际项目中,建议先从最简单的方案开始尝试,逐步增加复杂度。同时,务必尊重网站的robots.txt协议,控制爬取频率,避免对目标网站造成过大压力,在法律和道德的框架内进行技术探索。

← 返回列表