爬虫不断换IP抓站怎么防?IP段与ASN封禁实战

很多站长都遇到这种情况:日志里全是同一个爬虫的请求,封掉几个 IP 之后它换个 IP 接着抓,速度一点没降。这叫分布式爬虫,靠 IP 池轮换绕过单点封禁。本文用 9 个问答讲清怎么判断一堆请求来自同源、如何按 C 段和 ASN 聚合封禁、ipset 高性能封段怎么配,以及怎么避免误伤真实用户和搜索引擎蜘蛛。

Q:为什么单封一个 IP 对换 IP 的爬虫没用?

因为抓取方用的是 IP 池。常见来源有三类:

  • 云厂商 / IDC 弹性 IP:购买一批 VPS 或按量计费弹性 IP,脚本里配置代理池轮换,成本极低。
  • 四层代理与隧道服务:住宅代理、机房代理按流量计费,可以设置每次请求换 IP,肉眼看日志就像“遍布全国的真人”。
  • 被入侵的第三方主机:用他人被黑的服务器当跳板,IP 五花八门但都指向同一批 ASN。

关键结论是:分布式爬虫的 IP 是易变的,但它所属的网段和 ASN 是稳定的。封 IP 是打点,封段和封 ASN 才是打面。所以排查思路要反过来——先做日志聚合,找出“一小撮网段贡献了绝大部分请求”,再按段处置。

Q:怎么判断抓你站的是一批同源 IP?

看日志聚合结果,而不是逐条看。几条命令就能看出端倪。

先看 Top IP 和它们的请求量:

awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -30

再按 /24 聚合成 C 段,看是不是集中在少数几段:

awk '{split($1,a,"."); print a[1]"."a[2]"."a[3]".0/24"}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -rn | head -30

典型的分布式爬虫长这样:单个 IP 的请求量并不夸张(比如几百次),但同一个 /24 里冒出几十个 IP,合计几千上万次;而且 UA 高度一致、请求路径集中在列表页和详情页。

进一步验证 UA 与请求特征的一致性:

awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

如果某个 UA 占比异常高、同时对应多条 C 段,基本可以确认是同一批分布式抓取。

Q:Nginx 里怎么按 C 段批量封禁?

最省事的做法是把段列表单独放一个文件,用 include 引入,不要直接堆在主配置里。

# /etc/nginx/block_cidr.conf
deny 45.61.128.0/18;
deny 103.28.52.0/22;
deny 185.220.100.0/22;

在 http 或 server 块里引入:

http {
    include /etc/nginx/block_cidr.conf;
}

几点注意事项:

  • deny 属于 access 阶段,匹配开销极低,几十到几百条规则对性能基本无影响。
  • 被封的请求默认返回 403。如果不想给爬虫任何反馈,改用一个统一的返回更干净:if ($blocked) { return 444; } 配合 map 使用,444 会直接断开连接,不返回任何内容。
  • 一旦配置超长(上千条),优先用 ipset 而不是继续堆 deny,Nginx 会逐条匹配,而 ipset 是哈希查找。
  • 改完必须 nginx -t 校验再 reload,段写错会让整站规则加载失败。

Q:ASN 封禁是什么?为什么比 IP 段更彻底?

ASN(自治系统号)是网络运营商或机房在全球路由体系里的编号,一个 ASN 对应一整套 IP 网段。攻击者和爬虫用的 IP 池,通常来自少数几个廉价机房 ASN。

所以封 ASN 的逻辑是:与其追着它换 IP 封,不如把它背后的整块来源封掉。一个被抓取站点的常见案例是,几千个轮换 IP 实际只落在 3 到 5 个 ASN 里,封掉这几个 ASN 的所有网段,爬虫的 IP 池直接失效。

但 ASN 封禁有两道门槛必须清楚:

  • 粒度粗,误伤风险高。一个 ASN 动辄几十万个 IP,里面可能混着正常用户和你的合作方。所以 ASN 封禁适合“确认是纯机房 / 纯代理来源”的场景,并且要先在 WAF / CDN 层做观察模式,确认只影响恶意流量再真正拦截。
  • 需要自己维护网段清单。ASN 的实际路由前缀会变化,建议定期(比如每月)重新拉取一次,别用一份永不更新的静态表。

实操上更稳妥的组合是:对纯机房 ASN 直接封段,对混合型 ASN 只做限速和行为识别,不搞一刀切。

Q:怎么查一个 IP 属于哪个 ASN、有哪些网段?

先用现成接口查 IP 的归属:

curl -s https://ipinfo.io/45.61.130.10/org
# 输出示例:AS396982 Google LLC

拿到 ASN 后,可以拉取该 ASN 对外宣告的全部路由前缀:

whois -h whois.radb.net -- '-i origin AS396982' | grep -E '^route' | head -40

如果宿主机装了 whois 但没装 radb 相关工具,也可以直接访问 bgp.he.net 或 ipinfo 的 ASN 详情页人工核对网段清单。

把查到的段整理成 deny 列表时,建议只保留 /18 到 /24 这类较窄的段,避免把整个 /8 级别的超大段封掉,那是自伤。

另外别忘了核对:封之前先确认这些段里没有搜索引擎蜘蛛和你的正常合作方。Googlebot 与百度蜘蛛的 IP 段官方都有公布,直接拉取一份作为白名单,优先级放在封禁列表之前。

Q:用 iptables / ipset 封 IP 段怎么配?

当封禁列表到几百上千条时,iptables 逐条规则匹配会成为瓶颈,标准做法是 ipset + 一条 iptables 规则

创建集合并加入网段,设置自动过期避免长期累积:

ipset create crawler nethash timeout 86400
ipset add crawler 45.61.128.0/18
ipset add crawler 103.28.52.0/22

iptables -I INPUT -m set --match-set crawler src -j DROP

查看和清理:

ipset list crawler | head -20
ipset flush crawler        # 清空但保留集合
ipset destroy crawler      # 彻底删除

几个必须知道的点:

  • timeout 参数很重要。设了 86400 秒后,条目会自动过期,避免你几个月后还封着一批早就换人的 IP。
  • 集合要用 nethash 而不是 hash:ip,前者支持网段,后者只能加单个 IP。
  • ipset 在 Docker 或部分云主机内核上可能不可用,先 modprobe ip_set 确认模块能加载;不能用就退回 Nginx 层 deny。
  • 这条 iptables 规则要放在放行业务端口的规则之前,否则生效不了。

Q:误封风险怎么控制?会不会伤到真实用户和蜘蛛?

会,而且这是封段 / 封 ASN 最大的风险。四条控制手段:

  • 先观察再拦截。所有段封禁规则先在日志模式下跑 24 小时,看被命中的请求里有没有正常 UA、有没有正常的登录 / 下单行为。云 WAF 一般都支持“观察模式”,务必用上。
  • 蜘蛛白名单优先。把 Googlebot、百度蜘蛛、必应蜘蛛的官方网段做白名单,排在封禁逻辑之前。注意要用反向 DNS 正查 + 正查结果再解析的方式验证,不能只看 UA。
  • 段尽量窄。能封 /24 就别封 /16。真正的分布式爬虫即使只封掉它最活跃的几个 /24,抓取效率也会掉一大截。
  • 设置告警。封禁规则命中量突然放大、或者误封投诉出现时要有通知,别封完就不管了。

Q:有没有比封 IP 更省事的思路?

有,而且长期看更划算。封 IP 是消耗战,爬虫换 IP 的成本远低于你维护名单的成本。更高效的三个方向:

  • 给爬虫降速,而不是拒绝。Nginx 对可疑 UA 或段做严格限速,请求照样返回 200,但每秒只放 1 到 2 个,爬虫抓同样量级的数据要花几十倍时间,很多抓取方会主动放弃。
    # 用 map 构造限速键:正常浏览器 UA 得到空字符串,Nginx 不计入限速
    map $http_user_agent $crawler_key {
        default                                            "";
        "~*(python-requests|Scrapy|Go-http-client|curl/)"   $binary_remote_addr;
    }
    
    limit_req_zone $crawler_key zone=slow:10m rate=1r/s;
    
    server {
        location / {
            limit_req zone=slow burst=5 nodelay;
        }
    }

    这里的空键技巧很关键:Nginx 对限速键为空的请求不做计数,所以命中规则的可疑 UA 被单独限到 1 r/s,正常用户完全不受影响。

  • 在数据层防采集。列表页只返回摘要,核心字段通过带签名和时效的接口按需加载;再埋几个正常用户不会访问的蜜罐链接,谁点谁进黑名单。
  • 关键接口加签名 + 时效。给 API 请求带上带时间戳的签名,一次性有效,脚本拿不到签名规则就抓不走数据。

这三招的共同点是:不依赖“认得出发起方是谁”,而是让抓取变得费力。对换了 IP 的分布式爬虫尤其有效。

Q:还有什么补充建议?

把处置顺序固定下来,遇到分布式爬虫就不会乱:① 日志聚合找出贡献最大的 /24 段和 ASN → ② 核对 ASN 里有没有正常来源 → ③ 蜘蛛白名单先行 → ④ 用 ipset 封最活跃的几个段止血 → ⑤ 对剩余可疑流量做严格限速 → ⑥ 上线蜜罐与接口签名做长期防护 → ⑦ 每月复查一次名单,清掉过期条目

要提醒的是:封 IP 只能解决一时的抓取压力,解决不了数据被搬走。如果对方的目的是拿走你的内容,最终还是要靠数据层设计(接口签名、蜜罐、内容加密分发)来防。另外,如果服务器前面有 CDN 或 WAF,记得在源站 Nginx 里正确还原真实客户端 IP,否则你封的全是 CDN 节点的 IP。