404分诊:先分清哪一批值得修

一、404 天天有,但处理的优先级天差地别

一个跑了几年的站,每天几千条 404 是常态。绝大多数站长看到 404 报告的第一反应是「一条一条补」,结果一个月下来补了几十个,流量一点没涨,人先累了。问题在于 404 的性质完全不同:有的是用户手打错的网址,有的是旧文章被删但外链还在,有的是扫描器在探测 /wp-admin,有的是模板里的图片路径写错了导致每个页面都请求一次不存在的资源。

这四类的处置方式、投入产出比、时间窗口都不一样。混在一起处理,等于把最贵的时间花在最没价值的那一批上。所以第一步不是修,是分类。本文记的是我这两年在一台 Nginx + PHP 的小站上磨出来的分诊流程:从日志切出真正值得管的 404,按「有没有外部链接指向」「是不是页面级资源」两个维度分诊,再落到 nginx 配置或批量重定向上。

二、日志里根本没有完整的 404 记录

先说一个很多人卡住的地方:默认的 combined 日志格式里有 referer 和 user_agent,但没有状态码之外的任何线索,且被大量静态资源的 404 淹没。要做分诊,先给 404 单独开一份日志,带上耗时和上游状态,并且只记 404:

# /etc/nginx/conf.d/404map.conf
map $status $loggable_404 {
    404     1;
    default 0;
}

log_format not_found 'time=$time_iso8601 host=$host uri=$request_uri '
                     'ref="$http_referer" ua="$http_user_agent" '
                     'ip=$remote_addr rt=$request_time ups=$upstream_status';

# 站点 server 块里
access_log /var/log/nginx/404.log not_found if=$loggable_404;

要注意 access_log ... if= 依赖 map 定义的变量,map 必须写在 http 层,也就是 conf.d/ 下面,不能写进 server 块。这一步配错的报错是「unknown "loggable_404" variable」,一查就知道。

有了这份日志,一天的 404 能压到几十 KB,可以直接人眼看。再按请求路径聚合一次,就能看出谁在刷:

awk '{for(i=1;i<=NF;i++) if($i ~ /^uri=/){print substr($i,5)}}' \
  /var/log/nginx/404.log | sort | uniq -c | sort -rn | head -30

输出里如果出现几千条 /wp-login.php、/.env、/phpmyadmin,那不是内容问题,是扫描。这类请求的处理方式完全相反——不补、不跳,直接 444 掉最省资源。

三、分诊的两个维度

第一维:这个 URL 外面还有人指向它吗。判据不是「我记得发过」,而是可查的:日志里那条 404 的 referer 是不是站外域名;搜索引擎的「已发现但未编入索引」或外链工具里有没有这个地址;如果有用户举报或合作方来信,那更是明确信号。第二维:它是页面还是资源。.html、目录路径是页面,.jpg、.css、.js 是资源。资源类的 404 往往说明模板有问题,优先级比页面更高——因为它每个受影响的页面都会触发一次。

于是得到四类:

  • A 类:页面 + 有外部链接 → 优先处理,301 到最相近的现有内容,没有的话补一篇或做聚合页。
  • B 类:页面 + 无外部链接 → 低优先,可以持续观察;三个月没人来就让它保持 404,别为了「好看」乱跳首页。
  • C 类:资源 + 出现在多处 → 当天就修,因为它是模板级 bug,影响全站评分和渲染。
  • D 类:资源 + 单点 → 多半是用户收藏的旧图片,可以批量映射到同类资源,或直接放弃。

这个分类的价值在于可直接执行:A 类和 C 类当天做,B 类每月做一次,D 类一个季度清一次。不需要每天都翻日志。

四、重定向:301 的正确姿势与最容易犯的错

确认要跳的,用 return 301 精确匹配,不要用 rewrite 加正则的模糊跳转:

# 旧文章 → 新地址
location = /old-post.html {
    return 301 /new-post-title.html;
}

# 一整批过期分类页 → 归档页
location ~ ^/category/(old-a|old-b|old-c)/?$ {
    return 301 /category/archive/;
}

# 一批路径统一改前缀
location ~ ^/2019/(.*)$ {
    return 301 /archive/2019/$1;
}

三条纪律:

一、只跳「一对一有意义」的地址。 找不到对应内容的 404 不要统统 301 到首页。这会让搜索引擎判断成软 404,比直接 404 更糟;用户点了三个不同链接都回到首页,会立刻关掉。真没有替代内容的,就让它 404,同时给一个有用的 404 页面。

二、跳转不要多级连跳。 A → B → C 是一次跳转链,浏览器和服务端都多花一次往返,权重也会被稀释。写完用 curl -I -L 看 num_redirects,超过 2 就整理:

curl -s -o /dev/null -L -w 'code=%{http_code} hops=%{num_redirects} url=%{url_effective}' http://www.example.com/old-post.html

三、注意大小写和斜杠。 /old-post.html 和 /Old-Post.html 是两个 URL,/category/a 与 /category/a/ 也是。批量跳转时若不归一化,日志里看起来已经修过的地址还会继续冒出来。用 merge_slashes on(默认已开)处理重复斜杠,尾巴斜杠的问题要么在应用层统一,要么在跳转规则里同时匹配两种形态。

五、真正该有的 404 页面

404 不等于「什么都没了」,它照样能留住人。一个能用的 404 页至少要有三样东西:一句说明(不是你操作错了)、一条明确的返回路径(首页链接 + 站内搜索框)、一组相关推荐(最近文章列表或热门标签)。

location = /404.html {
    internal;
    add_header Cache-Control "no-store" always;
}

error_page 404 /404.html;

internal 表示这个页面只能由内部跳转访问,外部直接请求 /404.html 会返回 404,避免它被索引。同时给 no-store,否则用户点「返回」再进来看到的还是缓存的旧状态。

还有一条很多人漏掉的:404 页面必须返回真正的 404 状态码,而不是 200。有些 CMS 的「友好 404」是前端渲染后返回 200,这对搜索引擎是灾难——它会把这批地址当成有效页面收录,数量一多就可能被判为低质。验法很简单:

curl -s -o /dev/null -w '%{http_code}\n' https://www.example.com/definitely-not-exist-12345

必须是 404 或 410,不能是 200。

六、410 与软 404 的取舍

如果内容是永久且有意删除的(比如下架的产品页、违规内容),用 410 比 404 更明确:告诉搜索引擎「这东西没了,别再来了」。搜索引擎对 410 的处理更快,通常几周就能把它从索引里清掉,而 404 可能要更久。

但 410 不该滥用到「所有 404 都改成 410」——用户手打错网址时给 410 是没有意义的,语义也不对。我的做法是只对明确的「曾经存在、现已下架」的地址返回 410,用一个精确匹配的 location 列出清单,配合下架流程一起维护。

反过来的坑是软 404:地址不存在但返回 200,或者重定向到首页。前面说过 301 到首页的问题,如果是在应用层把未知路由渲染成首页模板并返回 200,危害更隐蔽——前台看起来正常,收录数量却在膨胀。定期抽查一批随机不存在的地址,看状态码是否真为 404,是最便宜的体检:

for u in /a-random-1 /a-random-2 /a-random-3; do
  printf '%s ' "$u"
  curl -s -o /dev/null -w '%{http_code}\n' "https://www.example.com$u"
done

七、从 404 报告里反查模板 bug

资源类 404 往往藏着真正的故障。下面这几种我在自己站上都遇到过:

  • 图片路径大小写不一致:本地 Windows/macOS 文件系统不区分大小写,服务器上区分,于是 /images/Logo.png 在线上 404、本地正常。
  • CDN 或 CDN 回源路径改过:换了前缀之后,旧文章正文里写死的绝对地址全部失效,而文章正文是不受模板管理的。
  • 主题升级后资源指纹变了:app.3f2a1c.js 变成了 app.8b7e2d.js,但页面缓存里还引用旧的。
  • Nginx 的 try_files 顺序写反:先找 $uri/ 再找文件,导致本应命中的文件被目录规则截走。

反查方法:从 404 日志里按扩展名统计,看哪一类资源占比突然升高:

awk '{for(i=1;i<=NF;i++) if($i ~ /^uri=/){print substr($i,5)}}' /var/log/nginx/404.log \
  | grep -oE '\.[a-zA-Z0-9]+$' | sort | uniq -c | sort -rn

如果某天 .webp 的 404 突然从每天 3 条变成 3000 条,那多半是图片转换任务刚上线、生成路径和模板写法不一致,去查那个任务的输出目录就能定位。

八、用日志做一张可持续维护的表

分诊要长期有效,得有一个「已知 404 台账」,否则每个季度都会重新分析一遍同样的问题。我用一个简单的 Markdown 表,每周追加几条:

| 路径 | 首次出现 | 外部链接 | 处置 | 完成日期 |
|---|---|---|---|---|
| /2019/seo-tips.html | 2026-09-12 | 有(2站) | 301→/seo/ | 2026-09-13 |
| /img/banner-old.png | 2026-10-01 | 无 | 模板修好 | 2026-10-01 |
| /feed/atom | 2026-10-20 | 有(1站) | 301→/feed/ | 2026-10-21 |

表里最重要的是「外部链接」这一列——它决定了要不要花时间。每周只需处理「有外部链接」和「资源类高频」两行,其余放着不动。三个月没动静的条目直接删掉,避免表本身变成负担。

九、别为了清零而制造新问题

最后说三个「想当然」的处置,它们比 404 本身更伤站。

把所有 404 重定向到首页。 最常见的错误,一次能制造上千条软 404。搜索引擎会认为你在用首页承接所有死链,轻则忽略跳转、重则整批降权。

用 JS 做跳转。 window.location = '/' 返回的是 200 + 脚本,搜索引擎看不到 301,用户在有脚本拦截的环境下会卡在白页。跳转一律走服务端 return 301。

把 404 页面加上 noindex 就以为万事大吉。 noindex 只影响收录,不解决「这个地址已经没了、用户该去哪」的问题。状态码和页面内容各自管一件事,不能互相替代。

还有一个小细节:如果站点用了 CDN,404 响应也可能被缓存。改完跳转规则后如果没有立刻生效,先加上时间戳参数绕过缓存确认一次,再等 CDN 的 TTL 到期,不要急着回滚配置。

十、小结

404 治理的核心不是「消灭 404」——一个健康的站永远会有一堆 404,扫描器、旧外链、手打错字都会产生。核心是分诊:先给 404 单独开日志,按「有无外部链接」和「页面还是资源」分出四类,只对 A 类和 C 类动手;跳转只用精确匹配的 301,绝不整片跳首页;给 404 页面配上真 404 状态码和有用的引导;再留一张已知台账,让它不随季度复查而重复劳动。这么做之后,处理 404 从「每天翻报告」变成「每周半小时」,效果反而更好。

Last modification:November 7th, 2026 at 07:28 pm

Leave a Comment