1. 项目概述当自动化脚本遇上“反爬”高墙如果你用过 Playwright 或 Puppeteer 这类现代浏览器自动化工具大概率经历过这种挫败感脚本在本地跑得飞起一部署到服务器或者跑上几百次目标网站就弹出了验证码或者直接返回一堆乱码甚至干脆把你的 IP 给封了。这感觉就像你开着坦克去参加化装舞会引擎的轰鸣和履带的痕迹让你瞬间暴露。我们今天要聊的Playwright Stealth就是给这辆“坦克”披上隐形斗篷让它能悄无声息地融入“舞会”的秘密武器。简单来说Playwright Stealth 是一个专门为 Playwright 设计的插件它的核心使命是让你的自动化脚本模拟得更像一个真实的人类用户在使用浏览器从而绕过网站的各种反爬虫和自动化检测机制。这不仅仅是改个 User-Agent 那么简单它涉及到浏览器指纹的伪装、行为模式的模拟、以及各种自动化特征我们常说的“WebDriver 属性”的抹除。在数据采集、自动化测试、RPA机器人流程自动化等场景下这几乎是保证脚本长期稳定运行的刚需。无论你是想自动化处理一些网页任务还是需要稳定地抓取一些公开数据这个工具都能显著提升你的成功率。2. 核心原理网站如何识别“机器人”在深入使用 Stealth 之前我们必须先搞清楚对手——网站的反爬系统——到底在检测什么。知己知彼才能知道 Stealth 在哪个环节帮我们“化妆”。2.1 自动化特征检测WebDriver 属性这是最基础也是最常见的一层检测。当浏览器被 Selenium、Playwright 或 Puppeteer 这类自动化工具驱动时浏览器环境会暴露出一些特殊的属性。最著名的就是navigator.webdriver属性。在普通浏览器中这个属性通常是undefined或false而在自动化控制的浏览器中它会被设置为true。Stealth 插件会通过注入 JavaScript 代码巧妙地覆盖或删除这些属性使其返回值与普通浏览器一致。2.2 浏览器指纹Browser Fingerprinting这是更高级、更隐蔽的检测手段。网站会收集你浏览器的大量信息来生成一个几乎唯一的“指纹”包括User-Agent浏览器类型、版本、操作系统信息。屏幕分辨率与色彩深度screen.width,screen.height,screen.colorDepth。插件列表navigator.plugins的长度和内容。字体列表通过 Canvas 或特定 API 检测系统已安装的字体。硬件信息如 CPU 核心数 (navigator.hardwareConcurrency)、内存大小估算。WebGL 渲染器信息显卡型号和驱动信息。时区和语言设置navigator.language,Intl.DateTimeFormat().resolvedOptions().timeZone。一个真实的浏览器其指纹的各个维度是自洽的。例如一个声称是“Chrome 120 on Windows 11”的 User-Agent其插件列表里却出现了只有旧版 Firefox 才有的插件或者屏幕分辨率是手机尺寸却报告了桌面版 Chrome这就会立刻触发警报。Stealth 的核心工作之一就是确保它为你伪装的所有指纹信息在逻辑上保持一致。2.3 行为模式分析即使指纹完美笨拙的行为也会出卖你。这包括鼠标移动轨迹真实的鼠标移动是带有随机弧度和速度变化的曲线而自动化脚本的移动往往是两点之间的直线或简单的贝塞尔曲线。点击位置与频率总是精准点击元素中心且点击间隔时间完全一致如固定 1 秒。页面停留时间与滚动快速、匀速地滚动到底部或在每个页面停留完全相同的时间。键盘输入速度每个字符的输入间隔毫秒级精确相同。Stealth 主要解决前两层的“静态特征”问题。对于第三层的行为模式它提供了基础支持如更真实的 User-Agent 和视口设置但更高级的模拟需要你结合 Playwright 的 API如page.mouse.move()的随机坐标偏移、page.type()的随机延迟来自行实现。3. 环境搭建与基础配置理论讲完我们开始实战。首先把你的“武器库”准备好。3.1 安装 Playwright 与 Stealth 插件假设你已经在使用 Python 环境。首先确保安装了 Playwright。pip install playwright playwright install chromium # 安装一个浏览器推荐 Chromium 或 Chrome接下来安装playwright-stealth这个 Python 包。请注意社区中有多个类似名称的包我们使用目前维护相对活跃的playwright-stealth。pip install playwright-stealth注意Node.js 版本的 Playwright 也有对应的puppeteer-extra-plugin-stealth生态其原理和配置项与 Python 版类似但 API 不同。本文以 Python 版为例思路是相通的。3.2 编写你的第一个“隐形”脚本让我们从一个最简单的例子开始看看 Stealth 如何被集成。from playwright.sync_api import sync_playwright from playwright_stealth import stealth_sync # 导入同步版 stealth def main(): with sync_playwright() as p: # 1. 启动浏览器这里推荐使用有头模式先进行调试 browser p.chromium.launch(headlessFalse) # 调试时设为 False context browser.new_context( viewport{width: 1920, height: 1080}, # 设置一个常见的桌面分辨率 user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 # 设置一个真实的 UA ) page context.new_page() # 2. 应用 stealth 插件这是关键一步。 stealth_sync(page) # 3. 导航到目标页面 page.goto(https://bot.sannysoft.com/) # 4. 等待一下然后截图看看效果 page.wait_for_timeout(5000) # 等待5秒让页面检测脚本运行 page.screenshot(pathstealth_test.png) browser.close() if __name__ __main__: main()这段代码做了几件事启动一个浏览器实例并为其创建了一个上下文Context同时设置了视口和 User-Agent。在应用 Stealth 前就设置好基础指纹信息是一个好习惯。stealth_sync(page)这一行是魔法所在。它向页面注入了一系列脚本覆盖了数十个可能暴露自动化特征的属性和方法。我们导航到一个著名的自动化检测测试页bot.sannysoft.com。这个页面会直观地显示你的浏览器有多少特征被识别为自动化工具。截图保存方便我们查看伪装效果。运行这个脚本后打开stealth_test.png你应该会看到大部分检测项都通过了显示为绿色尤其是关键的WebDriver、Chrome等检测。如果未使用 Stealth这些项通常是红色的警告。3.3 配置项详解定制你的隐形衣playwright-stealth提供了一些配置选项允许你启用或禁用某些特定的伪装功能。这在某些特定场景下很有用。查看其源码或文档你可以找到类似如下的配置方式具体参数请以最新版为准from playwright_stealth import StealthConfig, stealth_sync # 创建一个配置对象 config StealthConfig( # 是否启用对 navigator.webdriver 属性的伪装 webdriverTrue, # 是否启用对 Chrome 运行时特性的伪装如 window.chrome chrome_runtimeTrue, # 是否伪装插件和 mimetype 列表 pluginsTrue, # 是否伪装为真实的连接类型如 ‘4g’ connectionTrue, # ... 其他配置 ) with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context() page context.new_page() # 应用带有自定义配置的 stealth stealth_sync(page, configconfig) page.goto(https://example.com) browser.close()实操心得在绝大多数情况下使用默认配置即不传递config参数就是最佳选择。除非你明确知道目标网站依赖某种未被默认覆盖的检测点并且你了解禁用某项伪装能解决兼容性问题极少数情况否则不要轻易修改默认配置。默认配置是社区经验积累的平衡点。4. 高级伪装与行为模拟实战仅仅应用 Stealth 插件只是第一步要真正做到“隐形”还需要在行为层面进行精心设计。下面我们结合 Playwright 的强大 API 来打造一个更高级的脚本。4.1 指纹一致性与上下文Context管理Playwright 的BrowserContext是管理浏览器指纹的核心单元。每个 Context 拥有独立的 Cookie、缓存、指纹信息。最佳实践是为每个需要独立身份的任务创建一个新的 Context并在任务完成后将其销毁。复用同一个 Context 访问多个敏感网站可能会因为 Cookie 或指纹关联导致被批量封禁。from playwright.sync_api import sync_playwright from playwright_stealth import stealth_sync import random def get_random_ua(): 生成一个随机的桌面版 Chrome User-Agent chrome_versions [120.0.0.0, 121.0.0.0, 122.0.0.0] os_list [ (Windows NT 10.0; Win64; x64), (Macintosh; Intel Mac OS X 10_15_7), (X11; Linux x86_64) ] chrome_ver random.choice(chrome_versions) os_spec random.choice(os_list) return fMozilla/5.0 {os_spec} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{chrome_ver} Safari/537.36 def create_stealth_context(browser): 创建一个应用了Stealth的随机指纹上下文 viewports [ {width: 1920, height: 1080}, {width: 1366, height: 768}, {width: 1536, height: 864} ] viewport random.choice(viewports) ua get_random_ua() context browser.new_context( viewportviewport, user_agentua, # 可以进一步设置时区、语言等 localeen-US, timezone_idAmerica/New_York, ) page context.new_page() stealth_sync(page) # 对页面应用stealth return context, page def main(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) # 生产环境用无头 # 任务1使用一个独立身份访问网站A context_a, page_a create_stealth_context(browser) page_a.goto(https://website-a.com) # ... 执行任务A ... context_a.close() # 任务完成关闭上下文清理指纹 # 任务2使用一个全新的独立身份访问网站B context_b, page_b create_stealth_context(browser) page_b.goto(https://website-b.com) # ... 执行任务B ... context_b.close() browser.close()这个例子展示了如何为每次任务生成随机的、但内部自洽的指纹UA、视口匹配并通过独立的 Context 进行隔离。stealth_sync在 Context 创建后立即应用确保底层属性也被伪装。4.2 模拟人类交互行为Stealth 解决了“你是谁”的问题而“你怎么做”则需要我们用 Playwright API 来模拟。鼠标移动模拟 不要使用page.click()直接点击。而是模拟移动、悬停、再点击的过程。import random import math from playwright.sync_api import Page def human_like_click(page: Page, selector: str): 模拟人类点击随机轨迹移动后点击 element page.wait_for_selector(selector) box element.bounding_box() if not box: page.click(selector) # 降级方案 return # 目标点元素中心稍微随机偏移 target_x box[x] box[width] / 2 random.uniform(-5, 5) target_y box[y] box[height] / 2 random.uniform(-5, 5) # 当前鼠标位置假设从屏幕左上角附近开始 current_x, current_y random.randint(50, 100), random.randint(50, 100) # 生成一条带控制点的贝塞尔曲线路径简化版分段移动 steps random.randint(3, 6) for i in range(steps): # 不是直线移动加入一些随机扰动 intermediate_x current_x (target_x - current_x) * (i1)/steps random.uniform(-30, 30) intermediate_y current_y (target_y - current_y) * (i1)/steps random.uniform(-30, 30) page.mouse.move(intermediate_x, intermediate_y) page.wait_for_timeout(random.randint(50, 200)) # 每步随机停顿 # 最终移动到目标点并点击 page.mouse.move(target_x, target_y) page.wait_for_timeout(random.randint(100, 500)) # 点击前犹豫一下 page.mouse.click(target_x, target_y) # 使用示例 # human_like_click(page, button#submit)输入与滚动模拟def human_like_type(page: Page, selector: str, text: str): 模拟人类输入随机延迟 page.click(selector) # 先聚焦输入框 for char in text: page.keyboard.type(char, delayrandom.uniform(50, 150)) # 每个字符输入延迟50-150毫秒 def human_like_scroll(page: Page, pixels: int None): 模拟人类滚动非匀速、有停顿 if pixels is None: # 随机滚动一屏到两屏 viewport page.viewport_size pixels random.randint(viewport[height], viewport[height] * 2) current 0 while current pixels: step random.randint(200, 500) # 每次滚动200-500像素 step min(step, pixels - current) page.mouse.wheel(0, step) current step page.wait_for_timeout(random.randint(500, 2000)) # 滚动后随机停顿0.5-2秒将这些行为模式封装成函数并在你的自动化流程中替代直接的 API 调用能极大增加脚本的隐蔽性。4.3 处理验证码与高级挑战即使有了完美的指纹和行为模拟一些网站仍会抛出验证码如 reCAPTCHA、hCaptcha作为终极挑战。Stealth 对此无能为力因为这是服务器端的有意质询。此时你需要一套应对策略降低触发频率这是最有效的方法。在脚本中大量增加随机等待时间 (page.wait_for_timeout(random.randint(3000, 10000)))模拟真实用户阅读和思考的时间。避免在短时间内发起大量相同模式的请求。使用代理IP池通过 Playwright 的Browser.new_context(proxy...)或BrowserType.launch(proxy...)参数轮换使用不同的 IP 地址。这对于防止基于 IP 的速率限制和封禁至关重要。验证码识别服务对于必须解决的验证码可以集成第三方打码平台如 2Captcha、Anti-Captcha的 API。流程是从页面提取验证码图片或站点密钥 - 发送到打码平台 - 获取答案 - 回填到页面。人工介入兜底在关键流程设置检查点。如果检测到验证码出现脚本可以暂停并截图通过邮件、短信等方式通知你等你手动解决后脚本再继续执行。重要警告任何关于绕过验证码的讨论都必须严格在法律和网站服务条款允许的范围内进行。仅将其用于测试自己拥有的网站或明确允许自动化的公开 API/数据源。滥用自动化工具攻击或爬取受保护的数据可能导致法律风险。5. 实战案例构建一个健壮的自动化数据采集脚本让我们综合以上所有知识设计一个用于从公开新闻网站采集标题和链接的健壮脚本。假设目标网站有基本的反爬措施。import random import time import json from datetime import datetime from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError from playwright_stealth import stealth_sync class StealthNewsScraper: def __init__(self, headlessTrue, proxyNone): self.headless headless self.proxy proxy # 格式: {server: http://ip:port} self.playwright sync_playwright().start() self.browser None def __enter__(self): launch_options { headless: self.headless, args: [--disable-blink-featuresAutomationControlled] # 额外启动参数 } if self.proxy: launch_options[proxy] self.proxy self.browser self.playwright.chromium.launch(**launch_options) return self def __exit__(self, exc_type, exc_val, exc_tb): if self.browser: self.browser.close() self.playwright.stop() def create_stealth_page(self, viewportNone, uaNone): 创建带有随机指纹和stealth的页面 if not viewport: viewport random.choice([ {width: 1920, height: 1080}, {width: 1536, height: 864}, ]) if not ua: ua self._generate_ua() context self.browser.new_context( viewportviewport, user_agentua, localeen-US, timezone_idAmerica/Los_Angeles, # 忽略HTTPS错误某些代理下可能需要 ignore_https_errorsFalse, ) page context.new_page() stealth_sync(page) # 应用隐身术 return context, page def _generate_ua(self): 生成随机UA templates [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{ver} Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{ver} Safari/537.36, ] ver f{random.randint(118, 122)}.0.0.0 return random.choice(templates).format(verver) def scrape_news_page(self, url, max_retries3): 采集单个新闻列表页包含重试逻辑 for attempt in range(max_retries): context, page self.create_stealth_page() try: print(f尝试 {attempt1}/{max_retries}: 访问 {url}) # 设置一个合理的超时 page.goto(url, wait_untilnetworkidle, timeout30000) # 模拟人类阅读前等待 page.wait_for_timeout(random.randint(2000, 5000)) # 模拟轻微滚动 self._human_scroll(page) # 使用更健壮的选择器避免因动态加载失败 # 假设新闻条目在带有 classarticle-item 的元素内 articles page.locator(.article-item).all() if not articles: # 如果选择器没找到尝试备用选择器 articles page.locator(article).all() data [] for article in articles[:10]: # 只取前10条避免太多 try: title_elem article.locator(h2 a, h3 a, .title a).first title title_elem.inner_text(timeout2000) if title_elem.count() else N/A link title_elem.get_attribute(href, timeout2000) if title_elem.count() else N/A if link and not link.startswith(http): link url.rstrip(/) / link.lstrip(/) data.append({title: title.strip(), link: link}) except PlaywrightTimeoutError: print(f 条目提取超时跳过) continue print(f 成功提取 {len(data)} 条数据) context.close() return data except Exception as e: print(f 尝试 {attempt1} 失败: {e}) context.close() page.wait_for_timeout(random.randint(5000, 15000) * (attempt 1)) # 失败后等待时间递增 finally: # 确保上下文关闭 try: context.close() except: pass print(f 所有 {max_retries} 次尝试均失败) return [] def _human_scroll(self, page): 页面内随机滚动 scroll_times random.randint(1, 3) for _ in range(scroll_times): page.mouse.wheel(0, random.randint(300, 800)) page.wait_for_timeout(random.randint(1000, 3000)) def run(self, start_urls): 主运行函数 all_results [] for url in start_urls: print(f\n开始处理: {url}) data self.scrape_news_page(url) all_results.extend(data) # 在请求之间加入随机延迟模拟用户浏览间隔 delay random.randint(10, 30) # 10-30秒 print(f 等待 {delay} 秒后继续...) time.sleep(delay) # 保存结果 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) filename fnews_data_{timestamp}.json with open(filename, w, encodingutf-8) as f: json.dump(all_results, f, ensure_asciiFalse, indent2) print(f\n所有数据已保存至: {filename}) return all_results # 使用示例 if __name__ __main__: # 可以配置代理 proxy_config None # proxy_config {server: http://your-proxy-ip:port} start_urls [ https://example-news-site-1.com/latest, https://example-news-site-2.com/tech, ] with StealthNewsScraper(headlessTrue, proxyproxy_config) as scraper: results scraper.run(start_urls) print(f总共采集到 {len(results)} 条新闻。)这个案例集成了我们讨论的多个关键点Stealth 集成在每个新创建的页面上都应用了stealth_sync。指纹管理通过create_stealth_page方法为每个任务甚至每次重试创建独立的、带有随机但合理指纹的浏览器上下文。人类行为模拟包含了随机等待、随机滚动。健壮性设计重试机制scrape_news_page方法包含重试逻辑单次失败不会导致整个脚本崩溃。备用选择器在主要选择器失效时尝试备用方案。超时处理使用了PlaywrightTimeoutError来捕获元素加载超时并优雅地跳过该条目。节奏控制在请求间加入了长时间的随机延迟 (time.sleep(random.randint(10, 30)))这是降低被封风险最关键的措施之一。资源清理使用with语句和try...finally确保浏览器上下文和浏览器实例被正确关闭避免资源泄漏。6. 常见问题与排查技巧实录即使准备充分在实际运行中你仍可能遇到问题。下面是一些常见坑点及解决方案。6.1 Stealth 插件“失效”了现象应用了 Stealth但检测网站如bot.sannysoft.com仍然显示部分失败红色。排查步骤检查执行顺序确保stealth_sync(page)在page.goto()之前被调用。Stealth 需要在页面加载任何网站脚本之前注入其伪装代码。检查上下文Context如果你在创建页面后手动修改了某些属性例如通过page.add_init_script注入其他脚本可能会意外覆盖 Stealth 的设置。确保 Stealth 是最后一个对相关属性进行修改的脚本。更新插件和浏览器确保你使用的playwright-stealth和playwright都是最新版本。反检测技术在不断进化旧版本的伪装可能已过时。使用无头模式有些检测对无头模式headlessTrue特别敏感。即使 Stealth 尽力伪装无头模式仍有一些难以完全掩盖的特征如navigator.plugins长度可能为0。对于极其严格的网站可以尝试在调试时使用有头模式 (headlessFalse) 观察是否通过。生产环境如果必须无头可以考虑使用headlessshell新版 Playwright 选项或args: [--headlessnew]它比旧的无头模式更隐蔽。查看控制台日志在page.goto()后使用page.on(console, lambda msg: print(msg.text))监听控制台输出。有些检测脚本会将警告或日志打印到控制台这能给你提供线索。6.2 脚本运行速度慢或不稳定可能原因及解决过多的等待时间page.wait_for_timeout()是同步阻塞的。如果脚本中有大量固定或随机等待总运行时间会很长。优化策略是尽量使用事件驱动的等待如page.wait_for_selector(),page.wait_for_load_state(networkidle)它们只在条件满足前等待。资源泄漏没有正确关闭BrowserContext和Browser。每个未关闭的上下文都会占用内存和 CPU。务必使用with语句或try...finally块确保清理。代理问题如果使用了代理 IP速度慢和不稳定可能是代理质量导致的。考虑使用付费的优质代理服务并在脚本中实现代理健康检查如访问一个测速网站和自动切换机制。选择器性能避免使用page.locator(非常复杂且不稳定的CSS选择器)这可能导致 Playwright 需要花费大量时间计算和重试。尽量使用id、># 使用已有用户数据目录启动连接到一个真实的Chrome配置文件 browser p.chromium.launch_persistent_context( user_data_dir/path/to/your/chrome/profile, headlessFalse, args[f--remote-debugging-port9222] # 可选用于调试 ) # 注意这种方式下Stealth 插件可能不是必须的因为环境本身就是真实的。6.4 调试与日志记录建立一个好的调试习惯能事半功倍。开启慢动作和录制在调试阶段使用slow_mo参数让所有操作慢下来方便观察。browser p.chromium.launch(headlessFalse, slow_mo100) # 每个操作延迟100毫秒或者使用playwright codegen命令打开一个录制工具手动操作一遍它会生成对应的脚本。失败时截图和保存页面状态在catch异常块中保存页面源代码和截图这是分析问题最直接的证据。except Exception as e: timestamp int(time.time()) page.screenshot(pathferror_{timestamp}.png, full_pageTrue) html page.content() with open(ferror_{timestamp}.html, w, encodingutf-8) as f: f.write(html) print(f错误已保存至 error_{timestamp}.[png|html]) raise e # 重新抛出异常或进行其他处理监控网络请求有些检测是通过发送特定的网络请求到后端进行分析的。监听网络请求可以帮助你理解网站的检测逻辑。page.on(request, lambda request: print(f {request.method} {request.url})) page.on(response, lambda response: print(f {response.status} {response.url})) # 注意这会产生大量日志建议仅用于针对性调试。将 Playwright Stealth 与合理的指纹管理、人类行为模拟以及健壮的工程实践相结合你的自动化脚本就能在绝大多数场景下实现“隐形出击”。记住没有百分之百的隐形目标是与真实用户流量混合得足够好让维护成本高于封禁你的收益。保持对目标网站的尊重合理控制访问频率和规模才是长期可持续之道。