惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Blog — PlanetScale
Blog — PlanetScale
N
Netflix TechBlog - Medium
博客园 - 司徒正美
The GitHub Blog
The GitHub Blog
G
Google Developers Blog
Stack Overflow Blog
Stack Overflow Blog
博客园_首页
Google DeepMind News
Google DeepMind News
博客园 - 【当耐特】
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Recent Announcements
Recent Announcements
aimingoo的专栏
aimingoo的专栏
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Y
Y Combinator Blog
B
Blog RSS Feed
人人都是产品经理
人人都是产品经理
MongoDB | Blog
MongoDB | Blog
量子位
博客园 - Franky
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Cloudflare Blog
有赞技术团队
有赞技术团队
Jina AI
Jina AI
GbyAI
GbyAI

博客园 - Parry

PDF 多格式解析如何避免混用输出:TEXT、HTML、XML 与 TAG 数据契约 基金组合风险如何避免“一个分数说风险”:样本覆盖、VaR 与风险贡献 RAG 问答如何证明答案来自文档:知识库版本、引用片段与线程隔离 SEO 排名监控如何避免单次快照误判:SERP 任务、credits 与历史对比 ETF 盘中看板如何处理分钟行情:时间窗口、新鲜度与缺口标记 节气日历服务如何统一日期口径:时区、历法事实与文化参考分层 国际号码接入校验台案例方案 户外项目日照与节气计划台案例方案 门店二维码资产管理台案例方案 用传统历法宜忌接口构建节气日历服务 全球区域与 IP 定位工作台案例方案 基金组合风险分析台案例方案 A 股公告与科创板研究台案例方案 搜索可见性与 SEO 观测台案例方案 研究生招生信息决策台案例方案 微信资讯素材采编中心案例方案 多语言语料标注与实体分析台案例方案 智能文档字段提取工作台功能需求文档 咕咕监控 3.2.0:网页有重要变化时,第一时间告诉你 Python 接入 OCR:把图片文字整理成可校验的 JSON 字段 「鸭川小记」:让语音先流动起来,再慢慢变成可整理的内容 个人公开市场研究笔记功能需求文档 网页归档与报告生成系统功能需求文档 网站工具与内容转换台功能需求文档 全球大学排名查询网站功能需求文档 汽车车型内容资料库功能需求文档 资讯元数据管理平台功能需求文档 内容质检与纠错工作台功能需求文档 文本 NLP 分析平台功能需求文档 企业文档摘要翻译台功能需求文档
SEO 自动巡检怎么做:用 PageSpeed、DNS、SSL 与 WHOIS 定位...
Parry · 2026-08-20 · via 博客园 - Parry

Python 实现网站 SEO 巡检:正确处理 PageSpeed 与域名接口状态

把 PageSpeed、DNS、SSL 和 WHOIS 放进同一个 Python 任务时,难点不在发出四个 HTTP 请求,而在于正确处理不同返回契约、缓存信息和失败边界。

本文以 网页性能与 SEO 评分 API 为主,给出一套可复用的 Python 封装思路。示例使用标准库,AppKey 通过服务端环境变量读取。

网页性能与 SEO 评分

先定义统一结果,而不是统一原始响应

PageSpeed 的业务状态位于 DataStatus.StatusCode,成功值为 100;DNS、SSL、WHOIS v2 接口则返回 dataStatus.statusCode=200。强行用一套字段解析所有响应,会把正常结果误判为失败。

更合适的方法是分别验证原始契约,再映射到统一结果:

Python 巡检器分别校验 PageSpeed 成功码 100 与 v2 域名接口成功码 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", {})

需要保存的不只是 ScoresFinalUrl 可以帮助发现重定向,FetchTimeCached 用于解释数据时效,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。
  • 缓存结果保留 CachedFetchTime
  • 重试次数有上限,业务异常不会被当成网络错误。

正确处理接口边界之后,SEO 巡检代码才有资格进入定时任务;否则再漂亮的报告,也可能建立在错误的成功判断上。