侧边栏壁纸
  • 累计撰写 175 篇文章
  • 累计收到 2 条评论

页面下架了还在疯狂吃爬虫配额?排查软 404 (Soft 404)、410 Gone 彻底注销与 Nginx 错误码透传我踩过的几个坑

2026-10-8 / 0 评论 / 13 阅读
AI 摘要由 AI 生成

本文分析了网站运营中因错误配置导致的软404问题,揭示了爬虫抓取效率降低和内容质量分下降的严重后果。作者从Nginx状态码截获、单页应用(SPA)前端路由拦截等方面入手,探讨了软404的产生原因和排查方法,强调了正确配置错误页面和避免单页应用陷阱的重要性。

前阵子整理一个运营多年的老站点,顺手把早年采集的几千篇低质垃圾文章和一批早已下线的旧商品页面从数据库里直接物理删除了。原本以为数据库变轻巧了、薄弱内容清空了,搜索引擎对优质内容的抓取频次会跟着涨上来。

结果大半个月过去,在百度资源平台和 Google Search Console 后台一查抓取分析,新发的原创文章收录依然慢吞吞。翻开服务器的 Nginx access.log,发现 Baiduspider 和 Googlebot 每天成千上万次抓取,几乎全扑在那些早就删掉的死链接上。

我随手挑了几个删掉的 URL 在终端里跑了一遍 curl -I,当场愣在屏幕前:浏览器里点开虽然赫然显示着“抱歉,该内容不存在或已被删除”,但 HTTP 响应头里吐出来的状态码却是大大的 HTTP/1.1 200 OK。

这就是典型的软 404(Soft 404)。搜索引擎把几千个除了几行错误提示文字以外内容几乎雷同的废弃页面,全部当成了正常返回的新页面。爬虫不仅把有限的抓取预算全耗在了这些死链里,算法还因为整站暴增的大量重复薄页直接拉低了质量分。

排查过程中顺藤摸瓜揪出了好几个在配置和开发阶段留下的隐藏坑点,从 Nginx 状态码截获、单页应用(SPA)前端路由拦截,到 404 与 410 的真实注销效率差异,把这些要点捋顺后,整站的爬虫抓取配额才彻底回归正常。


坑点一:Nginx 错误页配置偷换了 HTTP 状态码

很多人知道在 Nginx 里配置友好的自定义 404 页面,但最常见的误操作就是让 Nginx 在返回自定义页面时把状态码给改了。

比如很多人图方便,在配置文件里写成这样:

# 典型错误示范:等号直接将原本的 404 强行篡改为 200 返回
error_page 404 =200 /404.html;

# 或者写成了外部重定向
location / {
    try_files $uri $uri/ @fallback;
}
location @fallback {
    # 302 临时重定向到 404.html,爬虫拿到的是 302 和 200,绝非 404
    rewrite ^ /404.html redirect;
}

上面这两种写法在搜索引擎眼里极其致命。不管是 =200 还是 rewrite ... redirect,搜索引擎拿到的都不是预期的 404,而是正常的页面内容或重定向跳板。

另外一个隐蔽的坑出现在后端动态应用(如 PHP-FPM、Node.js、Python 反向代理)中。如果后端程序在查询数据库未果后抛出了 404 响应头,但前端 Nginx 反向代理层没有开启错误码拦截,或者后端全局异常拦截器直接输出一段 HTML 却忘了设置底层 header,客户端最终拿到的依然是 200。

稳妥的 Nginx 错误页面配置应该保持原始状态码不变,并在反代环境开启状态码透传:

server {
    listen 80;
    server_name www.example.com;

    # 捕获后端 FastCGI/Proxy 抛出的 4xx 和 5xx 错误并展示统一静态页
    fastcgi_intercept_errors on;
    proxy_intercept_errors on;

    # 正确写法:不加 =200,严格保留 404 状态码输出
    error_page 404 /custom_404.html;

    location = /custom_404.html {
        root /var/www/common_errors;
        internal; # 限制仅允许 Nginx 内部错误跳转,禁止外部直接访问
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

配置完成后,必须在服务器外部直接使用 curl 抓取一个不存在的路径,确认首行返回的是 404 Not Found:

curl -I https://www.example.com/this-path-never-exists-12345

坑点二:单页应用 (SPA) 路由兜底的“默认 200”陷阱

现在很多项目采用 Vue、React 等前端框架开发。为了支持前端 History 路由模式,Nginx 上都会有这样一段经典规则:

location / {
    try_files $uri $uri/ /index.html;
}

这意味着只要请求在磁盘上找不到物理静态文件,Nginx 就会把 /index.html 吐给客户端,此时 HTTP 状态码毫无例外全都是 200 OK。

随后浏览器的 JavaScript 代码开始执行,路由没有匹配到任何页面,便渲染了一个带有“404 找不到页面”的组件。对于真实人类访客而言,这确实是个 404 页面;但对于网络爬虫来说,初始抓取阶段根本不保证一定能完整执行深层 JS 渲染(尤其是国内搜索引擎蜘蛛对客户端水合的执行成功率极不稳定),爬虫一检测首行响应状态码是 200,正文源码又是单薄的 <div id="app"></div>,直接认定该页面为软 404。

针对单页应用的死链处理,通常有两种可靠方案:

  1. 改造为服务端渲染 (SSR / Nuxt / Next.js):在服务端匹配不到数据时,明确通过上下文发送真正的 404 响应:

    // Next.js App Router 示例:直接调用 notFound()
    import { notFound } from 'next/navigation';
    
    export default async function PostPage({ params }) {
     const post = await getPost(params.id);
     if (!post) {
       notFound(); // 服务端直接输出标准 404 状态码
     }
     return <article>{post.content}</article>;
    }
  2. 纯静态 SPA 的前置拦截:如果是已知大规模下架的目录(比如 /product/old-123.html 或 /v1/article/*),不要把它们扔给 try_files 兜底,而是在 Nginx 层面通过规则提前截断并返回明确的错误码。

坑点三:大批量下架死链,为什么该用 410 Gone 而不是 404?

在绝大多数人的印象中,删了页面直接给 404 就行了。但如果面临的是被黑产批量挂马注入生成的几万个非法 URL、或者彻底砍掉且永不恢复的旧版业务线,410 Gone 比 404 的清理效率高出几个量级。

两者在搜索引擎底层爬虫协议中的待遇完全不同:

状态码 协议语义 爬虫处理机制 索引库注销周期
404 Not Found 资源未找到 爬虫认为可能是暂时性故障或服务器抽风,会在未来数周内反复多次重试探测 缓慢,通常需要 2 至 6 周反复确认后才退缩
410 Gone 资源已被永久移除且不打算恢复 爬虫视其为永久终态,立刻停止密集轮询,迅速从索引库注销 极快,通常几天内即停止高频探测并移出结果

几万个死链如果全返回 404,爬虫哪怕知道不存在,依然会在接下来一个月里每天抽出宝贵的配额挨个验证几十次。改成 410 之后,能以最快速度让搜索引擎释放对这些历史垃圾链接的执念。

在 Nginx 中可以通过正则或精确路径,对已下线的目录批量下发 410:

# 针对已被清理的黑产恶意目录或彻底下线的板块直接返回 410
location ~ ^/(hack-path|old-shop-2022|expired-feed)/ {
    return 410 "Resource Permanently Removed";
}

如果业务逻辑在后端 PHP 中判断商品已下架且永不再售,也可以在后端控制器中显式注入 410 头信息:

// PHP 后端判断商品已彻底注销
if ($item['is_permanently_deleted']) {
    http_response_code(410);
    header("Status: 410 Gone");
    echo json_encode(['error' => 'This page has been permanently removed']);
    exit;
}

坑点四:Sitemap 未同步过滤已删数据,形成爬虫死循环

修好了状态码,往往还会忽视数据源头的闭环。

很多站点的 sitemap.xml 是靠定时脚本每晚自动生成的。有的开发者在写 SQL 时,只写了:

-- 危险写法:只按主键倒序导出,没有判断删除状态字段
SELECT id, update_time FROM articles ORDER BY id DESC LIMIT 50000;

如果数据库采用的是软删除(即打上 is_deleted = 1 标记),或者数据已经物理删除但静态 Sitemap 文件没有同步全量刷新,就会导致爬虫每天拿到你主动推送的 Sitemap 列表,满心欢喜顺着清单抓过来,结果全碰一鼻子 404 / 410 的灰。

在 Google Search Console 中,这会直接产生大批“提交的网址返回 404”严重错误,削弱搜索引擎对 Sitemap 本身的信任权重。

因此,每次在后台清理或物理删除数据后,必须联动完成三步闭环:

  • 重新跑一次 Sitemap 生成任务,确认地图文件里的 URL 全是健康、可访问的正常页面;
  • 把筛查出的 404 和 410 死链清单汇总为单行文本,上传到百度资源平台的“死链提交”工具;
  • 在 Google Search Console 的“编入索引 -> 网页”报告中,持续观察“软 404”和“未找到 (404)”曲线是否稳步下行。

线上排查与验证命令

在日常维护中,不要单纯依赖浏览器或者三方站长工具来判断网页状态。用服务器终端直接伪造爬虫 User-Agent 抽检,看到的信息才最真实。

检查指定 URL 在百度爬虫视角下的响应头:

curl -I -s -A "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" \
  "https://www.example.com/item/old-deleted-page" | head -n 10

观察输出中第一行是 HTTP/1.1 404 Not Found 还是 HTTP/1.1 410 Gone,特别注意下方是否存在 Location:(如果有跳转,说明还存在二次重定向跳板)。

在 Nginx 日志中统计过去一段时间里搜索引擎爬虫在错误状态码上的分布情况:

# 抓取包含 Baiduspider 的日志行,统计状态码分布
grep "Baiduspider" /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -nr

如果看到 200 和 404/410 的比例逐渐趋向健康,且不再有莫名其妙的软 404 报警,就说明蜘蛛配额已经重新聚集到真正有价值的内容页面上了。

评论一下?

OωO
取消