百度收录量忽然腰斩:用日志分析追查 Baiduspider 抓取异常的完整过程
适用读者:维护传统网站的 SEO 技术同学、管 Nginx 的后端工程师,以及所有盯着百度索引量曲线发愁的站长。
两周时间,站点的百度索引量从 4200 掉到 1900,掉了一半还多。搜索资源平台的后台却一直显示「数据正常」,反馈中心提交问题,等来的也是模板回复。运营那边急了,把电话打到运维老周(化名)那里,老周看了一眼监控说:「服务器没报警,流量也稳,应该不是我们的问题。」这句话后来被证明完全不对——问题恰恰就出在我们自己身上,而且是两个叠加在一起的低级错误。
这是一家做本地生活服务的站点,页面量不小,本地服务类目的搜索流量占了大头。索引量腰斩意味着接下来几周自然流量会跟着往下塌,等流量曲线掉下去再动手就晚了。所以那天下午我们决定不猜了,直接去翻 Nginx 的访问日志。
后台说正常,日志里全是事故现场
百度搜索资源平台的索引量工具给的是一个事后统计值,更新有延迟,而且只告诉你掉了多少,不告诉你为什么掉。后台显示「正常」指的是平台服务本身没故障,跟蜘蛛抓没抓到你的页面是两码事。这个区别当时没人想明白,大家都在等后台自己恢复,一等就是一周。
访问日志不会撒谎。当晚我把前一周的 access.log 拉下来粗看了一眼,自称 Baiduspider(百度蜘蛛)的请求量并不少,但状态码很扎眼:大片的 404 和 302,中间还夹着成串的 429。情况清楚了——蜘蛛天天来,来的却大都在撞墙。
不过在分析状态码之前,还有一道必须先做的工序:分清真假蜘蛛。
真假 Baiduspider 的判定机制
网上有大量流量伪装成百度蜘蛛来爬站,有的想薅内容,有的干脆在扫漏洞。如果把假蜘蛛的请求算进抓取统计,后面所有结论都是歪的。判定真蜘蛛只有一个可靠办法:反向 DNS(Reverse DNS)验证,这也是 Google 官方推荐验证 Googlebot 的同一套思路,百度官方文档里的判定要求是一致的。
机制拆开看是这样:真蜘蛛的来源 IP 做 PTR 反查,得到的域名一定落在 baidu.com 或 baidu.jp 的子域下,形如 crawl-116-179-32-1.baidu.com。光有这一步还不够严谨,因为 PTR 记录本身可以伪造,所以要再做一次正向解析,把反查出来的域名解析回 IP,结果里必须包含原来那个 IP,两次都过才算数。UA 字段里带 Baiduspider 字样不构成任何证据,UA(User-Agent)是客户端自己填的字符串,想写什么写什么。
当天日志里自称 Baiduspider 的来源 IP 有 47 个,走完两步验证后只剩 9 个,其余 38 个全是伪装流量,占比超过八成。这批假流量的请求集中在站外推广落地页和搜索参数页,一看就是冲着内容库存来的。先把它们从统计里剔掉,才能看真蜘蛛到底遇到了什么。
日志分析脚本:反查验证与状态码统计
环境说明:CentOS 7,Nginx 1.20,日志为 combined 格式,主机上装有 bind-utils(提供 host 命令)。反查这步用 shell 就够,统计部分用 Python,无第三方依赖。
# 前置条件:能登录网站服务器,装有 bind-utils 或 dnsutils(提供 host 命令)# 目标:把日志里自称 Baiduspider 的来源 IP 逐一反查验证# 从当天日志抽出所有带 Baiduspider 的请求 IP,去重awk-F'"''/Baiduspider/ {print $1}'access.log|awk'{print $1}'|sort-u>spider_ips.txt# 逐个 IP 做反向 DNS,看 PTR 记录落在哪个域whilereadip;doptr=$(host"$ip"2>/dev/null|awk'{print $NF}')echo"$ip->$ptr"done<spider_ips.txt# 判定标准:PTR 必须是 *.baidu.com 或 *.baidu.jp 结尾# 其他任何形如 baiduspider-xxx.xxx 的域名都按假蜘蛛处理# 把两步验证都通过的 IP 手工整理进白名单文件cat>spider_ips_verified.txt<<'EOF' # 以下 IP 均已通过 PTR 反查 + 正向解析双重验证 # 116.179.32.x 段与 123.125.71.x 段为常见真蜘蛛来源 EOF# 对通过反查的域名再正向解析一次,确认解析回同一个 IPhostcrawl-116-179-32-1.baidu.com# 输出地址列表里必须包含原来那个 IP,双重验证才算过# 依赖:Python 3.8+,无第三方库# 用途:统计真蜘蛛(IP 已通过反查验证)请求的状态码分布# 日志格式:Nginx combinedimportrefromcollectionsimportCounter# 读取双重验证通过的真蜘蛛 IP 清单,每行一个REAL_IPS=set(open("spider_ips_verified.txt").read().split())# 匹配请求行里的状态码,例如 "GET /x HTTP/1.1" 404STATUS=re.compile(r'HTTP/1\.[01]" (\d{3})')# 匹配请求的目标 URL,用于单独统计 404 死链PATH=re.compile(r'"(?:GET|POST|HEAD) ([^ ?]+)')status_counter=Counter()path_counter=Counter()forlineinopen("access.log",encoding="utf-8",errors="ignore"):# 先按 UA 粗筛,减少后续正则的运算量if"Baiduspider"notinline:continue# 再按来源 IP 精筛,只留反查验证过的真蜘蛛ip=line.split()[0]ifipnotinREAL_IPS:continuem=STATUS.search(line)ifnotm:continuecode=m.group(1)status_counter[code]+=1# 404 的目标地址单独计数,方便后续回收旧链接ifcode=="404":p=PATH.search(line)ifp:path_counter[p.group(1)]+=1total=sum(status_counter.values())# 输出状态码占比,看清抓取配额被谁吃掉了forcode,ninstatus_counter.most_common():print(code,n,f"{n/total:.1%}")# 输出被抓得最多的死链 Top 20,作为回收清单的输入forp,ninpath_counter.most_common(20):print("404",n,p)跑完统计,问题全部摆在桌面上。
补充一个更聚焦的排查脚本:直接统计真蜘蛛请求中 404、302、429 三类异常状态码的 Top 20 URL,并输出每个 URL 对应的时间段分布,方便快速定位问题源头。
#!/bin/bash# 用途:统计真蜘蛛请求中 404、302、429 状态码的 Top 20 URL,并输出时间段分布# 用法:bash spider_anomaly_top.sh <access.log> <spider_ips_verified.txt># 输出格式:# [状态码] 次数 URL# [状态码] 时间段分布: 02:00-03:00 x 次, 03:00-04:00 y 次, ...# 前置条件:spider_ips_verified.txt 为已通过 DNS 双重验证的真蜘蛛 IP 清单LOG="${1:-access.log}"IPS="${2:-spider_ips_verified.txt}"# 读取真蜘蛛 IP 清单,构建 awk 可用的匹配集合IP_LIST=$(tr'\n''|'<"$IPS"|sed's/|$//')# 逐行过滤:先按 UA 粗筛,再按 IP 精筛,只保留 404/302/429 三类异常awk-vips="$IP_LIST"' /Baiduspider/ && $1 ~ "^(" ips ")$" { # 提取状态码(combined 格式中位于请求行之后) for (i = 1; i <= NF; i++) { if ($i ~ /^[0-9]{3}$/) { code = $i; break } } if (code == "404" || code == "302" || code == "429") { # 提取请求 URL(去掉查询参数,只留路径) for (i = 1; i <= NF; i++) { if ($i ~ /^(GET|POST|HEAD)/) { url = $(i+1); break } } # 提取小时段(combined 格式时间形如 [26/Sep/2026:09:20:42 +0800]) match($0, /\[[0-9]{2}\/[A-Za-z]{3}\/[0-9]{4}:[0-9]{2}/) hour = substr($0, RSTART + 17, 2) key = code "|" url count[key]++ hour_count[key "|" hour]++ } } END { # 按状态码分组输出 Top 20 for (key in count) { split(key, parts, "|") code = parts[1]; url = parts[2] print code, count[key], url } } '"$LOG"|sort-k1,1 -k2,2nr|head-60>/tmp/anomaly_top.txt# 分组展示:每个状态码下按次数降序,并附时间段分布forcodein404302429;doecho"===== 状态码$codeTop 20 ====="grep"^$code"/tmp/anomaly_top.txt|head-20|whilereadc n url;doecho"$n次$url"# 输出该 URL 的时间段分布awk-vtarget="$code|$url"' $0 ~ target { split($0, parts, "|") print " 时间段分布: " parts[3] " 点段 " parts[2] " 次" } '/tmp/anomaly_hour.txt2>/dev/nulldonedone脚本说明:先按 UA 和已验证 IP 双重过滤,只保留真蜘蛛请求;再筛出 404、302、429 三类异常状态码,统计每个 URL 的总次数并输出 Top 20,同时按小时段统计每个 URL 的分布,便于判断异常是集中在某个时段(如限流触发窗口)还是全天均匀出现。
状态码分布:抓取配额被谁吃掉了
真蜘蛛两周内一共留下约 12.6 万条请求,状态码分布如下。
| 状态码 | 占比 | 典型目标 | 问题定性 |
|---|---|---|---|
| 200 | 32% | 首页、栏目页 | 正常返回 |
| 404 | 26% | 旧新闻详情页 | 迁移后链接未回收 |
| 302 | 14% | 旧域名跳转 | 应改 301 永久跳转 |
| 429 | 20% | 全站随机 | 限流模块误伤真蜘蛛 |
| 5xx 及其他 | 8% | 动态接口 | 上游偶发超时 |
抓取配额(Crawl Budget)是个容易被忽略的东西:百度给一个站点的抓取频次有上限,蜘蛛在站内每次打在 404 或 302 上,都是在消耗这份配额。配额被死链和临时跳转吃掉四成,正常页面的抓取量自然上不去,新内容进不了索引,旧内容又因为长期得不到更新抓取被逐步清退,索引量就这么一路往下掉。
顺着 404 Top 20 的地址清单往下追,两个源头很快浮出来。一个是旧链接没回收:半年前站点从旧域名迁到新域名,新闻详情页的 URL 规则改过一次,迁移时只做了首页和栏目的跳转,十几万条详情页旧地址悬空,蜘蛛手里攥着的还是旧地址,来了就是 404。302 的来源类似,旧域名当初配的是临时跳转,一直没人改成 301(永久重定向)。另一个是 robots 文件误封:对比迁移前后的 robots.txt,新文件里多了一行 Disallow: /m/。本意是封掉移动端一套废弃模板,但移动端站点实际就挂在 /m/ 目录下,这一封等于把移动端全部页面挡在抓取之外。上线两个月,/m/ 下没有任何新页面被抓过。
至于 429,是迁移时新上的限流模块干的。运维照着网上教程配了 Nginx 限流(Rate Limiting),阈值给得激进,真蜘蛛抓取频次一冲高就撞墙,撞了就收到 429。假蜘蛛倒是不受影响——它们分散在各个 IP 上,没触发单 IP 限流。这个对比很讽刺,也说明限流策略压根没考虑过搜索引擎蜘蛛的来源特征。
修复时间线:六周从 1900 回到 5100
问题清单确定后,按影响面从大到小排期动手,前两周见效最快。
第 1 周先把 robots.txt 里的 Disallow: /m/ 删掉,这是零成本动作;同一周给限流配置加上了已验证蜘蛛 IP 段的白名单,429 从此归零。第 2 周到第 3 周处理旧链接,十几万条详情页旧地址按规则映射到新地址,直接在 Nginx 层做批量 301,同时把旧域名的 302 全部改成 301。第 4 周在搜索资源平台提交新链接和死链清单,让百度那边尽快更新对站点的认知。第 5、6 周基本是等,索引量从第三周末开始抬头,第 6 周末回到 5100,比掉之前的 4200 还多出 900——迁移后一直没被抓的移动端页面这回全进来了。
修复前后的关键数据对比:
| 指标 | 修复前(8 月中) | 修复后(10 月初) |
|---|---|---|
| 真蜘蛛日均抓取次数 | 约 9000 | 约 21000 |
| 404 占比 | 26% | 3% |
| 429 占比 | 20% | 0 |
| 302 占比 | 14% | 1% |
| 百度索引量 | 1900 | 5100 |
几条经验写给同行,少走弯路。索引量下降时别只盯后台,第一时间去服务器拉日志,日志是最接近事实的数据源。任何迁移,301 重定向和 robots 检查要当成发布清单的固定项,不能靠事后补。限流策略必须给搜索引擎蜘蛛留通道,否则等于亲手把蜘蛛往外推。真假蜘蛛的判定只能靠 DNS 反查加正向验证,UA 靠不住,这条没有变通余地。
顺带说说 GEO:AI 爬虫的日志长得不一样
这套日志分析方法同样适用于盯 AI 爬虫(GEO,Generative Engine Optimization 关注的那批机器人),但识别方式有差别。搜索引擎蜘蛛普遍提供官方反向 DNS 验证,百度蜘蛛认 baidu.com 和 baidu.jp,Googlebot 认 googlebot.com 和 google.com;而不少 AI 爬虫的反查验证要么没提供,要么域名不统一,GPTBot、ClaudeBot 这类主要靠 UA 加已知 IP 段对照来识别,误判率天然高一些。关注点也不同:搜索引擎蜘蛛看重抓取频次与收录规模,AI 爬虫更在意内容能不能被引用进生成的答案,日志里的地址分布、命中页面类型差别都很大。传统 SEO 和 GEO 短期内是两套并行的事,别混在同一个仪表盘里看。
误区澄清或趋势预判
两个常见误区顺手澄清。有人认为索引量下降一定是内容质量问题,先改文章再说——如果抓取环节满是 404 和 429,内容写得再好蜘蛛也拿不走,先修抓取链路再看内容,顺序不能反。也有人觉得给蜘蛛放开抓取会让服务器压力变大,实际上配合正确的限流白名单和缓存策略,蜘蛛的请求模式相当规律,压力完全可控。
往后看,搜索引擎对站点质量的判断会越来越依赖抓取环节的信号:返回 410 和正确 301 的站点,比满站 404 加 302 的站点,在同等内容条件下拿到的抓取配额只会差距更大。日志分析这门老手艺,做站点的人值得练熟。
常见问题排查清单
| 常见问题 | 快速排查步骤 | 解决建议 |
|---|---|---|
| 日志中 Baiduspider 请求量正常但索引量下降 | 1. 先按 DNS 反查 + 正向解析双重验证,剔除伪装流量;2. 统计真蜘蛛请求的状态码分布,重点看 404、302、429 占比;3. 对比近两周抓取量趋势,确认是否触及抓取配额上限 | 优先修复占比最高的异常状态码;若配额被死链消耗,先回收旧链接并下发 301,再在资源平台提交死链清单 |
| robots.txt 误封导致抓取骤降 | 1. 对比迁移前后 robots.txt 差异;2. 检查 Disallow 规则是否误伤正常目录;3. 用 curl 模拟蜘蛛抓取被 Disallow 的路径,确认返回 403 或 404 | 删除误封规则;对废弃目录改用 410 状态码明确告知蜘蛛;修改后到资源平台提交抓取诊断并等待重新抓取 |
| 限流模块误伤蜘蛛 | 1. 查看 access.log 中真蜘蛛请求的 429 占比;2. 检查 Nginx limit_req 配置的阈值与 burst 参数;3. 确认限流是否按单 IP 维度统计 | 为已验证的蜘蛛 IP 段配置白名单,绕过限流;适当放宽 burst 参数;保留对非蜘蛛来源的限流策略 |
| 迁移后旧链接未回收导致大量 404 | 1. 从日志中提取 404 目标地址 Top 20;2. 对比新旧 URL 规则,确认是否存在可映射关系;3. 检查旧域名跳转是 302 还是 301 | 在 Nginx 层批量下发 301 映射规则;旧域名统一改为 301 永久跳转;在资源平台提交死链清单加速清理 |
| 假蜘蛛流量干扰抓取统计 | 1. 对自称 Baiduspider 的来源 IP 做 PTR 反查;2. 对反查域名再做正向解析,确认解析回原 IP;3. 统计假蜘蛛请求的集中路径与频次 | 将未通过双重验证的 IP 加入防火墙黑名单或限流;在日志分析脚本中固定过滤规则,只统计真蜘蛛请求 |
参考与延伸
- 百度搜索资源平台学院(官方教程与规范入口):https://ziyuan.baidu.com/college/index
- Google 官方对验证 Googlebot 的反查 DNS 方法说明(思路与百度蜘蛛一致):https://developers.google.com/search/docs/crawling-indexing/verify-googlebot
- 百度搜索资源平台(索引量工具与反馈中心):https://ziyuan.baidu.com/
百度收录、Baiduspider、日志分析、抓取配额、Nginx、301 重定向