← 返回专栏
· 5 分钟阅读

不花钱查死链:找出并修复 404 错误

死链会悄悄浪费抓取配额、损耗外链权重,还让用户抓狂。本文教你用 Google Search Console、Screaming Frog、服务器日志和自写 Python 爬虫找出全站 404,不花一分钱,告别 Ahrefs 和 Semrush。

我在一个运营了六个月的网站上发现了 47 个失效链接。四十七个。而我完全不知道它们的存在。

最糟糕的不是数量。而是其中有三个失效链接,位于我流量最高的博客文章里,指向的是我在网站改版时移动过的资源。每个点击这些链接的访客都会撞上 404 页面。每个顺着链接爬过来的爬虫都浪费了一次请求。而这些锚点传递出去的每一点内链权重,都蒸发殆尽。

我发现它们不是因为我主动排查,而是因为在看完 crawl budget 的文章后一时兴起,跑了一次免费爬取。这次发现让我一头扎进了失效链接审计的兔子洞,彻底改变了我维护网站的方式。这篇指南会讲清楚我学到的所有东西,包括我自己写的 Python 爬虫——它比我测试过的任何免费在线工具都更可靠地抓出失效链接。

为什么失效链接比你想象的更重要

大部分 SEO 建议把失效链接当作小杂务处理。“看到就修”是常见的说法。这低估了问题的严重性。失效链接会造成三种不同类型的损害,而且叠加效应比任何单一问题所暗示的都更严重。

损害一:浪费爬取预算

Googlebot 给每个网站分配有限的 crawl budget,由你服务器的响应速度和网站整体大小决定。当 Googlebot 遇到 404 时,它不只是记下错误然后继续。它会记录 URL,安排再次访问来确认页面确实没了,几周后还会再来一次。我见过一些网站,30% 的爬取请求都花在返回 404 的 URL 上,而新内容好几天都没被爬取。

对于只有 50 个页面的小网站,这几乎无所谓。不管怎样 Googlebot 都会把所有页面爬完。但对于有几百上千个 URL 的网站,尤其是那些带动态 URL 参数或分层导航的站点,失效链接真的会阻止新页面被发现。我见过一个 300 页的网站,一篇新博客文章花了三周才被索引,因为爬取预算全被一个已删除产品目录留下的 80 个陈旧 URL 吃掉了。

损害二:泄露链接权重

内链传递 link equity。当 A 页面链接到 B 页面,A 页面的一部分排名权重就流向 B。这是少数几个你完全可控的排名因素之一。

当 A 页面链接到一个返回 404 的 URL 时,这部分权重哪儿都去不了。它就那么浪费了。想象一篇热门博客文章有 15 个内链,其中 3 个指向已删除的页面。那篇文章本可以分配给其他页面的权重,有 20% 凭空消失了。

外链会加剧这个问题。当另一个网站链接到你的页面但 URL 打错了,或者链接到一个你后来删掉的页面时,他们本想给你的链接权重全部撞上了 404。Google 的 John Mueller 已经确认,修复这些入站失效链接可以挽回可观的价值,尤其是当链接来源页面有很高权重的时候。

损害三:用户体验和信任

这是最难量化但最容易理解的一点。用户点击链接看到 404 页面,就会失去对你网站的信任。他们可能不会再点你的链接。他们可能直接离开。对电商网站来说,一个指向产品页的失效链接就是一笔直接损失掉的销售。对于内容站,它意味着失去一个读者。

我在自己的一个项目上追踪过这个数据。修复了一个高流量指南页面上的 12 个失效内链后,平均会话时长增加了 18%。跳出率下降了 7 个百分点。那个页面并不是突然排名升高了,而是访客停留时间更久了,因为他们真的能打开我引用给他们的参考资料了。

失效链接的三种类型

在深入检测方法之前,你需要明白:不是所有失效链接都一样。它们来自不同的来源,需要不同的检测方法,紧急程度也不一样。

类型 1:内部失效链接。 你自己页面上的链接,指向你网站上已经不存在的其他页面。你删了一篇博客文章、改了 URL 结构、或者在 href 里打错字。这是最常见的类型,也最容易修复,因为两端都在你控制范围内。一次站点爬取就能全部找到。

类型 2:入站失效链接(外部)。 其他网站上的链接,指向你网站上已经失效的 URL。也许他们链接到了 /blog/old-post,而你把那个地址改成了 /blog/new-post 但没有设置重定向。或者他们打错字了。你没法编辑他们的链接,但你可以自己这边创建 301 重定向来捕获权重。这些会出现在你的 404 日志和服务端日志里。

类型 3:外部出站失效链接。 你页面上的链接,指向那些已经下线、迁移或删除了目标页面的外部网站。你两年前链接了一个很好的资源,现在它返回 404。这不会那么严重地消耗你的爬取预算,但会损害用户体验,还可能影响你网站的感知质量。Google 已经表示,太多失效出站链接可能是一个负面质量信号。

每种类型需要不同的检测策略。我们一个个来。

方法 1:Google Search Console 覆盖率报告

Google Search Console 是检测内部失效链接的第一道防线。Google 已经爬过你的网站,找到了失效 URL,并且整齐地组织在报告里。这完全免费,除了 GSC 验证之外不需要任何技术配置。

在 GSC 中找到 404 错误

在旧版 GSC 界面中导航到 Settings > Coverage,或者在新版 Search Console 中进入 Pages。查找 **“Excluded”**或 **“Not indexed”**类别,然后筛选 **“404”**或 “Not found” 错误类型。

你会看到一个 Googlebot 尝试爬取但收到 404 响应的 URL 列表。对于每个 URL,GSC 会告诉你:

  • 返回 404 的确切 URL
  • Google 上次尝试爬取的时间
  • 引用页面的数量(如果有的话)
  • 发现方式(Google 是如何找到这个 URL 的)

“引用页面”数据非常关键。它告诉你失效链接在哪里。点击报告中任意 404 URL,GSC 会显示 **“Discovered from”**或 “Linked from” 页面。这些就是你网站上(或外部网站上)包含指向该失效 URL 的链接的页面。

GSC 不会告诉你的

GSC 的覆盖率报告有一些限制你应该了解:

它只显示 Google 已经发现的 URL。 如果失效链接存在于 Google 还没爬取的页面上,它不会出现。对于新网站或刚发布的页面,失效链接出现前会有几天到几周的延迟。

引用页面数据不完整。 GSC 显示的是一部分引用页面,不是全部。我见过一个失效 URL 被 15 个页面引用,但 GSC 只列出了 3 个。把它当作起点,而不是完整地图。

它不按严重程度分类。 一个没人链接的已删除博客文章的 404 是无害的。一个被最多人链接的页面的 404 是严重问题。GSC 把它们并列列出,不区分优先级。你需要交叉对照你的分析数据或内部链接数据来评估影响。

外部出站失效链接是看不见的。 GSC 追踪的是你域名上返回错误的 URL。它不会检查你页面上的外部链接是否还活着。

建立 GSC 工作流

下面是我用 GSC 做失效链接检测的方法:

  1. 每周检查 Pages 报告。 我会查找自上次检查以来出现的新 “not found” URL。GSC 的日期筛选在这里很有用。我会把本周的 404 列表和上周的做对比。

  2. 每月导出完整列表。 在 Pages 报告中点击 “Export”,把 URL 列表下载为 CSV。这给了你一个可以长期追踪的基线。如果 404 数量在增长,说明你的网站结构在生成失效 URL。

  3. 把 “Linked from” 和你的分析数据交叉对照。 对每个 404 URL,检查引用页面。如果高流量页面链接到一个 404,立即修复。如果低流量归档页链接到 404,优先级就低一些。

  4. 把合理的 404 标记为”无需处理”。 如果你有意删除了一个页面并希望它返回 404(对于永久删除的内容这是正确行为),就让它保持。Google 最终会停止爬取它。不要浪费时间给每个已删除页面做重定向,尤其是那些没有入站链接的。

方法 2:Screaming Frog SEO Spider(免费版)

Screaming Frog 是行业标准的网站爬虫,免费版最多可以爬取 500 个 URL。对大多数独立站点和小博客来说,500 个 URL 绰绰有余。如果你的网站更大,可以通过配置爬虫只跟随某些 URL 模式来爬取特定部分。

设置爬取

从官网下载 Screaming Frog。免费版无需注册。打开工具,在 URL 栏输入你的域名,点击 “Start”。

爬虫会爬取你的网站,像 Googlebot 一样跟随内部链接。它会检查每个页面上的每个链接,并记录 HTTP 状态码。一次爬取就能捕获内部失效链接、重定向链和失效出站链接。

爬取完成后,点击 **“Response Codes”**标签,按 “Client Error (4xx)” 筛选。这会显示所有返回 400 级错误的链接,404 是最常见的。

解读报告

对于每个失效链接,Screaming Frog 会显示:

  • Address:返回错误的 URL
  • Status Code:通常是 404,但也可能是 403(禁止访问)或 410(已消失)
  • Status Source:错误来自你的服务器还是外部服务器
  • Inlinks:你网站上链接到该失效 URL 的页面
  • Anchor Text:链接使用的锚文本
  • Links Through Redirect:链接在遇到错误之前是否经过了重定向

“Inlinks” 列是最值钱的一列。它精确告诉你哪些页面包含失效链接。你不需要在站点里到处找。报告直接把你指向问题所在。

查找失效出站链接

要查找失效外部链接,在 Response Codes 标签中把筛选切到 “External”。Screaming Frog 默认会检查外部链接,但免费版限制你只能检查前 500 个内部 URL 范围内的链接。对大多数网站来说,这已经覆盖了大部分出站链接,因为大多数外链都在你最显眼的页面上。

免费版的限制

500 URL 限制是主要约束。如果你的网站有 800 个页面,爬取会在 500 个时停下,剩下 300 个页面没被检查。解决方法:

按分区爬取。 不要爬整个域名,把 /blog/ 和 /resources/ 分开爬。每个分区很可能都不到 500 个页面。跑多次爬取然后合并结果。

排除无关 URL。 在 Screaming Frog 的 Configuration > Exclude 部分,添加不需要检查的 URL 模式,比如 /*?replytocom= 或 /*?amp=。这会减少 URL 数量,让爬虫专注在真正的页面上。

定期安排爬取。 设置提醒,每月跑一次完整爬取。随着你添加、修改、删除内容,失效链接会不断累积。月度爬取可以在问题叠加之前捕获它们。

方法 3:DIY Python 失效链接爬虫

这是我最常用的方法。在依赖免费在线工具几个月后,我受够了它们的速率限制、验证码墙和浅显的爬取深度。所以我写了自己的爬虫。它可以在任何装了 Python 的机器上运行,不管网站多大都能全站爬取,并输出一份干净的 CSV 报告。

脚本用了两个库:HTTP 用 requests,HTML 解析用 BeautifulSoup。都是免费的,几秒钟就能装好。

爬虫脚本

#!/usr/bin/env python3
"""
Broken Link Checker — crawls a website and reports all broken links.
Outputs: broken_links.csv with columns [source_page, broken_url, status_code, anchor_text, link_type]
"""

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
import csv
import time
from collections import deque

BASE_URL = "https://yourdomain.com"
DOMAIN = urlparse(BASE_URL).netloc
HEADERS = {"User-Agent": "BrokenLinkChecker/1.0 (SEO Audit)"}
DELAY = 0.5  # seconds between requests (be polite)
MAX_PAGES = 1000  # safety limit

visited = set()
broken_links = []
queue = deque([BASE_URL])

def is_internal(url):
    return urlparse(url).netloc == DOMAIN

def check_url(url):
    """Check a URL and return its status code."""
    try:
        response = requests.head(url, headers=HEADERS, timeout=10, allow_redirects=True)
        # Some servers block HEAD requests; fall back to GET
        if response.status_code == 405:
            response = requests.get(url, headers=HEADERS, timeout=10, stream=True)
            response.close()
        return response.status_code
    except requests.RequestException as e:
        return str(e)[:80]

def extract_links(url, html):
    """Extract all links from a page."""
    soup = BeautifulSoup(html, "html.parser")
    links = []
    for a_tag in soup.find_all("a", href=True):
        href = a_tag["href"]
        # Skip anchors, javascript, mailto, tel
        if href.startswith("#") or href.startswith("javascript:") or href.startswith("mailto:") or href.startswith("tel:"):
            continue
        full_url = urljoin(url, href)
        full_url = full_url.split("#")[0]  # strip fragments
        anchor_text = a_tag.get_text(strip=True)[:100]
        links.append((full_url, anchor_text))
    return links

# Main crawl loop
while queue and len(visited) < MAX_PAGES:
    current_url = queue.popleft()

    if current_url in visited:
        continue
    visited.add(current_url)

    try:
        response = requests.get(current_url, headers=HEADERS, timeout=10)
    except requests.RequestException:
        continue

    if "text/html" not in response.headers.get("Content-Type", ""):
        continue

    links = extract_links(current_url, response.text)

    for link_url, anchor_text in links:
        # Check external links immediately
        if not is_internal(link_url):
            status = check_url(link_url)
            if status and (isinstance(status, int) and status >= 400 or isinstance(status, str)):
                broken_links.append([current_url, link_url, status, anchor_text, "external"])
            time.sleep(0.2)
        else:
            # Queue internal links for crawling
            clean_url = link_url.split("?")[0]  # strip query params for dedup
            if clean_url not in visited and clean_url not in queue:
                queue.append(clean_url)

    time.sleep(DELAY)
    print(f"Crawled {len(visited)}/{MAX_PAGES}: {current_url}")

# Write results
with open("broken_links.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["source_page", "broken_url", "status_code", "anchor_text", "link_type"])
    writer.writerows(broken_links)

print(f"\nDone. Crawled {len(visited)} pages, found {len(broken_links)} broken links.")
print("Results saved to broken_links.csv")

它是怎么工作的

爬虫从你的基 URL 开始,提取页面上的所有链接,把内部链接加入队列继续爬取。对发现的每个链接,它检查 HTTP 状态码。如果状态是 400 或以上,链接就被记为失效。

脚本区分内部链接和外部链接。内部链接会被跟进并爬取。外部链接只检查一次(HEAD 请求,带 GET 兜底),不会跟进。这意味着爬取聚焦在你的域名上,同时仍然能捕获失效出站链接。

定制爬虫

调整延迟。 默认的 0.5 秒请求间隔是礼貌的。如果你的服务器很快并且你想加速,可以降到 0.2。如果你爬的是一个资源受限的共享主机,增加到 1.0。永远不要设置为 0。过度请求你自己的服务器可能导致超时,看起来像失效链接。

添加排除。 如果你的网站有分层导航或 URL 参数会生成无限 URL 组合,添加一个过滤器:

SKIP_PATTERNS = ["?sort=", "?filter=", "?page=", "/feed/", "/wp-json/"]

def should_skip(url):
    return any(pattern in url for pattern in SKIP_PATTERNS)

检查软 404。 有些服务器对不存在的页面返回 200 OK 状态(“软 404”)。添加内容检查:

SOFT_404_INDICATORS = ["page not found", "404 error", "no results found"]

def is_soft_404(response):
    text = response.text.lower()
    return any(indicator in text for indicator in SOFT_404_INDICATORS) and len(response.text) < 5000

导出 JSON 以便程序化使用。 如果你想把结果集成到仪表盘或 CI 流水线中,把 CSV 写入换成 JSON 输出:

import json
with open("broken_links.json", "w") as f:
    json.dump(broken_links, f, indent=2)

定时运行

我设置了一个 cron job 每周跑一次爬虫,并通过邮件把结果发给我。这是 crontab 条目:

0 9 * * 1 cd /path/to/crawler && python3 broken_link_checker.py && \
  [ -s broken_links.csv ] && \
  mail -s "Weekly Broken Link Report" me@example.com < broken_links.csv

这会在每周一早上 9 点运行。如果 CSV 有任何内容(发现了失效链接),它会把报告邮件发给我。如果没有发现失效链接,我就不收邮件。沉默是金。

方法 4:用服务器日志分析入站失效链接

这是 Google 的 John Mueller 在 Reddit 帖子里特别推荐的方法。这是查找失效入站链接最准确的方式,因为它捕获了打到服务器的每一个请求,包括来自外部网站指向不存在页面的请求。

服务器日志显示什么

你的 Web 服务器(Nginx、Apache、Cloudflare 或任何你用的)会记录收到的每个 HTTP 请求。每条日志包含:

  • Timestamp:请求时间
  • Client IP:谁发的请求
  • Method:GET、POST、HEAD 等
  • URL path:请求的路径
  • Status code:200、301、404、500 等
  • Referer:链接到该 URL 的页面(如果有)
  • User agent:请求方是什么(浏览器、Googlebot 等)

状态码和 referer 字段就是你需要的。筛选出 404 状态码,然后看 referer 来确认失效链接在哪里。

从 Nginx 日志中提取 404

如果你用的是 Nginx(VPS 环境里很常见),默认访问日志在 /var/log/nginx/access.log。这是一条提取所有 404 错误及对应 referer 的一行命令:

awk '($9 ~ /404/)' /var/log/nginx/access.log | \
  awk '{print $7, $11}' | sort | uniq -c | sort -rn > 404_report.txt

这会输出每个 404 URL 及其 referer,按频率排序。被访问最多的 404 URL 是最高优先级的修复对象,因为它们流量最大(也浪费最多的权重)。

Cloudflare Workers 日志

如果你用的是 Cloudflare Pages 或 Workers(我的大部分项目都在上面),你没有传统服务器日志。但你可以在 worker 里添加轻量级日志:

// In your worker's fetch handler
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const response = await handleRequest(request);

    if (response.status === 404) {
      const referer = request.headers.get("referer") || "direct";
      const ua = request.headers.get("user-agent") || "unknown";
      console.log(`404\t${url.pathname}\t${referer}\t${ua}`);
    }

    return response;
  }
};

这会把每个 404 记录到 Cloudflare 控制台的 Workers > Logs 下。你可以实时查看 404 命中及其 referer。如果要持久化日志,把它们管道输出到 KV store 或 D1 数据库。

用 GSC 代替日志

如果你没有服务器日志访问权限(很多共享主机不提供),Google Search Console 是你的备选方案。Pages 报告会显示 Google 发现的返回 404 的 URL。虽然这只覆盖了 Google 爬取过的 URL(不是所有入站流量),但它覆盖了最重要的那些,因为 Google 会优先爬取通过链接发现的 URL。

方法 5:用于单页审计的浏览器扩展

有时候你不需要完整爬取。你在编辑某个特定页面,想在发布前确认所有链接都是好的。浏览器扩展非常适合这个场景。

Check My Links 是一个免费的 Chrome 扩展,它会检查当前页面上的每个链接,并高亮有效链接(绿色)和失效链接(红色)。即时生效,适用于你在看的任何页面。

使用场景:

  • 发布前检查。 在发布新博客文章之前,在浏览器里加载草稿并运行 Check My Links。它能立刻抓住错别字和失效引用。
  • 编辑后验证。 更新一篇旧文章后,运行它确认你在编辑过程中没有弄坏任何链接。
  • 竞品分析。 在竞品页面上运行它,找出失效出站链接,这些可以用于 broken link building 的外联。

限制

浏览器扩展只检查当前页面上的链接。它们不会爬取你的网站。它们是抽查工具,不是全面审计。要和完整爬取配合使用,而不是替代它。

如何修复每种失效链接

找到失效链接是工作的一半。正确修复是另一半。修复方法取决于失效链接的类型。

修复内部失效链接

如果目标页面仍然存在但已移动: 设置从旧 URL 到新 URL 的 301 重定向。这能保住链接权重,并确保指向旧 URL 的书签和外链仍然有效。

Nginx 上:

location /blog/old-post/ {
    return 301 /blog/new-post/;
}

在 Cloudflare Pages 上,添加 _redirects 文件:

/blog/old-post/ /blog/new-post/ 301

如果目标页面已删除且没有替代: 你有两个选择。如果页面有可观的入站链接或流量,创建 301 重定向到最相关的现有页面。如果没有人链接到它,也没有流量,就让它 404。Google 已经确认,对真正删除的内容返回 404 是正确行为,不会伤害你的网站。

如果链接只是打错字了: 直接修复源页面中的 href。这是最常见也最容易的修复方式。你的爬取报告会告诉你确切的源页面和失效 URL,所以你知道该改哪儿。

修复入站失效链接

你不能编辑别人网站上的链接,但你可以在自己这边重定向失效 URL:

  1. 从服务器日志或 GSC 中识别失效入站 URL。
  2. 在网站上找到最相关的现有页面。
  3. 创建从失效 URL 到相关页面的 301 重定向。

这能捕获外部网站本想给你的链接权重。链接来源网站不需要做任何改变。

如果失效入站链接来自高权重网站,而重定向感觉很牵强(链接的内容确实不存在了),考虑在旧 URL 上重建一个带相关内容的新页面,而不是重定向。这给了链接来源网站他们期待的东西,也保持了链接的上下文相关性。

修复外部出站失效链接

对于你页面上指向已失效外部网站的链接:

找到替代。 如果链接的资源已经移动,把你的链接更新到新 URL。用 Wayback Machine 找内容去了哪里。搜索标题或关键短语来找到镜像。

移除链接。 如果资源没了也没有替代,直接完全移除链接,或者用一条简短说明替换,解释该资源已不再可用。

链接到 Wayback Machine。 如果资源很有价值且没有替代品,考虑链接到存档版本

想给自己的站点做同样的分析?

ZensInk Pro 把这个流程自动化了。一条命令,从种子词到内容计划。

查看 Pro →