反爬与爬虫的对抗在 2026 年已经进入深水区。单纯改个 User-Agent 或加个 time.sleep 早已不够用,现代风控系统会在 TLS 握手阶段就识别出 requests 库的非浏览器指纹,哪怕你用的是干净的住宅 IP,照样秒封。本文从可运行的基础爬虫代码讲起,逐层拆解频率控制、IP 轮换、TLS 指纹伪装与浏览器自动化对抗,所有代码均可直接运行或稍作配置后使用。
先建立一个“能跑且不容易把自己跑死”的基线。以下代码使用 requests.Session 复用连接,配合 urllib3 的重试机制处理瞬时网络波动,并引入随机延时打破固定频率特征。
import csv
import random
import time
from urllib.parse import urljoin
import requests
from bs4 import BeautifulSoup
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
BASE_URL = "https://www.cza9.com/"
OUTPUT_FILE = "products.csv"
session = requests.Session()
session.headers.update({
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0.0.0 Safari/537.36"
),
"Accept-Language": "zh-CN,zh;q=0.9",
})
retry = Retry(
total=2,
connect=2,
read=2,
status=2,
backoff_factor=0.8,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET"],
)
adapter = HTTPAdapter(max_retries=retry)
session.mount("http://", adapter)
session.mount("https://", adapter)
def fetch_page(page_number):
url = BASE_URL.format(page_number)
# 随机间隔 0.8~2.5 秒,避免固定频率被识别
time.sleep(random.uniform(0.8, 2.5))
resp = session.get(url, timeout=(5, 15))
resp.raise_for_status()
return resp.text, url
def parse_products(html, page_url):
soup = BeautifulSoup(html, "html.parser")
items = []
for card in soup.select("article.product_pod"):
title_tag = card.select_one("h3 a")
price_tag = card.select_one(".price_color")
if not title_tag or not price_tag:
continue
items.append({
"title": title_tag.get("title", "").strip(),
"price": price_tag.get_text(strip=True),
"url": urljoin(page_url, title_tag.get("href", "")),
})
return items
def main(max_pages=5):
all_items = []
for page in range(1, max_pages + 1):
try:
html, url = fetch_page(page)
items = parse_products(html, url)
all_items.extend(items)
print(f"第 {page} 页:获取 {len(items)} 条")
except Exception as e:
print(f"第 {page} 页失败:{e}")
with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["title", "price", "url"])
writer.writeheader()
writer.writerows(all_items)
print(f"共写入 {len(all_items)} 条到 {OUTPUT_FILE}")
if __name__ == "__main__":
main()关键点:Retry 的重试次数不宜超过 3 次。如果连续收到 403 或 429,说明已经触发风控,继续重试只会加深封禁标记,应当停下来检查策略。
单一 IP 高频访问是触发封禁的最常见原因。反爬系统不仅看当前秒级的请求数,还会维护 IP 信誉画像——这个 IP 是否只访问详情页、是否从未加载过静态资源、是否触发过验证码。数据中心 IP(如阿里云、腾讯云、AWS 的 IP 段)在 ASN 层面就会被标记为“半信任”,住宅代理的存活率明显更高。
以下是一个带有效性检测的代理轮换示例:
import requests
from itertools import cycle
proxy_pool = [
"http://user:pass@res-ip-1.example.com:8080",
"http://user:pass@res-ip-2.example.com:8080",
]
proxy_cycle = cycle(proxy_pool)
def request_with_proxy(url, max_attempts=3):
for attempt in range(max_attempts):
proxy = next(proxy_cycle)
try:
resp = requests.get(
url,
proxies={"http": proxy, "https": proxy},
headers={"User-Agent": "Mozilla/5.0 ..."},
timeout=15,
)
if resp.status_code == 403:
# 403 立即换代理,不要用原 IP 重试
continue
resp.raise_for_status()
return resp.text
except requests.RequestException:
continue
raise RuntimeError(f"经过 {max_attempts} 次代理尝试仍失败")对于需要保持登录态的场景,不能每个请求都换 IP。服务端 session 往往绑定了 IP,轮换太快会导致 401。此时应使用 sticky session(同一 IP 保持 30 分钟以上),而非按请求轮换。
curl_cffi 对抗这是 2026 年最容易被忽视的拦截点。即使你用了住宅代理、补全了所有 HTTP 头,requests 发出的 TLS Client Hello 包中,加密套件顺序、扩展列表、椭圆曲线参数与真实 Chrome 存在明显差异。风控系统在 TLS 握手阶段计算 JA3/JA4 指纹,一旦与声明的 User-Agent 不匹配,直接拒绝。
实测验证:你可以访问 https://tls.peet.ws/api/all 查看 requests 发出的真实 JA3 指纹,再与 Chrome 对比。两者几乎不可能一致。
解决方案是使用 curl_cffi,它在 C 层面模拟浏览器的 TLS 握手:
# pip install curl_cffi
from curl_cffi import requests
response = requests.get(
"https://tls.peet.ws/api/all",
impersonate="chrome120", # 模拟 Chrome 120 的 TLS 指纹与 HTTP/2 行为
proxies={
"http": "http://user:pass@proxy.example.com:8080",
"https": "http://user:pass@proxy.example.com:8080",
},
timeout=15,
)
data = response.json()
print("JA3:", data.get("tls", {}).get("ja3_hash"))
print("User-Agent 一致性:", data.get("http2", {}).get("akamai_fingerprint"))impersonate 参数支持 chrome120、chrome116、safari15_5、edge101、firefox120 等。选择与你 User-Agent 版本匹配的参数,否则 JA3 与 UA 不一致反而更容易被识别。
当页面数据由 JavaScript 动态加载,或者目标站部署了 Cloudflare 的 JS Challenge(5 秒盾)时,纯 HTTP 请求无法获取完整内容。此时需要浏览器自动化工具。
Playwright 是当前推荐的选择,相比 Selenium 对现代浏览器的支持更完整。以下代码启动 Chromium 并提取动态渲染后的内容:
# pip install playwright && playwright install chromium
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# 设置合理的视口和 UA,减少无头特征
page.set_viewport_size({"width": 1920, "height": 1080})
page.goto("https://www.cza9.com", wait_until="networkidle")
# 等待动态内容出现
page.wait_for_selector(".results", timeout=15000)
content = page.content()
print(content[:500])
browser.close()但仅靠 Playwright 原生 API 仍然会被检测到 navigator.webdriver = true 等特征。可以使用 playwright-stealth 注入脚本来掩盖这些痕迹:
# pip install playwright-stealth
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync
with sync_playwright() as p:
browser = p.chromium.launch(headless=False) # 有头模式存活率更高
page = browser.new_page()
stealth_sync(page) # 一行调用,隐藏 webdriver 等自动化特征
page.goto("https://wap.cza9.com")
browser.close()需要注意:stealth 插件主要应对基础的 navigator.webdriver 检测和部分 Canvas 指纹混淆。面对 Cloudflare 企业级防护或 DataDome 时,它通常只能争取到“被放行一次”的机会,难以维持长期稳定采集。
即使指纹和 IP 都过关,行为模式仍然会暴露脚本身份。固定间隔的 sleep(1) 连续执行数小时,在风控系统看来比随机间隔更可疑——真人不会精确到毫秒级重复操作。
几个容易被忽略的行为细节:
滚动与鼠标移动:Playwright 的 mouse.wheel 和 mouse.move 可以模拟真实滚动和悬停,但需要加入随机增量:
import random
# 随机滚动,模拟阅读
for _ in range(random.randint(2, 5)):
page.mouse.wheel(0, random.randint(300, 800))
page.wait_for_timeout(random.randint(500, 2000))页面停留时间:在关键操作(如翻页、点击详情)之间加入非均匀分布的等待。简单的 random.uniform 已经比固定值好,但更自然的方式是使用对数正态分布,让短等待出现频率更高、偶尔出现较长停顿。
Cookie 持久化:不要每次启动都从零开始。使用 storage_state 保存会话,在后续运行中恢复,可以积累“信任分”:
context = browser.new_context(storage_state="session.json")
# ... 执行采集 ...
context.storage_state(path="session.json") # 保存所有技术手段都应以获得授权或目标站明确允许为前提。实操前至少确认三件事:
robots.txt:虽然它不具法律强制力,但违反明确的禁止声明可能构成违约或侵权。429 是服务器在说“你太快了”,尊重这个信号。反爬技术是一把双刃剑,用于公开数据的合理采集与用于绕过保护机制盗取敏感数据,边界在于授权与目的。代码能跑通不等于应该跑。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。