
如何排查网站是否被“小偷程序”采集:从访问日志入手
网站内容突然出现在别处,或者服务器流量、带宽和数据库负载异常时,访问日志通常是最先值得检查的证据。它能帮助回答一些具体问题:哪些客户端在什么时间请求了哪些页面、请求频率有多高、是否集中访问文章或图片、是否反复触发分页和搜索接口。
不过,日志不能直接告诉你“这个 IP 一定是小偷程序”。搜索引擎、监控服务、浏览器预加载、企业出口代理和普通用户,都可能呈现出自动化访问的特征;反过来,采集程序也可以更换 IP、放慢速度或伪装 User-Agent。比较可靠的做法是先从日志中找出异常模式,再结合内容相似度、时间线、请求资源和来源信息交叉验证。
一、适用场景与排查边界
本文适用于能够访问 Web 服务器访问日志的网站运维人员,示例以常见的 Nginx combined 格式为主。Apache 使用相近格式时,思路可以复用,但日志字段和配置需要按实际情况调整。如果网站位于 CDN、负载均衡器或反向代理之后,还要先确认日志记录的是哪一层的客户端地址。
访问日志通常能提供以下线索:
- 某个来源在短时间内请求了大量文章、商品或图片页面。
- 请求路径呈现规律性,例如遍历连续编号、分页或分类目录。
- 同一 User-Agent 对大量页面进行高速访问,且请求间隔稳定。
- 某些页面被持续重复请求,或采集后出现异常的 404、403、429 状态码。
- 请求时间与外站转载内容首次出现的时间大致对应。
日志的边界同样重要:
- IP 地址不是身份。 多个用户可能共享一个 NAT 出口;采集者也可能使用代理、云主机或不断变化的地址。
- User-Agent 可以伪造。 声称自己是搜索引擎,并不能证明请求来自该搜索引擎。
- 请求不等于成功获取内容。 访问日志记录的是服务器处理的 HTTP 请求,不一定代表完整响应已被客户端收到或保存。
- 请求量不等于采集量。 缓存、条件请求、页面中的静态资源、客户端重试都会影响计数。
- 日志保留范围有限。 轮转、采样、CDN 缓存和上游日志配置都可能造成缺失。
因此,排查结论宜写成“某来源的访问模式与自动化采集相符”,而不是仅凭一次统计就断言“已确认盗取”。
二、先确认拿到的是哪一份日志
Nginx 常见访问日志位于 /var/log/nginx/access.log,但实际路径取决于站点配置。先查看配置中的 access_log 和 log_format:
sudo nginx -T 2>/dev/null | grep -nE 'log_format|access_log'
常见的 combined 日志行大致如下:
203.0.113.24 - - [15/May/2025:10:20:30 +0000] "GET /news/example HTTP/1.1" 200 18432 "https://example.com/" "Mozilla/5.0 ..."
其中通常包括客户端地址、时间、请求行、状态码、响应字节数、Referer 和 User-Agent。不同站点可能采用自定义格式,例如将真实客户端地址写入额外字段,或完全不记录 Referer。分析前应确认字段含义,不能把自定义日志误当成 combined 格式处理。
反向代理与 CDN 特别注意
如果 Nginx 前面还有 CDN 或负载均衡器,默认的 $remote_addr 可能是代理服务器地址,而不是访客地址。此时要查明:源站日志记录了哪一个字段、该字段是否由可信代理写入,以及代理是否会覆盖客户端提交的同名请求头。
不能直接相信任意请求中的 X-Forwarded-For。如果源站允许外部用户直接连接,攻击者可以自行伪造这个头,导致日志里的“客户端 IP”不可信。只有在网络层限制源站仅接受可信代理流量,并按代理产品文档配置真实地址解析后,相关字段才适合用于归因。若 CDN 提供独立访问日志,通常应把 CDN 日志与源站日志结合查看。
三、先用命令行快速找出异常来源
对标准 combined 格式,可先统计请求最多的客户端地址:
LOG=/var/log/nginx/access.log
awk '{print $1}' "$LOG" | sort | uniq -c | sort -nr | head -20
这只是一个粗略排行榜。它把日志第一列当作客户端地址,因此不适用于第一列不是 IP 的自定义格式;多个用户共用一个出口时,也可能被合并统计。
再看状态码分布,判断高请求量是否伴随大量失败:
awk -F '"' '{print $3}' "$LOG" \
| awk '{print $1}' \
| sort | uniq -c | sort -nr
这个写法依赖请求行和状态码位于双引号后的常见格式。遇到带有特殊字段、日志格式不同或引号处理不一致的记录时,结果可能不准确。它适合快速筛查,不适合用作审计结论。
查看某个候选 IP 请求最多的路径,可以使用:
IP=203.0.113.24
grep -F "$IP " "$LOG" | awk -F '"' '{print $2}' \
| awk '{print $2}' | sort | uniq -c | sort -nr | head -30
grep -F 会按普通文本匹配。对于共享网段、相似字符串或自定义格式,应使用能正确解析字段的工具,而不是依赖简单文本匹配。日志文件也可能包含敏感 URL 参数;把输出贴到公共论坛或第三方在线工具之前,应先脱敏。
日志较大时,不建议直接把整份文件反复复制到个人电脑。可以在服务器上筛选时间范围、压缩归档文件,或使用日志平台的聚合查询;同时检查磁盘余量,避免分析过程额外占满存储空间。
四、用 Python 汇总 IP、路径和 User-Agent
下面的脚本使用 Python 3 标准库,适用于每行基本符合 Nginx/Apache combined 格式的日志。它统计每个 IP 的总请求数、不同路径数、状态码分布、热门路径和 User-Agent。URL 查询参数会被去掉,避免将不同参数值展开显示;脚本不会主动访问网络,也不会修改日志。
将代码保存为 analyze_access.py:
#!/usr/bin/env python3
import argparse
import re
from collections import Counter, defaultdict
from urllib.parse import urlsplit
# 适用于常见 combined 格式:
# IP ident user [time] "METHOD target protocol" status bytes "referer" "UA"
LINE_RE = re.compile(
r'^(?P<ip>\S+) \S+ \S+ '
r'\[(?P<time>[^\]]+)\] '
r'"(?P<method>\S+) (?P<target>.*?) (?P<proto>[^"]+)" '
r'(?P<status>\d{3}) (?P<bytes>\S+) '
r'"(?P<referer>[^"]*)" "(?P<ua>[^"]*)"'
)
def safe_path(target):
# 只统计路径,不显示查询参数。
# urlsplit 对以 / 开头的普通请求目标可直接使用。
path = urlsplit(target).path or "/"
return path[:300]
def main():
parser = argparse.ArgumentParser(
description="汇总 combined 格式的 Web 访问日志"
)
parser.add_argument("logfile", help="日志文件路径;使用 - 表示标准输入")
parser.add_argument("--top", type=int, default=20,
help="输出多少个 IP,默认 20")
parser.add_argument("--min-requests", type=int, default=1,
help="只显示达到该请求数的 IP,默认 1")
args = parser.parse_args()
if args.top < 1 or args.min_requests < 1:
parser.error("--top 和 --min-requests 必须大于 0")
stats = defaultdict(lambda: {
"requests": 0,
"paths": Counter(),
"statuses": Counter(),
"uas": Counter(),
})
parsed = 0
skipped = 0
if args.logfile == "-":
import sys
stream = sys.stdin
close_stream = False
else:
stream = open(args.logfile, "r", encoding="utf-8", errors="replace")
close_stream = True
try:
for line in stream:
match = LINE_RE.match(line.rstrip("\n"))
if not match:
skipped += 1
continue
row = match.groupdict()
item = stats[row["ip"]]
item["requests"] += 1
item["paths"][safe_path(row["target"])] += 1
item["statuses"][row["status"]] += 1
# 避免终端输出过长的 UA 字符串
item["uas"][row["ua"][:180]] += 1
parsed += 1
finally:
if close_stream:
stream.close()
ranked = sorted(
(
(ip, data) for ip, data in stats.items()
if data["requests"] >= args.min_requests
),
key=lambda pair: pair[1]["requests"],
reverse=True,
)[:args.top]
print(f"成功解析 {parsed} 行;未匹配格式 {skipped} 行;独立 IP {len(stats)} 个")
for ip, data in ranked:
print(f"\nIP: {ip} 请求: {data['requests']} 不同路径: {len(data['paths'])}")
print("状态码:", ", ".join(
f"{code}={count}"
for code, count in data["statuses"].most_common()
))
print("常见路径:")
for path, count in data["paths"].most_common(10):
print(f" {count:6d} {path}")
print("常见 User-Agent:")
for ua, count in data["uas"].most_common(5):
print(f" {count:6d} {ua or '(空)'}")
if __name__ == "__main__":
main()
运行示例:
python3 analyze_access.py /var/log/nginx/access.log --top 30 --min-requests 100
若要分析压缩的轮转日志,可通过管道输入:
zcat /var/log/nginx/access.log.2.gz | python3 analyze_access.py - --top 30
脚本对无法匹配的行会计入“未匹配格式”,不会悄悄把它们当成有效请求。若未匹配数量很高,应先核对日志格式,而不是修改正则强行接受所有输入。脚本按 IP 汇总,无法自动识别 NAT、代理链或真实访问者;它也没有按时间窗口计算速率,因此不能单独判断“每秒多少次”。分析大量日志时,可先按日期拆分,或改用适合流式统计和时间窗口聚合的日志平台。
五、怎样判断“像采集”,而不是普通高流量
可疑程度通常来自多个迹象的组合,而不是某个单一阈值。对候选来源,建议至少检查以下方面:
1. 请求的内容类型和路径分布
逐条观察它访问的是文章正文、商品详情、分类页、图片,还是登录、搜索、接口和静态资源。一个客户端请求页面中的 CSS、JS 和图片,可能是正常浏览器渲染;只请求正文页面也可能是采集器,但并非必然。要留意页面链接结构:按栏目遍历、连续翻页、编号递增、短时间抓取大量历史内容,比单纯请求量高更有解释力。
2. 请求节奏与时间跨度
查看请求是否集中在短时间内,间隔是否近乎固定,是否全天持续,是否在短暂停顿后重复扫过同一批 URL。服务器访问日志通常记录请求时间,但聚合统计可能掩盖瞬时突发;最好按分钟甚至更细粒度观察。不同网站的合理访问量差异很大,不存在适用于所有站点的“每分钟超过多少次就一定是采集”的通用门槛。
3. 状态码、响应大小和重复行为
连续访问大量页面且大多返回 200,说明服务端成功处理了不少请求,但不能证明对方保存了响应内容。大量 304 表示条件请求命中缓存协商,并不等于对方没有已有副本;大量 404 可能是错误链接、扫描或采集器猜路径,也可能来自失效的站内链接。响应字节数可以作为线索,但日志里记录的具体字段可能是发送字节、响应体字节或自定义值,要先看 Nginx 日志变量定义。
4. User-Agent 和来源信息
空 User-Agent、明显的脚本库标识或自称爬虫的字符串,都可以用于筛查,但都可伪造。对声称来自某个搜索引擎的请求,不要只看名称;应按该服务公开的官方验证流程核验域名或 IP。Referer 也不是强身份凭证,客户端可以不发送或自行填写。
5. 与外部转载内容交叉验证
如果在其他网站发现高度相似的文章,可记录页面地址、首次发现时间、内容差异及截图或存档等证据,再与访问日志的请求时间、路径和来源模式对照。要注意外站页面的发布时间可能被回填或改写;某 IP 请求某篇文章,也不代表该 IP 所属主体就是转载者。技术线索和法律归属不是同一件事。
六、为后续分析补齐日志字段
如果现有日志不足以分析,可在变更前评估磁盘容量、隐私和性能,再增加必要字段。下面是一个 Nginx 示例,记录请求时间、地址、请求目标、状态码、发送字节数、处理耗时和 User-Agent:
http {
log_format audit
'$remote_addr [$time_local] "$request" '
'$status $body_bytes_sent $request_time '
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access_audit.log audit;
}
配置修改后先检查语法,再平滑重载:
sudo nginx -t
sudo systemctl reload nginx
实际部署时,应把配置放在适当的 http、server 或 location 上下文,并确认没有其他配置覆盖预期日志。加入 $request 会记录请求目标,查询参数中可能含有邮箱、令牌或其他敏感信息;若应用会在 URL 中放置秘密值,不应未经评估就把原始请求目标长期写入日志。必要时使用不记录查询参数的格式或在日志平台侧做脱敏。
日志的保存期限也应符合业务、隐私和合规要求。日志可能包含 IP 地址、访问路径、账号标识或其他个人信息,应限制读取权限、制定保留周期,并避免把原始日志随意传到外部服务。访问控制和日志轮转是分析方案的一部分,不是事后补救。
七、常见错误与误报来源
- 仅凭单个 IP 的请求数定性。 搜索引擎抓取、健康检查和大型公司出口可能产生高请求量。应结合路径、节奏、状态码和官方身份验证。
- 把 User-Agent 当作认证信息。 字符串很容易伪造。允许或阻挡请求时,不要只靠 User-Agent 名称做安全边界。
- 把共享出口中的所有请求归为一个主体。 NAT、校园网、移动运营商出口和代理服务会聚合许多真实用户。对单个 IP 限流可能误伤整个出口。
- 把 IP 归属查询当成责任证明。 IP 注册机构、云厂商和地理位置数据库只能提供有限线索,地址可能动态分配或由代理使用。
- 忽略 CDN 缓存造成的视角差异。 命中 CDN 的请求可能不会到源站;源站日志没有记录,不代表用户没有访问过。反之,回源请求的来源也可能是 CDN 节点。
- 混淆查询参数与路径。 同一条路径带不同参数可能是重复抓取,也可能是正常筛选、分页或追踪参数。分析时应分别观察规范化路径和参数含义,不要在未经审查时把参数值公开。
- 直接在生产环境运行耗资源的全量分析。 大文件排序会使用 CPU 和临时磁盘空间。先估算文件大小,尽量在副本、离峰时段或专用日志平台处理。
- 依据一次短时异常就长期封禁。 临时抓取、发布活动或爬虫集中重试都可能形成尖峰。先复核时间范围,再选择短期、可回滚的措施。
八、发现疑似采集之后怎么处理
先保留原始证据。记录日志文件来源、时区、查询时间范围、哈希值或只读副本,并注明分析脚本版本和筛选条件。保留证据时也要遵循组织的数据访问和隐私规范,避免把未经脱敏的用户信息扩大传播。
然后按风险分层处理:
- 低影响、尚未确认。 继续观察,增加适度的按路径和时间窗口统计,核对内容转载情况,不急于封禁。
- 明显异常且影响资源。 使用 CDN、WAF、反向代理或应用层做有期限的限速、挑战或针对性规则。设置监控和回滚方式,并关注误伤正常用户的投诉、错误率与转化数据。
- 持续高负载或影响可用性。 优先保护服务稳定性,例如对昂贵接口设置合理速率限制、使用缓存、限制不必要的批量查询,并按事件流程升级处理。不要把对单个 IP 的阻断当成唯一防护。
- 涉及版权或合同争议。 保存可核验材料,检查站点条款、授权范围和适用法律,必要时咨询专业人士。访问日志本身通常不足以单独证明侵权主体及其行为。
Robots Exclusion Protocol(常见的 robots.txt)用于告知遵循规则的爬虫哪些路径不希望被抓取。它不是访问控制机制,也不能阻止不遵守规则的程序。真正需要保密或限制访问的内容,应采用身份认证、授权校验、网络访问控制等措施,而不是只依赖 robots.txt、隐藏链接或修改页面地址。
同样,封禁 IP、修改 User-Agent 规则或对页面做混淆,都不能可靠阻止有能力更换来源的采集者。若内容不应公开,应从访问权限和数据发布流程解决;若内容本来公开,防护重点通常是控制资源消耗、提高可追溯性,并按实际情况处理转载问题。
九、建议的排查顺序
一个可复用的流程可以压缩为六步:
- 确定日志在哪一层产生、客户端 IP 字段是否可信、时区和日志格式是什么。
- 按时间范围统计来源请求量,找出偏离本站正常基线的候选对象。
- 查看候选对象的路径、时间分布、状态码、响应大小和 User-Agent。
- 结合 CDN/WAF 日志、服务器负载曲线和外部转载页面交叉验证。
- 保存必要证据,区分“可疑模式”和“已确认行为”,记录不确定性。
- 按影响程度采取限速、监控、访问控制或法律咨询等措施,并观察误伤与效果。
访问日志最适合回答“发生了什么请求”,不适合单独回答“请求者是谁、是否保存了内容、是否构成侵权”。把日志当作一组可核验的线索,先校准数据来源,再做多维度比对,才能把误报控制在合理范围内,并让后续的技术和业务处置有可靠依据。









