从“秒封”到“日爬十万”:谈谈5个风控机制

39 阅读2分钟

经常在知乎看到有刚学完 Python 基础的小白提问:“为什么我刚写好的爬虫,才跑了十几页就被封了?是我代码写得太烂,还是运气不好?”

点开大图一看,代码基本上长这样:

import requests

for url in urls:
    response = requests.get(url)
    print(response.text)

实话说,不是你运气不好,是你特么在用高射炮炸网站的服务器。

很多人以为爬虫是一门“只要拿到 HTML 就万事大吉”的技术,却忽略了防守方(运维和反爬工程师)手里的武器。作为一名写了多年爬虫的硬核谢绝剧本的程序员,我今天不和你聊什么复杂的逆向、高深混淆,就聊聊最底层的 5个反爬与抗反爬逻辑

读懂这 5 条,你的爬虫生存率至少提升 10 倍。

一、 请求频率失控 —— 你不是在爬数据,你是在做 访问攻击

1. 现场还原

前 10 个请求顺风顺水,第 11 个请求突然抛出: 429 Too Many Requests 或者更恶心一点:网站返回 200,但里面的数据全是假的(蜜罐数据)。

2. 底层逻辑:Rate Limit(速率限制)

稍微懂点运维的都知道,Nginx 里面有一行经典的配置:

limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;

这意味着:单个 IP 每秒最多允许 10 个请求。 你的循环在好几百多核的机器上狂飙,一秒砸过去 100 个请求,运维不封你封谁?你这不叫采集,你这叫无照驾驶的分布式拒绝服务攻击。

3. 破局

很多新手会说:“那我加个 time.sleep(0.1) 不就行了?” 太天真了。

  • 效率问题:如果数据量是 10 万条,0.1 秒的固定间隔意味着你要跑接近 3 个小时。
  • 特征暴露固定间隔比随机间隔更容易被算法识别。 谁家好人能做到连续 3 小时、每隔精准的 100 毫秒点击一次网页?

正确姿势:引入随机波动,模拟人类阅读停顿。

import random
import time
import requests

def safe_request(url):
    # 随机间隔 0.5 到 2 秒,打破固定的频率特征
    time.sleep(random.uniform(0.5, 2.0))
    return requests.get(url)

二、 IP 信誉透支 —— 一个 IP 薅到死,等于自投罗网

1. 现象

你控制了频率,爬虫跑了 1 个小时,起初很正常。渐渐地,超时越来越多,最后连首页都打不开了。换个手机热点,又正常了。

2. 底层逻辑:IP 信誉评分(IP Reputation)

现在的反爬系统不仅仅看你“当下”这一秒发了多少请求,它们有一个全天候的风控画像

  • 这个 IP 是否频繁访问敏感 API?
  • 这个 IP 的访问链路是否只去详情页,从不去首页?
  • 这个 IP 是否触发过滑块验证码?

一旦你的 IP 累计积分超过阈值,直接打入“灰名单”。轻则让你做永无止境的图片验证码,重则直接 TCP Reset。

3. 破局

单一 IP 访问量大是原罪。唯一的解法是:IP 轮换(Proxy Rotation)。 但是,市面上几块钱一万个的免费代理就别碰了,99% 都是早就死透的或者本身就是钓鱼蜜罐。

我们需要搭建一个代理轮换机制,并且必须配合有效性检测

import requests

# 基础代理池轮换示例
proxy_list = [
    {"http": "http://user1:pass1@101.xxx.xxx.xxx:8080"},
    {"http": "http://user2:pass2@102.xxx.xxx.xxx:8080"},
]

def check_and_get(url, proxies):
    try:
        # 使用 httpbin 测试 IP 是否穿透成功
        r = requests.get(url, proxies=proxies, timeout=5)
        if r.status_code == 200:
            return r.text
    except Exception as e:
        print(f"当前代理失效,准备切换...")
        return None

三、 请求特征暴露 —— 你的 User-Agent 正在大声出卖你

1. 现象

频率也控了,代理也加了,结果刚跑 5 分钟,依然秒封。

2. 底层逻辑:HTTP 指纹与特征识别

别总盯着你的 Python 逻辑看,看看你发出去的 HTTP 请求头。如果你用 requests 且不带 headers,对方服务器的日志里会赫然写着: User-Agent: python-requests/2.31.0

这等于在脸上写着:“我是爬虫,快来封我”。

更高级的反爬(如 Cloudflare、Akamai)不仅看 UA,还会校验:

  • Accept-Language(浏览器语言偏好)
  • Accept-Encoding(压缩算法,现代浏览器基本都支持 br)
  • TLS 指纹(JA3/JA4):看你握手阶段的加密套件像不像 Python。

3. 破局

不要只寒酸地改一个 User-Agent,要伪装成一整套高拟真浏览器请求头集群,并进行随机轮换:

import random
import requests

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "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",
]

def get_random_headers():
    return {
        "User-Agent": random.choice(USER_AGENTS),
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        "Accept-Encoding": "gzip, deflate, br",
        "Connection": "keep-alive",
        "Referer": "https://www.google.com/",  # 模拟搜索引擎跳转来源
    }

_注:如果是极其严苛的 TLS 级别指纹校验,建议直接出门左转用 curl_cffi 库代替 requests。_curl_cffirequests

四、 行为模式异常 —— 你的爬虫“不像个正常人”

1. 现象

请求头完美,IP 天天换,频率也拉低了。但只要数据量一上万,风控系统还是能把你揪出来。

2. 底层逻辑:漏斗模型与访问路径(Behavior Analysis)

正常人类看知乎是怎样的? 输入 zhihu.com -> 看看推荐页 -> 点击某一个感兴趣的回答 -> 停留 30 秒 -> 偶尔点进答主主页。

你的爬虫是怎样的? 直接请求 zhihu.com/question/123456/answer/78910,然后不知疲倦地连续请求 5000 个详情页

反爬系统在后台计算你的 访问量/会话数(PV/Session)比例。当一个 Session 只有深度详情页的请求,且漏斗模型完全是直线的,机器学习算法会一秒判定你不是人。

3. 破局

模拟真实跳转链(Referer)与分层爬取。 别一上来就舔详情页,先去首页晃一圈,拿到 Session,再顺着列表页点进去。

def realistic_crawl(session, target_url):
    # 1. 假装访问首页,建立 Session 信任
    session.get("https://example.com/")
    time.sleep(random.uniform(1.0, 2.5))
    
    # 2. 访问列表页
    list_response = session.get("https://example.com/list")
    time.sleep(random.uniform(2.0, 4.0))
    
    # 3. 带着列表页的上下文,去请求详情页
    session.headers.update({"Referer": "https://example.com/list"})
    detail_response = session.get(target_url)
    return detail_response

五、 缺乏高匿名保护 —— 真实 IP 暴露等于裸奔

1. 核心痛点

很多同学按照上面的教程做了前 4 步,觉得自己已经是架构师了。结果一查后台,发现所有的请求还是通过家里的宽带或者公司机房的固定 IP 发出去的。

结果就是:你家宽带的 IP 信誉彻底废了。 在现代大厂的盾面前,没有代理保护的采集无异于裸奔。我们需要一个能提供海量高匿名 IP、超低延迟、并且支持毫秒级轮换的“防弹衣”。

2. 工业级破局:高匿名隧道代理

在商业高频多线程采集场景下,如果我们自己去维护一个几万大小的代理池,光是验证 IP 有效性、处理超时重试的代码就能写死你。

这时候通常会直接接入成熟的工业级方案 —— 比如爬虫代理。它利用隧道代理技术,在服务端帮我们自动实现毫秒级 IP 切换,网络延迟能压到 100ms 左右。

针对高频多线程采集,可以直接用下面的动态转发标准模式,在代码里只需要配置一个固定的隧道入口,每次请求出去,爬虫代理会自动帮你变幻出一个全新的真实出口 IP:

import threading
import requests

# 亿牛云动态隧道代理配置
PROXY_DYNAMIC = "http://ip.16yun.cn:8080"  # 隧道服务器地址
PROXY_USER = "YOUR_YINIUYUN_USER"         # 你的用户名
PROXY_PASS = "YOUR_YINIUYUN_PASSWORD"     # 你的密码

def fetch_data(url, thread_id):
    # 每次请求,爬虫代理服务端会自动分配不同的高匿名出口 IP
    proxies = {
        "http": f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_DYNAMIC}",
        "https": f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_DYNAMIC}",
    }
    try:
        response = requests.get(url, proxies=proxies, timeout=10)
        if response.status_code == 200:
            print(f"[线程 {thread_id}] 成功穿透!当前返回状态: {response.status_code}")
    except Exception as e:
        print(f"[线程 {thread_id}] 发生异常: {e}")

# 模拟 5 个线程并发请求,测试多线程并发切换 IP 的稳定性
threads = []
target_url = "https://httpbin.org/ip"  # 用于验证出口 IP 的测试地址

for i in range(5):
    t = threading.Thread(target=fetch_data, args=(target_url, i))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

总结:给新手的打怪通关指南

这五个逻辑从来不是孤立的,它们是层层递进的防御矩阵:

控制频率高匿名代理保护伪装请求特征模拟人类行为\text{控制频率} \rightarrow \text{高匿名代理保护} \rightarrow \text{伪装请求特征} \rightarrow \text{模拟人类行为}

维度解决什么问题核心手段优先级
请求频率避免触发 Rate Limit 阈值random.uniform 随机延时⭐⭐⭐
代理保护隐藏真实身份,防止宽带/机房被封接入高匿名隧道代理⭐⭐⭐⭐⭐
请求特征避免被识别出是 Python/脚本模拟完整 Header、处理 TLS 指纹⭐⭐⭐⭐
行为模式破坏反爬系统的机器学习画像遵循“首页->列表->详情”访问路径⭐⭐⭐

爬虫和反爬是一场没有终点的攻防战。别指望有一段代码能一劳永逸地解决所有网站,培养从防守方视角看问题的思维,比你收藏 100 个“高效爬虫模板”管用得多。