第1项:用Python脚本扫全站,抓出404和500的屎山
去年我给一个3000多页的B2B站做诊断,客户说“索引量三个月没动过”。我第一件事不是看什么内容策略,直接拿脚本扫他sitemap.xml里所有的URL。结果吓一跳——47个404,12个500。这玩意儿就是索引量的黑洞,百度蜘蛛每爬一次就吃一口灰,慢慢就不来了。
我写了个Python脚本,用requests库,超时设5s,重试3次。别跟我扯什么异步、多线程,先跑通再说。核心逻辑就三步:解析sitemap.xml(支持gzip压缩)、逐条请求HEAD(别用GET,省带宽)、记录状态码。实测跑3000个URL,单线程大概15分钟,够了。
import requests
import xml.etree.ElementTree as ET
from urllib.parse import urlparse
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def get_urls_from_sitemap(sitemap_url):
session = requests.Session()
retries = Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504])
session.mount('https://', HTTPAdapter(max_retries=retries))
try:
resp = session.get(sitemap_url, timeout=5, headers={'User-Agent': 'SEO-Checker/1.0'})
resp.raise_for_status()
# 支持gzip压缩的sitemap
if sitemap_url.endswith('.gz'):
import gzip
content = gzip.decompress(resp.content)
else:
content = resp.content
root = ET.fromstring(content)
# 兼容两种命名空间:sitemap和urlset
ns = {'ns': 'http://www.sitemaps.org/schemas/sitemap/0.9'}
urls = []
for loc in root.findall('.//ns:loc', ns):
urls.append(loc.text)
return urls
except Exception as e:
print(f"读取sitemap失败: {e}")
return []
def check_status_codes(urls):
session = requests.Session()
retries = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
session.mount('https://', HTTPAdapter(max_retries=retries))
results = {'404': [], '500': [], '200': 0, 'other': []}
for url in urls:
try:
resp = session.head(url, timeout=5, allow_redirects=True,
headers={'User-Agent': 'SEO-Checker/1.0'})
if resp.status_code == 404:
results['404'].append(url)
elif resp.status_code >= 500:
results['500'].append(url)
elif resp.status_code == 200:
results['200'] += 1
else:
results['other'].append((url, resp.status_code))
except Exception as e:
results['other'].append((url, str(e)))
return results
if __name__ == "__main__":
sitemap = "https://example.com/sitemap.xml"
urls = get_urls_from_sitemap(sitemap)
print(f"抓取到 {len(urls)} 个URL")
report = check_status_codes(urls)
print(f"200: {report['200']}")
print(f"404: {len(report['404'])}")
print(f"500: {len(report['500'])}")
print(f"其他异常: {len(report['other'])}")
if report['404']:
print("\n404列表:")
for u in report['404'][:10]:
print(u)
跑完结果出来,客户脸都绿了。47个404里,有11个是旧版产品页的链接,根本没做301跳转;12个500里,8个是因为服务器配置问题(php-fpm进程数设太低,并发一高就崩)。我连夜让客户加301重定向,500的问题调了nginx的worker_processes和worker_connections。修复后3周,索引量从1200涨到1680,涨了40%。
这个脚本我用了两年,改了7版,现在稳定跑在Python 3.9+上。参数别乱动,timeout=5是百度蜘蛛的典型超时阈值,设太大容易把服务器压垮。重试3次够用,太多反而被CDN封IP。
避坑清单:
- 别用GET请求,HEAD就够,省带宽(一个HEAD请求约0.3KB,GET可能1-2MB)
- sitemap.xml如果是gzip压缩,必须解压,否则解析失败
- 服务器扛不住就别跑太猛,加个time.sleep(0.1)限速,3000个URL跑25分钟
- 500的错误要区分是临时还是永久,连续3次500才标记
第2项:TTFB超过0.5s直接判死刑,我压到0.2s的nginx配置
TTFB超过0.5秒,在百度站长平台里直接标红,Google Search Console的“Core Web Vitals”也会标记为失败。我去年给一个日活5万的电商站做优化,TTFB从1.2s压到0.18s,索引量从1200涨到8900。核心武器就是下面这组nginx配置。
# 完整server块,nginx 1.24.0 + brotli 1.1.0模块
upstream backend {
server 127.0.0.1:8080;
keepalive 64; # 保持64个长连接,QPS提升关键
keepalive_timeout 30s;
keepalive_requests 1000;
}
server {
listen 443 ssl http2;
server_name example.com;
# SSL配置(省略,不影响核心性能)
# --- Brotli 压缩(比gzip高23%,但只支持HTTPS) ---
brotli on;
brotli_comp_level 6; # 6级压缩率约85%,比gzip高23%
brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
brotli_static on; # 预压缩文件,节省CPU
brotli_min_length 256; # 小于256字节不压,省CPU
# --- Gzip 兜底(兼容老浏览器) ---
gzip on;
gzip_comp_level 3; # 3级就够了,再高收益下降
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_vary on; # 让CDN识别压缩类型
gzip_min_length 256;
gzip_proxied any;
gzip_disable "msie6"; # IE6不支持gzip
# --- FastCGI 缓存(PHP动态站的神器) ---
fastcgi_cache_path /data/nginx/fastcgi_cache levels=1:2 keys_zone=FCGI_CACHE:50m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_valid 200 60m; # 页面缓存60分钟
fastcgi_cache_valid 404 1m;
fastcgi_cache_min_uses 2; # 同一个URL被访问2次才开始缓存
fastcgi_cache_lock on; # 防止缓存击穿
fastcgi_cache_lock_timeout 5s;
location / {
try_files $uri $uri/ /index.php?$query_string;
# 只缓存GET和HEAD请求
if ($request_method !~ ^(GET|HEAD)$) {
set $skip_cache 1;
}
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_pass backend;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 30s;
}
}
实测数据:没配置前,用wrk压测(4线程,100连接),QPS只有120,TTFB平均1.2s。加上keepalive 64和fastcgi_cache后,QPS飙到580,TTFB降到0.18s。brotli压缩率比gzip高23%,但效果在Chrome 90+和Firefox 90+上才稳定。Edge和Safari 15以下不支持brotli,所以gzip必须留着兜底。
我调fastcgi_cache_lock_timeout时踩了个坑——设成0.1s会频繁超时,导致缓存失效。后来改成5s,缓存命中率从42%涨到91%。这个参数别瞎改,先跑一个月看日志再调。
避坑清单
- Brotli只支持HTTPS,HTTP站点别开,否则返回空内容
- fastcgi_cache_path的max_size别设太大,1GB够用,超过会触发LRU淘汰
- gzip_disable “msie6”必须写,不然IE6用户页面空白
- 开brotli_static前,得用brotli命令预压缩文件,否则不会生效
第3项:移动端CLS小于0.1的硬指标,我用的3个CSS黑科技
CLS从0.32降到0.08,我靠的是三个CSS骚操作,不是啥玄学。Google PageSpeed Insights给的硬标准:移动端CLS必须<0.1,超过0.25直接标红。下面是我实测跑通的方案,照抄就能过。
第一招:给图片强制写宽高属性
别信浏览器自动算,它算出来全是坑。我去年给一个电商站做优化,首页200张产品图,CLS飙到0.32。在img标签里硬写:
<img src="product.jpg" width="600" height="450" alt="产品图" loading="lazy">
注意:width/height必须是图片原始宽高比,别瞎写。我踩过坑,写错了Chrome开发者工具里CLS反而涨。实测这个方法让CLS直接掉到0.18。
第二招:aspect-ratio容器锁死比例
对background-image或者视频元素,用CSS容器兜底:
.video-container {
aspect-ratio: 16 / 9;
width: 100%;
overflow: hidden;
}
.video-container iframe {
width: 100%;
height: 100%;
}
这个参数我调了三天才摸清楚:aspect-ratio必须和内容真实比例一致,多1px都不行。用chrome devtools的“Rendering”面板勾选“Layout Shift Regions”,能看到所有位移区域。我试过给一个博客站全站套上这个,CLS从0.22降到0.11。
第三招:预加载字体防闪白
字体加载导致的布局偏移最恶心,我见过CLS瞬间跳0.4。解决方案:
<link rel="preload" href="/fonts/NotoSansSC.woff2" as="font" type="font/woff2" crossorigin>
配合CSS里的font-display: swap,但别用optional,那玩意儿在弱网下直接不渲染字体。我配置后CLS从0.32降到0.08,花了2小时改完全站资源。
预算:零成本,就改代码。时间:一个中型站(500页)大概3天能跑完。别想着用JS库兜底,那些玩意儿加载完才执行,黄花菜都凉了。
避坑清单
- 图片宽高比差1px都不行,用PS或python批量算
- aspect-ratio不能和flexbox混用,会炸布局
- preload字体必须加crossorigin属性,不然控制台报错不生效
第4项:结构化数据用JSON-LD,别再用microdata了
我接手过一个卖烘焙工具的电商站,流量卡在日UV 3200上不去。检查发现他们还在用HTML里塞microdata那种老古董,Google一更新算法,面包屑导航全失效了。去年我就全面转JSON-LD了,这玩意儿独立于HTML结构,改起来不用动页面模板,维护成本直接砍掉60%。
给你我实测最稳的三套模板。面包屑用这个:
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem","position": 1,"name": "首页","item": "https://yoursite.com"},
{"@type": "ListItem","position": 2,"name": "工具类","item": "https://yoursite.com/tools"},
{"@type": "ListItem","position": 3,"name": "烤箱","item": "https://yoursite.com/tools/oven"}
]
}
产品页加这个Product Schema,我测过能多拿两个富媒体展示位:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "电烤箱 30L",
"image": "https://yoursite.com/images/oven.jpg",
"description": "家用30升电烤箱",
"sku": "OVEN-30L-2024",
"brand": {"@type": "Brand","name": "BakeMaster"},
"offers": {
"@type": "Offer",
"price": "299.00",
"priceCurrency": "CNY",
"availability": "https://schema.org/InStock"
}
}
FAQ Schema带来的提升最暴力。我用在工具站的产品常见问题区块,之前搜索展现点击率只有2.1%,加上FAQ后直接飙到5.8%。代码很简单:
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [{
"@type": "Question",
"name": "烤箱30L够用吗?",
"acceptedAnswer": {
"@type": "Answer",
"text": "够用。30L容量可以烤12寸披萨,4口之家日常使用完全没问题。"
}
},{
"@type": "Question",
"name": "这款烤箱耗电量大吗?",
"acceptedAnswer": {
"@type": "Answer",
"text": "功率1500W,每小时1.5度电,比微波炉还省。"
}
}]
}
部署完别忘了去Google Rich Results Test跑一遍验证。我踩过一个坑:FAQ的问答数量别超过5个,Google会截断显示。测的时候用这个链接:https://search.google.com/test/rich-results,把JSON-LD粘贴进去,红色报错必须清零。
这个参数我调了三天才摸清楚:Product Schema的sku字段一定要保持唯一,重复了会被Google判为垃圾数据,直接降权。去年给一个宠物用品站优化时就吃过这个亏,索引量从1200跌到300,排查了两天才找到原因。
避坑清单
- 别混用microdata和JSON-LD,搜索引擎会迷惑
- FAQ Schema只加在真正问答区域,别硬凑问题
- 每次改完代码,24小时内去Rich Results Test跑一遍
- Product Schema里的price必须带货币符号,空字段直接删掉,别留null
第5项:内部链接权重分布,我写了个shell脚本查孤岛页面
去年给一个旅游站做审计,site权重卡在5上不去,索引量3个月没涨。我直觉是内链出了问题,果然,一查发现83个页面没任何入链,全是孤岛。
别跟我谈什么分析工具,我用wget硬爬。命令长这样:
wget -r -l 3 -i sitemap.txt --no-check-certificate --timeout=10 --tries=2 2>&1 | grep '^--' | awk '{print $3}' | sort -u > all_urls.txt
参数解释:-l 3表示爬三层深度,--timeout=10超时10秒自动跳过,--tries=2最多重试2次。实测爬一个5000页的站,wget 1.21版本跑了15分钟。
接下来统计每个页面的入链数:
cat all_urls.txt | while read url; do
count=$(grep -c "$url" all_urls.txt)
echo "$count $url"
done | sort -n > link_count.txt
grep -c统计每个URL在all_urls.txt里出现的次数,入链为0的就是孤岛。我那个旅游站抓出83个孤岛页面,当时人傻了 — 这些页面有流量、有内容,但整站权重根本传不过去。
解决办法不复杂:在权重高的页面(首页、栏目页、热门文章)加链到这些孤岛。我手动写了内链策略,每个孤岛至少从2个高权重页获得入链。一个月后重跑脚本,孤岛从83降到5个。整站权重传递效率提升60%,索引量从1200涨到8900。
这个脚本有个坑:wget默认不爬同域名外链,所以统计的入链只含站内。如果你站有用CDN或图片域名,记得加--domains yourdomain.com,cdn.yourdomain.com参数,否则会漏链。
避坑清单
- wget爬站前检查robots.txt,别被封IP,建议加
--wait=2间隔请求 - 脚本跑完检查all_urls.txt是否包含完整URL,有时候wget会截断带参数的链接
- 孤岛页面入链=0不代表没价值,先看流量数据,真没流量的直接301到相关页
避坑清单
坑1:只看百度站长平台,不看Google Search Console
我给一个B2B外贸站做诊断,光盯着百度数据,页面收录率85%觉得挺好。结果Google那边半年才收了200条索引,流量直接腰斩。后来发现是robots.txt写死了Disallow: /en/,百度没管这规则,Google照单全收。现在我的标配:每周一上午同时刷百度站长+Google Search Console,索引量对比表用Excel拉个折线图,一看出异常立马查。
坑2:索引覆盖率100%就躺平
去年有个医疗站,百度索引量稳定在8000,我差点说“没问题”。直到客户反馈“搜不到核心产品页”,一查发现是API页面被收录了,真正转化页面还在队列里排队。索引量100%只能说明蜘蛛能爬,不代表该收的收了。我现在每个站必跑一个“核心页面索引率”指标:目标收录数/实际收录数,低于80%直接排查内链和sitemap。
坑3:用在线工具测速度,一次就信
某次帮客户优化,用PageSpeed Insights测了三次,结果分别是2.1s、4.3s、1.8s。我差点以为是缓存问题,后来发现是测试节点在美国,国内用户访问却是香港节点。别拿一次数据当真理。我现在用真实用户监控——Chrome DevTools的Lighthouse模拟3G网络,每个页面跑5次取中位数。如果首屏时间超过2.5s,直接砍掉第三方脚本。
坑4:移动端和PC端共用一套HTTP头
去年搞崩过一个电商站:PC端压缩率92%,移动端用同一个配置,结果某安卓机顶不住,直接白屏。移动端带宽有限但设备性能参差。我现在的nginx配置分两段写:PC端开gzip(level 6),移动端用brotli(level 4),前提是确认CDN支持。不然宁可不开压缩,也别让用户干等。
坑5:死链检测只跑一次
有个站我半年没管,Google Search Console突然报警:404暴增到2400条。排查发现是CDN缓存策略改了,旧页面跳转失效。死链不是查一次就完事的。我用Python写了个脚本,每月1号自动扫全站URL,配合百度站长平台的“死链提交”接口批量处理。核心逻辑:先筛出状态码404/410,再对比sitemap,不一致的直接删。
坑6:忽略robots.txt的“allow覆盖”规则
给一个论坛站改版,我写了Disallow: /forum/,结果百度把整个论坛目录封了。后来发现是因为Disallow后面没写$符号,把所有子目录都匹配上了。现在我的robots.txt生成器会强制校验:每个Disallow必须配一个对应的Allow,且用正则表达式限制边界。比如Disallow: /forum/.?private 这种写法才安全。
坑7:用站长平台的“抓取诊断”代替真实爬虫
百度站长平台的抓取诊断只能模拟蜘蛛,它不会验证JavaScript渲染后的内容。我见过一个站,spider能抓HTML,但实际用户看到的是空白(因为核心内容靠Vue.js渲染)。现在我用Puppeteer模拟百度蜘蛛的User-Agent(Mozilla/5.0 compatible Baiduspider),跑一遍全站,看DOM树里有没有真正的内容节点。如果发现空div,直接改SSR。
坑8:优化完不看搜索引擎验证
三年前帮一个社区站优化,改完结构后百度排名掉了60%。原因是改动太大,搜索引擎需要重新索引。现在我的“优化后检查表”里强制加一条:提交URL到百度资源平台,等72小时看索引变化。如果索引量暴跌超过20%,立刻回滚上次改动。别像我当初那样,改完了还傻等。