从零写一个"像人"的爬虫:反爬攻防实战指南

不是教你绕过一切,而是教你活得更久。


一、前言:为什么你的爬虫总被封?

写爬虫的人基本都经历过这个循环:

  1. 代码跑通了,数据拿到了 ✅
  2. 跑了 10 分钟,IP 被封了 ❌
  3. 加了个代理,跑了 1 小时,验证码来了 ❌
  4. 上了 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%,请检查")

九、法律与合规:技术有边界

写爬虫前,必须明确红线:

  1. 遵守 robots.txt

    bash 复制代码
    curl https://example.com/robots.txt

    如果明确禁止,不要硬爬。

  2. 不碰个人隐私数据

    • 手机号、身份证号、住址
    • 未公开的个人信息
  3. 控制请求压力

    • 单 IP 请求间隔 ≥ 3 秒
    • 避开网站高峰期(如电商大促期间)
  4. 数据用途合法

    • 个人学习:✅
    • 竞品分析(公开数据):✅
    • 批量注册、薅羊毛:❌
    • 转卖个人隐私数据:❌

技术是无罪的,但使用技术的人有责任。


十、总结:反爬对抗的层次

层次 手段 核心思路
1 请求头伪装 看起来像浏览器
2 频率控制 看起来像人类
3 代理轮换 看起来像多个用户
4 JS 渲染 行为像真实浏览器
5 行为模拟 交互像真人操作

最终建议:

  • 80% 的网站:Session + 随机延迟 + 代理池 就能搞定
  • 15% 的网站:需要逆向 API 或 Playwright
  • 5% 的网站:反爬太强,评估成本后决定是否值得投入

爬虫不是炫技,是用最低成本稳定拿到数据。希望这篇文章能帮你在攻防战中活得更久。


如果对你有帮助,欢迎点赞收藏。有问题可以在评论区讨论,看到都会回复。

相关推荐
贰先生1 小时前
Xiuno BBS 审计之问题13:管理员发帖 doctype=0 时存储型 XSS
后端
贰先生1 小时前
Xiuno BBS 审计之问题12:HTTPS 请求关闭 SSL 证书校验,存在中间人攻击风险
后端
HZ_YZ2 小时前
清理服务器硬盘脚本
后端
布朗克1682 小时前
Go 入门到精通-16-字符串深入
开发语言·后端·golang·字符串
北冥you鱼2 小时前
Go flag 包详解:从命令行解析到实战应用
开发语言·后端·golang
她说彩礼65万3 小时前
Asp.net core 模型绑定
后端·asp.net
用户34232323763173 小时前
BLE 传感器节点实战
后端
fliter3 小时前
Go 1.26 新增 `bytes.Buffer.Peek`:只看数据,不移动读取位置
后端
羑悻3 小时前
我受够了 Coding Agent 的“假聪明,真串行”,于是做了 Meta-Orchestrator!
后端