Python 实现网站 SEO 巡检:正确处理 PageSpeed 与域名接口状态
Python 实现网站 SEO 巡检:正确处理 PageSpeed 与域名接口状态
把 PageSpeed、DNS、SSL 和 WHOIS 放进同一个 Python 任务时,难点不在发出四个 HTTP 请求,而在于正确处理不同返回契约、缓存信息和失败边界。
本文以 网页性能与 SEO 评分 API 为主,给出一套可复用的 Python 封装思路。示例使用标准库,AppKey 通过服务端环境变量读取。

先定义统一结果,而不是统一原始响应
PageSpeed 的业务状态位于 DataStatus.StatusCode,成功值为 100;DNS、SSL、WHOIS v2 接口则返回 dataStatus.statusCode=200。强行用一套字段解析所有响应,会把正常结果误判为失败。
更合适的方法是分别验证原始契约,再映射到统一结果:
from dataclasses import dataclass, field
from typing import Any
@dataclass
class CheckResult:
check_name: str
target: str
ok: bool
collected_at: str
raw_status: int
data: dict[str, Any] = field(default_factory=dict)
error: str | None = None
统一结果用于报告和存储,原始响应仍可在受控日志或对象存储中按合规要求保留。
封装 GET 请求
import json
import os
from urllib.parse import urlencode
from urllib.request import Request, urlopen
API_ROOT = "https://api.gugudata.com"
def get_json(path: str, params: dict[str, str], timeout: int = 60) -> dict:
query = urlencode(params)
request = Request(
f"{API_ROOT}{path}?{query}",
headers={"X-GUGUDATA-APPKEY": os.environ["GUGUDATA_APPKEY"]},
method="GET",
)
with urlopen(request, timeout=timeout) as response:
return json.load(response)
不要把 AppKey 放进 params,这样更容易避免密钥进入访问日志。生产实现还应限制读取大小,并分别处理超时、连接错误和非 2xx HTTP 状态。
PageSpeed 校验器
def fetch_pagespeed(page_url: str) -> dict:
payload = get_json(
"/websitetools/pagespeed-score",
{
"url": page_url,
"strategy": "mobile",
"locale": "zh-CN",
"categories": "performance,accessibility,best-practices,seo",
"forceRefresh": "false",
},
)
status = payload.get("DataStatus", {})
if int(status.get("StatusCode", 0)) != 100:
raise RuntimeError(status.get("StatusDescription", "PageSpeed failed"))
return payload.get("Data", {})
需要保存的不只是 Scores。FinalUrl 可以帮助发现重定向,FetchTime 与 Cached 用于解释数据时效,TopIssues 才能把分数转成具体优化项。
v2 域名接口校验器
def fetch_domain_check(path: str, domain: str) -> dict:
payload = get_json(path, {"domain": domain}, timeout=30)
status = payload.get("dataStatus", {})
if int(status.get("statusCode", 0)) != 200:
message = status.get("statusDescription", "Domain check failed")
raise RuntimeError(message)
return payload.get("data", {})
def fetch_dns(domain: str) -> dict:
return fetch_domain_check("/v2/websitetools/dns-lookup", domain)
def fetch_ssl(domain: str) -> dict:
return fetch_domain_check("/v2/websitetools/sslcertinfo", domain)
def fetch_whois(domain: str) -> dict:
return fetch_domain_check("/v2/websitetools/whois", domain)
这里按当前 v2 契约判断 200,不要把 PageSpeed 的 100 复制过来。
从 URL 安全提取域名
from urllib.parse import urlsplit
def extract_domain(page_url: str) -> str:
parsed = urlsplit(page_url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise ValueError("A valid HTTP or HTTPS URL is required")
return parsed.hostname.encode("idna").decode("ascii")
不要通过字符串切割提取域名。真实 URL 可能包含端口、认证信息、IPv6、国际化域名或路径中的相似文本。
URL 与域名任务分别去重
假设一次巡检包含同一域名下的 20 个页面,PageSpeed 需要按 20 个 URL 执行;DNS、SSL、WHOIS 通常只需按域名执行一次。
def build_targets(page_urls: list[str]) -> tuple[list[str], list[str]]:
urls = list(dict.fromkeys(page_urls))
domains = list(dict.fromkeys(extract_domain(url) for url in urls))
return urls, domains
这个小步骤能直接减少重复查询,也让缓存键和失败重试更清晰。
不要把所有失败都重试
建议按三层分类:
| 类型 | 例子 | 处理 |
|---|---|---|
| 传输失败 | 超时、连接中断、临时 5xx | 有上限地退避重试 |
| 契约失败 | 业务状态非成功、字段缺失 | 记录原始状态,进入失败队列 |
| 内容异常 | 分数下降、证书临近到期、DNS 变化 | 生成行动项,不重试掩盖结果 |
对参数错误、权限问题和稳定复现的业务失败不断重试,只会消耗额度并延迟真正需要处理的问题。
报告记录建议
每次检查至少保存:目标、检查类型、HTTP 状态、原始业务状态、采集时间、是否缓存、关键结果摘要、首次发现时间、连续出现次数和负责人。
PageSpeed 评分应在相同 strategy 下比较;WHOIS 字段缺失要标记为未知,因为隐私保护或注册局差异都可能造成空值。最终报告要告诉读者“下一步做什么”,而不是只打印 JSON。
接入前自测
- 使用无效 URL 验证参数失败路径。
- 使用可重定向 URL 验证
FinalUrl保存逻辑。 - 确认 PageSpeed 成功码 100 与 v2 域名成功码 200 分开处理。
- 多个同域 URL 只生成一组域名任务。
- 日志中不出现 AppKey。
- 缓存结果保留
Cached与FetchTime。 - 重试次数有上限,业务异常不会被当成网络错误。
正确处理接口边界之后,SEO 巡检代码才有资格进入定时任务;否则再漂亮的报告,也可能建立在错误的成功判断上。
更多推荐


所有评论(0)