☰
Playwright电商爬虫实战:五大反爬绕过技巧与工程化落地
2026/10/22 6:27:43 网站建设 项目流程

我一直觉得,搞电商数据采集的朋友,这两年应该都有一个共同感受:以前用requests写几十行脚本就能拉回一堆数据的日子,越来越远了。现在打开任何一个主流电商站点,页面里密密麻麻的 JS 渲染逻辑、动态加密参数、行为验证码,动不动就把你的脚本卡死在第一步。也就是在这种环境下,我开始把重心彻底转到 Playwright 这类浏览器自动化方案上,并且在实战里慢慢沉淀出一套能稳定绕过反爬的招法。

这篇博文不聊虚的,就围绕一个核心主题:用 Playwright 做电商爬虫时,怎么系统性地处理反爬对抗。我会把这套实战经验拆成 5 个技巧,从环境指纹、启动参数、交互行为到验证码处理和任务恢复,全部讲透。内容适合那些已经在用爬虫框架、但经常被验证码和封 IP 搞到崩溃的开发者。就算你是只写过简单脚本的新手,把后面这些思路和代码拿过去,也能直接少走几个月的弯路。

1. 为什么电商采集这一轮我选 Playwright 而不是 requests

1.1 动态渲染页面才是主战场

先说个很直观的现象。现在电商网站的前端页面,大量数据是由 JavaScript 在浏览器里面动态拼出来的。你requests.get拿到的 HTML 里,往往只有个空壳子,数据藏在形形色色的异步接口里,而调用这些接口往往还要带着时间戳、签名甚至加密参数。这意味着,如果你想继续用纯 HTTP 请求,就得把每一个接口的加密逻辑都逆向出来,维护成本指数级上升,签名算法一变就得连夜改代码。

我之前也做过一段时间的逆向,把某个平台的搜索接口、详情接口全部抠出来,自己构造curl去请求。后面发现一个问题:风控策略升级后,服务器端的校验不只是看请求头和签名,还会分析你的行为序列,包括请求频率、User-Agent 的指纹、甚至 TLS 握手特征。这种时候,普通 HTTP 客户端无论如何模拟,都很容易被识别出来。而在真实浏览器里执行的 Playwright,在服务端看来就是一个正常用户在访问,天然规避了低层特征的检测。

有人可能会说,既然都是跑浏览器,Selenium 不也行吗?是的,Selenium 确实能用,但它的问题在于生态老化、安装麻烦,浏览器驱动还要单独下载管理,如果是做分布式采集,光是环境部署就够你折腾半天。Playwright 在这些方面要省心得多。

1.2 Playwright 对比 Selenium 和 Puppeteer 的几个真实差距

先直接亮观点:同样是浏览器自动化,Playwright 在三个维度上碾压了 Selenium。

第一是安装体验。pip install playwright之后就一条playwright install chromium,浏览器版本、运行依赖全都自动配好,不像 Selenium 还要手动找驱动。我另外常备一个 Python 3.10 的虚拟环境专门跑采集,环境干净,出了问题直接删了重建,成本很低。

第二是自动等待机制。Selenium 里你要自己写WebDriverWait,写多了就像在代码里撒跳跳糖,到处都是繁琐的等待逻辑。Playwright 的page.click()、page.fill()天然内置 Actionability 检查,元素不可见、被遮挡、处于 loading 状态时它会自动重试,直到稳定可操作。这个特性在采集动态加载的电商页面时太重要了,省掉了我大量的无用功。

第三是浏览器的 Context 隔离。Playwright 每一个browser_context都是完全独立的“浏览器实例”,Cookie、缓存、LocalStorage 互不相通,而且创建销毁速度极快。做多账号、多会话采集时,我可以给每个任务分配一个全新的浏览器环境,不需要重启浏览器进程,这种灵活度 Selenium 给不了。

至于 Puppeteer,它只绑定 Node.js,而且它的协议封装没有 Playwright 这么干净,如果你主要在 Python 生态里混,选 Playwright 是毫无悬念的。

2. 核心实战:绕过反爬的五个可复用技巧

电商场景里的反爬,说白了就是“如何证明你是你、且不是机器人”。风控系统会从指纹、行为、网络三个维度打分,分数高了就弹验证码或直接拒绝访问。针对这套逻辑,我这 5 个技巧做了一个组合拳式的应对,层层递进,保证你在大多数站点上都能打出不错的稳定率。

2.1 技巧一:浏览器指纹统一伪装

服务端采集的浏览器指纹,远比你想的要多。它是很多维度的集合:UA、语言、时区、屏幕分辨率、色彩深度、WebGL 渲染器信息、Canvas 哈希值、音频指纹等等。它们在单个维度上看着都正常,但组合在一起却可能暴露自动化环境。

最典型的问题就是“不一致”。比如你的 User-Agent 是 Windows 平台的 Chrome 111,但navigator.platform却返回Linux x86_64,或者你的系统时区设置的是 UTC,但语言偏好却是中文简体。这些矛盾信息在风控模型里就是明显的机器人标志。真人浏览器不会出现这种自相矛盾。

我目前的处理方式是在创建上下文时集中统一设置:

from playwright.sync_api import sync_playwright UA = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" ) context = browser.new_context( user_agent=UA, locale="zh-CN", timezone_id="Asia/Shanghai", viewport={"width": 1920, "height": 1080}, screen={"width": 1920, "height": 1080}, color_scheme="light", )

这些参数不仅仅影响 HTTP 请求头,还会同步改变浏览器内部对象的返回值。设置好之后,navigator.userAgent、navigator.language、new Date().getTimezoneOffset()都是配套的,指纹一致性就稳了。

再补充两个小细节。第一,viewport 和 screen 的尺寸最好错开一点,不要都设置成同样大小的整数,很多无头浏览器会默认用 800x600 之类的规整分辨率,容易被看穿。第二,很多站点会调用 WebGL 获取显卡渲染器信息,如果需要更高强度,可以考虑往上下文注入一段初始化脚本去修改 WebGL 的返回值,但这种做法的维护成本偏高,除非是被重点盯防的大站,否则前面的基本设置已经够用。

2.2 技巧二:抹除自动化特征与启动参数调优

很多人以为 Playwright 是无痕的,其实它在浏览器里会留下明显的自动化痕迹。最典型的就是navigator.webdriver这个属性,在自动化环境里是true,普通浏览器里是undefined。稍微严谨一点的站点,第一层检测就是这个。

处理方式有两种。一个是无视它,靠别的策略转移注意力;另一个是主动抹除特征。我建议直接初始化脚本覆盖:

context.add_init_script( """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); """ )

但这只是第一步。Chrome 本身还有一些命令行开关和内部标识,暴露了自动化状态。一种常见做法就是往启动参数里加--disable-blink-features=AutomationControlled,并屏蔽掉那些外围的自动化相关组件。

browser = pw.chromium.launch( headless=True, args=[ "--disable-blink-features=AutomationControlled", "--disable-features=IsolateOrigins,site-per-process", "--no-sandbox", ], )

这里有个经验要强调:不要一上来就无头模式。调试阶段建议先跑有头模式,打开headless=False,肉眼盯着页面的渲染情况,确认选择器和行为逻辑都正常,再切回无头模式做量产。为什么?因为无头模式下很多显卡渲染、字体加载、动画表现和真实环境不一样,同一个元素在无头和正常模式下可能大小、位置都不同,直接切无头调试会让你疯掉。

除了启动参数,你还可以在浏览器层面做一些“去特征化”的处理。比如去掉--enable-automation这个默认开关带来的页面顶部提示条,以及隐藏chrome://flags里的某些自动化设置。用 Playwright 的 API 层面来规范化这些内建标识,就能让浏览器环境更接近普通用户的真实状态。

2.3 技巧三:设计“像人”的交互行为序列

指纹伪装只是静态信息,行为序列才是真正的动态考核。我在实际测试里观察过,有些站点你跳动鼠标、滚轮、点击行为的间隔和轨迹,如果过于机械化,哪怕指纹伪装得再好,也会在半分钟内触发风控。

所以设计的核心是让操作路径符合人类的阅读节奏。你不能一进页面就直直奔着搜索框去输入关键词、回车、立刻抓取数据,那太“目标明确”了。正常用户会先滚动一下页面,看几眼推荐区的商品,再缓慢滑到搜索框,中间可能还会停顿思考。

我实践下来比较有效的组合是,用page.mouse模拟轨迹,并往真实操作之间插入随机停顿。例如先滚动几百像素,停 0.6 秒,再滚动几百,然后移动鼠标到一个商品卡片悬停一下,最后在页面上停留两三秒再点击。

page.mouse.move(500, 300) page.wait_for_timeout(800) page.mouse.move(700, 450, steps=15) page.wait_for_timeout(400) page.mouse.move(400, 600, steps=10) page.mouse.wheel(0, 800) page.wait_for_timeout(1200)

关键是每次移动都要加steps参数,让鼠标轨迹有平滑的中间插值,而不是瞬移。而且在两次大的行为之间用随机数控制时间间隔,比如random.uniform(0.8, 2.0)秒,不要让停顿间隔形成固定规律。

滚动和悬停也有讲究。很多电商网站是懒加载模式,你必须一直往下滚动才能让后面的列表内容渲染出来。而我试过直接一次性滚到最底部,页面加载的反而是混乱的。正确做法是分段滚动、每段之间随机暂停。并且滚动幅度符合人工习惯,比如从 600px 到 1000px 再回到 800px,这种“回头看一眼”的行为会大幅度拉高你的行为得分。

2.4 技巧四:验证码分级处置与冷却策略

不管你的行为模拟得多像人,总会有触发验证码的时候。这里我的原则不是“彻底不让验证码出现”,而是用工程化的方式把它变成一个可控故障而不是流程终结者。提前把所有可能遇到的验证码场景做分类,并给每一类设计好解决方案。

第一类是滑动验证码。电商站点最常见的滑块,需要把滑块从起点拖到缺口位置。Playwright 里可以用drag_to或者直接操作坐标定位。但要注意,直接一步拖到位是最容易被识别的。真人拖动滑块会经历一个“先快后慢、最后微调”的过程,而且可能在终点附近来回小幅调整几次。我用mouse.move分多段移动,最后再往回调整一点点,让轨迹更自然。

第二类是图像点选类验证码(比如“请点开图中的XX”)。这种就得借助 OCR 或简单图像模型了。Python 这边可以用离线识别模块先跑一轮,识别结果出来后定位坐标点击。如果识别成功率低,就把它抛到一个“人工验证队列”,由人工接管处理。

第三类是网页里的行为验证码,比如滑块和点击之间夹杂的“旋转验证”。这种通常是不常见的场景。我的原则是优先冷却,不跟它硬刚直接就暂停该任务,等个十几分钟再重试。

比较重要的一点是验证码处理后的冷却策略。我之前犯过一个低级错误:识别成功后马上继续下一轮请求,结果没过多久又触发验证码,几分钟就被限制了。后来我养成了一个习惯,验证码通过之后,至少等 5-10 秒再发起下一步操作,让行为自然过渡,别再一进来就急着点搜索。

如果同一个账号反复触发验证码,就直接停止这个会话,换一个新的上下文重新开始,不要让同一条链路连续失败而浪费太多时间。

2.5 技巧五:断点续抓与任务状态机设计

做大规模采集最怕的不是慢,而是跑到一半任务挂了。进程崩溃、网络抖动、验证码卡死、代理超时,随便一个因素都能让整个采集流程尸横遍野,最后除了 log 全无用处。针对这一点,我把整个采集过程改造成一个“状态机驱动的任务系统”,并且在每一个关键节点进行状态落盘。

简单说,我会在本地用 SQLite 维护一个任务表:

字段含义
task_id任务唯一编号
url待采集的商品或详情页地址
statuspending / running / done / failed
retry_count当前重试次数
last_error最后一次的错误信息
update_time最后更新时间

采集流程从一个队列里取pending的任务,处理时把它置为running,成功获取数据并落盘后置为done。如果过程中出现异常,就检查重试次数,少于上限则加冷却时间重新入队;超过上限就置为failed并记录原因。这样即使整个脚本第二天才重新启动,也能从上次中断的位置继续,不会重复采集也不会漏掉数据。

还有一个很多人忽略的点——Cookie 持久化。电商站点的登录态来之不易,如果因为脚本重启就丢掉会很可惜。Playwright 提供了storage_state参数,可以把 Cookie 保存到本地文件里,重启后加载:

context = browser.new_context( storage_state="cookie_state.json" if os.path.exists("cookie_state.json") else None )

配合定时保存,就能保持一个长期存活的会话。这样一来,只要有任务还在跑,我就不会因为重启脚本而频繁重登。

3. 完整跑通:一次商品搜索页采集的落地过程

3.1 场景设定与环境准备

为了把上面 5 个技巧串起来,我拿一个具体的例子说。目标站点是一个国内常见的电商网站(这里不点名,但逻辑是通用的),我要采集某个关键词下的搜索结果列表,包括商品标题、价格、月销量、店铺名和后缀链接。

环境方面,我准备了一台 4 核 8G 的云主机,系统是 Ubuntu 22.04,Python 版本 3.10。安装依赖就两行:

pip install playwright playwright install chromium

如果服务器缺少系统依赖,可以跑playwright install-deps chromium一次性解决。这一步在实际部署时最容易被卡,铺出来单独强调一下。

3.2 核心代码结构与关键配置

这里是核心代码骨架,我把上面的技巧都整合进去了:

import os import random import json import sqlite3 from playwright.sync_api import sync_playwright UA = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" ) def create_browser_context(browser, cookie_file=None): return browser.new_context( user_agent=UA, locale="zh-CN", timezone_id="Asia/Shanghai", viewport={"width": 1680, "height": 940}, screen={"width": 1920, "height": 1080}, storage_state=cookie_file if cookie_file and os.path.exists(cookie_file) else None, ) def crawl_search_page(keyword): with sync_playwright() as pw: browser = pw.chromium.launch( headless=False, args=["--disable-blink-features=AutomationControlled"] ) context = create_browser_context(browser, "cookie_state.json") context.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})") page = context.new_page() page.goto("https://example-shop.com/search?q=" + keyword) page.wait_for_timeout(2000) # 模拟人类的滚轮浏览 for _ in range(random.randint(2, 4)): page.mouse.wheel(0, random.randint(400, 600)) page.wait_for_timeout(random.uniform(800, 1500)) # 翻页前停留 page.wait_for_timeout(random.uniform(1000, 2000)) # 解析商品列表 items = page.query_selector_all("div.s-item") result = [] for item in items: title = item.query_selector("h4 a") price = item.query_selector("span.price") sales = item.query_selector("span.sales") result.append({ "title": title.inner_text() if title else "", "price": price.inner_text() if price else "", "sales": sales.inner_text() if sales else "", }) # 保存 Cookie with open("cookie_state.json", "w") as f: json.dump(context.storage_state(), f) browser.close() return result if __name__ == "__main__": data = crawl_search_page("蓝牙耳机") print(data)

这个结构看着不复杂,但它其实包含了自动等待、异常恢复、行为模拟、Cookie 持久化等多层设计。page.query_selector_all这种写法是同步 API 下的标准姿势,如果你用异步模式,记得把sync_playwright换成async_playwright,对应操作类也需要加await。

有个比较重要的细节:代码里的等待时间我全部用了page.wait_for_timeout而不是time.sleep。这两者在 Playwright 里作用不一样,前者会等待浏览器事件循环空闲之后才继续,后者是纯粹的 Python 阻塞,对页面交互没有任何感知。用wait_for_timeout能让程序的节奏更贴合浏览器真实运行状态。

3.3 数据去重与落盘方案

采集回来的数据,直接打进数据库的要注意去重。我的方案是给每条商品数据算一个md5哈希,以“平台+商品ID+标题前20个字符”作为唯一键。如果表里已有记录就先update,没有就insert,这样既保留了历史价格趋势,也不会产生重复记录。

增量采集的时候,这个方案尤其好用。每次脚本跑完都会输出一个汇总表,哪些商品是新出现的、哪些价格变动了、哪些商品已经下架,一目了然。

3.4 我踩过的三个坑

这部分要重点说,都是我实际遇到过的。

第一个坑是页面元素加载顺序问题。起初我用的选择器是直接从浏览器开发者工具里复制的完整 XPath,结果在脚本运行到第二页时突然定位失败。排查半天才发现,电商网站的列表项在滚动时会动态回收 DOM 节点,复用同一个 XPath 不保险。后来我改用基于 CSS 的选择器,并且配合wait_for_selector等待元素出现,问题才解决。

第二个坑是价格字段带上了奇怪的 Unicode 字符。有几次采集回来的价格字符串里多了全角空格和不间断空格\u00a0,直接丢给float()转换直接报错。处理时要用unicodedata.normalize或简单的正则清洗:

import re price = re.sub(r"[^\d.]", "", price_text)

第三个坑是登录状态丢失。我之前有个任务需要登录后才能查看完整的销量数据,一开始每次启动脚本都会通过扫码登录,后来发现storage_state保存的 Cookie 有时会过期。解决方式不是每次登录都重扫,而是用一个单独的脚本定期巡检会话状态,发现失效就提前唤醒,再保存一次新的 Cookie,把影响降到最低。

4. 高频问题排查手册

4.1 出手就被识别,连滑块都没看到

排查顺序:先看 UA 是否合理,再看 locale 和时区是不是配套,其次检查navigator.webdriver有没有被标记。如果这些都没问题,就换成有头模式看看页面加载过程,是不是一进入页面就被 406 拒绝或重定向到了验证页。我遇到过一个情况是网站对搜索引擎 UA 才会放行,普通 UA 直接跳转风险页,这种时候要针对站点特性做差异化配置。

4.2 元素定位老是超时或无结果

大概率是选择器写得太死或者页面用了 Shadow DOM。先在page.content()里确认页面 HTML 里确实包含目标数据,如果发现是 Shadow DOM,就要用 piercing 选择器或者先执行 JS 获取shadowRoot内容。还有一种情况是懒加载导致列表还没渲染你就开始查询,解决方法是让滚轮先走一轮,再开始查询。

4.3 采集速度上不去,CPU 和内存又吃紧

核心原因往往是并发开得太多。有人可能开了十几个浏览器上下文,结果每开一个页面就要吃几百 MB 内存。我实测下来,单机 8G 内存最多稳定跑 4 到 5 个并发上下文,再多就频繁 GC、页面卡死。建议用有限并发队列,控制单个 Context 的页面数量,并且开完即关。

另外优先跑headless=True,无头模式下资源占用能降低到有头的一半左右。但无头模式下指纹被识别的概率也会提升,这个平衡点要根据你的目标站点来定。在我这边实操来看,中等流量站点无头模式配合行为模拟完全够用,只有大型风控特别严格的站点才需要切换到有头模式加虚拟显示器方案。

5. 合规边界和工程化复盘

5.1 拿到数据之前先回答三个问题

聊到最后一节,我必须先把边界问题说清楚。爬虫这块越往后走,越不是纯技术问题。我给自己定下的底线是三不碰:不碰用户个人隐私数据、不碰平台非公开接口、不绕过账号体系去获取无权访问的内容。技术手段再漂亮,也要先过门槛。

关于 robots.txt 和平台服务条款,我建议在做采集之前先花十分钟读一遍目标网站的规范。电商页面里的商品名称、价格、销量这些公开信息,基本都归入可公开访问的范畴,适合做研究型采集;但如果你要把采集而来的数据直接商业化,那就要谨慎核实授权链条,别等数据落地了才发现有合规地雷。

5.2 从单脚本到批量任务的演进路径

很多人把爬虫写成了一个巨大的“一次性脚本”,跑完就扔。实际上,当采集规模上来后,我推荐把它拆分成几个独立的模块:抓取器、解析器、存储层、任务队列和监控告警。每个模块单独维护,任何一个环节挂了都不会影响全局。

任务队列和状态机的重要性前面已经提过,这里补充一下告警。花二十分钟给脚本接入一个简单的企业微信或邮件告警,当任务失败率超过阈值或者长时间没有新数据落库时,主动通知人介入,这比你在第二天早晨发现昨天一整晚都在空转要强得多。

5.3 技术边界与心态

最后说句实在话。反爬对抗本质上是攻防双方的一场动态博弈,没有一劳永逸的银弹。你今天总结出的技巧,可能在对方一次风控策略升级后就失效,但核心的方法论不会过时:保持指纹一致、模拟人类行为、让系统具备容错能力、控制节奏避免过度访问。这套组合拳打下来,大部分电商站点的公开数据采集需求都能稳定覆盖。

我个人在实际操作中最深的体会是:永远不要追求“100% 绕过率”,而是要把自己的采集行为控制在一个合理的边界内,在尊重站点服务能力的前提下获取数据。高并发、高频次的抓取既浪费资源,也容易被列入重点观察名单。放松一点,目标数据跑慢一点,但每条都稳稳落袋,这才是长期稳定运行的盈利之道。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询