网站上线后,SEO检查经常依靠人工逐页查看。当网站页面数量增加时,人工检查容易遗漏一些基础问题,例如:
页面无法正常访问;
标题或页面描述为空;
页面没有主标题;
规范地址缺失;
页面被设置为不参与索引;
没有配置JSON-LD结构化数据;
Sitemap中存在失效页面。
本文使用Python编写一个基础巡检工具,自动读取网站的robots.txt和sitemap.xml,并批量检查页面中的常见SEO与GEO基础信息。
这套工具主要用于技术检查,不分析关键词排名,也不代表页面一定会被搜索系统收录或引用。
一、SEO与GEO巡检需要检查什么
SEO技术检查主要关注页面能不能被正常访问、抓取和理解。
本文将GEO基础检查理解为以下几个方面:
页面主题能不能通过标题和正文清楚表达;
企业、文章或产品信息能不能结构化呈现;
JSON-LD中的信息与页面正文能不能保持一致;
不同页面中的名称和内容描述有没有明显冲突。
因此,脚本除了检查传统SEO标签,还会读取页面中的JSON-LD数量和类型。
二、准备Python运行环境
本文使用Python 3,需要安装两个第三方库:
pip install requests beautifulsoup4
新建一个Python文件:
seo_geo_audit.py
然后将下面的代码复制到文件中。
三、完整巡检代码
import csv
import json
import sys
import time
import xml.etree.ElementTree as ET
from urllib.parse import urljoin, urlparse
import requests
from bs4 import BeautifulSoup
TIMEOUT_SECONDS = 10
MAX_PAGES = 20
def normalize_site(site: str) -> str:
"""统一站点地址格式。"""
site = site.strip()
if not site.startswith(("http://", "https://")):
site = "https://" + site
parsed = urlparse(site)
if not parsed.netloc:
raise ValueError("站点地址格式不正确")
return f"{parsed.scheme}://{parsed.netloc}"
def fetch_page(url: str):
"""请求页面并统计响应时间。"""
start_time = time.perf_counter()
response = requests.get(
url,
timeout=TIMEOUT_SECONDS
)
elapsed_ms = int(
(time.perf_counter() - start_time) * 1000
)
response.raise_for_status()
return response, elapsed_ms
def find_sitemap_urls(site: str) -> list[str]:
"""从robots.txt中查找Sitemap地址。"""
robots_url = urljoin(site + "/", "robots.txt")
sitemap_urls = []
try:
response, _ = fetch_page(robots_url)
for line in response.text.splitlines():
line = line.strip()
if line.lower().startswith("sitemap:"):
sitemap_url = line.split(":", 1)[1].strip()
if sitemap_url:
sitemap_urls.append(sitemap_url)
except requests.RequestException:
pass
if not sitemap_urls:
sitemap_urls.append(
urljoin(site + "/", "sitemap.xml")
)
return list(dict.fromkeys(sitemap_urls))
def get_xml_tag_name(tag: str) -> str:
"""移除XML命名空间。"""
return tag.rsplit("}", 1)[-1]
def parse_sitemap(
sitemap_url: str,
visited: set[str] | None = None
) -> list[str]:
"""解析普通Sitemap和Sitemap索引。"""
if visited is None:
visited = set()
if sitemap_url in visited:
return []
visited.add(sitemap_url)
try:
response, _ = fetch_page(sitemap_url)
root = ET.fromstring(response.content)
except (
requests.RequestException,
ET.ParseError
):
return []
root_name = get_xml_tag_name(root.tag)
locations = []
for node in root.iter():
if (
get_xml_tag_name(node.tag) == "loc"
and node.text
):
locations.append(node.text.strip())
if root_name == "sitemapindex":
page_urls = []
for child_sitemap in locations[:20]:
page_urls.extend(
parse_sitemap(
child_sitemap,
visited
)
)
return page_urls
if root_name == "urlset":
return locations
return []
def collect_jsonld_types(
soup: BeautifulSoup
) -> tuple[int, list[str]]:
"""读取JSON-LD数量和数据类型。"""
blocks = soup.find_all(
"script",
attrs={"type": "application/ld+json"}
)
type_names = []
def walk_json(value):
if isinstance(value, dict):
current_type = value.get("@type")
if isinstance(current_type, str):
type_names.append(current_type)
elif isinstance(current_type, list):
type_names.extend(
str(item)
for item in current_type
)
for child in value.values():
walk_json(child)
elif isinstance(value, list):
for child in value:
walk_json(child)
for block in blocks:
raw_text = block.string or block.get_text()
if not raw_text.strip():
continue
try:
parsed_data = json.loads(raw_text)
walk_json(parsed_data)
except json.JSONDecodeError:
continue
return len(blocks), sorted(set(type_names))
def audit_page(url: str) -> dict:
"""检查单个页面的基础信息。"""
result = {
"url": url,
"status_code": "",
"response_ms": "",
"title": "",
"title_length": 0,
"description": "",
"description_length": 0,
"h1_count": 0,
"canonical": "",
"robots_value": "",
"jsonld_count": 0,
"jsonld_types": "",
"text_chars": 0,
"error": ""
}
try:
response, elapsed_ms = fetch_page(url)
result["status_code"] = response.status_code
result["response_ms"] = elapsed_ms
soup = BeautifulSoup(
response.text,
"html.parser"
)
if soup.title:
title = soup.title.get_text(
" ",
strip=True
)
result["title"] = title
result["title_length"] = len(title)
description_tag = soup.find(
"meta",
attrs={
"name": lambda value:
value
and value.lower() == "description"
}
)
if description_tag:
description = description_tag.get(
"content",
""
).strip()
result["description"] = description
result["description_length"] = len(
description
)
result["h1_count"] = len(
soup.find_all("h1")
)
canonical_tag = soup.find(
"link",
rel=lambda value:
value and "canonical" in value
)
if canonical_tag:
result["canonical"] = canonical_tag.get(
"href",
""
).strip()
robots_tag = soup.find(
"meta",
attrs={
"name": lambda value:
value
and value.lower() == "robots"
}
)
if robots_tag:
result["robots_value"] = robots_tag.get(
"content",
""
).strip()
jsonld_count, jsonld_types = (
collect_jsonld_types(soup)
)
result["jsonld_count"] = jsonld_count
result["jsonld_types"] = "|".join(
jsonld_types
)
for node in soup(
["script", "style", "noscript"]
):
node.decompose()
page_text = "".join(
soup.stripped_strings
)
result["text_chars"] = len(page_text)
except requests.RequestException as error:
result["error"] = str(error)
return result
def save_csv(
results: list[dict],
filename: str
) -> None:
"""将巡检结果保存为CSV文件。"""
if not results:
raise ValueError("没有可保存的巡检结果")
with open(
filename,
"w",
newline="",
encoding="utf-8-sig"
) as file:
writer = csv.DictWriter(
file,
fieldnames=results[0].keys()
)
writer.writeheader()
writer.writerows(results)
def main() -> None:
if len(sys.argv) < 2:
print(
"使用方法:python seo_geo_audit.py "
"https://example.com"
)
return
try:
site = normalize_site(sys.argv[1])
except ValueError as error:
print(error)
return
sitemap_urls = find_sitemap_urls(site)
page_urls = []
for sitemap_url in sitemap_urls:
page_urls.extend(
parse_sitemap(sitemap_url)
)
page_urls = list(dict.fromkeys(page_urls))
if not page_urls:
print("没有读取到Sitemap,改为检查首页。")
page_urls = [site + "/"]
selected_urls = page_urls[:MAX_PAGES]
print(
f"发现{len(page_urls)}个页面,"
f"本次检查{len(selected_urls)}个页面。"
)
results = []
for index, url in enumerate(
selected_urls,
start=1
):
print(
f"[{index}/{len(selected_urls)}] "
f"{url}"
)
results.append(
audit_page(url)
)
output_file = "seo_geo_audit.csv"
save_csv(results, output_file)
print(
f"检查完成,结果已保存到:"
f"{output_file}"
)
if name == "main":
main()
四、运行巡检脚本
打开命令行,进入Python文件所在目录,执行:
python seo_geo_audit.py https://example.com
将示例地址替换成需要检查的网站地址。
运行后,程序会依次完成以下工作:
读取robots.txt;
尝试查找Sitemap地址;
没有找到时读取sitemap.xml;
提取Sitemap中的页面地址;
检查前20个页面;
生成seo_geo_audit.csv文件。
为了避免一次请求过多页面,示例代码只检查前20个页面。
需要增加检查数量时,可以修改:
MAX_PAGES = 20
例如改为:
MAX_PAGES = 50
五、怎样查看CSV巡检结果
脚本执行完成后,会在当前目录生成:
seo_geo_audit.csv
可以使用表格软件打开该文件。
status_code
表示页面返回状态。
正常页面通常会返回200。
如果出现404,说明页面不存在或地址已经失效;如果出现500,可能是程序或服务运行异常。
response_ms
表示页面请求所用时间,单位为毫秒。
该数据会受到网络环境、页面大小和服务性能影响,适合同类型页面之间进行比较。
title和title_length
记录页面标题及标题长度。
标题为空时,需要检查页面有没有正确设置title标签。
description
记录页面中的description内容。
description可以用来概括页面主题,方便搜索系统和用户快速理解页面内容。
h1_count
记录页面中的H1数量。
数量为0时,需要检查页面主标题是否使用了正确标签。
数量较多时,需要检查页面标题层级是否清楚。
canonical
记录页面中的规范地址。
当网站存在多个相似地址时,可以通过规范地址说明主要页面版本。
robots_value
记录页面中的robots设置。
如果发现noindex,需要确认该页面是不是有意不参与索引。
jsonld_count
记录页面中JSON-LD代码块的数量。
数量为0不代表页面一定存在问题,但说明页面没有使用JSON-LD结构化描述。
jsonld_types
记录页面中出现的数据类型,例如:
Article
Organization
BreadcrumbList
Product
FAQPage
通过这个字段,可以检查文章页、企业页和产品页有没有使用对应的数据类型。
text_chars
统计删除脚本和样式后的正文字符数量。
该数据不能直接代表内容质量,但可以帮助发现正文为空、页面内容没有正常加载等问题。
六、为文章页面增加JSON-LD
如果文章页面没有配置JSON-LD,可以根据页面真实内容添加Article数据。
示例代码如下:
结构化数据中的内容需要与页面中实际展示的信息保持一致。
不能在JSON-LD中加入页面上没有展示的产品、评价、服务范围或其他内容。
七、从GEO角度继续进行人工检查
自动巡检只能发现部分技术问题,还需要人工检查内容表达。
页面主题是否明确
文章标题、H1和正文内容应该围绕同一个主题展开。
不要出现标题写SEO,正文却大篇幅介绍无关内容的情况。
基础信息是否统一
网站名称、企业名称、产品名称和作者名称应该保持相对统一。
不同页面使用多种互相冲突的名称,会增加理解难度。
内容是否回答具体问题
技术文章需要说明:
问题背景;
操作步骤;
使用代码;
检查方法;
运行结果;
注意事项。
只介绍概念而没有实际过程,内容参考价值会比较有限。
JSON-LD是否与正文一致
JSON-LD是对页面内容的结构化说明,不能代替正文。
结构化数据中的标题、作者、发布日期和主体名称,应当能够在页面中找到对应信息。
重要页面能否直接访问
公开文章、产品说明和帮助文档应当能够正常打开。
如果页面无法访问,搜索系统和普通用户都无法读取相关内容。
八、后续可以增加哪些功能
当前脚本属于基础版本,还可以继续增加以下功能:
检查图片alt属性;
检查页面内部链接;
检查重复标题;
检查重复description;
检查重定向情况;
检查Open Graph标签;
检查分页页面;
检查JSON-LD格式错误;
生成HTML可视化报告;
定期运行并保存历史结果。
也可以将脚本放到云主机上,通过计划任务定期运行,对网站新增页面和状态变化进行持续检查。
总结
SEO与GEO不只是内容发布工作,也包含网站结构、页面状态、规范地址和结构化数据等技术环节。
本文的Python脚本可以自动读取Sitemap,并批量检查页面状态、标题、描述、H1、规范地址和JSON-LD信息。
脚本不能代替完整的网站分析,但可以用于网站上线检查、改版验收和日常维护,帮助开发人员快速发现容易遗漏的基础问题。