← 返回专栏
· 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 →