← 返回专栏
· 4 分钟阅读

不花一分钱优化抓取预算,让谷歌多抓多收录

抓取预算是隐形瓶颈,正在悄悄拖垮你的收录。本文手把手教你用 Google Search Console、服务器日志、robots.txt 和自写 Python 分析器来测量、诊断、优化它——完全不用付费买 Ahrefs、Botify 或 OnCrawl,就能让谷歌多抓多收录。

我有个网站,340 个页面。Google 只索引了 89 个。三个月过去,74% 的内容在搜索里完全不可见。

我第一反应是内容质量问题。也许页面太单薄,也许内链太弱,也许我需要更多外链。我花了两周重写 meta description、加内链、用 Google Search Console 的 URL Inspection 工具提交 URL。什么都没变。页面依旧卡在 “Crawled - currently not indexed” 的炼狱里。

然后我看了下抓取统计。Googlebot 每天发 400-500 个抓取请求。听起来很多,直到我意识到其中 280 个请求打向的是我几个月前就删掉的 URL。过期的参数 URL、旧的 tag 页面、已经不存在的 faceted navigation 组合。Googlebot 把 60% 的 crawl budget 花在垃圾上,剩下的 40% 根本不够发现和处理我的新页面。

这不是内容质量问题。这是 crawl budget 问题。一旦我搞清楚是什么在吃预算,一个下午就修好了。两周之内,索引页数从 89 跳到了 241。

这篇指南会讲完我学到的关于 crawl budget 优化的所有东西,包括我写的用来分析抓取模式、识别浪费的 Python 脚本。不需要 500 刀/月的 Botify,也不需要 200 刀/月的 OnCrawl。只要免费工具和一套清晰的方法论。

Crawl Budget 到底是什么(以及不是什么)

Crawl budget 就是 Googlebot 在给定时间内抓取你网站的页面数量。它由两个因素决定:crawl rate limit(Googlebot 在不过载你服务器的前提下能爬多快)和 crawl demand(Google 根据内容热度和新鲜度想要爬多少)。

Crawl Rate Limit(抓取速率上限)

Googlebot 根据三个信号调整抓取速度:

  1. 服务器响应时间。 页面 200ms 响应,Googlebot 就爬得快。3 秒才响应,它就会降速以免压垮服务器。这里看的是滚动平均值,单个慢页面不会拖垮抓取速率,但长期慢响应会。

  2. 错误率。 如果 Googlebot 遇到大量 5xx 错误(服务器错误),它会退避。这是保护机制。服务器已经在挣扎了,Googlebot 不想火上浇油。

  3. 并发连接数。 Google 限制了对你的服务器的并发连接数。对大多数小网站这不是瓶颈。对几千个 URL 的大网站,可能是。

你可以在 Google Search Console 的 Settings > Crawl Stats 里看到当前抓取速率。报告显示 90 天内的每日请求数、下载大小和响应时间。

Crawl Demand(抓取需求)

就算服务器扛得住高速抓取,Googlebot 也不会超出它认为需要的量。需求由下面几个因素驱动:

  1. 热度。 在搜索结果中获得更多点击、更多内链、更多外链的页面,抓取频率更高。Google 知道用户在乎这些页面。

  2. 新鲜度。 经常变化的页面(新闻站、商品列表)比几年不更新的页面爬得更勤。Google 试图保持索引新鲜。

  3. 新 URL。 Google 发现新 URL(通过 sitemap、内链或外链)后,会优先抓取它们来扩展索引。

你控制不了的预算 vs 你控制得了的浪费

这里有个大多数 SEO 指南都忽略的关键洞察:你没办法增加 crawl budget。 Google 根据服务器容量和内容的感知价值来设定。求 Googlebot 多爬一点是没用的。

你能做的是消除浪费。每个花在低价值 URL 上的抓取请求,都是从你真正想要索引的页面上偷走的。Crawl budget 优化的重点不是拿到更多预算,而是更高效地花掉现有的。

这就是为什么小网站也要关注 crawl budget。如果你有 50 个页面,Googlebot 每天爬 50 个,没有一点余量。10 个请求浪费在垃圾 URL 上,就是 20% 的预算没了。500 个页面的网站,问题会滚雪球。

怎么衡量你的 Crawl Budget

你没法优化一个衡量不了的东西。下面是用免费工具评估 crawl budget 的方法。

方法一:Google Search Console Crawl Stats

GSC 的 Crawl Stats 报告是最容易获取的抓取数据。进入 Settings > Crawl Stats(这替代了 Search Console 里旧的 “Crawl” 部分)。

报告显示过去 90 天的三个关键指标:

  • 每日总抓取请求数。 这就是你的有效 crawl budget。50 个页面的网站可能每天 30-80 个请求。500 个页面的网站可能每天 200-500 个。单独看没有”好”的数字,重要的是抓取请求数跟总 URL 数的比例。

  • 每日总下载大小。 Googlebot 在消耗多少带宽。如果相对于你的页面大小来说这个值偏高,说明 Googlebot 可能在下载不需要索引的大文件(图片、PDF)。

  • 平均响应时间。 Googlebot 视角下你的服务器速度。500ms 以下都是好的。超过 1 秒是警告信号。超过 3 秒意味着 Googlebot 在主动降速。

报告还按响应类型拆分了抓取请求:2xx(成功)、3xx(重定向)、4xx(客户端错误)、5xx(服务器错误)。这个拆分就是找到浪费的地方。

浪费公式: 算一下返回 3xx、4xx 或 5xx 的抓取请求占比。这些是非生产性抓取。健康的网站上,90% 以上的抓取请求应该返回 2xx。如果你看到 30% 以上落在错误类别,就是有大量浪费。

在我那个出问题的网站上,拆分是这样的:

  • 2xx 响应:42% 的抓取请求
  • 3xx 重定向:18%
  • 404 错误:28%
  • 5xx 错误:2%
  • 其他:10%

58% 的抓取请求是非生产性的。难怪新页面一直没被索引。

方法二:主机级响应分析

GSC Crawl Stats 报告还按响应类型展示了 Googlebot 请求的具体 URL。点进 “Not Found (404)” 或 “Redirected” 部分,就能看到哪些 URL 在浪费预算。

你通常会看到这些模式:

  • ?sort=price&order=desc 这样产生无限组合的参数 URL
  • 迁移产生的旧 URL 结构,301 到新 URL
  • 已删除的页面,因为别处还有链接,所以继续收到抓取请求
  • CMS 自动生成的 tag 和 category 页面
  • 重复内容的翻页 URL

导出这些 URL 做分类。模式会明确告诉你该修什么。

方法三:服务器日志分析(如果有的话)

服务器访问日志是抓取分析的金标准。它们记录 Googlebot 的每个请求,包括精确 URL、时间戳、响应码、响应大小和 user agent。GSC 展示聚合数据,日志展示原始细节。

如果你的主机提供访问日志(大多数 VPS 提供商都有,Cloudflare Workers 也可以记录到 R2),你可以用一个简单的过滤器提取 Googlebot 请求。

下面是我用来分析服务器日志中 crawl budget 浪费的 Python 脚本:

#!/usr/bin/env python3
"""Analyze server logs for Googlebot crawl patterns."""
import re
import sys
from collections import Counter
from urllib.parse import urlparse, parse_qs

# Googlebot user agents (simplified pattern)
GOOGLEBOT_PATTERN = re.compile(
    r'(?:Googlebot|Googlebot-Image|Googlebot-News|AdsBot-Google)', re.I
)

def parse_log_line(line):
    """Parse a common log format line."""
    # Common Log Format: host ident authuser date request status bytes
    # Nginx Combined: ... "request" status bytes "referer" "user-agent"
    match = re.match(
        r'\S+ \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) (\S+)" (\d+) (\d+) "[^"]*" "([^"]*)"',
        line
    )
    if not match:
        return None
    return {
        'timestamp': match.group(1),
        'method': match.group(2),
        'path': match.group(3),
        'protocol': match.group(4),
        'status': int(match.group(5)),
        'size': int(match.group(6)),
        'user_agent': match.group(7),
    }

def categorize_url(url):
    """Categorize URL for waste analysis."""
    parsed = urlparse(url)
    path = parsed.path
    qs = parse_qs(parsed.query)

    categories = []
    
    # Parameter URLs
    if qs:
        categories.append('parameter-url')
    
    # Trailing slash inconsistency
    if path != '/' and not path.endswith('/'):
        categories.append('no-trailing-slash')
    
    # Pagination
    if '/page/' in path or 'page=' in parsed.query:
        categories.append('pagination')
    
    # Tag/category pages
    if path.startswith('/tag/') or path.startswith('/category/'):
        categories.append('taxonomy')
    
    # Faceted navigation
    if any(k in parsed.query for k in ['sort', 'filter', 'facets', 'color', 'size', 'brand']):
        categories.append('facet')
    
    # File types that shouldn't be crawled
    if any(path.endswith(ext) for ext in ['.css', '.js', '.png', '.jpg', '.gif', '.svg', '.ico']):
        categories.append('static-asset')
    
    return categories if categories else ['content']

def analyze_logs(filepath):
    """Analyze log file for crawl budget waste."""
    googlebot_requests = []
    
    with open(filepath) as f:
        for line in f:
            if not GOOGLEBOT_PATTERN.search(line):
                continue
            parsed = parse_log_line(line)
            if parsed and parsed['method'] == 'GET':
                googlebot_requests.append(parsed)
    
    if not googlebot_requests:
        print("No Googlebot requests found.")
        return
    
    # Response code distribution
    status_counts = Counter(r['status'] for r in googlebot_requests)
    total = len(googlebot_requests)
    
    print(f"=== CRAWL BUDGET ANALYSIS ===")
    print(f"Total Googlebot requests: {total}")
    print(f"\n--- Response Code Distribution ---")
    for status in sorted(status_counts.keys()):
        count = status_counts[status]
        pct = count / total * 100
        print(f"  {status}: {count} ({pct:.1f}%)")
    
    productive = sum(c for s, c in status_counts.items() if 200 <= s < 300)
    waste = total - productive
    print(f"\n--- Budget Efficiency ---")
    print(f"  Productive crawls (2xx): {productive} ({productive/total*100:.1f}%)")
    print(f"  Wasted crawls (3xx/4xx/5xx): {waste} ({waste/total*100:.1f}%)")
    
    # URL category distribution for non-2xx responses
    waste_requests = [r for r in googlebot_requests if r['status'] >= 300]
    if waste_requests:
        waste_categories = Counter()
        for r in waste_requests:
            cats = categorize_url(r['path'])
            for cat in cats:
                waste_categories[cat] += 1
        
        print(f"\n--- Waste Sources ---")
        for cat, count in waste_categories.most_common(15):
            print(f"  {cat}: {count}")
    
    # Most-crawled paths
    path_counts = Counter(r['path'] for r in googlebot_requests)
    print(f"\n--- Top 20 Most-Crawled Paths ---")
    for path, count in path_counts.most_common(20):
        status = next(
            (r['status'] for r in googlebot_requests 
             if r['path'] == path), '?'
        )
        print(f"  [{status}] {path}: {count} requests")
    
    # Unique vs total ratio
    unique_paths = len(set(r['path'] for r in googlebot_requests))
    print(f"\n--- URL Discovery ---")
    print(f"  Unique URLs crawled: {unique_paths}")
    print(f"  Total requests: {total}")
    print(f"  Recrawl ratio: {total/unique_paths:.1f}x "
          f"(each URL crawled {total/unique_paths:.1f} times on average)")

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <access-log-file>")
        sys.exit(1)
    analyze_logs(sys.argv[1])

这个脚本会精确告诉你 crawl budget 花在了哪里。在服务器日志上跑一下,你会得到类似这样的拆分:

=== CRAWL BUDGET ANALYSIS ===
Total Googlebot requests: 12,847

--- Response Code Distribution ---
  200: 5,395 (42.0%)
  301: 2,312 (18.0%)
  404: 3,597 (28.0%)
  500: 257 (2.0%)

--- Budget Efficiency ---
  Productive crawls (2xx): 5,395 (42.0%)
  Wasted crawls (3xx/4xx/5xx): 7,452 (58.0%)

--- Waste Sources ---
  parameter-url: 2,847
  no-trailing-slash: 1,923
  pagination: 1,102
  taxonomy: 847
  facet: 578

--- URL Discovery ---
  Unique URLs crawled: 487
  Total requests: 12,847
  Recrawl ratio: 26.4x (each URL crawled 26.4 times on average)

这个 recrawl ratio 才是杀手。Googlebot 在 90 天内对同样的 487 个 URL 平均抓取了 26 次,也就是每个 URL 每 3.4 天被爬一次。与此同时,新页面要等好几周才迎来第一次抓取。

方法四:Cloudflare Workers 日志(Serverless 替代方案)

如果你用的是 Cloudflare Pages 或 Workers,没有传统服务器日志,可以用一个简单的 middleware 模式记录 Googlebot 请求。加到你的 worker 或 Astro middleware 里:

// Log Googlebot requests to R2 or KV
const ua = request.headers.get('user-agent') || '';
if (/Googlebot/i.test(ua)) {
  const url = new URL(request.url);
  const logEntry = JSON.stringify({
    ts: Date.now(),
    path: url.pathname + url.search,
    method: request.method,
    status: response.status,
  });
  // Option 1: Store in KV (simpler, shorter retention)
  await env.CRAWL_LOG.put(
    `crawl:${Date.now()}:${Math.random().toString(36).slice(2)}`,
    logEntry,
    { expirationTtl: 7776000 } // 90 days
  );
  // Option 2: Store in R2 (no size limit)
  await env.CRAWL_BUCKET.put(
    `logs/${new Date().toISOString().slice(0,10)}/${Date.now()}.json`,
    logEntry
  );
}

这样不需要服务器访问日志也能拿到抓取数据。收集 30 天数据,导出成 JSON 文件,稍微改一下就能跑同一个分析脚本。

Crawl Budget 浪费的五大来源

通过分析多个网站的抓取数据,我总结出了五个反复出现的 crawl budget 消耗模式。每个网站至少有其中两个。

来源一:参数污染

URL 参数是 crawl budget 的头号杀手。每个唯一的参数组合都会产生一个唯一 URL。Googlebot 把每个都当成独立页面。看参数是怎么翻倍的:

  • ?sort=price — 1 个变体
  • ?sort=price&order=desc — 2 个变体

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

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

查看 Pro →