从调试到稳定抓取:一次 Python requests 反爬突破的完整记录

从调试到稳定抓取:一次 Python requests 反爬突破的完整记录

引言

很多数据抓取项目在本地试跑一切正常,一上真实目标站就频繁 403/429、页面返回空数据或重定向到验证码页。本文选择“Python 编程语言”为主题,分享我用 requests 对接某资讯站点时,从出现问题到稳定抓取的完整调试过程与关键实现,希望为你提供一套可迁移的方法论。

背景与问题现象

  • 目标:按关键词抓取资讯搜索页的结果列表,并解析标题、链接和摘要。
  • 初版方案:直接用 requests.get(url) 抓取 HTML 再用选择器解析。
  • 现象:
    • 第一轮能拿到部分数据,稍微加快频率后迅速被 403 Forbidden。
    • 偶发 429 Too Many Requests,或被重定向到验证码页面。
    • 相同 URL 在浏览器可正常访问,说明“请求特征”被识别为爬虫。

排查步骤与思路

  1. 复现与最小化问题
    • 保留最小请求参数,只打印状态码、关键响应头、是否被重定向。
  2. 观察指纹差异
    • 对比浏览器与脚本:UA、Accept-Language、Accept、Referer、Cookie 是否缺失;是否启用了压缩;是否跟随重定向。
  3. 会话与 Cookie 持久化
    • 使用 requests.Session 复用连接、自动携带 Cookie,减少“冷启动”特征。
  4. 标准化请求头
    • 模拟常见浏览器头部,尤其是 User-Agent、Accept、Accept-Language、Referer、Cache-Control、Accept-Encoding。
  5. 限速与重试
    • 对 429/5xx 实施指数退避重试;为连接错误配置 Retry;在成功-失败之间加抖动延时。
  6. IP 维度治理(可选)
    • 使用稳定代理池,遇到持续性 403 时切换出口;注意代理质量与合规。
  7. 动态内容与 JS 渲染
    • 若页面主要数据由前端接口渲染,优先直连 API;实在需要可引入 Playwright/Selenium,但要评估成本。
  8. 合规与友好
    • 尊重目标站 robots/ToS,设置合理频率与缓存,必要时申请正式数据接口。

最小复现代码(问题版)

1
2
3
4
5
6
7
import requests

url = "https://example.com/search?q=python" # 替换为目标地址
r = requests.get(url, timeout=10)
print(r.status_code, r.is_redirect)
print(r.headers.get("Server"), r.headers.get("Retry-After"))
print(r.text[:200]) # 可能是验证码页或空数据

关键修复实现(稳定版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# language: python
import random
import time
from typing import Optional, Dict
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

DEFAULT_HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Cache-Control": "no-cache",
}

class StableFetcher:
def __init__(self, base_delay: float = 0.6, max_delay: float = 8.0, proxies: Optional[Dict[str, str]] = None):
self.sess = requests.Session()
retry = Retry(
total=5, # 总重试次数
connect=3, # 连接错误重试
read=3, # 读取错误重试
backoff_factor=0.5, # 指数退避因子
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET", "POST"],
respect_retry_after_header=True,
)
adapter = HTTPAdapter(max_retries=retry, pool_connections=50, pool_maxsize=50)
self.sess.mount("http://", adapter)
self.sess.mount("https://", adapter)
self.base_delay = base_delay
self.max_delay = max_delay
self.proxies = proxies or {}

def _sleep_with_jitter(self, step: int):
# 指数级退避 + 抖动
delay = min(self.base_delay * (2 ** (step - 1)), self.max_delay)
jitter = random.uniform(0, delay * 0.25)
time.sleep(delay + jitter)

def get(self, url: str, headers: Optional[Dict[str, str]] = None, max_attempts: int = 4) -> requests.Response:
final_headers = {**DEFAULT_HEADERS, **(headers or {})}
last_exc = None
for i in range(1, max_attempts + 1):
try:
r = self.sess.get(url, headers=final_headers, proxies=self.proxies, timeout=15, allow_redirects=True)
# 若命中人机验证页或非预期重定向,可在此做规则判断
if r.status_code in (403, 429):
self._sleep_with_jitter(i)
continue
return r
except requests.RequestException as e:
last_exc = e
self._sleep_with_jitter(i)
raise last_exc if last_exc else RuntimeError("请求失败且无异常信息")

if __name__ == "__main__":
fetcher = StableFetcher(proxies=None) # 若需要代理:{"http": "http://<ip:port>", "https": "http://<ip:port>"}
url = "https://example.com/search?q=python" # 替换为目标地址
resp = fetcher.get(url)
print(resp.status_code, resp.url)
# 示例:这里可继续用选择器解析 HTML 内容
# from bs4 import BeautifulSoup
# soup = BeautifulSoup(resp.text, "lxml")
# for item in soup.select(".result-item"):
# print(item.select_one(".title").get_text(strip=True))

代码要点说明

  • Session + 连接池:减少握手成本、提升吞吐,且可保留 Cookie。
  • Retry 策略:对 429/5xx 与连接错误实施指数退避;尊重 Retry-After。
  • 头部伪装:尽量贴近真实浏览器请求,必要时带上 Referer。
  • 抖动与速率控制:避免等间隔请求形成“节拍特征”。
  • 代理与降级方案:长时间 403 时切换出口;获取不到关键数据时要能优雅降级或回退缓存。

效果与复盘

  • 修复后,抓取在中低速率下稳定,无明显 403/429;峰值时仍需结合 IP 池与更严格的节流策略。
  • 真正的“反爬突破”不是一招鲜,而是请求指纹治理 + 会话/重试 + 速率/代理 + 业务降级的组合拳。

小结与建议

  • 从“最小可复现”开始,优先观察指纹差异与服务端提示(状态码、Retry-After、重定向)。
  • 固化稳定基线:Session、标准化 UA/头部、指数退避重试、抖动与缓存。
  • 能用官方 API 就别硬爬;确需抓取时务必遵循站点规则与合规要求。
  • 将调试经验沉淀为组件:请求模板、拦截器、限速器、代理抽象、可观测性(请求轨迹、耗时、错误分布)。