首屏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-if。v-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爬虫要不要你
避坑清单
-
坑:给AI爬虫单独开robots.txt白名单
我去年给一个在线教育站配了User-agent: GPTBot单独放行,结果百度算法直接判我“刻意诱导AI获取内容”,流量从日均1.8万跌到6000。后果:整站降权,恢复用了2个月。正确做法:robots.txt对所有爬虫一视同仁,别搞特殊化,AI爬虫和百度爬虫用同一个目录权限。 -
坑:课程页图片用WebP就完事
我优化第一版时把首屏图片全转WebP,以为万事大吉。结果在核子GEO上跑了一遍结构化数据检测,发现图片体积占总页面65%,加载时间从2.1s飙到6.7s。原因:WebP压缩率不够,分辨率还是1920x1080。正确做法:图片宽度控制在780px以内,质量压到70%,然后用srcset做响应式加载,首屏图片大小必须<200KB。 -
坑:资讯页和课程页共用一个图片压缩策略
资讯页图片多(每周更新30篇),课程页图片少(就主图+2张详情图)。我统一压到80%质量,结果资讯页快了(加载3.2s→1.8s),课程页反而模糊到用户投诉转化率掉15%。教训:资讯页图片用80%质量+500px宽,课程页用95%质量+800px宽。分开配置,别偷懒。 -
坑:Flask里用静态文件直接路由图片
我图省事把图片放/static/下,Nginx没配置缓存。百度爬虫抓取时,Flask返回304状态码但没带Cache-Control头,导致爬虫重复请求,索引量从8900掉到3400。正确做法:Nginx配expires 7d; add_header Cache-Control "public, immutable";,Flask只负责生成路径,图片全部走CDN。 -
坑:图片懒加载用
loading="lazy"就以为安全
百度医疗算法对lazy加载有严格限制:首屏内不能用,否则直接判“无效内容”。我踩坑后首屏跳出率从21%升到78%。正确做法:首屏图片用eager强制加载,第二屏开始才用lazy。用Intersection Observer手动控制,别依赖懒加载属性。 -
坑:AI内容优选不考虑图片Alt的SEO权重
我让AI生成课程页内容,Alt文本全写“课程图片”这种垃圾。结果核子GEO的结构化数据检测报告显示:图片语义缺失率100%,AI引用评分从87分掉到34分。正确做法:Alt必须包含关键词+数字+场景,比如“Python入门教程第3节课实战代码截图”,每张图Alt不重复,长度30-50字。 -
坑:月预算5万全砸在内容上,忽略图片CDN
去年Q2我做了个蠢决定:把预算全给AI生成资讯页(每周60篇),图片CDN只用免费版。结果页面加载慢,百度医疗算法判定“低质量内容”,流量直接腰斩。正确分配:预算分40%给图片CDN(选腾讯云或阿里云,月费大概2万),30%给结构化数据,30%给内容。别学我光顾着堆量。