基于网站地图的抓取:在获取之前找到网址
Scraping and Proxy Management Expert
TL;DR:
- 大多数爬虫通过跟随链接发现 URL,这意味着需要获取一些你不想要的页面,以便找到你想要的页面。
- 网站地图提前提供给你 URL 列表:一次 HTTP 请求就能返回网站希望被索引的每个 URL,已经由发布者去重。
- 本指南中过滤一个真实网站地图将 7,577 个条目减少到与目标部分匹配的 597 个,在获取任何页面之前。
- 从该列表设置明确的爬取预算,而不是爬取到某些事情阻止你——限制应该在代码中,而不是在你的意图中。
- 仅在数据需要时渲染 JavaScript;本演示中的页面在服务器端提供其标题,因此未渲染的获取速度更快,并返回相同的字段。
- 开始使用 Scrapeless 免费计划,并将发现步骤指向你被允许爬取的网站地图。
跟随链接的爬虫是有效的,但浪费资源。要到达网站的文章页面,你需要获取主页、分类列表、分页、标签页面,以及其他在入口点和内容之间的内容——然后丢弃大部分。
一个网站地图跳过了这些。它是一个包含发布者明确希望被发现的 URL 列表,这使其成为可用的最便宜的发现机制,同时也是最不可能让人烦恼的。
网站地图为你提供了什么
网站地图是一个 XML 文件,列出了一个网站的 URL,由 网站地图协议 定义。存在两种格式,你需要处理两者:
- urlset 包含指向页面的
<url><loc>条目。 - sitemapindex 包含指向其他网站地图的
<sitemap><loc>条目,当一个网站超出协议每个文件的 50,000 个 URL 或 50 MB 限制时使用。
两者都使用相同的 <loc> 元素,这就是下面的解析器读取 <loc> 后再决定它正在查看的内容的原因。网站地图通常也以压缩格式提供,因此抓取工具应该透明地处理这一点。
在哪里可以找到一个:首先检查 /sitemap.xml,然后检查 robots.txt 中的 Sitemap: 指令,机器人排除协议标准 将其定义为广告位的规范位置。
先决条件
- Python 3.9 或更高版本。发现步骤仅使用标准库。
- 用于获取步骤的 Scrapeless API 密钥:
bash
export SCRAPELESS_API_KEY="your_api_key_here"
下面的示例指向 Scrapeless 网站地图,因此 walkthrough 可以按原样安全运行。当你适配时,替换为你被允许爬取的网站地图。
发现和过滤
一次请求,然后在内存中过滤。网站尚未获取任何内容:
python
import gzip, re, urllib.request
SITEMAP = "https://www.scrapeless.com/sitemap.xml"
LOC_RE = re.compile(r"<loc>\s*([^<\s]+)\s*</loc>", re.I)
def fetch_xml(url: str) -> str:
req = urllib.request.Request(url, headers={"User-Agent": "sitemap-demo/1.0"})
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
if url.endswith(".gz") or raw[:2] == b"\x1f\x8b":
raw = gzip.decompress(raw)
return raw.decode("utf-8", errors="replace")
xml = fetch_xml(SITEMAP)
is_index = "<sitemapindex" in xml.lower()
urls = LOC_RE.findall(xml)
print(f"文档类型: {'网站地图索引' if is_index else 'URL 集'}")
print(f"总 <loc> 条目: {len(urls)}")
blog = sorted({u for u in urls if "/en/blog/" in u})
print(f"过滤到 /en/blog/: {len(blog)}")
print(f"首个: {blog[0]}")
text
文档类型: URL 集
总 <loc> 条目: 7,577
过滤到 /en/blog/: 597
首个: https://www.scrapeless.com/zh/blog/10-best-no-code-web-scrapers
这就是这一方法的全部论点:列出了 7,577 个 URL,并缩小到 597 个相关的 URL,成本为一次请求。跟随链接的爬虫会获取数百个页面才能到达相同的列表,且仍可能错过任何不是从其恰好走过的路径链接的内容。
代码中的三个细节很重要。
gzip 检查检测魔法字节,而不是信任文件扩展名,因为许多服务器从 .xml URL 返回压缩内容。它寻找的两个字节 \x1f\x8b 是在 GZIP 文件格式规范 中定义的头部。
sorted({...}) 是一个集合推导,因此重复的 URL 在计数之前就会合并。网站地图确实包含重复项,特别是在一个网站由多个生成器拼接而成时。
检测 <sitemapindex> 很重要,因为索引意味着你刚收集的 <loc> 值是更多网站地图,而不是页面。如果 is_index 为真,获取每一个并重复此操作,然后再将任何内容视为页面 URL。
设置明确的预算,然后获取
发现是便宜的;获取则不是。你将检索的页面数量应该是代码中的一个常量,而不是循环运行时间的自然属性:
python
import json, os, re, urllib.request
SITEMAP = "https://www.scrapeless.com/sitemap.xml"
API = "https://api.scrapeless.com/api/v2/unlocker/request"
LOC_RE = re.compile(r"<loc>\s*([^<\s]+)\s*</loc>", re.I)
TITLE_RE = re.compile(r"<title[^>]*>(.*?)</title>", re.S | re.I)
def fetch_sitemap(url: str) -> str:
req = urllib.request.Request(url, headers={"User-Agent": "sitemap-demo/1.0"})
with urllib.request.urlopen(req, timeout=60) as r:
return r.read().decode("utf-8", errors="replace")
def fetch_page(url: str) -> str:
body = json.dumps({
"actor": "unlocker.webunlocker",
"input": {"url": url, "js_render": False},
}).encode()
req = urllib.request.Request(API, data=body, headers={
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
"Content-Type": "application/json",
})
with urllib.request.urlopen(req, timeout=180) as r:
return json.loads(r.read())["data"]
urls = sorted({u for u in LOC_RE.findall(fetch_sitemap(SITEMAP)) if "/en/blog/" in u})
print(f"过滤去重后的候选项数量: {len(urls)}")
BUDGET = 3
print(f"爬取预算: {BUDGET}")
for url in urls[:BUDGET]:
html = fetch_page(url)
m = TITLE_RE.search(html)
title = re.sub(r"<[^>]+>", "", m.group(1)).strip() if m else "(无标题)"
print(f" {url.rsplit('/', 1)[-1]} -> {title}")
text
过滤去重后的候选项数量: 597
爬取预算: 3
10-best-no-code-web-scrapers -> 2025年轻松数据提取的10个最佳无代码网络爬虫
20-ways-for-web-scraping-without-getting-blocked -> 20种防止网络爬虫被屏蔽的方法
403-web-scraping -> 网络爬虫中的403错误:10个解决禁止请求的简单方案
BUDGET 是一个应用于切片的命名常量。这是故意的:一个停止条件为“列表耗尽”的爬虫在几百个 URL 的部分与在数万个 URL 的部分上表现是非常不同的,这种差异仅在生产环境中显现。
请注意 js_render 设置为 False。这些页面在初始 HTML 中提供它们的 <title>,因此渲染将会增加成本和延迟,而这个字段已经存在。当你需要的数据是通过脚本写入 DOM 时再渲染——而不是默认情况下。Scrapeless 通用抓取 API 同时处理这两种情况,而 JS 渲染指南 介绍了如何判断你处于哪种情况。
URL 计数是一个快照。网站地图是一个动态文档,因此在不同的日期重新运行发现会返回不同的数字——这正是每次运行时读取它而不是硬编码列表的意义。
故障排除
/sitemap.xml 返回 404。 读取 robots.txt 并查找 Sitemap: 行,这可能指向完全不同的地方。有些网站只在这里发布;有些根本不发布,在这种情况下,跟随链接是你剩下的选项。
解析器从一个看起来有效的文件中返回零 URL。 检查响应是否被 gzip 压缩并未解压——上面的魔法字节检查处理了常见情况。还要确认你获取的是 XML 而不是 HTML 错误页面,这通常是友好的 404 重定向所产生的。
计数看起来太低。 文档可能是一个网站地图索引。每个 <loc> 都是另一个需要获取的网站地图,页面 URL 在下一层。
条目指向的 URL 不再存在。 网站地图是生成的,生成器存在延迟。将列表视为候选项而不是保证,并期望某些条目返回 404。
在爬取任何内容之前检查网站的条款及其 robots.txt,保持收集在公开页面上,并保持预算与网站可以舒适地服务的内容比例适当。网站地图告诉你出版商愿意被索引的内容;这不是以最快的速度获取所有内容的许可证。
结论
基于网站地图的爬取用一个请求替代了爬虫中最浪费的部分——发现。在这个演练中,这意味着枚举 7,577 个 URL,缩减到 597 个重要的,并仅获取 3 个,将限制写入代码中而不是留给偶然。
超越网站地图的一个习惯是排序:首先枚举,过滤和去重时它仍然是免费的,明确决定你将抓取多少,然后再开始发起请求。大多数陷入麻烦的爬虫往往是以相反的顺序进行这些步骤的。
从Scrapeless的免费计划开始,对您被允许爬取的源执行抓取步骤,并在您规划定期任务时查看Scrapeless定价。
常见问题
问:我怎么找到一个网站的站点地图?
首先尝试 /sitemap.xml,然后查看 robots.txt 中的 Sitemap: 指令,RFC 9309 将其定义为广告的规范位置。大型网站通常在该路径下发布一个索引,指向多个部分的站点地图,而不是单个平面文件。
问:站点地图索引和 URL 集合有什么区别?
URL 集合列出了页面 URL;站点地图索引列出了其他站点地图文件。两者都使用 <loc> 元素,因此一个只查看 <loc> 的解析器会高兴地返回站点地图 URL,而你可能认为自己拥有页面。在找到 <sitemapindex> 根元素时检查并递归——这种拆分存在是因为协议将单个文件限制为 50,000 个 URL 或 50 MB。
问:从站点地图爬取比跟随链接更好吗?
对于发现,通常是的:一次请求返回发布者明确列出的 URL,而不是获取中间页面来查找它们。权衡在于覆盖范围——站点地图仅包含生成者选择包含的内容,因此存在但被省略的页面仍然是不可见的。当您需要访问所有可达的内容而不仅仅是所有广告内容时,跟随链接仍然占优势。
问:从站点地图爬取时我应该渲染 JavaScript 吗?
仅当您想要的字段不在初始 HTML 中时。此演示中的页面在服务器端提供其 <title>,因此 js_render 设置为 False,抓取更便宜且更快。先检查一页:如果数据在查看源代码中出现,则不需要渲染。
问:我每次应该抓取多少页面?
明确设置一个数字并将候选列表切片到该数字,如上面的 BUDGET 所示。正确的值取决于网站的容量和您的需求,但重要的是这是在代码中记录的决策,而不是列表长度的副作用——这就是将小部分任务和整站任务转变为非常不同事件的原因。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



