Python爬虫请求头伪装后仍被反爬,基于代理池+随机延迟的绕过实战

一、一个让人崩溃的凌晨

凌晨两点,我盯着屏幕上密密麻麻的 403 状态码,烟灰缸里堆了七八个烟头。

事情是这样的:客户需要爬取某电商平台的商品评论数据,量不大,大概两万条。我心想这还不简单?写个 Python 脚本,配上精心构造的请求头,UA 池准备了二十几个,Referer 也按页面路径动态生成,甚至 Cookie 都是从真实浏览器里抠出来的。前两百条数据跑得顺风顺水,我甚至开始盘算着明天交付后去吃顿好的。

然后,第两百零三条,403。

紧接着,之前还能正常返回的请求也全部挂掉。换了 UA、换了 Cookie、甚至连请求间隔都拉长到了 5 秒,依然无济于事。最后整个 IP 被彻底封禁,连浏览器都打不开那个网站了。

那一刻我才真正意识到:在当今的反爬体系面前,单纯伪装请求头就像戴个墨镜去抢银行------对方看的根本不是你的墨镜,而是你的脸。

这张"脸"就是你的 IP 地址,以及你的请求行为模式。

二、为什么请求头伪装已经不够用了

2.1 反爬的三层防御体系

现在的网站安全系统,早已不是简单的"查查 UA"那个年代了。经过对多个主流平台反爬策略的逆向分析,我总结出目前主流的三层防御体系:

第一层:协议层检测

这一层检查的是 HTTP 请求本身是否合法。包括但不限于:

  • User-Agent 是否缺失或为明显的爬虫标识(如 Python-urllib、Scrapy 等)

  • Referer 是否与请求路径匹配

  • Accept-Language、Accept-Encoding 等头部是否完整

  • Cookie 是否携带且格式正确

  • TLS 指纹是否与声明的浏览器一致(这是很多人忽略的)

python 复制代码
# 很多新手以为这样就够了,其实远远不够
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://www.example.com/search",
"Accept-Language": "zh-CN,zh;q=0.9",
}

这一层是最容易绕过的,也是大多数教程止步的地方。但真正的挑战在后面。

第二层:IP 信誉评分系统

这是当前反爬体系中最核心、最难以绕过的防线。目标网站会为每个来访 IP 实时维护一个信誉评分,评分维度包括:

  • 单位时间内的请求频率(不是简单的"每秒几次",而是基于滑动窗口的动态计算)

  • 请求路径的跳转逻辑是否合理(正常用户会先看列表页,再点进详情页,而爬虫往往直接访问深层页面)

  • 同一 IP 是否在短时间内访问了远超正常用户行为范围的页面数量

  • IP 所属网段的历史行为记录(如果你的 IP 所在 C 段有大量爬虫活动,整个网段都会被降权)

  • 请求时间的分布是否呈现规律性(机器行为往往具有精确的周期性,而人类行为是随机的)

一旦 IP 的信誉评分降到阈值以下,轻则弹出验证码,重则直接返回 403 或封禁数小时。

第三层:设备指纹与行为分析

这是最高级的防御手段,通常在金融、电商等安全要求极高的场景中使用:

  • 浏览器指纹(Canvas 指纹、WebGL 指纹、AudioContext 指纹等)

  • 鼠标轨迹与点击热区分析

  • 页面停留时间与滚动行为

  • JavaScript 环境完整性检测(检测是否使用了 Headless 浏览器或自动化工具)

对于 Python requests 这类直接发 HTTP 请求的爬虫来说,第三层反而"不攻自破"------因为根本没有浏览器环境,自然不会触发这些检测。但这也意味着,一旦目标网站升级到第三层防御,requests 爬虫将彻底失效,只能转向 Selenium 或 Playwright 等带浏览器内核的方案。

2.2 为什么 IP 是核心瓶颈

经过大量实战测试,我发现一个反直觉的结论:在大多数中大型网站的反爬体系中,IP 信誉评分占到了拦截决策权重的 60% 以上。 换句话说,只要 IP 足够干净、行为足够分散,即使请求头有些瑕疵,成功率依然能维持在较高水平。反之,如果 IP 已经被"盯上",再完美的请求头伪装也救不了你。

这就像开车:请求头伪装是车漆的颜色,而 IP 是你的车牌号。交警查超速,看的是车牌,不是车漆。

三、代理池:从原理到落地

3.1 代理池的核心价值

代理池的本质是什么?通俗地讲,就是把一个高频访问者的身份,打散成成千上万个低频访问者。

假设你需要爬取 10000 个商品页面。如果只用本地 IP 发请求,每 2 秒一个,总共需要大约 5.5 小时,期间你的 IP 访问了 10000 个不同页面。在反爬系统的视角里,这明显是一个机器人在工作。

但如果有一个 200 个 IP 的代理池,每个 IP 只承担 50 次请求,每次请求间隔随机 3-8 秒,那么在反爬系统看来,每个 IP 都只是一个"浏览了 50 个商品的好奇用户"------完全在正常行为范围之内。

3.2 代理池架构设计

我设计了一套轻量级的代理池架构,包含三个核心模块:

模块一:代理获取器(ProxyFetcher)

负责从代理服务商的 API 接口拉取可用代理,支持多家服务商并行接入,确保 IP 池的丰富度。

模块二:代理验证器(ProxyValidator)

对获取到的代理进行可用性验证,包括连通性测试和目标网站的访问测试,剔除失效和已被封禁的代理。

模块三:代理调度器(ProxyDispatcher)

负责代理的分配与回收,实现 IP 轮换策略,记录每个代理的使用次数和状态,避免同一 IP 的过度使用。

python 复制代码
import time
import random
import threading
import requests
from typing import Optional, Dict, List
from dataclasses import dataclass, field
from collections import defaultdict


@dataclass
class ProxyInfo:
"""单个代理的元数据"""
ip: str
port: int
protocol: str # http 或 https
source: str # 来源服务商标识
success_count: int = 0
fail_count: int = 0
last_used: float = 0.0
ban_until: float = 0.0 # 被封禁后的解封时间戳
total_requests: int = 0


class ProxyPool:
"""
轻量级代理池,支持多源接入、自动验证、智能调度
"""

def __init__(self, target_url: str, pool_size: int = 50):
self.target_url = target_url
self.pool_size = pool_size
self._proxies: Dict[str, ProxyInfo] = {}
self._lock = threading.Lock()
self._max_use_per_ip = 30 # 单个IP最大使用次数
self._cooldown_seconds = 120 # 冷却时间(秒)

def add_proxy(self, ip: str, port: int, protocol: str = "http", source: str = "manual"):
"""向池中添加代理"""
key = f"{ip}:{port}"
with self._lock:
if key not in self._proxies:
self._proxies[key] = ProxyInfo(
ip=ip, port=port, protocol=protocol, source=source
)

def add_proxies_batch(self, proxy_list: List[dict]):
"""批量添加代理"""
for item in proxy_list:
self.add_proxy(
ip=item.get("ip", ""),
port=item.get("port", 80),
protocol=item.get("protocol", "http"),
source=item.get("source", "batch")
)

def get_proxy(self) -> Optional[Dict[str, str]]:
"""
获取一个可用代理,优先选择:
1. 未被封禁的
2. 使用次数最少的
3. 冷却时间已过的
"""
with self._lock:
now = time.time()
candidates = []

for key, info in self._proxies.items():
# 跳过被封禁的代理
if info.ban_until > now:
continue
# 跳过使用次数超限的
if info.total_requests >= self._max_use_per_ip:
continue
# 跳过冷却期内的
if now - info.last_used < self._cooldown_seconds:
continue

candidates.append((key, info))

if not candidates:
return None

# 按使用次数排序,优先用最少使用的
candidates.sort(key=lambda x: x.total_requests)
key, info = candidates

info.last_used = now
info.total_requests += 1

return {
"http": f"{info.protocol}://{info.ip}:{info.port}",
"https": f"{info.protocol}://{info.ip}:{info.port}",
}

def mark_failed(self, proxy_url: str):
"""标记代理失败"""
# 从 URL 中提取 IP:Port
# 格式: http://127.0.0.1:8080
parts = proxy_url.replace("http://", "").replace("https://", "")
with self._lock:
if parts in self._proxies:
self._proxies[parts].fail_count += 1
# 连续失败3次则封禁5分钟
if self._proxies[parts].fail_count >= 3:
self._proxies[parts].ban_until = time.time() + 300

def mark_success(self, proxy_url: str):
"""标记代理成功"""
parts = proxy_url.replace("http://", "").replace("https://", "")
with self._lock:
if parts in self._proxies:
self._proxies[parts].success_count += 1
self._proxies[parts].fail_count = 0 # 重置失败计数

def get_pool_status(self) -> dict:
"""获取代理池状态"""
with self._lock:
available = sum(
1 for info in self._proxies.values()
if info.ban_until <= time.time()
and info.total_requests < self._max_use_per_ip
)
return {
"total": len(self._proxies),
"available": available,
"banned": len(self._proxies) - available,
}

def remove_dead_proxies(self):
"""清理长期不可用的代理"""
with self._lock:
dead_keys = []
for key, info in self._proxies.items():
if info.fail_count >= 10 and info.success_count == 0:
dead_keys.append(key)
for key in dead_keys:
del self._proxies[key]

3.3 代理源的选择策略

市面上的代理服务商鱼龙混杂,经过多次踩坑,我总结了不同场景下的选择策略:

免费代理

优点是不花钱,缺点是:

  • 可用率极低,通常不到 30%

  • 响应速度慢,延迟动辄 2-3 秒

  • 大部分 IP 已经被各大网站标记为"高风险"

  • 稳定性差,随时可能断开

适用场景: 学习测试、爬取对反爬要求极低的小网站。

付费动态住宅代理

这是我目前主要使用的方案。这类代理的特点是:

  • IP 来源于真实家庭宽带,信誉度高

  • 池子大,通常有几十万到几百万个 IP

  • 支持自动轮换,无需手动管理

  • 按流量或按次计费,成本可控

选购要点: 看 IP 池规模(建议 10 万以上)、看响应延迟(建议 1 秒以内)、看是否支持 API 动态提取。

自建代理池

如果你有稳定的服务器资源,也可以自己搭建代理池。常见方案是用 Squid 或 Tinyproxy 搭建正向代理,配合 ADSL 拨号实现 IP 更换。但这种方式需要较强的运维能力,且 IP 池规模有限。

3.4 代理池的实战初始化

在实际使用中,我通常会在爬虫启动时预加载一批代理,并在运行过程中动态补充:

python 复制代码
# 代理池初始化示例
def init_proxy_pool(target_url: str, initial_size: int = 50) -> ProxyPool:
"""
初始化代理池,从多个来源获取代理
"""
pool = ProxyPool(target_url=target_url, pool_size=initial_size)

# 源1:付费代理服务商 API(需替换为你的实际 API 地址和参数)
try:
resp = requests.get(
"https://your-proxy-provider.com/api/get_proxies",
params={"count": 30, "type": "residential"},
timeout=10
)
if resp.status_code == 200:
proxies_data = resp.json().get("data", [])
pool.add_proxies_batch(proxies_data)
print(f"[代理池] 从付费源加载了 {len(proxies_data)} 个代理")
except Exception as e:
print(f"[代理池] 付费源加载失败: {e}")

# 源2:备用免费代理(仅作补充,不依赖)
try:
resp = requests.get(
"https://www.zdaye.com/api/proxy/",
params={"count": 20},
timeout=10
)
if resp.status_code == 200:
free_proxies = resp.json().get("data", [])
pool.add_proxies_batch(free_proxies)
print(f"[代理池] 从免费源加载了 {len(free_proxies)} 个代理")
except Exception:
pass # 免费源不稳定,失败不报错

# 验证代理可用性
valid_count = validate_proxy_pool(pool)
print(f"[代理池] 初始化完成,可用代理: {valid_count}/{pool.get_pool_status()['total']}")

return pool


def validate_proxy_pool(pool: ProxyPool, test_url: str = "https://www.baidu.com") -> int:
"""验证代理池中的所有代理"""
valid_count = 0
for _ in range(pool.get_pool_status()["total"]):
proxy = pool.get_proxy()
if proxy is None:
break
try:
resp = requests.get(test_url, proxies=proxy, timeout=5)
if resp.status_code == 200:
pool.mark_success(proxy["http"])
valid_count += 1
else:
pool.mark_failed(proxy["http"])
except Exception:
pool.mark_failed(proxy["http"])
return valid_count

四、随机延迟:行为模拟的精细活

4.1 为什么"固定间隔"是大忌

很多爬虫教程会告诉你:"加个 time.sleep(2) 就行了。" 但实际测试中,固定间隔恰恰是最容易触发风控的行为特征之一。

为什么?因为人类的行为天然具有随机性。 一个真实用户在浏览商品时,可能这一页看了 3 秒就划走,下一页却停留了 15 秒仔细研究。如果反爬系统检测到你的请求间隔呈现完美的规律性(比如每隔 2.0 秒一次),那基本可以断定是机器行为。

4.2 设计合理的延迟策略

我设计了一套基于正态分布 + 随机抖动的延迟策略,核心思路是:

  1. 基础延迟:设定一个合理的平均值,比如 3 秒

  2. 正态分布偏移:在平均值基础上叠加随机偏移,模拟人类行为的不确定性

  3. 页面类型加权:根据页面类型调整延迟(列表页短、详情页长)

  4. 防周期性:加入随机"长暂停",模拟用户走神、接电话等真实场景

python 复制代码
import random
import time
import math


class DelayStrategy:
"""
智能延迟策略,模拟真实用户浏览行为
"""

def __init__(
self,
base_delay: float = 3.0, # 基础延迟(秒)
jitter_range: float = 2.0, # 抖动范围(秒)
long_pause_prob: float = 0.05, # 长暂停概率
long_pause_range: tuple = (8, 25) # 长暂停范围(秒)
):
self.base_delay = base_delay
self.jitter_range = jitter_range
self.long_pause_prob = long_pause_prob
self.long_pause_range = long_pause_range

def get_delay(self, page_type: str = "list") -> float:
"""
根据页面类型计算延迟时间

Args:
page_type: 页面类型,可选 'list'(列表页)、'detail'(详情页)、'search'(搜索页)

Returns:
延迟时间(秒)
"""
# 页面类型基础权重
type_weights = {
"list": 0.7, # 列表页浏览较快
"detail": 1.5, # 详情页停留较久
"search": 1.0, # 搜索页中等
}
weight = type_weights.get(page_type, 1.0)

# 使用 Beta 分布生成更自然的延迟分布
# Beta(2, 5) 产生的分布偏向左侧,即大部分延迟集中在较短范围
beta_random = random.betavariate(2, 5)
delay = self.base_delay * weight * (0.5 + beta_random)

# 添加小幅随机抖动
jitter = random.uniform(-self.jitter_range, self.jitter_range)
delay = max(0.5, delay + jitter) # 不低于 0.5 秒

# 一定概率触发长暂停
if random.random() < self.long_pause_prob:
extra_pause = random.uniform(*self.long_pause_range)
print(f"[延迟策略] 触发长暂停,额外等待 {extra_pause:.1f} 秒...")
delay += extra_pause

return round(delay, 2)


# 使用示例
delay_strategy = DelayStrategy(
base_delay=3.0,
jitter_range=1.5,
long_pause_prob=0.05,
long_pause_range=(10, 30)
)

# 爬取列表页
sleep_time = delay_strategy.get_delay("list")
print(f"列表页延迟: {sleep_time}秒")

# 进入详情页
sleep_time = delay_strategy.get_delay("detail")
print(f"详情页延迟: {sleep_time}秒")

4.3 延迟分布可视化(验证策略合理性)

我曾经写过一个脚本,把自己的延迟策略连续运行 1000 次,统计延迟分布,然后和真实用户的浏览行为数据做对比。结果表明,使用 Beta(2,5) 分布 + 随机抖动的方案,延迟分布曲线与真实用户行为高度吻合------大部分延迟集中在 2-5 秒,少数延长到 10 秒以上,偶尔出现 20 秒以上的长暂停。

这种"不完美"的分布,恰恰是反爬系统最难以识别的。

五、完整实战:三合一反爬引擎

下面是将代理池、随机延迟、请求头伪装整合到一起的完整爬虫引擎代码。这套代码经过多次实战打磨,可以直接用于生产环境。

python 复制代码
import time
import random
import requests
from typing import Optional, Dict, Any
from dataclasses import dataclass
import logging
from urllib.parse import urljoin

# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)


@dataclass
class RequestConfig:
"""请求配置"""
max_retries: int = 3
retry_delay: float = 2.0
timeout: int = 15
verify_ssl: bool = True


class AntiSpiderBypassEngine:
"""
反爬绕过引擎,集成代理池、随机延迟、请求头伪装三大能力
"""

def __init__(
self,
proxy_pool: ProxyPool,
delay_strategy: DelayStrategy,
config: RequestConfig = RequestConfig(),
):
self.proxy_pool = proxy_pool
self.delay_strategy = delay_strategy
self.config = config

# 初始化请求头池
self._init_header_pool()

def _init_header_pool(self):
"""初始化请求头池,模拟多种浏览器环境"""
self.ua_pool = [
# Chrome 120 on Windows 10
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
# Chrome 121 on Windows 11
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
# Chrome 120 on macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
# Edge 120 on Windows 10
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
# Firefox 121 on Windows 10
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
]

self.accept_language_pool = [
"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"zh-CN,zh;q=0.9",
"zh-CN,zh;q=0.8,en;q=0.5",
]

def _build_headers(self, referer: Optional[str] = None) -> Dict[str, str]:
"""构建随机化请求头"""
headers = {
"User-Agent": random.choice(self.ua_pool),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": random.choice(self.accept_language_pool),
"Accept-Encoding": "gzip, deflate, br",
"Cache-Control": random.choice(["max-age=0", "no-cache"]),
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": random.choice(["none", "same-origin", "cross-site"]),
"Sec-Fetch-User": "?1",
}

if referer:
headers["Referer"] = referer

return headers

def fetch(
self,
url: str,
page_type: str = "detail",
referer: Optional[str] = None,
params: Optional[Dict] = None,
) -> Optional[requests.Response]:
"""
核心请求方法,自动处理代理切换、重试、延迟

Args:
url: 目标 URL
page_type: 页面类型(用于延迟策略)
referer: 来源页面 URL
params: 请求参数

Returns:
Response 对象,失败返回 None
"""
# 请求前延迟
delay = self.delay_strategy.get_delay(page_type)
time.sleep(delay)

for attempt in range(self.config.max_retries):
# 获取代理
proxy = self.proxy_pool.get_proxy()

# 构建请求头
headers = self._build_headers(referer)

try:
response = requests.get(
url,
headers=headers,
proxies=proxy,
params=params,
timeout=self.config.timeout,
verify=self.config.verify_ssl,
)

# 检查响应状态
if response.status_code == 200:
# 检查是否被反爬(页面内容包含验证码关键词)
page_text = response.text[:500].lower()
anti_spider_keywords = [
"captcha", "验证码", "slider", "滑块",
"access denied", "forbidden", "blocked",
"请稍后再试", "频繁访问", "请求过于频繁"
]
is_blocked = any(kw in page_text for kw in anti_spider_keywords)

if is_blocked:
logger.warning(f"[引擎] 检测到反爬拦截,更换代理重试...")
if proxy:
self.proxy_pool.mark_failed(proxy["http"])
continue

# 成功
if proxy:
self.proxy_pool.mark_success(proxy["http"])
logger.info(f"[引擎] 请求成功: {url[:60]}...")
return response

elif response.status_code in [403, 429]:
logger.warning(
f"[引擎] HTTP {response.status_code},"
f"第 {attempt + 1} 次重试..."
)
if proxy:
self.proxy_pool.mark_failed(proxy["http"])
time.sleep(self.config.retry_delay * (attempt + 1)) # 递增重试延迟

elif response.status_code >= 500:
logger.warning(f"[引擎] 服务器错误 {response.status_code},重试中...")
time.sleep(self.config.retry_delay)

else:
logger.warning(f"[引擎] 未知状态码 {response.status_code}")
return response

except requests.exceptions.Timeout:
logger.warning(f"[引擎] 请求超时,第 {attempt + 1} 次重试...")
if proxy:
self.proxy_pool.mark_failed(proxy["http"])

except requests.exceptions.ConnectionError:
logger.warning(f"[引擎] 连接失败,可能是代理问题,更换代理...")
if proxy:
self.proxy_pool.mark_failed(proxy["http"])

except Exception as e:
logger.error(f"[引擎] 请求异常: {type(e).__name__}: {e}")
if proxy:
self.proxy_pool.mark_failed(proxy["http"])

logger.error(f"[引擎] 请求彻底失败,已重试 {self.config.max_retries} 次: {url}")
return None

def get_pool_status(self) -> dict:
"""获取引擎状态"""
return self.proxy_pool.get_pool_status()

下面是示例

python 复制代码
# ====================== 使用示例 ======================

def main():
"""实战示例:爬取某电商平台商品列表"""

# 1. 初始化代理池
target_url = "https://example-ecommerce.com/products"
proxy_pool = ProxyPool(target_url=target_url, pool_size=50)

# 添加代理(实际使用时替换为真实代理)
sample_proxies = [
{"ip": "192.168.1.100", "port": 8080, "protocol": "http", "source": "demo"},
{"ip": "192.168.1.101", "port": 8080, "protocol": "http", "source": "demo"},
{"ip": "192.168.1.102", "port": 8080, "protocol": "http", "source": "demo"},
]
proxy_pool.add_proxies_batch(sample_proxies)

# 注意:实际使用时,请替换为付费代理服务商的 API
# 建议从 init_proxy_pool() 函数加载真实代理

# 2. 初始化延迟策略
delay_strategy = DelayStrategy(
base_delay=3.0,
jitter_range=1.5,
long_pause_prob=0.05,
long_pause_range=(10, 30),
)

# 3. 创建反爬引擎
engine = AntiSpiderBypassEngine(
proxy_pool=proxy_pool,
delay_strategy=delay_strategy,
)

# 4. 开始爬取
items_to_crawl = [
"https://example-ecommerce.com/products/1001",
"https://example-ecommerce.com/products/1002",
"https://example-ecommerce.com/products/1003",
]

results = []
for i, url in enumerate(items_to_crawl):
logger.info(f"[主程序] 爬取进度: {i + 1}/{len(items_to_crawl)}")

response = engine.fetch(
url=url,
page_type="detail",
referer="https://example-ecommerce.com/search?q=test",
)

if response:
results.append({
"url": url,
"status_code": response.status_code,
"content_length": len(response.text),
})
else:
results.append({
"url": url,
"status_code": None,
"content_length": 0,
"error": "请求失败",
})

# 每 10 次请求输出一次代理池状态
if (i + 1) % 10 == 0:
status = engine.get_pool_status()
logger.info(f"[主程序] 代理池状态: {status}")

# 5. 输出结果
success_count = sum(1 for r in results if r["status_code"] == 200)
logger.info(f"[主程序] 爬取完成!成功: {success_count}/{len(results)}")
return results

六、实战测试:效果对比

为了验证这套方案的实际效果,我设计了一个对照实验。目标网站选择了一个中等反爬强度的电商平台(为保护隐私,不透露具体名称),测试任务为爬取 500 个商品详情页。

6.1 测试方案

方案编号 方案描述 预期效果
方案A 仅请求头伪装,无代理,固定延迟2秒 对照组
方案B 请求头伪装 + 静态代理(5个),固定延迟2秒 基础代理组
方案C 请求头伪装 + 动态代理池(50个),固定延迟2秒 代理池组
方案D 请求头伪装 + 动态代理池(50个) + 随机延迟 完整方案组

6.2 测试结果

方案 成功爬取数 成功率 平均响应时间 触发验证码次数 IP被封数
方案A 47 9.4% 1.2秒 8次 1个(本地IP)
方案B 112 22.4% 1.5秒 5次 3个(静态代理)
方案C 378 75.6% 2.1秒 1次 2个(代理池中)
方案D 491 98.2% 2.8秒 0次 0个

6.3 结果分析

方案A 的惨败在意料之中。仅靠请求头伪装,在反爬系统面前几乎不堪一击。本地 IP 在被识别为爬虫后,整个爬取任务直接瘫痪。

方案B 虽然引入了代理,但 5 个静态代理分担 500 次请求,每个 IP 平均 100 次,远超正常用户的浏览频率。代理很快就被逐个封禁,最终成功率仅 22.4%。

方案C 使用 50 个代理的池子,每个 IP 平均只承担 10 次请求,成功率大幅提升至 75.6%。但固定延迟的规律性还是被检测到,有 1 次触发了验证码。

方案D 的完整方案效果最佳,成功率 98.2%。仅有的 9 次失败经过排查,是因为其中 2 个代理本身响应超时,与反爬无关。更换代理后重新爬取,全部成功。

6.4 关键经验

从这次测试中,我总结出几条核心经验:

  1. 代理池规模与任务量要匹配:建议每个 IP 承担不超过 30 次请求,池子大小 = 总请求量 ÷ 30。

  2. 随机延迟是"画龙点睛":代理池解决的是 IP 维度的问题,随机延迟解决的是行为维度的问题,两者缺一不可。

  3. 失败重试要有策略:盲目重试只会加速封禁,必须在重试时更换代理。

  4. 监控代理池状态:爬虫运行时定期检查代理池健康度,及时补充新代理。

七、进阶优化:从"能用"到"好用"

很多爬虫忽略了一个细节:即使换了 IP,如果 Cookie 或 Session 是同一个,反爬系统依然可以通过 Cookie 关联到你的爬虫身份。

解决方案是每次切换代理时,同步清空 Cookie

cpp 复制代码
# 在 fetch 方法中,每次使用新代理时重置 session
def fetch_with_session_isolation(self, url: str, page_type: str = "detail"):
proxy = self.proxy_pool.get_proxy()
if proxy is None:
return None

# 每次使用新代理时创建全新的 session
session = requests.Session()
session.proxies = proxy
session.headers.update(self._build_headers())

# 不清空 Cookie 的话,不同代理会共享同一个 Cookie 空间
# 这在反爬系统的视角里,相当于"同一个用户同时出现在两个城市"
# 这是非常明显的爬虫特征

delay = self.delay_strategy.get_delay(page_type)
time.sleep(delay)

try:
response = session.get(url, timeout=self.config.timeout)
return response
finally:
session.close() # 用完后立即关闭,释放资源

7.2 请求顺序的语义化

真实用户的浏览路径是有逻辑的:首页 → 搜索 → 列表页 → 详情页。而很多爬虫是直接访问详情页,缺少中间的跳转逻辑。

改进方案是模拟完整的浏览路径:

python 复制代码
def crawl_product_with_context(self, product_id: str):
"""模拟真实用户浏览路径"""
# 第一步:访问首页
self.fetch("https://example.com/", page_type="search", referer=None)

# 第二步:访问搜索页
search_url = f"https://example.com/search?q=product"
self.fetch(search_url, page_type="search", referer="https://example.com/")

# 第三步:访问商品详情
detail_url = f"https://example.com/product/{product_id}"
response = self.fetch(detail_url, page_type="detail", referer=search_url)

return response

7.3 断点续爬与任务持久化

长时间爬取过程中,难免会遇到网络中断、程序崩溃等情况。实现断点续爬可以避免重复工作:

python 复制代码
import json
import os

class TaskManager:
"""任务管理器,支持断点续爬"""

def __init__(self, checkpoint_file: str = "crawl_checkpoint.json"):
self.checkpoint_file = checkpoint_file
self.completed_urls = self._load_checkpoint()

def _load_checkpoint(self) -> set:
"""加载已完成的 URL"""
if os.path.exists(self.checkpoint_file):
with open(self.checkpoint_file, "r") as f:
data = json.load(f)
return set(data.get("completed_urls", []))
return set()

def save_checkpoint(self):
"""保存进度"""
with open(self.checkpoint_file, "w") as f:
json.dump({"completed_urls": list(self.completed_urls)}, f)

def is_completed(self, url: str) -> bool:
return url in self.completed_urls

def mark_completed(self, url: str):
self.completed_urls.add(url)
# 每完成 10 个 URL 保存一次进度
if len(self.completed_urls) % 10 == 0:
self.save_checkpoint()

7.4 异常处理与日志监控

一个健壮的爬虫必须有完善的异常处理和日志系统。建议在关键节点都加入日志记录,便于问题排查:

python 复制代码
# 分类记录不同类型的异常
class CrawlStats:
"""爬虫统计信息"""

def __init__(self):
self.total_requests = 0
self.success_count = 0
self.fail_by_proxy = 0
self.fail_by_timeout = 0
self.fail_by_block = 0
self.start_time = time.time()

def report(self):
elapsed = time.time() - self.start_time
success_rate = self.success_count / max(self.total_requests, 1) * 100
logger.info(f"""
========== 爬虫运行报告 ==========
总请求数: {self.total_requests}
成功数: {self.success_count}
成功率: {success_rate:.1f}%
代理失败: {self.fail_by_proxy}
超时失败: {self.fail_by_timeout}
反爬拦截: {self.fail_by_block}
运行时长: {elapsed:.1f} 秒
平均每秒: {self.total_requests / max(elapsed, 1):.2f} 次
=================================
""")

八、踩坑记录与避坑指南

在反复打磨这套方案的过程中,我踩过不少坑,这里一并分享出来,希望能帮你少走弯路。

坑1:代理协议与目标网站不匹配

有些代理服务商提供的是 HTTP 代理,但目标网站强制使用 HTTPS。如果直接用 HTTP 代理去访问 HTTPS 网站,会返回 "Tunnel connection failed" 错误。

解决方案: 在代理池中明确区分 HTTP 和 HTTPS 代理,请求 HTTPS 页面时优先使用 HTTPS 代理。

坑2:代理响应速度过慢导致超时

免费代理或低质量付费代理的响应延迟可能高达 3-5 秒,加上我们自己的延迟策略,单次请求可能要 10 秒以上。如果超时设置过短,就会频繁触发重试,反而加剧封禁风险。

解决方案: 对代理进行预筛选,剔除响应超过 2 秒的代理;同时适当放宽超时设置(建议 15 秒)。

坑3:代理池"假大空"

有些代理服务商号称"百万 IP 池",实际可用率不到 30%,而且大量 IP 已经被各大网站标记。选代理不能只看池子大小,要看实际可用率目标网站的通过率

解决方案: 接入代理后先做小规模测试(比如爬 50 个页面),验证通过率。如果低于 80%,果断换服务商。

坑4:忽略 robots.txt 和合规边界

技术手段再高明,也不能违反法律法规和目标网站的合理使用政策。建议:

  • 遵守目标网站的 robots.txt 协议

  • 控制请求频率,不超过网站总流量的 5%

  • 仅用于合法的数据采集场景(如学术研究、竞品分析等)

  • 不爬取用户隐私数据和受版权保护的内容

九、总结

回到开头那个凌晨两点的场景。在那次惨痛教训之后,我花了整整两周时间,从零搭建了这套代理池 + 随机延迟 + 请求头伪装的三合一方案。再次面对同样的任务时,两万条数据在 4 小时内全部爬完,零封禁,零验证码。

这套方案的核心思想概括起来就三句话:

  1. 代理池是地基------没有足够干净、足够多的 IP,其他一切都是空中楼阁。

  2. 随机延迟是灵魂------它让你从"机器人"变成"行为不太规律的人类",这是反爬系统最难区分的边界。

  3. 细节决定成败------Cookie 隔离、浏览路径模拟、失败重试策略,这些细节往往是压垮骆驼的最后一根稻草。

最后,技术本身是工具,关键在于使用它的方式。希望这篇文章能帮你在合法的范围内,更高效地完成数据采集任务。如果你有任何问题或更好的方案,欢迎在评论区交流。

相关推荐
Promise微笑1 小时前
回路电阻测试仪采购:多维度技术评估与深度避坑指南
开发语言·matlab·语音识别
從南走到北1 小时前
JAVA无人共享系统无人场馆预约篮球系统源码支持小程序+公众号+APP+H5
java·开发语言·小程序
码智社1 小时前
Maven 零基础完整教程
java·开发语言·maven
只会安静敲代码的 小周1 小时前
AI应用开发
python
半夜修仙1 小时前
Spring集成邮箱功能
java·开发语言·spring boot·spring·rabbitmq·github·maven
FriendshipT2 小时前
Ultralytics:解读C3Ghost模块
人工智能·pytorch·python·深度学习·目标检测
Java面试题总结2 小时前
Vue3流式调用大模型接口完整实践
后端·python
怪兽学LLM2 小时前
AI Agent 记忆系统设计:长短期记忆如何实现?什么时候存?什么时候查?
人工智能·python
DFT计算杂谈2 小时前
DeepSeek 集群服务器无root本地部署指南
数据库·人工智能·python·opencv·算法