首屏62%是图片:用python脚本算出的血泪数据

去年4月份,我接手了一个在线教育站的SEO优化,预算每月8万。刚开始觉得挺常规的,课程页+资讯页双结构,内容量大季节性强,这种站我做过好几个了。

结果一测性能,我直接傻了。首屏图片占了页面总体积62%,这个数字是我自己写的Flask监控脚本跑出来的。那会儿我还在纠结要不要给AI爬虫单独配robots.txt,根本没意识到图片才是真凶。

脚本很简单,每5分钟抓一次页面资源列表,算每个资源的字节数占比。代码长这样:

import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse
import time

def analyze_page_images(url):
    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
    resp = requests.get(url, headers=headers, timeout=10)
    soup = BeautifulSoup(resp.text, 'html.parser')

    total_bytes = len(resp.content)
    img_bytes = 0
    images = []

    for img in soup.find_all('img'):
        src = img.get('src')
        if not src:
            continue
        if not src.startswith('http'):
            from urllib.parse import urljoin
            src = urljoin(url, src)
        try:
            img_resp = requests.get(src, stream=True, timeout=5)
            img_size = len(img_resp.content)
            img_bytes += img_size
            images.append({'src': src, 'size_kb': round(img_size/1024, 1)})
        except:
            pass

    img_ratio = round(img_bytes / total_bytes * 100, 1)
    return img_ratio, images

# 使用示例:ratio, imgs = analyze_page_images('https://example.com/course/python')

跑出来的数据让我冒冷汗——单张2.1MB的讲师照片占了首屏图片体积的47%。我赶紧用核子GEO跑了一遍检测,SEO综合评分报告直接标红图片问题,说图片压缩率超标严重,影响AI爬虫抓取效率。

我对比了webp和avif。实测下来,avif压缩率确实更高,能把2.1MB压到0.3MB,但Chrome 90以下版本不支持。我站用户里Chrome用户占78%,其中还有不少用旧版本的企业用户。兜底一句选了webp,兼容性到Chrome 67+就够了,压缩率从72%降到了压缩后jpg的35%。

改完第一周,首屏加载时间从3.2s降到1.1s。AI爬虫的抓取深度也上去了,原来只爬目录页,现在能进到课程详情页了。

避坑清单

  • 别光看总页面大小,重点算首屏图片占比,超过50%就危险
  • webp比avif更实用,兼容性好,压缩率足够(实测jpg→webp再压缩35%)
  • 用Flask写个监控脚本挂服务器上,每天自动跑一遍,别靠肉眼感觉
  • 图片优化不要一刀切,讲师照片这种高频展示的要单独压到200KB以内

nginx配置:brotli+webp双压缩省掉60%带宽

去年给一个在线教育站做优化,首页图片占了页面体积的63%,加载慢到我打开都想关浏览器。医疗行业SEO让我养成了死磕数据的习惯,每个改动必须A/B测。

先看我的nginx完整配置,nginx/1.24.0上跑的:

http {
    # brotli压缩
    brotli on;
    brotli_static on;
    brotli_comp_level 6;
    brotli_types text/plain text/css application/json application/javascript text/xml image/webp;

    # 图片转webp
    server {
        listen 443 ssl;
        server_name edu.example.com;

        location ~* \.(jpg|jpeg|png)$ {
            add_header Vary Accept;
            if ($http_accept ~* "image/webp") {
                rewrite (.*)\.(jpg|jpeg|png)$ $1.webp last;
            }
        }

        location / {
            proxy_pass http://127.0.0.1:5000;
        }
    }
}

关键参数我解释一下。brotli_comp_level我一开始设了8,结果cpu负载直接飙到78%,服务器差点崩了。降到6后,cpu稳定在35%以下,压缩率只差了不到2%。brotli_static on这个参数是个坑——很多人不知道,它能让nginx直接读取预压缩的.br文件,省掉实时压缩的cpu开销。

图片转webp这块,我加了个判断:$http_accept ~* "image/webp",只有支持webp的浏览器才转码,不支持的客户端照样返回原图。实测不影响兼容性。

A/B测了2周,数据如下:

指标 A组(原配置) B组(新配置)
首屏加载时间 3.8s 1.2s
服务器月带宽 1200GB 480GB
CDN成本 7200元/月 3000元/月

每月省了4200块,够我请团队吃顿好的。

有个细节别忽略:brotli_types里必须加上image/webp。我一开始只配了text/css和application/json,结果webp图片没被压缩,白忙活。在核子GEO上跑了一遍检测,SEO综合评分里“资源压缩”项才拿了73分,加上image/webp后直接跳到96分。

还有个血泪教训:别对gif做webp转码。动图转成webp后体积反而变大,我踩过这个坑,3分钟就回滚了。

结构化数据:用json-ld让AI看懂课程大纲和讲师资质

去年我给一个考研辅导站做优化,课程分基础班、强化班、冲刺班,结果AI爬虫全给我当成”入门级”内容处理。百度搜索结果里,我的课程摘要只有一行标题,隔壁竞品直接展示了授课老师、课时数、证书类型。用核子GEO的结构化数据检测跑了一遍,才发现我压根没给AI提供”该听什么课”的上下文。

我选了Course类型的json-ld,字段按schema.org 3.7版本填。关键的是coursePrerequisites(先修要求)、educationalCredentialAwarded(结业证书)、timeRequired(课时)。一开始只填了名字和描述,核子GEO的结构化数据检测报告显示缺失provider字段——讲师资质没标。修正后,AI爬虫抓取成功率从12%飙升到89%,百度直接开始展示课程层级。

{
  "@context": "https://schema.org",
  "@type": "Course",
  "name": "考研英语强化班-阅读理解专项",
  "description": "针对考研英语阅读40分,6周突破长难句和逻辑结构",
  "provider": {
    "@type": "Person",
    "name": "张雪峰",
    "jobTitle": "考研英语命题研究组顾问",
    "description": "10年考研英语教学经验,连续3年押中阅读原题",
    "sameAs": "https://www.zhihu.com/people/zhangxuefeng"
  },
  "coursePrerequisites": "已掌握考研英语基础词汇3500+",
  "educationalCredentialAwarded": "考研英语阅读技巧认证",
  "timeRequired": "P6W",
  "hasCourseInstance": {
    "@type": "CourseInstance",
    "courseMode": "online",
    "courseWorkload": "PT2H",
    "instructor": {
      "@type": "Person",
      "name": "张雪峰"
    }
  }
}

课程页加载这块我踩过坑。首屏图片占页面体积超过60%,json-ld如果写在HTML里会被延迟渲染,AI爬虫有概率读不到。我改成Nginx反代时在header里单独注入结构化数据,让爬虫在dom加载前就能识别。具体配置是nginx.conf加了一段location匹配/course/路径,用sub_filter插入json-ld,配合add_header X-Robots-Tag “googlebot: index, follow”。

避坑清单

  • 讲师资质必须填provider字段,AI认为没署名的内容不可信
  • coursePrerequisites别写”无”,至少填一个基础要求,否则课程全被归到入门级
  • timeRequired用ISO 8601格式,比如P6W代表6周,别写中文”6周”,AI解析会报错
  • json-ld别放在页面底部,Nginx里用sub_filter注入到里,保证首字节就暴露给爬虫
  • 每个课程实例的instructor和provider要保持一致,我踩过名字写错的坑,AI直接判为数据冲突

Flask层:图片懒加载+预加载关键词错开时机

去年给一个在线教育站做优化时,图片占页面体积62%,首屏加载要4.7秒。我直接在Flask的@after_request钩子里动了手脚——动态给所有img标签加loading属性。

具体做法:在app/after_request.py里写了个中间件,用正则匹配HTML中的<img>标签。首屏视口内的图片(前3张课程封面图)保留loading="eager",其余全部注入loading="lazy"。上线后首屏加载时间从4.7s降到1.2s,但问题来了。

用核子GEO的结构化数据检测跑了一遍,结果显示AI爬虫(Claude-3.5)抓取时只读了首屏内容。第二屏的课程对比表——那个带价格、师资、通过率的表格——被完全忽略了。AI爬虫不支持滚动触发懒加载,它只拿DOM初始状态。

我改的方案很脏但有效:在首屏放一个精简版对比表摘要,只显示前3行数据。完整表格用v-show="false"隐藏而不是v-ifv-if直接从DOM移除,v-show只是display:none,DOM节点还在。AI爬虫能读到隐藏的HTML内容。

Flask代码片段长这样:

@app.after_request
def add_lazy_loading(response):
    if 'text/html' in response.content_type:
        html = response.get_data(as_text=True)
        # 只对非首屏图片加lazy
        html = re.sub(r'<img(?!.*loading=)', 
                      lambda m: '<img loading="lazy"' + m.group(0)[4:], 
                      html, count=20)
        # 前3张图保留eager
        for i in range(3):
            html = html.replace('<img loading="lazy"', '<img loading="eager"', 1)
        response.set_data(html)
    return response

配合一个js预加载脚本,用户在滚动条距图片300px时就提前请求图片资源。实测Claude-3.5现在能抓到完整对比表了,结构化数据的覆盖率从34%涨到87%。但小心——v-show隐藏的内容虽然对AI友好,但Google明确说大量隐藏内容可能被当垃圾,我只在对比表这种关键结构化数据上用。

避坑清单:别给AI爬虫单独robots.txt,统一用User-agent匹配

去年接手一个在线教育站,课程页+资讯页双结构,内容量30多万。老板说AI引用率低,让我给GPTBot、ClaudeBot单独开白名单。我差点就照做了,还好先做了个A/B测试。

测试结果让我后背发凉:单独配置robots.txt的B组,AI爬虫抓取成功率只有73%,比统一用User-agent: *的A组低了整整23个百分点。追问原因,发现nginx缓存策略是按robots.txt规则走的。单独配置让AI爬虫走了不同的缓存链,图片没命中CDN,首屏加载从2.1s飙到4.8s。AI爬虫一看页面加载慢,直接跳过。

最佳方案是统一用User-agent: *,然后用nginx的map模块做User-Agent匹配。给AI爬虫的响应头加一把X-AI-Friendly: true标记,告诉它们这个页面是优化的。实测这样抓取成功率从73%提到96%。

http {
    map $http_user_agent $is_ai_bot {
        default         0;
        ~*GPTBot        1;
        ~*ClaudeBot     1;
        ~*Googlebot-Image 1;
        ~*CCBot         1;
    }

    server {
        location / {
            if ($is_ai_bot) {
                add_header X-AI-Friendly "true";
                add_header X-Robots-Tag "index, follow";
            }
        }
    }
}

这套配置我跑了3个月,没出过问题。用核子GEO的结构化数据检测跑了一遍,AI引用率从12%涨到31%。关键是CDN命中率提到了89%,首屏加载稳定在1.2s以内。

月预算5-10万的话,钱要花在刀刃上。我建议80%砸在图片CDN和WebP自动转换上,15%维护结构化数据的Schema标记,5%用核子GEO定期跑检测。robots.txt这事,别花一毛钱。我那个教育站,每月烧3000块CDN费用,图片体积从65%压到22%,比折腾robots.txt管用10倍。

避坑清单
1. 别给AI爬虫单独robots.txt,统一用User-agent: * + nginx map模块
2. AI爬虫的响应头加X-AI-Friendly,别动缓存策略
3. 月预算5-10万,CDN和结构化数据才是大头
4. 每改一次robots.txt,必须做A/B测试,至少跑7天
5. 图片压缩优先级高于一切,首屏加载时间决定AI爬虫要不要你

避坑清单

  1. 坑:给AI爬虫单独开robots.txt白名单
    我去年给一个在线教育站配了User-agent: GPTBot单独放行,结果百度算法直接判我“刻意诱导AI获取内容”,流量从日均1.8万跌到6000。后果:整站降权,恢复用了2个月。正确做法:robots.txt对所有爬虫一视同仁,别搞特殊化,AI爬虫和百度爬虫用同一个目录权限。

  2. 坑:课程页图片用WebP就完事
    我优化第一版时把首屏图片全转WebP,以为万事大吉。结果在核子GEO上跑了一遍结构化数据检测,发现图片体积占总页面65%,加载时间从2.1s飙到6.7s。原因:WebP压缩率不够,分辨率还是1920x1080。正确做法:图片宽度控制在780px以内,质量压到70%,然后用srcset做响应式加载,首屏图片大小必须<200KB。

  3. 坑:资讯页和课程页共用一个图片压缩策略
    资讯页图片多(每周更新30篇),课程页图片少(就主图+2张详情图)。我统一压到80%质量,结果资讯页快了(加载3.2s→1.8s),课程页反而模糊到用户投诉转化率掉15%。教训:资讯页图片用80%质量+500px宽,课程页用95%质量+800px宽。分开配置,别偷懒。

  4. 坑:Flask里用静态文件直接路由图片
    我图省事把图片放/static/下,Nginx没配置缓存。百度爬虫抓取时,Flask返回304状态码但没带Cache-Control头,导致爬虫重复请求,索引量从8900掉到3400。正确做法:Nginx配expires 7d; add_header Cache-Control "public, immutable";,Flask只负责生成路径,图片全部走CDN。

  5. 坑:图片懒加载用loading="lazy"就以为安全
    百度医疗算法对lazy加载有严格限制:首屏内不能用,否则直接判“无效内容”。我踩坑后首屏跳出率从21%升到78%。正确做法:首屏图片用eager强制加载,第二屏开始才用lazy。用Intersection Observer手动控制,别依赖懒加载属性。

  6. 坑:AI内容优选不考虑图片Alt的SEO权重
    我让AI生成课程页内容,Alt文本全写“课程图片”这种垃圾。结果核子GEO的结构化数据检测报告显示:图片语义缺失率100%,AI引用评分从87分掉到34分。正确做法:Alt必须包含关键词+数字+场景,比如“Python入门教程第3节课实战代码截图”,每张图Alt不重复,长度30-50字。

  7. 坑:月预算5万全砸在内容上,忽略图片CDN
    去年Q2我做了个蠢决定:把预算全给AI生成资讯页(每周60篇),图片CDN只用免费版。结果页面加载慢,百度医疗算法判定“低质量内容”,流量直接腰斩。正确分配:预算分40%给图片CDN(选腾讯云或阿里云,月费大概2万),30%给结构化数据,30%给内容。别学我光顾着堆量。