前言
先把标题的说法补全一下:「爬虫泛滥」如果理解成「所有爬虫都是坏事」,那是不准确的。搜索引擎的收录、监控系统的可用性探测、你自己写的日志分析脚本,都是爬虫,它们遵守 robots.txt、控制频率、带清晰的身份标识,对站点是有益的。真正要处理的是滥用式抓取:高频无节制、刻意隐藏身份、绕过访问限制,把站点带宽和服务器资源拖垮,或者把受版权保护的内容整体搬走。
所以本文的题目可以更准确地表述为「如何识别并应对滥用式抓取」。全文只讲防御侧——怎么发现异常、有哪些防御手段、怎么用 Python 做日志分析。不会给出任何绕过限制、伪造身份、破解验证码的手法,那既不合规,也帮不上真正要解决问题的站长。
示例适用于 Python 3.8 及以上。Python 2 已于 2020 年 1 月 1 日停止维护,本文只给 Python 3 写法。
一、先分清善意与滥用
判断的依据不是「是不是爬虫」,而是行为特征。三条最关键的区分标准:
| 维度 | 善意爬虫 | 滥用式抓取 |
|---|
| 是否遵守 robots.txt | 遵守,并读取Crawl-delay | 无视 |
| 请求频率 | 有间隔,尊重Retry-After | 高频、规律、无停顿 |
| 身份标识 | UA 写明程序名与联系方式 | UA 为空、伪造或高度雷同 |
| 抓取范围 | 有限、聚焦 | 全站遍历、含他人隐私页 |
| 负载感知 | 低谷抓取、并发受限 | 与访问高峰叠加,拖垮服务 |
站点的处理策略也应分级:善意的放行并给清晰的 robots 规则,可疑的降速,确认滥用的才封禁。一上来就无差别封 IP,最先误伤的可能就是搜索引擎蜘蛛。
二、从访问日志里识别异常
识别的主要手段是分析访问日志。滥用式抓取在日志里会留下组合特征:
- 频率异常:单个 IP(或少数 IP 段)的请求数远超正常用户。
- UA 异常:UA 为空、UA 里出现脚本库名称、或大量请求共用同一个 UA。
- 路径分布异常:只请求详情页,不请求任何 CSS、图片、脚本。
- 会话异常:不携带 Cookie、不看来源页(referer 为空)。
- 时间分布异常:请求间隔高度均匀,或集中在极短时间窗内。
单一特征都可能误判,组合起来区分度才高。比如一个公司出口 IP 也可能发出大量请求,但它会正常加载静态资源、带浏览器 UA。
三、可用的防御手段
| 手段 | 作用 | 注意 |
|---|
| 写清 robots.txt | 给善意爬虫明确边界 | 它只是约定,恶意方不会遵守 |
| 按 IP / 会话限速 | 削掉高频抓取 | 阈值要留余量,别误伤正常用户 |
| 校验 User-Agent | 拦掉无标识的脚本 | 不要只靠 UA,它可被伪造 |
| 要求 JS 渲染或登录 | 提高抓取成本 | 会影响真实用户的可用性 |
| 蜜罐链接 | 识别「无脑全抓」的程序 | 正常用户不会点击隐藏链接 |
| CDN / WAF | 在边缘拦截与吸收流量 | 规则要与业务频率特征匹配 |
| 验证码 / 人机校验 | 拦住自动化 | 只讲防御侧,不做破解 |
还有一条常被忽略:给搜索引擎蜘蛛留出验证通道。业界常见做法是结合 UA 与反向 DNS 验证来确认对方身份,通过验证的放行、给合理的抓取额度。这既避免误伤,也不给滥用者可乘之机。
实战:一个访问日志频率分析器
下面的脚本读取 Nginx/Apache 的 common / combined 格式日志,找出「请求量大、且带非浏览器 User-Agent」的 IP。它是纯防御性的分析工具。
# 适用于 Python 3.8+
import re
from collections import defaultdict
LINE_RE = re.compile(
r'(?P<ip>\S+)\s+\S+\s+\S+\s+\[(?P<time>[^\]]+)\]\s+'
r'"(?P<request>[^"]*)"\s+(?P<status>\d{3})\s+(?P<size>\S+)'
r'(?:\s+"(?P<referer>[^"]*)"\s+"(?P<agent>[^"]*)")?'
)
SUSPICIOUS_AGENT_HINTS = ("python", "curl", "wget", "scrapy", "requests")
def parse_log(path):
records = []
with open(path, encoding="utf-8", errors="replace") as f:
for line in f:
m = LINE_RE.match(line)
if m:
records.append(m.groupdict())
return records
def path_of(request):
parts = request.split(" ")
return parts[-1] if parts else ""
def analyze(records, threshold=100):
"""按 IP 汇总:总请求数、可疑 UA 数、访问过的不同路径数。"""
per_ip = defaultdict(lambda: {"total": 0, "suspicious": 0, "paths": set()})
for r in records:
info = per_ip[r["ip"]]
info["total"] += 1
agent = (r.get("agent") or "").lower()
if not agent or any(h in agent for h in SUSPICIOUS_AGENT_HINTS):
info["suspicious"] += 1
info["paths"].add(path_of(r["request"]))
report = []
for ip, info in per_ip.items():
if info["total"] >= threshold and info["suspicious"] > 0:
report.append((ip, info["total"], info["suspicious"], len(info["paths"])))
report.sort(key=lambda row: row[1], reverse=True)
return report
def main():
records = parse_log("access.log")
print("日志条数:", len(records))
for ip, total, suspicious, uniq_paths in analyze(records, threshold=100)[:10]:
print("可疑 IP:", ip, "总请求:", total,
"可疑UA:", suspicious, "路径数:", uniq_paths)
if __name__ == "__main__":
main()使用要点:阈值threshold要按自己站点的正常流量调,别照抄 100。analyze返回的是待人工复核的名单,不是可以直接封禁的结论——发现可疑后应结合时间窗、路径分布再判断,并优先采取降速而非直接封禁。
常见坑点
- 把「爬虫」整体当敌人
❌ 见到非浏览器 UA 就封 —— 搜索引擎蜘蛛、监控探针都被误伤。 ✅ 按行为分级处理:善意放行、可疑降速、滥用封禁。
- 只靠 User-Agent 判断
❌if "python" in agent: block—— UA 可以随便伪造,拦不住真正的滥用者。 ✅ 组合频率、路径分布、静态资源请求等多个特征。
- 阈值定得太死
❌ 直接把请求数超过 20 的 IP 全封 —— 正常用户翻页也会超。 ✅ 结合时间窗与业务特征设定阈值,留出余量。
- 误伤搜索引擎
❌ 无差别封 UA 里含bot的请求。 ✅ 用 UA 加反向 DNS 验证确认身份,给验证通过的放行并配额。
- 只封 IP,不封会话
❌ 封了 IP,对方换出口继续抓。 ✅ 结合会话、Cookie、验证码等多维手段,提升抓取成本。
- robots.txt 写成摆设却指望它拦人
❌ 写了Disallow就以为万事大吉 —— 恶意方不遵守约定。 ✅ 把 robots 当作「对善意方的告知」,真正的防护交给限速与边缘拦截。
- 解析日志用错的格式假设
❌ 用固定位置切分日志 —— 不同 log_format 字段数不同,直接错位。 ✅ 用带命名组的正则并留可选组,解析失败的行跳过。
- 搬 Python 2 的老写法
❌for k, v in d.iteritems():、print "x"—— Python 2 已停止维护。 ✅ 用d.items()和print(...);这些在 Python 3 里才成立。
总结
| 阶段 | 动作 | 工具 / 手段 |
|---|
| 告知 | 写清楚的 robots.txt | User-agent/Disallow/Crawl-delay |
| 识别 | 分析访问日志的组合特征 | 频率、UA、路径分布、会话 |
| 缓解 | 限速优先于封禁 | 按 IP / 会话限速、CDN / WAF |
| 保留 | 放行真实搜索引擎 | UA + 反向 DNS 验证 |
应对滥用式抓取的核心思路是「先识别、再分级、缓处理」:用日志分析找出真正异常的那一小部分流量,对善意方放行,对可疑方降速,对确认的滥用才封禁。防御的目标是保住站点可用性,而不是把所有自动化访问都赶尽杀绝。