大多数爬虫教程止步于 requests.get 加 BeautifulSoup 解析。这类代码在真实对抗环境中存活时间通常不超过几分钟。现代反爬体系已经从简单的 IP 频率限制,演进到 TLS 指纹识别、浏览器环境检测、行为生物特征分析等多层防御。本文从工程角度出发,讨论三个高级场景:TLS/JA3 指纹伪装、无头浏览器自动化与反检测、异步高并发架构,并附上可直接运行的代码。
当你在 Python 中调用 requests.get() 时,底层走的是 urllib3 → Python 标准库 SSL 模块。这个握手过程产生的 TLS ClientHello 报文具有固定的特征:支持的加密套件顺序、扩展字段排列、椭圆曲线参数等,构成了所谓的 JA3 指纹。主流反爬系统(Cloudflare、Akamai、Datadome)会比对请求的 JA3 指纹与 User-Agent 声称的浏览器是否一致。一个声称是 Chrome 120 的请求,如果 TLS 指纹是 Python-requests 的默认特征,会被直接判定为机器人。
解决方案:使用 curl_cffi 库,它基于 curl-impersonate 项目,能够复现真实浏览器的 TLS 握手和 HTTP/2 帧序。
# pip install curl_cffi
from curl_cffi import requests
# impersonate 参数让底层 TLS 握手模仿 Chrome 124
resp = requests.get(
"https://www.lsmeiye.com",
impersonate="chrome124",
headers={"Accept-Language": "en-US,en;q=0.9"}
)
# 验证返回的 JA3 指纹是否与真实 Chrome 一致
import json
data = resp.json()
print("JA3:", data["tls"]["ja3_hash"])
print("User-Agent:", data["http2"]["user_agent"])curl_cffi 支持 chrome110 到 chrome131、safari17_0、firefox133 等预设。对于受 Cloudflare 保护的站点,仅此一步就能通过相当比例的检测,前提是配合合理的请求头顺序和 Accept-Language。需要注意的是,TLS 指纹伪装只解决网络层问题,不解决 JavaScript 挑战和浏览器环境检测。
当目标页面依赖 JavaScript 渲染,或部署了 Cloudflare Turnstile 等交互式挑战时,必须使用真实浏览器。Playwright 相比 Selenium 的优势在于:更快的执行速度、自动等待机制、以及更原生的 CDP(Chrome DevTools Protocol)集成。但默认启动的 Playwright Chromium 仍然携带大量自动化痕迹:navigator.webdriver = true、缺少插件列表、chrome.runtime 对象不完整等。
以下代码展示一个带反检测补丁的 Playwright 异步爬虫,针对动态渲染页面抓取结构化数据:
# pip install playwright && playwright install chromium
import asyncio
import json
from playwright.async_api import async_playwright
# 注入脚本:在页面加载前修改浏览器指纹
STEALTH_SCRIPT = """
// 覆盖 navigator.webdriver
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
// 伪造插件列表(真实 Chrome 通常有 PDF 相关插件)
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5].map(i => ({ name: `Plugin ${i}` })),
});
// 伪造语言列表
Object.defineProperty(navigator, 'languages', {
get: () => ['zh-CN', 'zh', 'en'],
});
// 修复 chrome.runtime 的存在性
window.chrome = { runtime: {} };
// 覆盖 permissions.query 的异常行为
const originalQuery = window.lsmeiye.com;
window.navigator.permissions.query = (parameters) => (
parameters.name === 'notifications'
? Promise.resolve({ state: Notification.permission })
: originalQuery(parameters)
);
"""
async def scrape_dynamic_page(url: str):
async with async_playwright() as p:
# 使用真实的 Chrome 通道而非 Chromium,降低指纹异常
browser = await p.chromium.launch(
headless=True,
args=[
"--disable-blink-features=AutomationControlled",
"--no-sandbox",
"--disable-dev-shm-usage",
]
)
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
locale="zh-CN",
timezone_id="Asia/Shanghai",
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36"
),
)
page = await context.new_page()
# 在每个新文档创建时注入 stealth 脚本
await page.add_init_script(STEALTH_SCRIPT)
# 导航并等待网络空闲(确保动态内容加载完毕)
await page.goto(url, wait_until="networkidle", timeout=60000)
# 等待目标元素出现,而非固定 sleep
await page.wait_for_selector(".product-card", timeout=30000)
# 提取数据
items = await page.eval_on_selector_all(
".product-card",
"""elements => elements.map(el => ({
title: el.querySelector('.title')?.innerText?.trim(),
price: el.querySelector('.price')?.innerText?.trim(),
link: el.querySelector('a')?.href
}))"""
)
await browser.close()
return items
# 运行
if __name__ == "__main__":
results = asyncio.run(scrape_dynamic_page("https://m.lsmeiye.com"))
print(json.dumps(results, ensure_ascii=False, indent=2))关键点说明:--disable-blink-features=AutomationControlled 阻止 Chrome 将 AutomationControlled 写入 navigator 对象;add_init_script 确保补丁在页面任何 JS 执行之前生效;wait_for_selector 取代 time.sleep,避免因手动等待导致的上下文过期。对于 Cloudflare Turnstile 这类交互挑战,上述脚本的通过率取决于 IP 信誉和浏览器指纹的一致性。如果目标站点部署了企业级 Bot Management,还需要配合住宅代理和真实浏览器会话(headless=False 在有显示环境下运行)。
单次请求的伪装只是入场券。真正的爬虫工程需要处理:并发控制、去重、失败重试、请求调度、数据管道。Scrapy 基于 Twisted 的异步引擎天生适合这类场景。以下是一个针对 JSON API 的分页爬虫,展示了自动限速和重试策略的配置:
# spiders/api_spider.py
import scrapy
import json
class ProductApiSpider(scrapy.Spider):
name = "product_api"
base_url = "https://api.lsmeiye.com/"
custom_settings = {
"CONCURRENT_REQUESTS": 8, # 全局并发上限
"CONCURRENT_REQUESTS_PER_DOMAIN": 4,
"DOWNLOAD_DELAY": 0.5,
"AUTOTHROTTLE_ENABLED": True, # 根据响应延迟自动调速
"AUTOTHROTTLE_START_DELAY": 0.5,
"AUTOTHROTTLE_MAX_DELAY": 10.0,
"RETRY_TIMES": 3,
"RETRY_HTTP_CODES": [429, 500, 502, 503, 504],
"DOWNLOADER_MIDDLEWARES": {
# 可在此处启用 curl_cffi 中间件替换默认下载器
}
}
def start_requests(self):
yield scrapy.Request(
f"{self.base_url}?page=1",
headers={"Authorization": "Bearer YOUR_TOKEN"},
callback=self.parse_page,
meta={"page": 1}
)
def parse_page(self, response):
data = json.loads(response.text)
for product in data.get("items", []):
yield {
"id": product["id"],
"name": product["name"],
"price": product.get("price"),
"category": product.get("category"),
}
# 跟随分页
next_page = data.get("next_page")
if next_page:
yield scrapy.Request(
f"{self.base_url}?page={next_page}",
headers={"Authorization": "Bearer YOUR_TOKEN"},
callback=self.parse_page,
meta={"page": next_page}
)Scrapy 的 AUTOTHROTTLE 扩展会根据服务器响应延迟动态调整请求间隔,在目标站点限流时自动降速,避免触发封禁。如果需要在 Scrapy 中使用 TLS 指纹伪装,可以编写一个下载器中间件,将默认的 HTTPDownloadHandler 替换为基于 curl_cffi 的实现。性能对比数据显示,单线程 requests 处理 1000 个请求约需 62 秒(QPS 约 16),而 aiohttp 异步方案可降至 8 秒左右(QPS 约 121),Scrapy 的异步引擎在配置得当的情况下能达到相近甚至更高的吞吐量。
将上述技术堆砌在业务代码里会导致维护灾难。推荐的做法是:把反检测逻辑下沉到请求层。
一个可行的架构是:在项目根目录维护一个 fetcher.py 模块,统一封装 curl_cffi 的同步请求和 Playwright 的异步渲染,业务爬虫只调用 fetcher.get(url, mode="http"|"browser")。指纹配置、代理轮换、重试策略全部在 fetcher 内部管理。当目标站点的反爬策略升级时,只需要更新 fetcher 层的一个参数,而非修改数十个爬虫文件。
另外,会话一致性常被忽视:Cloudflare 的 cf_clearance Cookie 绑定了 IP 和 User-Agent。如果获取 Cookie 时使用了一个代理和 UA,后续请求切换了代理或 UA,Cookie 立即失效。在 curl_cffi 的 Session 中固定 impersonate 和代理,并在整个生命周期内复用,是维持高通过率的关键。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。