很多人看到"网站 SEO 优化"会觉得这是运营的事情,测试只要打开首页、看一眼 robots.txt 和 sitemap.xml 就结束了。
实际不是这样。
搜索引擎访问网站时,既要能找到页面,也要能读到正文;不该公开的后台、账号、隐私页面又不能被放出来。测试的目标,就是把这几件事同时确认清楚。
本文用一个已经脱敏的网站改造过程,讲清楚三个问题:
- SEO 页面到底在测什么;
- 在浏览器里怎么手工验证;
- 哪些手工步骤值得写成 Python + Playwright 脚本。
文中所有项目、域名、人员和工单信息都使用通用名称。
一、先用大白话理解三个文件
1. robots.txt:给爬虫看的"访问说明书"
搜索引擎的爬虫访问网站前,通常会先读:
text
https://你的域名/robots.txt
它告诉爬虫:哪些页面可以看,哪些页面不要看。
比如新闻、产品、供求详情这类公开页面,应该允许抓取;后台、账号中心、会员内容、接口地址,应该禁止抓取。
2. sitemap.xml:给爬虫看的"页面目录"
站点地图相当于把希望搜索引擎收录的页面地址列成清单:
text
https://你的域名/sitemap.xml
它不是"能打开就行"。如果里面混入登录页、后台地址、旧域名,或者页面点进去全是 404,搜索引擎照样会抓错。
3. User-Agent:浏览器报给服务器的"我是哪个访问者"
普通浏览器会带类似 Chrome 的 User-Agent;搜索引擎爬虫会带 Googlebot、Baiduspider 之类的标识。
有些网站会根据这个标识返回不同内容,所以要验证:
- 爬虫访问公开详情页时,能看到标题和正文;
- 伪装成爬虫后,也不能绕过后台或账号权限。
#mermaid-svg-UhzYzbN1dFNBhwRC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UhzYzbN1dFNBhwRC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UhzYzbN1dFNBhwRC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UhzYzbN1dFNBhwRC .error-icon{fill:#552222;}#mermaid-svg-UhzYzbN1dFNBhwRC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UhzYzbN1dFNBhwRC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UhzYzbN1dFNBhwRC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UhzYzbN1dFNBhwRC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UhzYzbN1dFNBhwRC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UhzYzbN1dFNBhwRC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UhzYzbN1dFNBhwRC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UhzYzbN1dFNBhwRC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UhzYzbN1dFNBhwRC .marker.cross{stroke:#333333;}#mermaid-svg-UhzYzbN1dFNBhwRC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UhzYzbN1dFNBhwRC p{margin:0;}#mermaid-svg-UhzYzbN1dFNBhwRC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UhzYzbN1dFNBhwRC .cluster-label text{fill:#333;}#mermaid-svg-UhzYzbN1dFNBhwRC .cluster-label span{color:#333;}#mermaid-svg-UhzYzbN1dFNBhwRC .cluster-label span p{background-color:transparent;}#mermaid-svg-UhzYzbN1dFNBhwRC .label text,#mermaid-svg-UhzYzbN1dFNBhwRC span{fill:#333;color:#333;}#mermaid-svg-UhzYzbN1dFNBhwRC .node rect,#mermaid-svg-UhzYzbN1dFNBhwRC .node circle,#mermaid-svg-UhzYzbN1dFNBhwRC .node ellipse,#mermaid-svg-UhzYzbN1dFNBhwRC .node polygon,#mermaid-svg-UhzYzbN1dFNBhwRC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UhzYzbN1dFNBhwRC .rough-node .label text,#mermaid-svg-UhzYzbN1dFNBhwRC .node .label text,#mermaid-svg-UhzYzbN1dFNBhwRC .image-shape .label,#mermaid-svg-UhzYzbN1dFNBhwRC .icon-shape .label{text-anchor:middle;}#mermaid-svg-UhzYzbN1dFNBhwRC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UhzYzbN1dFNBhwRC .rough-node .label,#mermaid-svg-UhzYzbN1dFNBhwRC .node .label,#mermaid-svg-UhzYzbN1dFNBhwRC .image-shape .label,#mermaid-svg-UhzYzbN1dFNBhwRC .icon-shape .label{text-align:center;}#mermaid-svg-UhzYzbN1dFNBhwRC .node.clickable{cursor:pointer;}#mermaid-svg-UhzYzbN1dFNBhwRC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UhzYzbN1dFNBhwRC .arrowheadPath{fill:#333333;}#mermaid-svg-UhzYzbN1dFNBhwRC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UhzYzbN1dFNBhwRC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UhzYzbN1dFNBhwRC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UhzYzbN1dFNBhwRC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UhzYzbN1dFNBhwRC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UhzYzbN1dFNBhwRC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UhzYzbN1dFNBhwRC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UhzYzbN1dFNBhwRC .cluster text{fill:#333;}#mermaid-svg-UhzYzbN1dFNBhwRC .cluster span{color:#333;}#mermaid-svg-UhzYzbN1dFNBhwRC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UhzYzbN1dFNBhwRC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UhzYzbN1dFNBhwRC rect.text{fill:none;stroke-width:0;}#mermaid-svg-UhzYzbN1dFNBhwRC .icon-shape,#mermaid-svg-UhzYzbN1dFNBhwRC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UhzYzbN1dFNBhwRC .icon-shape p,#mermaid-svg-UhzYzbN1dFNBhwRC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UhzYzbN1dFNBhwRC .icon-shape .label rect,#mermaid-svg-UhzYzbN1dFNBhwRC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UhzYzbN1dFNBhwRC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UhzYzbN1dFNBhwRC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UhzYzbN1dFNBhwRC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 搜索引擎爬虫
先读 robots.txt
从 sitemap.xml 获取页面地址
访问公开详情页
读取标题和正文
推荐一个测试插件:浏览器身份伪装:UserAgent-Switcher完全指南
二、为什么"接口返回 200"还不能算通过
HTTP 200 的意思只是"服务器给了响应"。它并不代表页面真的正确。
举一个常见例子:sitemap.xml 返回 200,但里面的 URL 仍然是旧域名。搜索引擎顺着旧域名抓取,可能跳转、404,甚至进入错误站点。
所以验证 Sitemap 时,我通常按下面顺序检查:
#mermaid-svg-9m55aIJSw4zzEN9a{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9m55aIJSw4zzEN9a .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9m55aIJSw4zzEN9a .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9m55aIJSw4zzEN9a .error-icon{fill:#552222;}#mermaid-svg-9m55aIJSw4zzEN9a .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9m55aIJSw4zzEN9a .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9m55aIJSw4zzEN9a .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9m55aIJSw4zzEN9a .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9m55aIJSw4zzEN9a .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9m55aIJSw4zzEN9a .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9m55aIJSw4zzEN9a .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9m55aIJSw4zzEN9a .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9m55aIJSw4zzEN9a .marker.cross{stroke:#333333;}#mermaid-svg-9m55aIJSw4zzEN9a svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9m55aIJSw4zzEN9a p{margin:0;}#mermaid-svg-9m55aIJSw4zzEN9a .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9m55aIJSw4zzEN9a .cluster-label text{fill:#333;}#mermaid-svg-9m55aIJSw4zzEN9a .cluster-label span{color:#333;}#mermaid-svg-9m55aIJSw4zzEN9a .cluster-label span p{background-color:transparent;}#mermaid-svg-9m55aIJSw4zzEN9a .label text,#mermaid-svg-9m55aIJSw4zzEN9a span{fill:#333;color:#333;}#mermaid-svg-9m55aIJSw4zzEN9a .node rect,#mermaid-svg-9m55aIJSw4zzEN9a .node circle,#mermaid-svg-9m55aIJSw4zzEN9a .node ellipse,#mermaid-svg-9m55aIJSw4zzEN9a .node polygon,#mermaid-svg-9m55aIJSw4zzEN9a .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9m55aIJSw4zzEN9a .rough-node .label text,#mermaid-svg-9m55aIJSw4zzEN9a .node .label text,#mermaid-svg-9m55aIJSw4zzEN9a .image-shape .label,#mermaid-svg-9m55aIJSw4zzEN9a .icon-shape .label{text-anchor:middle;}#mermaid-svg-9m55aIJSw4zzEN9a .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9m55aIJSw4zzEN9a .rough-node .label,#mermaid-svg-9m55aIJSw4zzEN9a .node .label,#mermaid-svg-9m55aIJSw4zzEN9a .image-shape .label,#mermaid-svg-9m55aIJSw4zzEN9a .icon-shape .label{text-align:center;}#mermaid-svg-9m55aIJSw4zzEN9a .node.clickable{cursor:pointer;}#mermaid-svg-9m55aIJSw4zzEN9a .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9m55aIJSw4zzEN9a .arrowheadPath{fill:#333333;}#mermaid-svg-9m55aIJSw4zzEN9a .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9m55aIJSw4zzEN9a .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9m55aIJSw4zzEN9a .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9m55aIJSw4zzEN9a .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9m55aIJSw4zzEN9a .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9m55aIJSw4zzEN9a .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9m55aIJSw4zzEN9a .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9m55aIJSw4zzEN9a .cluster text{fill:#333;}#mermaid-svg-9m55aIJSw4zzEN9a .cluster span{color:#333;}#mermaid-svg-9m55aIJSw4zzEN9a div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9m55aIJSw4zzEN9a .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9m55aIJSw4zzEN9a rect.text{fill:none;stroke-width:0;}#mermaid-svg-9m55aIJSw4zzEN9a .icon-shape,#mermaid-svg-9m55aIJSw4zzEN9a .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9m55aIJSw4zzEN9a .icon-shape p,#mermaid-svg-9m55aIJSw4zzEN9a .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9m55aIJSw4zzEN9a .icon-shape .label rect,#mermaid-svg-9m55aIJSw4zzEN9a .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9m55aIJSw4zzEN9a .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9m55aIJSw4zzEN9a .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9m55aIJSw4zzEN9a :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
是
否
否
是
打开 sitemap.xml
返回 200?
入口问题
读取每个 URL
域名正确?
域名配置问题
含登录/后台/API 地址?
收录范围问题
抽几条 URL 实际访问
能打开且不跳登录?
页面可访问性问题
该项通过
浏览器手工怎么测
- 打开
https://测试域名/sitemap.xml; - 搜索是否有旧域名、
/login、/admin、/api/; - 从里面随机复制几条资讯、产品、标签页地址,在新标签页打开;
- 确认地址没有跳到登录页,也没有 404;
- 如果有相同 URL,先问产品:它是误重复,还是同一链接在不同 Tab 下复用。不要看到重复就直接提 Bug。
这里还有一个脚本经验:判断旧域名不能用字符串包含。
python
# 不推荐:测试域名里可能恰好也包含 old.example.com 的部分字符
if "old.example.com" in url:
print("旧域名")
应当解析 URL,再比较 hostname:
python
from urllib.parse import urlparse
host = urlparse(url).hostname
assert host != "old.example.com"
这样不会把"名字相似的测试域名"误判成旧域名。
三、爬虫页面怎么测:不要只看 robots.txt
假设 robots.txt 写了"允许抓取资讯详情页",还要继续问:爬虫打开这个详情页后,看到的是正文,还是一个空壳页面?
完整验证分三步:
| 检查层次 | 要确认什么 | 失败说明什么 |
|---|---|---|
| robots 规则 | 公开页允许,后台页禁止 | 规则配置错了 |
| HTTP 请求 | 爬虫 UA 访问公开页返回 200、不跳登录 | 服务端拦截或跳转异常 |
| 浏览器渲染 | 标题、摘要、正文真正显示出来 | 前端异步渲染或缓存有问题 |
手工验证方法
安装 User-Agent Switcher 之类的浏览器扩展,将 UA 切换为 Googlebot 或 Baiduspider,然后依次打开:
- 一条资讯详情;
- 一条产品/供求详情;
- 一个应受保护的账号或会员页面。
公开详情页应能看到标题、正文;受保护页面应该跳登录、提示无权限,或返回拒绝访问。
四、图片懒加载到底怎么判断对不对
loading="lazy" 的作用很简单:浏览器先不加载暂时看不到的图片,等用户滚动到附近再加载,可以减少首页第一次打开时的流量。
但不能所有图片都加它:
- 首屏 Banner、首屏文章主图:用户一打开就要看见,不应该懒加载;
- 首页很靠下的资讯卡片图:应该懒加载;
- 图标、背景图、隐藏图片:要看实际产品规则,不要简单套用。
#mermaid-svg-4whHQbp9dXeKS1sX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4whHQbp9dXeKS1sX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4whHQbp9dXeKS1sX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4whHQbp9dXeKS1sX .error-icon{fill:#552222;}#mermaid-svg-4whHQbp9dXeKS1sX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4whHQbp9dXeKS1sX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4whHQbp9dXeKS1sX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4whHQbp9dXeKS1sX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4whHQbp9dXeKS1sX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4whHQbp9dXeKS1sX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4whHQbp9dXeKS1sX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4whHQbp9dXeKS1sX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4whHQbp9dXeKS1sX .marker.cross{stroke:#333333;}#mermaid-svg-4whHQbp9dXeKS1sX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4whHQbp9dXeKS1sX p{margin:0;}#mermaid-svg-4whHQbp9dXeKS1sX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4whHQbp9dXeKS1sX .cluster-label text{fill:#333;}#mermaid-svg-4whHQbp9dXeKS1sX .cluster-label span{color:#333;}#mermaid-svg-4whHQbp9dXeKS1sX .cluster-label span p{background-color:transparent;}#mermaid-svg-4whHQbp9dXeKS1sX .label text,#mermaid-svg-4whHQbp9dXeKS1sX span{fill:#333;color:#333;}#mermaid-svg-4whHQbp9dXeKS1sX .node rect,#mermaid-svg-4whHQbp9dXeKS1sX .node circle,#mermaid-svg-4whHQbp9dXeKS1sX .node ellipse,#mermaid-svg-4whHQbp9dXeKS1sX .node polygon,#mermaid-svg-4whHQbp9dXeKS1sX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4whHQbp9dXeKS1sX .rough-node .label text,#mermaid-svg-4whHQbp9dXeKS1sX .node .label text,#mermaid-svg-4whHQbp9dXeKS1sX .image-shape .label,#mermaid-svg-4whHQbp9dXeKS1sX .icon-shape .label{text-anchor:middle;}#mermaid-svg-4whHQbp9dXeKS1sX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4whHQbp9dXeKS1sX .rough-node .label,#mermaid-svg-4whHQbp9dXeKS1sX .node .label,#mermaid-svg-4whHQbp9dXeKS1sX .image-shape .label,#mermaid-svg-4whHQbp9dXeKS1sX .icon-shape .label{text-align:center;}#mermaid-svg-4whHQbp9dXeKS1sX .node.clickable{cursor:pointer;}#mermaid-svg-4whHQbp9dXeKS1sX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4whHQbp9dXeKS1sX .arrowheadPath{fill:#333333;}#mermaid-svg-4whHQbp9dXeKS1sX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4whHQbp9dXeKS1sX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4whHQbp9dXeKS1sX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4whHQbp9dXeKS1sX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4whHQbp9dXeKS1sX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4whHQbp9dXeKS1sX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4whHQbp9dXeKS1sX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4whHQbp9dXeKS1sX .cluster text{fill:#333;}#mermaid-svg-4whHQbp9dXeKS1sX .cluster span{color:#333;}#mermaid-svg-4whHQbp9dXeKS1sX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4whHQbp9dXeKS1sX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4whHQbp9dXeKS1sX rect.text{fill:none;stroke-width:0;}#mermaid-svg-4whHQbp9dXeKS1sX .icon-shape,#mermaid-svg-4whHQbp9dXeKS1sX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4whHQbp9dXeKS1sX .icon-shape p,#mermaid-svg-4whHQbp9dXeKS1sX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4whHQbp9dXeKS1sX .icon-shape .label rect,#mermaid-svg-4whHQbp9dXeKS1sX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4whHQbp9dXeKS1sX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4whHQbp9dXeKS1sX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4whHQbp9dXeKS1sX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 能看见
看不见
找到 img 标签
打开首页时能看见?
关键图片:不要 lazy
内容图片:建议 lazy
手工检查方法
- 打开首页,不要滚动;
- 按 F12,进入 Elements;
- 找首屏 Banner 和首屏文章图的
<img>; - 首屏图片不应有
loading="lazy"; - 再找到首屏以下的资讯列表图,确认它们有
loading="lazy"。
遇到问题时,最好拆成两个 Bug:
- 首屏图片错误使用懒加载:影响用户第一眼看到内容的速度;
- 首屏以下图片没有懒加载:影响首页首次加载的资源量。
不要把两个问题混在一个 Bug 里,开发修复和回归都会更清楚。
五、脚本不是凭空写出来的:从手工步骤翻译成代码
脚本的可视化页面:

我写自动化时,通常按下面的路线走:
#mermaid-svg-NkkwzjWYNFNXeMIZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NkkwzjWYNFNXeMIZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NkkwzjWYNFNXeMIZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NkkwzjWYNFNXeMIZ .error-icon{fill:#552222;}#mermaid-svg-NkkwzjWYNFNXeMIZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NkkwzjWYNFNXeMIZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NkkwzjWYNFNXeMIZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NkkwzjWYNFNXeMIZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NkkwzjWYNFNXeMIZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NkkwzjWYNFNXeMIZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NkkwzjWYNFNXeMIZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NkkwzjWYNFNXeMIZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NkkwzjWYNFNXeMIZ .marker.cross{stroke:#333333;}#mermaid-svg-NkkwzjWYNFNXeMIZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NkkwzjWYNFNXeMIZ p{margin:0;}#mermaid-svg-NkkwzjWYNFNXeMIZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NkkwzjWYNFNXeMIZ .cluster-label text{fill:#333;}#mermaid-svg-NkkwzjWYNFNXeMIZ .cluster-label span{color:#333;}#mermaid-svg-NkkwzjWYNFNXeMIZ .cluster-label span p{background-color:transparent;}#mermaid-svg-NkkwzjWYNFNXeMIZ .label text,#mermaid-svg-NkkwzjWYNFNXeMIZ span{fill:#333;color:#333;}#mermaid-svg-NkkwzjWYNFNXeMIZ .node rect,#mermaid-svg-NkkwzjWYNFNXeMIZ .node circle,#mermaid-svg-NkkwzjWYNFNXeMIZ .node ellipse,#mermaid-svg-NkkwzjWYNFNXeMIZ .node polygon,#mermaid-svg-NkkwzjWYNFNXeMIZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NkkwzjWYNFNXeMIZ .rough-node .label text,#mermaid-svg-NkkwzjWYNFNXeMIZ .node .label text,#mermaid-svg-NkkwzjWYNFNXeMIZ .image-shape .label,#mermaid-svg-NkkwzjWYNFNXeMIZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-NkkwzjWYNFNXeMIZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NkkwzjWYNFNXeMIZ .rough-node .label,#mermaid-svg-NkkwzjWYNFNXeMIZ .node .label,#mermaid-svg-NkkwzjWYNFNXeMIZ .image-shape .label,#mermaid-svg-NkkwzjWYNFNXeMIZ .icon-shape .label{text-align:center;}#mermaid-svg-NkkwzjWYNFNXeMIZ .node.clickable{cursor:pointer;}#mermaid-svg-NkkwzjWYNFNXeMIZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NkkwzjWYNFNXeMIZ .arrowheadPath{fill:#333333;}#mermaid-svg-NkkwzjWYNFNXeMIZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NkkwzjWYNFNXeMIZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NkkwzjWYNFNXeMIZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NkkwzjWYNFNXeMIZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NkkwzjWYNFNXeMIZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NkkwzjWYNFNXeMIZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NkkwzjWYNFNXeMIZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NkkwzjWYNFNXeMIZ .cluster text{fill:#333;}#mermaid-svg-NkkwzjWYNFNXeMIZ .cluster span{color:#333;}#mermaid-svg-NkkwzjWYNFNXeMIZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NkkwzjWYNFNXeMIZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NkkwzjWYNFNXeMIZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-NkkwzjWYNFNXeMIZ .icon-shape,#mermaid-svg-NkkwzjWYNFNXeMIZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NkkwzjWYNFNXeMIZ .icon-shape p,#mermaid-svg-NkkwzjWYNFNXeMIZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NkkwzjWYNFNXeMIZ .icon-shape .label rect,#mermaid-svg-NkkwzjWYNFNXeMIZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NkkwzjWYNFNXeMIZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NkkwzjWYNFNXeMIZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NkkwzjWYNFNXeMIZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 先手工操作一次
确定稳定入口
写清楚通过标准
配置测试数据
编写脚本
失败时输出可定位证据
例如,手工规则是:"用爬虫身份打开公开详情页,应该看到标题和正文。"翻译成脚本,就是:
python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context(user_agent="Googlebot")
page = context.new_page()
response = page.goto(detail_url, wait_until="networkidle")
assert response.status == 200
assert "/login" not in page.url
text = page.locator("body").inner_text()
assert "文章标题中的关键字" in text
assert "正文中的关键字" in text
其中:
user_agent="Googlebot":模拟爬虫;wait_until="networkidle":等待页面接口基本加载完成;response.status == 200:页面入口正常;"/login" not in page.url:没有被跳去登录页;- 正文关键字断言:确认不是"200 但空页面"。
图片懒加载脚本怎么写
浏览器脚本不应该只看 HTML 字符串,而要看图片在当前视口的位置:
python
def get_visible_images(page):
return page.evaluate("""
() => Array.from(document.images).map(img => {
const rect = img.getBoundingClientRect();
return {
src: img.currentSrc || img.src,
loading: img.loading,
top: rect.top,
bottom: rect.bottom,
visible: rect.width > 0 && rect.height > 0
};
}).filter(item => item.visible)
""")
images = get_visible_images(page)
first_screen = [x for x in images if x["top"] < 900 and x["bottom"] > 0]
wrong_images = [x for x in first_screen if x["loading"] == "lazy"]
assert not wrong_images, wrong_images
它的原理是:浏览器计算每张图片距离视口顶部的位置。图片的上边在 900 像素以内、下边又没有离开页面上方,就认为它处于首屏。
失败时把图片 URL 一起打印出来,开发一眼就能找到对应组件。
六、还有三类很适合补进脚本的场景
1. Sitemap 抽样访问
不必每次访问几千条 URL,可以均匀抽取十几条:资讯、详情、标签、频道页都覆盖到。
python
samples = sitemap_urls[::max(1, len(sitemap_urls) // 12)][:12]
for url in samples:
response = session.get(url, allow_redirects=False)
assert response.status_code == 200
assert "/login" not in response.headers.get("Location", "")
2. 首页导航和搜索
先在配置文件中写明可访问频道和测试关键词,不要把真实数据硬编码在脚本里:
json
{
"navigation_paths": ["/news", "/product", "/company"],
"search": {"keyword": "示例关键词", "result_path": "/search"}
}
这样换测试环境、预发环境时,只改配置,不改测试代码。
3. 图片加载失败后的页面可用性
可以让 Playwright 故意拦截一张图片,再判断标题和正文是否还在:
python
blocked = []
def block_one_image(route):
if route.request.resource_type == "image" and not blocked:
blocked.append(route.request.url)
route.abort()
else:
route.continue_()
context.route("**/*", block_one_image)
page.goto(base_url, wait_until="networkidle")
assert len(page.locator("body").inner_text().strip()) > 100
注意:只拦截图片请求。若拦截所有请求,首页 HTML 文档也会被误拦截,得到的是测试脚本自己制造的失败。
七、测试计划怎么标状态才不乱
一个测试项不只有"通过"和"失败"。建议至少分四种:
| 状态 | 什么时候使用 |
|---|---|
| 通过 | 已执行,有证据,结果符合预期 |
| 失败 | 已执行,有证据,结果不符合预期 |
| 延期 | 缺固定页面、敏感字段、判定标准,暂时无法可靠执行 |
| 待执行 | 已准备好,但本轮尚未开始 |
例如"爬虫不能看会员正文"这条用例,如果没有会员页面地址和敏感字段清单,不能随便判通过或失败,应标延期,并写清楚:需要开发提供一个固定页面和应隐藏字段。
当 Bug 修复后,必须按同一条脚本、同一视口、同一 UA 再跑一次:
#mermaid-svg-SzY930dnJazKd86c{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SzY930dnJazKd86c .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SzY930dnJazKd86c .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SzY930dnJazKd86c .error-icon{fill:#552222;}#mermaid-svg-SzY930dnJazKd86c .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SzY930dnJazKd86c .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SzY930dnJazKd86c .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SzY930dnJazKd86c .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SzY930dnJazKd86c .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SzY930dnJazKd86c .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SzY930dnJazKd86c .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SzY930dnJazKd86c .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SzY930dnJazKd86c .marker.cross{stroke:#333333;}#mermaid-svg-SzY930dnJazKd86c svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SzY930dnJazKd86c p{margin:0;}#mermaid-svg-SzY930dnJazKd86c .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SzY930dnJazKd86c .cluster-label text{fill:#333;}#mermaid-svg-SzY930dnJazKd86c .cluster-label span{color:#333;}#mermaid-svg-SzY930dnJazKd86c .cluster-label span p{background-color:transparent;}#mermaid-svg-SzY930dnJazKd86c .label text,#mermaid-svg-SzY930dnJazKd86c span{fill:#333;color:#333;}#mermaid-svg-SzY930dnJazKd86c .node rect,#mermaid-svg-SzY930dnJazKd86c .node circle,#mermaid-svg-SzY930dnJazKd86c .node ellipse,#mermaid-svg-SzY930dnJazKd86c .node polygon,#mermaid-svg-SzY930dnJazKd86c .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SzY930dnJazKd86c .rough-node .label text,#mermaid-svg-SzY930dnJazKd86c .node .label text,#mermaid-svg-SzY930dnJazKd86c .image-shape .label,#mermaid-svg-SzY930dnJazKd86c .icon-shape .label{text-anchor:middle;}#mermaid-svg-SzY930dnJazKd86c .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SzY930dnJazKd86c .rough-node .label,#mermaid-svg-SzY930dnJazKd86c .node .label,#mermaid-svg-SzY930dnJazKd86c .image-shape .label,#mermaid-svg-SzY930dnJazKd86c .icon-shape .label{text-align:center;}#mermaid-svg-SzY930dnJazKd86c .node.clickable{cursor:pointer;}#mermaid-svg-SzY930dnJazKd86c .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SzY930dnJazKd86c .arrowheadPath{fill:#333333;}#mermaid-svg-SzY930dnJazKd86c .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SzY930dnJazKd86c .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SzY930dnJazKd86c .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SzY930dnJazKd86c .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SzY930dnJazKd86c .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SzY930dnJazKd86c .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SzY930dnJazKd86c .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SzY930dnJazKd86c .cluster text{fill:#333;}#mermaid-svg-SzY930dnJazKd86c .cluster span{color:#333;}#mermaid-svg-SzY930dnJazKd86c div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SzY930dnJazKd86c .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SzY930dnJazKd86c rect.text{fill:none;stroke-width:0;}#mermaid-svg-SzY930dnJazKd86c .icon-shape,#mermaid-svg-SzY930dnJazKd86c .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SzY930dnJazKd86c .icon-shape p,#mermaid-svg-SzY930dnJazKd86c .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SzY930dnJazKd86c .icon-shape .label rect,#mermaid-svg-SzY930dnJazKd86c .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SzY930dnJazKd86c .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SzY930dnJazKd86c .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SzY930dnJazKd86c :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
脚本发现失败
一个问题一个 Bug
开发修复
按原条件回归
通过?
测试计划改为通过
关闭 Bug
八、最后总结:一套能复用的测试习惯
SEO 测试最重要的不是记住多少术语,而是养成下面几个习惯:
- 入口能打开,不等于功能正确。 Sitemap 要继续看域名、内容和 URL 可访问性。
- robots 写了允许,不等于爬虫真能读到正文。 要模拟 UA 真正访问。
- 手工测试先走通,再写脚本。 把稳定的 URL、关键词、预期结果放到配置里。
- 失败信息要能让开发定位。 带上页面、视口、UA、图片 URL 或关键字。
- 没有前置条件,就标延期。 不猜测、不硬判。
- 修复后按原条件重跑。 回归通过后再关闭缺陷。
把这些流程沉淀下来后,SEO 测试就不再是"检查两个文件",而是一套可持续复用的网站质量保障方法。