反爬进入了”指纹识别+行为分析”深水区
如果你这两年还在用 requests + BeautifulSoup 的组合跑爬虫,大概率已经感受到了一个明显变化:以前换个 User-Agent、加点随机延迟就能稳定跑的任务,现在动不动返回 403 或者直接给你一个验证码页面。
这不是你的代码写错了,而是网站的反爬体系在 2025 到 2026 年间完成了一轮”降维升级”。以 Cloudflare Turnstile v4 为代表的验证服务,已经不再依赖简单的 JS Challenge,而是通过 JA4 指纹、TLS 握手特征、Canvas 渲染哈希、WebGL 参数、甚至鼠标移动轨迹和滚动节奏来做综合判断。阿里系的风控更进一步,叠了四层:WAF 层做 HTTP/2 指纹校验,行为层做轨迹分析,设备层做 Canvas 和音频指纹采集,业务层做账号维度的异常关联。
这意味着,Python 爬虫如果还停留在”发请求、解析 HTML”的思维模型里,基本上寸步难行。我们需要在两个方向上同时升级:渲染层要做到浏览器指纹级别的隐身,解析层要摆脱对 CSS 选择器和 XPath 的重度依赖,转向 AI 驱动的智能提取。这恰恰是 Playwright + LLM 解析这套组合在 2026 年成为主流方案的原因。
为什么是 Playwright,而不是 Selenium 或 requests
很多团队从 Selenium 迁移到 Playwright,最初的理由可能只是”Playwright 更快”。但真正在反爬场景下用了一段时间后,你会发现这两者的差距远不止性能。
Selenium 的 WebDriver 架构决定了它天生带有一堆自动化痕迹:navigator.webdriver 属性为 true,CDP 连接特征明显,甚至连浏览器进程的启动参数都和正常用户不一样。虽然可以通过一些 hack 手段抹除部分特征,但每次 Chrome 更新都可能让这些补丁失效。Playwright 的优势在于它直接通过 CDP 协议控制浏览器内核,不经过 WebDriver 中间层,自动化痕迹本身就少很多。再加上 playwright-stealth 插件,可以在浏览器启动阶段就完成全特征混淆,而不是事后打补丁。
另一个关键点是 Playwright 对网络层的控制能力。它原生支持请求拦截、响应监听、Mock 返回,这意味着你可以直接拦截页面里的图片、广告等无关资源,大幅降低渲染时间和带宽消耗。在面对反爬验证时,你还可以监听网络请求来分析接口结构,比纯抓页面 HTML 要高效得多。
不过 Playwright 并非万能。它的并发能力受限于浏览器实例的内存开销,单机跑几十个 Context 就开始吃力了。这个问题我们在后面讲架构时会专门讨论。
浏览器指纹隐身:从 UA 伪装到底层混淆
先说一个常见误区:很多开发者以为只要在请求头里把 User-Agent 换成真实浏览器的就够了。在 2026 年的反爬体系下,这几乎是最低级的防护,检测系统根本不看 UA。
真正会被检测的指纹维度非常多,我列几个最关键的:
- Navigator 指纹:navigator.webdriver、navigator.plugins、navigator.languages 等属性的值和结构是否与真实浏览器一致
- Canvas 指纹:通过 Canvas API 绘制特定图形后提取哈希值,如果所有爬虫实例返回的哈希都一样,直接判定为自动化
- WebGL 指纹:GPU 信息、渲染器名称、着色器精度等参数是否合理
- TLS 指纹:握手阶段 Client Hello 包中的密码套件列表、扩展顺序、椭圆曲线选择是否符合真实 Chrome 特征(JA3/JA4 指纹)
- 行为指纹:鼠标移动轨迹是否过于直线、点击间隔是否固定、滚动是否有人类常见的加速减速
playwright-stealth 插件能帮你解决大部分静态特征问题,但行为指纹和 TLS 指纹需要额外处理。下面是一段在实际项目中使用的指纹隐身启动配置:
import asyncio
import random
from playwright.async_api import async_playwright
from playwright_stealth import stealth_async
async def create_stealth_context(playwright, proxy_url=None):
browser = await playwright.chromium.launch(
headless=True,
args=[
"--disable-blink-features=AutomationControlled",
"--disable-features=IsolateOrigins,site-per-process",
"--no-sandbox",
"--disable-dev-shm-usage",
]
)
context = await browser.new_context(
locale=random.choice(["zh-CN", "zh-TW"]),
timezone_id="Asia/Shanghai",
viewport={"width": random.choice([1366, 1440, 1920]),
"height": random.choice([768, 900, 1080])},
user_agent="Mozilla/5.0 ... Chrome/130.0.0.0 Safari/537.36",
proxy={"server": proxy_url} if proxy_url else None,
)
page = await context.new_page()
await stealth_async(page)
return page, context, browser
async def human_scroll(page):
for _ in range(random.randint(2, 5)):
await page.mouse.wheel(0, random.randint(200, 600))
await asyncio.sleep(random.uniform(0.4, 1.2))
这段代码做了几件事:通过 Chromium 启动参数关闭自动化控制特征标记,随机化视口和语言环境让每个实例看起来不一样,stealth_async 在页面层面注入 JS 抹除 navigator.webdriver 等标识,human_scroll 则模拟人类滚动行为。在实际项目中,如果你的目标站点行为检测比较严格,还需要加入鼠标贝塞尔曲线移动、随机点击偏移等更细致的模拟。
TLS 指纹的问题稍微复杂一些。Playwright 默认使用 Chromium 的网络栈,TLS 握手特征和真实 Chrome 基本一致,所以这个问题不像用 requests + urllib 那么严重。但如果你在代理转发的过程中改变了 TLS 行为(比如某些中间代理会重新做 TLS 握手),就可能暴露。这种情况下需要考虑使用 curl_cffi 这类支持模拟真实 Chrome TLS 指纹的 HTTP 库来做辅助请求。
AI 解析:用 LLM 替代脆弱的选择器规则
指纹隐身解决的是”能不能拿到页面”的问题,但拿到页面之后怎么提取数据,是另一个容易翻车的地方。
传统做法是写 CSS 选择器或 XPath,从 HTML 里抠目标字段。这种方式在页面结构稳定时很好用,但现实是——现在很多网站频繁改版,尤其是用 React、Vue 这类前端框架开发的页面,class 名经常是自动生成的(比如 class=”sc-1abc2def”),每次部署都可能变化。更麻烦的是,有些站点会故意做 HTML 结构混淆,加入大量干扰元素,让你的选择器规则失效。
2026 年越来越多团队开始用 LLM 来做数据提取。核心思路很直接:把页面渲染后的内容(可以是 HTML、纯文本、或 Markdown)丢给大模型,用自然语言描述你想要什么字段,让模型返回结构化的 JSON。
这种方案最大的价值不在于”更智能”,而在于鲁棒性。网站改版了、class 名变了、元素结构变了——只要页面内容的语义没变,LLM 照样能正确提取。这比维护一套随时会坏掉的选择器规则要省心得多。
下面是一个用 LLM 做结构化提取的简化示例:
from pydantic import BaseModel, Field
from openai import OpenAI
class ProductInfo(BaseModel):
title: str = Field(description="商品名称")
price: float = Field(description="当前售价,单位元")
original_price: float | None = Field(description="原价,如无则为空")
shop: str = Field(description="店铺名称")
sales: int | None = Field(description="月销量,如页面未显示则为空")
client = OpenAI()
def extract_product_data(page_text: str) -> ProductInfo:
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "从电商页面内容中提取商品信息,严格按照 schema 返回。"},
{"role": "user", "content": page_text},
],
response_format=ProductInfo,
)
return completion.choices[0].message.parsed
这里用到了 Pydantic 模型定义 + OpenAI 的 structured output 能力。模型会严格按照你定义的字段名和类型返回数据,不需要你写正则或选择器。当页面结构发生变化时,只要人类读者还能看懂商品名称和价格在哪,LLM 基本也能正确提取。
传统选择器 vs AI 解析:该用哪个
很多团队在接触 AI 解析方案后会产生一个误区:觉得可以彻底抛弃选择器,全盘用 LLM 提取。这个想法过于理想化了。LLM 解析的成本和延迟是实打实的——一次 API 调用可能要 1-3 秒,如果一个列表页有 50 条数据全部走 LLM,耗时和费用都会成问题。
| 维度 | 传统选择器 / XPath | LLM 智能解析 |
|---|---|---|
| 页面改版容忍度 | 低,选择器失效需立即修复 | 高,语义不变即可正确提取 |
| 单条提取延迟 | 毫秒级 | 1-3 秒(取决于模型和内容长度) |
| 单条成本 | 几乎为零 | 0.001-0.01 元(取决于 token 量) |
| 开发维护成本 | 高,每站需单独编写规则 | 低,统一 prompt + schema 即可 |
| 准确率稳定性 | 页面稳定时接近 100% | 90%-98%,偶尔需人工校验 |
| 适用数据量 | 适合大规模批量提取 | 适合中小量或结构复杂场景 |
更务实的做法是混合策略:对于结构相对稳定的字段(比如商品列表页每条商品的标题和价格),先用 Playwright 定位到列表容器的范围,然后用简单的选择器做初步提取;如果遇到结构变化导致提取失败,自动 fallback 到 LLM 解析。对于详情页这类结构复杂、字段多、各站差异大的场景,直接走 LLM,因为维护选择器的成本已经超过了 API 调用费用。
另一个实际场景是:有些团队做的是多站点通用采集,今天采集 A 平台、明天采集 B 平台,页面结构完全不一样。这种情况下,传统方式需要为每个站点单独写一套解析规则,而 LLM 只需要一套 schema 定义和一段通用的 prompt,适配成本几乎为零。这正是 FireCrawl、Crawl4AI 这类 2025 年兴起的开源爬虫框架的核心卖点——它们把 Playwright 渲染 + LLM 提取封装成了开箱即用的产品。
完整架构:从单机脚本到可扩展的采集系统
指纹隐身和 AI 解析解决的是单页面的获取和提取问题,但真正在项目中落地,你需要的是一套能稳定运行、可扩展、可监控的完整架构。我把实际项目中验证过的一套架构分享出来,供参考。
整体分为四层:
调度层负责任务分发。可以用 Redis 做任务队列,也可以直接上 Scrapy 的调度器。关键是每个任务要带上目标站点信息、采集深度、代理要求等上下文。
渲染层是 Playwright 实例池。这里踩过最大的坑是内存管理——Chromium 进程吃内存很猛,一个 Context 大约占 100-200MB,如果你的采集任务是长周期的(比如要滚动加载几百条数据),内存会持续增长直到 OOM。解决办法是设置 Context 的最大生命周期,定期回收重建。一般建议单个 Context 处理不超过 20 个页面后销毁重建。
提取层是 AI 解析 + 选择器 fallback。这一层需要做 token 控制——不是所有页面内容都丢给 LLM,可以先做一轮 HTML 清洗,去掉 script、style、nav、footer 等无关标签,把页面文本压缩到模型能高效处理的长度。对于长页面,可以做分段提取再合并。
存储层没什么特别的,结构化数据写数据库,原始 HTML 留一份到对象存储做回溯。建议保留原始页面快照,因为当你发现 LLM 提取的数据有误差时,需要能回到原始页面做排查。
一个典型场景:团队需要每天采集 30 个不同电商平台的商品数据,每个平台页面结构不同、反爬策略不同。如果用传统方式,需要为 30 个站点分别编写渲染脚本和解析规则,维护成本极高。用上面这套架构后,渲染层统一用 Playwright + stealth 处理反爬差异,提取层用统一的 LLM schema 定义,新增站点只需要配置一个 URL 模板和几条页面导航动作,一天就能上线一个新站点的采集。
实际踩坑经验
最后分享几个在实际项目中反复踩过的坑,可能帮你少走弯路。
坑一:stealth 插件不是万能的。playwright-stealth 能抹除大部分静态特征,但对于某些用 Wasm 做指纹采集的站点(比如 Akamai 的传感器脚本),它力不从心。遇到这类站点,可能需要配合更底层的指纹注入方案,或者考虑用真实浏览器配合远程控制的方式。
坑二:无头模式比有头模式更容易被检测。很多团队为了节省资源用 headless=True,但无头浏览器在某些特征上和有头浏览器有差异,比如某些 WebGL 扩展在无头模式下不可用。如果目标站点检测比较严格,建议用 headless=False 配合 Xvfb(Linux 下的虚拟显示器)来运行,既不需要真实屏幕,又能避免无头特征。
坑三:LLM 提取的准确率不是 100%。尤其是涉及数字精度、多币种、嵌套结构复杂的数据时,LLM 可能会出错。务必要在架构里设计一个校验层,对关键字段做范围检查和格式验证。比如商品价格不应该为负数、不应该超过某个合理区间,检测到异常时可以触发二次提取或人工审核。
坑四:代理 IP 的质量比数量重要。用一堆数据中心 IP 去访问严格风控的站点,换多少个都一样被拦。住宅代理的价格虽然贵 3-5 倍,但在高防护站点上的可用率差异是数量级的。一个经验数据:数据中心代理对某头部电商的可用率大约 15%,而优质住宅代理能到 85% 以上。
方案选型的几个判断
最后做一个务实的总结。如果你的采集需求是中小规模(日采几千到几万条数据),目标站点反爬强度中等,那么 Playwright + playwright-stealth + 本地 LLM 提取的组合就够用了,部署成本不高,维护负担也可控。
如果是大规模采集(日采百万级),且目标站点反爬很强,你需要认真考虑分布式架构:Playwright 渲染节点做水平扩展,代理池做动态调度,LLM 提取层用批量推理来降低单条延迟。这个阶段,可能 Scrapy + Playwright 中间件的组合比纯 Playwright 脚本更适合做任务编排。
如果只是临时采集、数据量不大、站点反爬不强,requests + httpx 这类轻量方案加上简单的 UA 伪装可能就够了,没必要上来就重炮。技术选型的核心原则始终是——用最小成本解决问题,而不是用最复杂的方案证明自己能写复杂系统。
反爬和反反爬是一场持续的博弈。2026 年的趋势已经很清楚:指纹检测会越来越深、行为分析会越来越准、而 AI 解析会让数据提取从”写规则”变成”写描述”。能在这个趋势里站稳的方案,不是某个单一工具的胜利,而是渲染隐身 + 智能提取 + 工程化架构的体系化组合。希望这篇文章能帮你在这条路上少踩几个坑。
原创文章,作者:,如若转载,请注明出处:https://fudengji.cn/article/298/