不是教你绕过一切,而是教你活得更久。
一、前言:为什么你的爬虫总被封?
写爬虫的人基本都经历过这个循环:
- 代码跑通了,数据拿到了 ✅
- 跑了 10 分钟,IP 被封了 ❌
- 加了个代理,跑了 1 小时,验证码来了 ❌
- 上了 Selenium,内存爆了,云服务器账单炸了 ❌
问题不在于技术不够强,而在于思路不对。
真正稳定的爬虫不是"攻破"反爬,而是融入正常流量。本文从实战出发,讲清楚怎么写一个"像人"的爬虫。
二、先搞清楚:反爬在反什么?
网站识别爬虫,通常看这几个维度:
| 检测维度 | 爬虫特征 | 正常人特征 |
|---|---|---|
| 请求头 | python-requests/2.31.0 |
完整的浏览器 UA、Accept、Referer |
| 请求频率 | 1 秒 10 次,规律如钟表 | 随机间隔,有停顿、有回退 |
| IP 行为 | 同一 IP 连续访问 1000 页 | 不同地区、不同时间段分散访问 |
| JS 执行 | 拿不到动态渲染内容 | 完整执行页面 JS,有 Cookie、LocalStorage |
| 行为轨迹 | 直接访问 API,无页面滚动 | 有鼠标移动、点击、滚动行为 |
核心原则:反爬检测的是"非人类特征",而不是"技术能力"。
三、第一层伪装:请求头不是只改 UA
最基础的反爬是检查 User-Agent。但很多人只改 UA,其他头信息暴露得干干净净。
3.1 完整的请求头伪装
python
import requests
import random
# 不要只改 UA,要改一整套
def get_headers():
ua_list = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
]
return {
"User-Agent": random.choice(ua_list),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Cache-Control": "max-age=0"
}
# 使用
resp = requests.get("https://example.com", headers=get_headers())
3.2 保持会话一致性
爬虫每次请求都新建 session,而浏览器会复用 TCP 连接、Cookie、缓存。
python
# ❌ 错误:每次新建 session
requests.get(url1)
requests.get(url2)
# ✅ 正确:复用 session,保持 Cookie 和连接池
session = requests.Session()
session.headers.update(get_headers())
# 第一次访问首页,拿到 Cookie
session.get("https://example.com")
# 第二次访问详情页,带着 Cookie
session.get("https://example.com/detail/123")
四、第二层伪装:请求频率要"像人"
人浏览网页不是匀速的。你会停顿、回退、刷新,甚至中途去倒杯水。
4.1 随机延迟 + 正态分布
python
import time
import random
def human_delay(min_sec=1.5, max_sec=5.0):
"""模拟人类阅读延迟,服从正态分布"""
mean = (min_sec + max_sec) / 2
std = (max_sec - min_sec) / 4
delay = random.gauss(mean, std)
delay = max(min_sec, min(max_sec, delay)) # 截断异常值
time.sleep(delay)
# 使用
for url in url_list:
resp = session.get(url)
parse(resp)
human_delay() # 随机等 1.5~5 秒
4.2 访问路径要合理
正常人不会直接访问 https://example.com/api/data?page=1000。
python
# ❌ 机器人行为:直接批量请求 API
for i in range(1, 1000):
session.get(f"https://example.com/api/data?page={i}")
# ✅ 人类行为:从列表页进入,再点详情
list_page = session.get("https://example.com/list")
detail_urls = extract_links(list_page)
for url in detail_urls[:10]: # 先看前 10 条
session.get(url)
human_delay(2, 8)
# 模拟"回退到列表页"
session.get("https://example.com/list?page=2")
human_delay(1, 3)
五、第三层伪装:IP 代理池
当请求量上去后,IP 封禁是必然的。代理池不是可选项,是必选项。
5.1 代理池的架构设计
python
import random
class ProxyPool:
def __init__(self):
self.proxies = []
self.failed = set()
def get_proxy(self):
"""轮询 + 权重,优先用成功率高的"""
available = [p for p in self.proxies if p['url'] not in self.failed]
if not available:
self.failed.clear() # 全部失败,重置
available = self.proxies
# 按成功率加权随机
weights = [p.get('success_rate', 1.0) for p in available]
proxy = random.choices(available, weights=weights)[0]
return {
"http": proxy['url'],
"https": proxy['url']
}
def report_result(self, proxy_url, success):
"""上报代理使用情况,动态调整权重"""
for p in self.proxies:
if p['url'] == proxy_url:
p['success_rate'] = p.get('success_rate', 1.0) * 0.9 + (1.0 if success else 0.0) * 0.1
# 使用
pool = ProxyPool()
# ... 初始化代理列表 ...
proxy = pool.get_proxy()
try:
resp = session.get(url, proxies=proxy, timeout=10)
pool.report_result(proxy['http'], True)
except Exception:
pool.report_result(proxy['http'], False)
代理选择建议:
- 学习/测试:免费代理(稳定性差,可用率 < 20%)
- 小规模生产:付费代理 API(按量计费,适合万级请求)
- 大规模生产:住宅代理(IP 质量高,适合百万级请求)
六、第四层伪装:搞定 JS 渲染
现代网站大量内容靠 JS 动态加载。requests 拿到的是空壳 HTML。
6.1 方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| requests + 逆向 API | 最快、最省资源 | 需要分析 JS,门槛高 | 能抓到接口的情况 |
| Playwright | 真实浏览器,反爬最强 | 内存占用高,启动慢 | 复杂反爬、需要交互 |
| Splash | 轻量,可部署为服务 | 维护成本高,功能有限 | 中等复杂度渲染 |
6.2 实战:优先逆向 API
打开浏览器开发者工具 → Network → XHR,找到真实数据接口:
python
# 假设通过抓包发现数据来自这个 API
API_URL = "https://example.com/api/v2/items"
# 分析发现需要 sign 参数,从 JS 中逆向
import hashlib
import time
def generate_sign(params: dict, secret: str) -> str:
"""逆向得到的签名算法"""
param_str = '&'.join([f'{k}={v}' for k, v in sorted(params.items())])
raw = f'{param_str}&{secret}'.encode()
return hashlib.md5(raw).hexdigest()
params = {
'page': 1,
'size': 20,
'timestamp': int(time.time())
}
params['sign'] = generate_sign(params, 'secret_key_from_js')
resp = session.get(API_URL, params=params)
data = resp.json()
能直接调 API 就不开浏览器,效率差 10 倍以上。
6.3 备用:Playwright 模拟真实浏览
当逆向成本太高时,用 Playwright:
python
from playwright.sync_api import sync_playwright
def scrape_with_browser(url):
with sync_playwright() as p:
# 启动浏览器,模拟真实环境
browser = p.chromium.launch(
headless=True,
args=['--disable-blink-features=AutomationControlled']
)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...'
)
# 注入脚本,隐藏 webdriver 特征
context.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
""")
page = context.new_page()
page.goto(url)
# 等待内容加载
page.wait_for_selector('.data-list', timeout=10000)
# 模拟人类滚动
page.mouse.move(100, 200)
page.scroll(0, 500)
time.sleep(random.uniform(1, 3))
content = page.content()
browser.close()
return content
七、第五层:应对验证码
验证码是最后一道防线。不要试图 100% 破解,要降低触发概率。
7.1 预防 > 破解
- 控制频率:单 IP 每秒不超过 1 次请求
- 行为模拟:有滚动、有停留、有鼠标移动
- 使用住宅代理:数据中心 IP 最容易触发验证码
7.2 如果触发了
python
# 方案 1:第三方打码服务(如 2Captcha)
def solve_captcha(image_data):
# 上传图片到打码平台
# 返回识别结果
pass
# 方案 2:接入 AI 视觉模型(如 GPT-4V)
# 成本较高,适合复杂验证码
# 方案 3:直接放弃该 IP,换代理重试
# 最经济的方式
八、工程化:让爬虫稳定运行
单脚本跑几次没问题,但生产环境需要:
8.1 完整的请求重试机制
python
from functools import wraps
import logging
def retry(max_retries=3, backoff_factor=2):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_retries - 1:
raise
wait = backoff_factor ** attempt + random.uniform(0, 1)
logging.warning(f"Retry {attempt+1}/{max_retries} after {wait:.1f}s: {e}")
time.sleep(wait)
return None
return wrapper
return decorator
@retry(max_retries=3)
def fetch(url):
return session.get(url, timeout=10)
8.2 监控与报警
python
# 记录关键指标
stats = {
'total_requests': 0,
'success': 0,
'failed': 0,
'blocked': 0, # 被反爬拦截
'captcha': 0
}
# 每 100 次请求输出报告
if stats['total_requests'] % 100 == 0:
success_rate = stats['success'] / stats['total_requests']
print(f"成功率: {success_rate:.1%} | 拦截: {stats['blocked']} | 验证码: {stats['captcha']}")
if success_rate < 0.8:
send_alert("爬虫成功率低于 80%,请检查")
九、法律与合规:技术有边界
写爬虫前,必须明确红线:
-
遵守 robots.txt
bashcurl https://example.com/robots.txt如果明确禁止,不要硬爬。
-
不碰个人隐私数据
- 手机号、身份证号、住址
- 未公开的个人信息
-
控制请求压力
- 单 IP 请求间隔 ≥ 3 秒
- 避开网站高峰期(如电商大促期间)
-
数据用途合法
- 个人学习:✅
- 竞品分析(公开数据):✅
- 批量注册、薅羊毛:❌
- 转卖个人隐私数据:❌
技术是无罪的,但使用技术的人有责任。
十、总结:反爬对抗的层次
| 层次 | 手段 | 核心思路 |
|---|---|---|
| 1 | 请求头伪装 | 看起来像浏览器 |
| 2 | 频率控制 | 看起来像人类 |
| 3 | 代理轮换 | 看起来像多个用户 |
| 4 | JS 渲染 | 行为像真实浏览器 |
| 5 | 行为模拟 | 交互像真人操作 |
最终建议:
- 80% 的网站:Session + 随机延迟 + 代理池 就能搞定
- 15% 的网站:需要逆向 API 或 Playwright
- 5% 的网站:反爬太强,评估成本后决定是否值得投入
爬虫不是炫技,是用最低成本稳定拿到数据。希望这篇文章能帮你在攻防战中活得更久。
如果对你有帮助,欢迎点赞收藏。有问题可以在评论区讨论,看到都会回复。