跳至内容
免费开始
返回博客

网站爬取网站地图:在安全范围内发现 URL

Robert Munceanu最后更新于 2 min read
网站爬取网站地图:在安全范围内发现 URL

开始提取

免费试用 WebScrapingAPI

领取 1,000 个免费 API 积分。无需信用卡。

开始使用
简而言之:网站地图是一种发现输入,而非完整的数据集,也不代表可以抓取其中列出的每个 URL。应区分 URL 集合与索引,有意识地解析命名空间,将时效性元数据视为来源声明,并在将目标添加到集合队列之前严格执行检索边界。

某个产品类别显示了二十个链接,但其网站地图却列出了数千个产品地址。通过抓取网站地图,您可以发现这个更大的候选集合,而无需逐一浏览每个菜单。关键区别在于:网站地图仅提供位置信息和有限的元数据;它既不提供产品记录本身,也不保证所列出的每个页面均可访问。

基于网站地图的发现还会改变信任边界。XML 文件及其内部的 URL 属于外部输入。解析器可能成功识别出某个文档,但生成的地址却超出了您的采集范围。一个索引可能指向许多文档,而压缩文件解压后的体积可能远超其传输大小。

本指南将发现、验证和收集三个环节区分开来。您将检查两种主要的 XML 格式,运行本地 Python 测试脚本,定义遍历限制,并决定如何使用 lastmod ,但不要将其视为变更的证明。本练习使用存储在内存中的虚构文档,不会发起任何网络请求。在将发现的页面添加到真实的抓取队列之前,请将相同的推理方法应用于已获批准的网站。

将网络爬取的站点地图用作发现输入

从采集目标入手。如果您需要产品详情,页面 URL 列表只是一个中间结果。您仍需识别哪些地址代表产品、检索相应的页面、提取字段,并验证所得记录。

网站地图可能由网站声明,也可能由其所有者直接提供。常规的文件名是一个有用的候选项,但这不能保证每个网站都会使用它。将网站地图地址的发现及其内容收集的授权视为两个独立的问题。

对于网站地图的网络爬取,应建立候选地址队列,而非立即抓取所有位置。记录每个 URL 的发现来源以及将其纳入页面队列的具体规则。这有助于日后更轻松地排查范围过广的索引或意外出现的文档。

分层

输入

有用输出

网站地图发现

已批准的网站和已知的网站地图位置

网站地图文档工作清单

XML 解析

受限的网站地图内容

已分类的子网站地图或页面位置

范围验证

已解析的候选URL

允许的目标及被拒绝的目标原因

页面集合

已批准页面队列

检索到的表示形式

记录提取

页面内容和字段规则

已验证的业务记录

请勿将发布意图与完整性混为一谈。一个网站可能会省略某些页面、发布过时的条目,或为不同内容维护多个网站地图集合。您的发现报告应描述所检查文档的具体内容,而非断言不存在其他页面。

此外,应区分获取失败与页面已被删除的情况。发现过程中出现的临时网关错误,并不能确切说明所列内容是否仍然存在。《代理状态诊断指南》解释了为何在将响应类别作为关于源的结论之前,需要提供证据。

将初始范围控制在可手动检查的范围内。在进行大规模遍历导致不匹配情况被掩盖之前,通过检查一份已批准的站点地图和几种具有代表性的页面类型,即可判断该发现方法是否适合当前任务。

区分 URL 集合、索引和扩展数据

XML 根节点会告知文档包含的内容。一个 urlset 包含页面条目;而 sitemapindex 包含指向站点地图文档的条目。两者均使用 Sitemaps 命名空间,且其必需的 loc 元素具有不同的功能。Sitemaps协议定义了这些结构。

文档或元素

对收集器的意义

处理方式

urlset 包含 url/loc

页面候选项

验证并添加到页面发现输出中

sitemapindex 包含 sitemap/loc

网站地图文档候选项

验证并添加到网站地图遍历中

页面条目 lastmod

发布者的页面修改值

作为元数据保留,并附带其来源

索引条目 lastmod

发布者的网站地图修改值

与页面新鲜度分开处理

扩展命名空间元素

其他领域特定元数据

仅在明确的扩展协议下进行解释

即使可见的标签看起来很简单,XML 命名空间依然很重要。在默认命名空间下,XML 解析器会结合命名空间 URI 和本地名称来识别元素。因此,搜索未带命名空间的 url 元素,可能会导致原本有效的网站地图无法返回任何结果。

在抓取网站地图时,请选择您所需的具体结构路径。若广泛搜索所有名为 loc 的广泛搜索,可能会将页面位置与网站地图引用或扩展元数据混为一谈。应区分主命名空间与图像、视频、新闻或其他扩展,而非不加区分地移除所有命名空间。

让 XML 解析器负责实体转义。包含查询分隔符的 URL 在 XML 中可能以 & ,并在 XML 中呈现为 & 。请勿在解析前替换任意字符串,也不要反复解码 URL 直到其形式变得“方便”为止。

在进行任何规范化处理的同时,请保留最初发现的原始地址。规范化应遵循已知的标识规则,而非臆测不同的路径或查询字符串是等价的。只有当 URL 列表的解释始终与源协议及您的收集范围保持一致时,它才成为有用的输入。

解析一个支持本地命名空间的站点地图测试用例

将此练习保存为 sitemap_fixture.py 并使用 Python 3 运行它。该程序仅使用标准库,解析内存中的固定 Unicode 字符串,且不进行任何网络检索。该 fixture 对一个索引和一个 URL 集合进行分类,然后验证是否会拒绝无关的主机。

from urllib.parse import urlsplit
from xml.etree import ElementTree as ET

NS = "http://www.sitemaps.org/schemas/sitemap/0.9"
INDEX = f'''<sitemapindex xmlns="{NS}">
  <sitemap><loc>https://catalog.example/products.xml</loc></sitemap>
</sitemapindex>'''
URLS = f'''<urlset xmlns="{NS}">
  <url><loc>https://catalog.example/products/lamp</loc>
    <lastmod>2026-09-01</lastmod></url>
  <url><loc>https://catalog.example/products/book?lang=en&amp;view=full</loc></url>
</urlset>'''

def allowed_url(value):
    if not value or any(ord(char) <= 32 for char in value):
        raise ValueError("missing URL or control/space character")
    parts = urlsplit(value)
    if (parts.scheme != "https" or parts.netloc != "catalog.example"
            or parts.fragment):
        raise ValueError("URL outside fixture scope")
    return value

def parse_fixture(text):
    if len(text.encode("utf-8")) > 10_000:
        raise ValueError("fixture byte budget exceeded")
    upper = text.upper()
    if "<!DOCTYPE" in upper or "<!ENTITY" in upper:
        raise ValueError("declarations not accepted in this fixture")
    root = ET.fromstring(text)
    shapes = {
        f"{{{NS}}}urlset": ("pages", "url"),
        f"{{{NS}}}sitemapindex": ("sitemaps", "sitemap"),
    }
    if root.tag not in shapes:
        raise ValueError("unsupported root or namespace")
    kind, child = shapes[root.tag]
    entries = root.findall(f"{{{NS}}}{child}")
    if len(entries) > 10:
        raise ValueError("fixture entry budget exceeded")
    result = []
    for entry in entries:
        locations = entry.findall(f"{{{NS}}}loc")
        if len(locations) != 1:
            raise ValueError("expected exactly one location")
        location = allowed_url((locations[0].text or "").strip())
        modified = entry.findtext(f"{{{NS}}}lastmod")
        result = [*result, {"url": location, "lastmod_raw": modified}]
    return kind, result

kind, children = parse_fixture(INDEX)
assert kind == "sitemaps" and len(children) == 1
documents = {"https://catalog.example/products.xml": URLS}
kind, pages = parse_fixture(documents[children[0]["url"]])
assert kind == "pages" and len(pages) == 2
assert pages[0]["lastmod_raw"] == "2026-09-01"
assert pages[1]["url"].endswith("?lang=en&view=full")
for invalid in [
    URLS.replace("catalog.example", "other.example"),
    '<!DOCTYPE urlset><urlset/>',
    '<urlset><url><loc>https://catalog.example/x</loc></url></urlset>',
]:
    try:
        parse_fixture(invalid)
    except ValueError:
        pass
    else:
        raise AssertionError("invalid fixture accepted")
print("index classified; 2 page URLs parsed; rejection checks passed")

该测试用例特意允许一个精确的 HTTPS 权威源。它不会跟随重定向、解析 DNS、解压缩数据,也不声称字符串级别的 URL 检查能防止服务器端请求伪造。这些控制措施属于生产环境的检索层。

声明检查也仅限于该测试用例中已解码的字符串。它并非针对任意远程编码的通用 XML 防火墙。对于不可信的输入,请使用配置适当的解析器和检索限制,而非将此检查直接复制为完整的防御措施。

请注意 lastmod 作为原始源元数据被保留。该练习并未将其转换为页面已更改的证明或调度命令。第二个页面没有修改值,这与已知的旧时间戳不同。

这个小型测试涵盖了网页抓取站点地图中的解释边界:根分类、支持命名空间的路径、实体解码、允许一个主机,以及拒绝格式错误的结构。在添加传输层时保留这些测试,然后针对传输层的重定向、大小和目标规则添加单独的测试。

利用预算和已访问集遍历索引

为站点地图文档和页面候选项使用独立的队列。从已批准的索引开始构建文档队列,获取并分类一个受限文档,然后将其子站点地图位置或页面位置放入适当的目标位置。不要仅仅因为两者都包含 URL,就将一个页面作为另一个站点地图加入队列。

为文档位置维护已访问集,以免重复引用导致工作无限期重复。如果提供商发布了意外的索引嵌套结构,请明确您的收集器是拒绝该结构还是支持有限遍历。不要假设任意嵌套是可移植协议的保证。

协议限制与操作预算有所不同。标准站点地图允许最多 50,000 个 URL 和 50 MB(具体为 52,428,800 字节,未压缩时)。站点地图索引还具有条目数和大小限制。压缩可减少传输大小,但不会消除未压缩时的限制。 您的任务可能需要更低的限制,以适应其内存、时间及批准的范围。

对于大规模抓取网站地图的情况,请对传输字节数、解压后字节数、文档数、条目数、耗时以及(如适用)遍历深度设置上限。一个较小的压缩响应在解压后仍可能产生庞大的正文。应在读取和解压过程中强制执行限制,而不仅仅是在分配完整的解压文档之后。

记录被拒绝和被跳过的引用。“已访问”与“主机不允许”、“文档过大”或“无法解析 XML”不同。这些类别解释了为何发现的页面计数与最初遇到的条目数量存在差异。

在记录持久化输出后进行检查点。如果发现结果与遍历状态分开存储,则应设计能够安全重放文档的重启行为。去重应防止队列条目重复,同时不应掩盖对作业至关重要的元数据更改。

最后,应保留文档的检索时间及其最终批准的位置。否则,重定向或文档变更可能导致两次运行看似完全相同,而实际上它们检查的输入内容却不同。

将 lastmod 及其他元数据视为来源声明

页面级别的 lastmod 字段代表发布者声明的该页面最后修改时间。它不应被理解为网站地图生成器运行的时间。索引级别的修改值则指代网站地图文档本身。混淆这些含义可能会触发不必要的页面刷新,或导致跳过相关页面。

对于用于网络爬取的站点地图,应保留原始值,并仅在明确的日期策略下进行解析。仅含日期而无时间的值,其精确度低于时间戳。缺失或格式错误的值应保持未知状态,而非被默认为当日日期或暗示确定性的默认值。

在来源证明其准确性对您的任务有用之前,请将该值视为优先级提示。将公布的变更样本与实际页面观察结果进行对比。对于编辑类内容而言,一个值得信赖的时间戳对于频繁变动的股票价格而言可能仍然过于粗略。

谷歌的站点地图指南解释了其对精确 lastmod 值,并指出会忽略 priority 和 changefreq。这描述的是谷歌的行为,并非要求您的采集器复制其调度策略。请勿将可选的发布者元数据转化为通用的抓取指令。

HTTP 验证仍是独立的。根据 HTTP 规则,服务器的实体标签 (Entity-Tag) 或 last-modified 响应头可用于条件检索。站点地图日期不能替代这些验证器,也不能保证 304 响应。RFC 9111 定义了缓存新鲜度及验证行为。

对于抓取报告,请分别保留源修改元数据、发现时间和页面观察时间。它们分别表明发布者何时声明了更改、您何时发现了该地址,以及您何时观察到了实际的表示形式。

在安排任务时请运用这一区分。优先处理最近发布的变更可优化工作顺序,而定期核对则可弥补缺失或不可靠的日期信息。请将该策略记录在案,以便用户理解“最近更新”的真实含义。

在信任发现的 URL 之前限制检索

XML 解析和 URL 解析并不授予发起网络请求的权限。某个位置可能指向未经批准的网站之外、包含凭据、使用意外的方案,或访问收集器本不应访问的地址。应将每个发现的位置视为不受信任的输入,并进行单独的准入检查。

Python 的 URL 解析文档明确警告称,解析函数不会对输入进行验证。因此,成功将 URL 拆分为方案和主机名并不构成安全决策。针对方案、主机、端口、用户信息和作用域,应应用明确的策略。

当自动化服务根据外部输入获取目标地址时,服务器端请求伪造(SSRF)便成为需要关注的问题。请在实际网络边界处实施目标控制,包括重定向和解析后的地址。仅靠主机名白名单无法应对所有 DNS 或重定向场景,且验证一个跳点并不意味着授权下一个跳点。

在抓取网站地图时,应避免将原始请求中的凭据转发至新发现的主机。部分发布者会采用受支持的跨站点托管方案,但您的采集器应仅启用其已明确批准的方案。外部引用并不意味着应自动扩展采集范围。

不可信的 XML 构成了另一道边界。实体展开、外部资源访问以及大令牌行为取决于解析器及其配置。Python XML 安全文档总结了相关风险,并指出了底层解析器版本的重要性。切勿假设所有 XML 库都采用相同的默认设置。

请结合使用最新且合适的解析器防护措施,并配合大小和执行配额限制。应拒绝不支持的文档特性,而非通过启用外部实体解析来强行解析单个文件。将格式错误的站点地图归类为发现失败,并提供经过净化处理的诊断信息,以协助源所有者进行调查。

这些控制措施是相辅相成的。支持命名空间的解析可防止结构混淆;URL 策略控制目标地址;网络强制措施检查连接;而资源预算则限制工作范围。任何一项都无法安全地替代所有其他措施。

在抓取页面之前测量发现覆盖率

遍历结束时,报告已检查的内容以及工作停止的原因。包括文档数量、被接受的页面候选项、重复项、被拒绝的目标、解析失败以及因预算限制而停止的情况。仅凭庞大的 URL 数量并不能证明发现过程是完整或正确的。

将通过验证的 URL 与目标架构进行比对。产品、分类、搜索和编辑类页面可共存于同一站点地图集中。为每种相关类型定义路由规则,并将无关类型排除在提取队列之外,以免其缺失字段污染记录验证指标。

通过网络爬取生成的网站地图仅能解决地址发现问题,而非页面渲染问题。如果所选页面仅在浏览器执行后才显示数据,请参阅屏幕抓取指南,了解如何选择就绪条件并提取渲染后的字段。切勿假设网站地图能保证静态 HTML 内容。

在该交接过程中应保留数据溯源信息。页面记录应保留已发现的 URL、网站地图来源、观察时间以及相关提取版本信息。对于重定向或规范化的地址,需要明确的身份识别策略,而非未记录的替换处理。

自动化采集工作流指南将发现过程与持久化处理、重试和交付环节相连接。确保这些阶段可被观察,以免因检索中断而导致网站地图解析器看似返回的条目减少。

在进行规模扩展之前,请测试以下情况:一个重复的文档引用、一个被拒绝的 URL、一个格式错误的 XML 文件以及一个预算耗尽的情况。有用的结果不仅仅是脚本停止运行,它还应附带一个分类说明,解释目标发现范围中哪些部分仍未解决。

关键要点

  • 利用网络爬取的站点地图构建候选队列,随后在不同阶段分别验证目标地址并提取页面记录。
  • 有针对性地解析根节点和命名空间,确保站点地图引用、页面位置和扩展名数据不被混淆。
  • 在执行更严格的运算预算(包括解压后的字节数和总遍历工作量)的同时,强制执行基于协议的尺寸检查。
  • 保留 lastmod 作为发布者元数据;将其与发现时间、观察时间及 HTTP 验证器区分开来。
  • 确保 URL 准入、网络目标控制、XML 防护和完整性报告可独立进行测试。

常见问题

一个网站可以有多个站点地图索引吗?

可以。该协议允许存在多个站点地图索引。对于网络爬取站点地图,应发现或接收已批准的集合,而不是假设找到一个索引就意味着识别了所有站点地图。应保留每个起始文档的来源信息,并定义其结果是合并到一个集合范围内,还是分别报告。

是否应从站点地图 URL 中移除查询参数?

不应自动移除。查询参数可标识语言、内容或其他有意义的变体。仅在针对该来源的身份验证规则得到确认后方可移除。保留发现的 URL 以供后续更正,并测试任何规范化操作是否会导致不同记录被合并或改变检索到的表示形式。

网站地图条目能否证明某页面可被索引?

不。被列出的地址仅是发现信号,并非证明搜索引擎能够或将会对其进行索引。其他页面和访问条件仍然适用。在数据采集过程中,应验证实际响应和所需内容,而非将网站地图的存在视为可用性或适用性的保证。

结论

当发现阶段仍被视为一个独立且可衡量的阶段时,基于网站地图的网络爬取效果最佳。对文档进行分类,解析正确的命名空间,保留源元数据,并仅接受符合采集策略的目标。随后,既要报告发现的候选页面,也要说明遍历部分内容仍未完成的原因。

从本地测试环境开始,并逐个扩展边界。在处理大文件前添加受限检索机制,在处理外部引用前添加目标验证机制,在进行长时间运行的遍历前添加持久化进度机制。保留那些在跨越这些边界时能显示明确失败结果的测试用例。

当批准的页面队列准备就绪后,评估 WebScrapingAPI 的 Scraper API 以用于检索阶段。保留您自己的发现溯源信息和记录检查,以便生成的数据集既能说明 URL 的来源,也能说明实际观察到的内容。

关于作者

Robert Munceanu, 全栈开发工程师 @ WebScrapingAPI

Robert Munceanu

全栈开发工程师

罗伯特·蒙塞阿努(Robert Munceanu)是 WebScrapingAPI 的全栈开发工程师,他在产品各领域均有贡献,并协助构建了支持该平台的可靠工具和功能。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。