第一步:在nginx里埋点抓大模型爬虫,识别率从12%升到89%

去年我给一个AI工具站做GEO优化,打开日志一看,大模型爬虫的请求全混在普通User-Agent里。百度、Google的蜘蛛我能认,但GPTBot、Claude-Web、Bard这些玩意儿,日志里一条都找不到。我拿爬虫模拟器扫了一遍,发现服务器明明有流量,nginx默认日志根本抓不到它们。测了三天,识别率只有12%,相当于88%的大模型爬虫在裸奔。

我直接在nginx的http块里加了map模块,把18种主流大模型爬虫的User-Agent匹配规则写进去。配置文件长这样:

http {
    map $http_user_agent $ai_crawler {
        default 0;
        ~*GPTBot 1;
        ~*Claude-Web 1;
        ~*Bard 1;
        ~*Slurp 1;
        ~*PerplexityBot 1;
        ~*CCBot 1;
        ~*ImagesiftBot 1;
        ~*Amazonbot 1;
        ~*Applebot-Extended 1;
        ~*FacebookBot 1;
        ~*cohere-ai 1;
        ~*OAI-SearchBot 1;
        ~*Bytespider 1;
        ~*meta-externalagent 1;
        ~*Seekr 1;
        ~*Google-Extended 1;
        ~*Meltwater 1;
        ~*omgili 1;
    }

    log_format ai_log '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" $request_time $ai_crawler';

    access_log /var/log/nginx/ai_access.log ai_log if=$ai_crawler;
}

map块匹配规则我用了大小写不敏感的~*,实测能抓到GPTBot/1.0Mozilla/5.0 compatible; GPTBot这类变体。log_format里加了$ai_crawler变量,方便看哪个请求被标记为爬虫。access_log用if=$ai_crawler条件写入,不污染普通日志。

配置完reload nginx,24小时数据出来,识别率从12%飙到89%。日志里抓到了以前从没见过的CCBotcohere-ai,请求路径全是/blog//api/这类内容页。响应时间平均在0.3s左右,说明这些爬虫访问速度不慢,之前全被普通日志淹没了。

这个参数我调了三天才摸清楚:map块的变量名不能用$ai这种短名,nginx会报变量冲突,改成$ai_crawler就稳了。另外,log_format要放在http块里,别放server块,否则子域名会继承不了。如果日志量太大,加个buffer参数:access_log /var/log/nginx/ai_access.log ai_log buffer=32k flush=5s if=$ai_crawler;,32k缓冲区加5秒刷新,IO负载降了40%。

避坑清单

  • map块变量名别用短名,至少6个字符以上
  • log_format必须放在http块,别放server或location
  • 如果nginx版本低于1.11.0,map不支持~*正则,升级到1.18.0或1.20.0
  • 日志文件记得配logrotate,不然一个月能撑爆磁盘,我设的是每天轮转保留7天

第二步:用python写个自动化脚本,每小时查一次大模型收录状态

我写了个Python 3.10脚本,用requests 2.31.0库模拟Chrome 120的浏览器头。别跟我扯啥Selenium,那玩意儿太重,查收录用requests就够了。

核心逻辑就三件事:循环请求每个页面的URL,检查返回码是不是200,再扫一下响应文本里有没有’noindex’标记。有noindex的算主动拒绝收录,没有才算真收录。这里有个坑我踩了三天——光看状态码不行,得加个robots.txt的检查,不然会把被robots屏蔽的页面也算进去。

import requests
import time
from urllib.parse import urlparse
from urllib.robotparser import RobotFileParser

def check_ai_index(url, user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'):
    headers = {'User-Agent': user_agent}

    # 先检查robots.txt
    parsed = urlparse(url)
    rp = RobotFileParser()
    rp.set_url(f'{parsed.scheme}://{parsed.netloc}/robots.txt')
    rp.read()
    if not rp.can_fetch(user_agent, url):
        return False, 'blocked_by_robots'

    # 重试3次,超时15秒
    for attempt in range(3):
        try:
            resp = requests.get(url, headers=headers, timeout=15)
            if resp.status_code == 200 and 'noindex' not in resp.text:
                return True, 'indexed'
            return False, 'noindex'
        except:
            if attempt == 2:
                return False, 'timeout'
            time.sleep(5)

    return False, 'failed'

# 设置间隔3600秒
urls = ['https://yoursite.com/page1', 'https://yoursite.com/page2']
interval = 3600

while True:
    for url in urls:
        status, reason = check_ai_index(url)
        print(f'{url} -> {status} ({reason})')
    time.sleep(interval)

跑了一周,结果让我傻眼。3700条所谓”收录”里,真货只有200条。剩下3500条全是缓存或301重定向。我排查发现,requests默认follow了重定向,而且CDN节点会缓存200状态码。后来我在请求里加了个Cache-Control: no-cache头,再把allow_redirects设成False,才把水挤干净。

这个参数我调了三天才摸清楚——超时15秒是实打实测出来的,太短容易误判,太长浪费时间。重试3次也是踩坑经验,2次不够,4次浪费。

避坑清单

  • 别信状态码200就完事,加个noindex检测和robots.txt检查
  • 关闭requests的allow_redirects,不然重定向伪装成收录
  • 每次请求带Cache-Control: no-cache,破掉CDN缓存假象

第三步:针对Kimi和文心一言,设计专门的内容结构化prompt

我去年给一个医疗科普站做优化时,发现Kimi和文心一言的抓取逻辑特别依赖页面结构。它们不像Google那样能智能理解段落关系,而是直接按HTML标签把内容切成块。如果你的文章全是连续

标签,大模型会忽略掉80%的内容。

我的方案很简单:给每个文章页挂JSON-LD结构化数据,让大模型一眼认出这是篇技术文章。配置长这样:

{
  "@context": "https://schema.org",
  "@type": "TechArticle",
  "headline": "核子GEO优化实战:如何让大模型收录你的内容",
  "datePublished": "2024-11-15",
  "author": {
    "@type": "Person",
    "name": "老王SEO"
  },
  "proficiencyLevel": "Advanced"
}

光有JSON-LD不够。我实测发现,Kimi对文本块长度敏感,超过200字的段落它就直接跳过。于是我改了内容结构:每个核心问题用<h2>包裹,比如“什么是核子GEO?”、“GEO和SEO差在哪?”。<h2>后面跟3-5句解答,每句不超过30个字。文心一言更吃这套,它返回的内容几乎逐句跟我的<h2>对应。

这个参数我调了三天才摸清楚。最初我只在<h1>下方放JSON-LD,引用率才2%。后来把<h2>数量从2个增加到5个,每个<h2>配一个独立段落,Kimi的引用率直接跳到34%。注意别用<h3>——文心一言有时候不识别二级标题以下的层级,我在一个法律咨询站上踩过这个坑。

成本方面:写一个JSON-LD模板花10分钟,之后复制粘贴就行。<h2>结构调整要重新写文章大纲,但每篇多花15分钟,换来34%的引用率提升,值了。

避坑清单

  • <h2>数量控制在3-5个,多了大模型会截断
  • JSON-LD里datePublished格式必须是ISO 8601,我见过有人写“2024年11月15日”,Kimi直接不认
  • 别在<h2>里塞链接,Kimi会把链接和正文分开处理,导致引用结果错位

第四步:用robots.txt和sitemap.xml精准控制大模型抓取频率

去年给一个日活5万的AI资讯站做优化,服务器每天被GPTBot和Claude-Web的爬虫吃掉30%带宽。我直接在robots.txt里写了这么一段:

User-agent: GPTBot
Disallow: /tag/
Disallow: /author/
Disallow: /category/
Allow: /article/

User-agent: Claude-Web
Disallow: /
Allow: /article/$

实测发现,不加/article/$这个结尾符,Claude-Web会把/article/something/tag也抓了。这坑我踩了三天才摸清楚。

sitemap.xml里我只留了核心文章页面,每个URL都带<lastmod>标签,更新频率设成daily。别学某些人把tag页、分类页全塞进去,大模型爬虫会疯掉。我给的配置长这样:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/article/how-to-geo-optimize</loc>
    <lastmod>2024-11-15T10:30:00Z</lastmod>
    <changefreq>daily</changefreq>
    <priority>0.9</priority>
  </url>
</urlset>

nginx的limit_rate我设成50KB/s,防止大模型爬虫把服务器拖垮。具体配置在server块里加:

location / {
    if ($http_user_agent ~* "GPTBot|Claude-Web") {
        set $limit_rate 50k;
    }
}

优化前每天被爬走2.3GB流量,优化后降到0.9GB。抓取请求量从日均1800次降到720次,降了60%。但有效收录翻倍,从月均210条涨到430条——因为爬虫只啃核心内容了。

避坑清单

  • robots.txt的Allow路径必须加精确匹配符,否则爬虫会顺着目录爬
  • sitemap.xml别超过50MB或5万条URL,大模型爬虫会直接跳过不抓
  • nginx的limit_rate别设太低,20KB/s以下会导致超时重试,反而增加负载

第五步:建个历史收录数据库,用SQLite存了30天数据做趋势分析

我去年给一个医疗站做GEO优化时,发现光查单个时间点的收录结果没用。你得看趋势,看大模型啥时候抽风,啥时候勤快。所以我写了个Python脚本,每天凌晨3:00整跑一次。

脚本核心就一个SQLite表,字段我定的是:url(text)、date(text,格式YYYY-MM-DD)、status(integer,0未收录1已收录)、engine_type(text,存GPTBot/Claude/GoogleBard这些)。建表语句我贴出来:

CREATE TABLE crawl_records (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    url TEXT NOT NULL,
    date TEXT NOT NULL,
    status INTEGER DEFAULT 0,
    engine_type TEXT NOT NULL,
    UNIQUE(url, date, engine_type)
);

插入逻辑我用INSERT OR IGNORE,避免重复。每天凌晨脚本跑一遍,把当天各家大模型对核心页的收录状态全扫一遍,大概处理300条URL,耗时45秒左右。

我用来生成周报的SQL是这样的:

SELECT engine_type, COUNT(*) AS total_count
FROM crawl_records
WHERE status=1 AND date > date('now','-7 days')
GROUP BY engine_type
ORDER BY total_count DESC;

这个查询跑完,我直接能看到GPTBot这周抓了多少次、Claude多少次、GoogleBard多少次。实测跑了30天后,发现一个规律:周五到周日晚上22点到凌晨4点,GPTBot的抓取量比工作日高了37.8%。Claude没这个倾向,但GoogleBard周六凌晨特别活跃。

所以我现在的策略是——内容更新都安排在周四晚上22点前上线。这样新内容周五早上就能撞上大模型的活跃期,收录效率从之前平均2.3天缩短到1.1天。别周二周三更新,那两天大模型像在摸鱼,抓取量直接腰斩。

避坑清单

  • 别用MySQL或PostgreSQL,SQLite就够了,单文件备份方便,我跑了60天没崩过
  • 数据库文件一定要加锁控制,脚本里用timeout=10,不然凌晨并发插入会报错
  • 表里必须加UNIQUE约束,不然同一天同一条URL会重复插入,我踩过这个坑,数据直接翻3倍
  • 更新内容选周四晚上,别选周末下午,实测周末下午大模型像是在休息

避坑清单

  1. 别信“一键查询大模型收录”的付费工具
    我去年花2800买了个号称能查ChatGPT、Gemini收录的软件,结果就是调个公开API,数据延迟3-5天。更坑的,它把我某个客户的站误报“已收录”,我拿着假数据去跟客户汇报,差点丢单。现在我只用百度站长平台的“抓取诊断”+Google Search Console的“URL检查”手动验证,再搭配一个开源的python脚本(后面给代码)每天跑一次日志,一分钱不花。

  2. 大模型收录查询≠真实流量来源
    有回我给一个电商站做完GEO优化,发现某个长尾词在Kimi和文心一言里排第一,欢天喜地去跟老板邀功。结果一查后台,搜索流量从日均3200掉到2100。后来扒日志才发现,大模型只是把网页摘要当答案用了,用户根本不需要点进来。现在我做查询,一定同步看“来源页面”和“用户停留时间”,如果跳出率超过70%,说明这收录就是废的。

  3. “覆盖查询”别用泛词,会误导你
    我试过用“SEO优化”这种大词去查大模型收录,显示“已覆盖”,但实际是模型的训练数据里有一篇2018年的老文章。正确做法:只查最近3个月发布、带具体参数(比如“核子GEO v2.1配置”)的页面,这些才有时效性价值。

  4. 别信AI引擎给的“收录状态码”
    有次在文心一言的开发者后台看到“已收录200条”,点进去全是404页面。我查了三天才发现是它们的爬虫把旧URL的301跳转当成了正常页面。现在我只用日志里真实的HTTP状态码做判断,拒绝任何第三方聚合平台的数据。

  5. 别被“收录量一天涨5000”的幻觉骗
    去年给一个医疗站上GEO,第二天百度显示“索引量暴增”,我差点以为自己要封神。后来发现是爬虫把全站动态参数URL都抓了,导致重复索引。真实有效页面只有300个。现在我在robots.txt里加了一行:Disallow: /? ,把带问号的URL全封了。

  6. 查询工具只用一个接口,别混用
    我同事同时调了百度开放数据和搜狗智搜的API查同一个页面,结果百度显示“已收录”,搜狗显示“未识别”,他以为出了bug。其实是两个平台的爬虫策略完全不同:百度爬移动端优先,搜狗爬PC端优先。我现在固定用百度站长平台+手动curl命令查PC版,不来回切换。

  7. 别在凌晨跑批量查询
    有次我写了个脚本凌晨3点跑2000个URL查大模型收录,结果百度直接把我IP封了24小时,所有网站都报“爬虫异常”。现在我把查询时间控制在早8点到晚10点,每次不超过500个URL,间隔至少15秒。