Scrapling 通用爬虫模板实战:CrawlSpider、SitemapSpider 与 LinkExtractor 深度解析
绝大多数站点抓取都逃不出两种模式:「跟随符合某种模式的链接」和「遍历站点 sitemap 中列出的每个 URL」。Scrapling 的通用爬虫模板(Generic Spider Templates)正是为此而生——它把这两类最常见的 parse() 样板代码封装成了 CrawlSpider 与 SitemapSpider,并提供 XMLFeedSpider、CSVFeedSpider 处理数据源场景。读完本篇,你将掌握如何用最少的代码声明式地驱动一场完整爬取,并理解每个模板底层的调度逻辑与 LinkExtractor 的完整参数语义。
模板总览:它们省掉了什么
所有模板都构建在 LinkExtractor 之上——这个原语负责从 Response 中抽取 URL(或者通过 matches() 对单个 URL 做过滤判断)。SitemapSpider 还会在内部解析 sitemap.xml / sitemap_index.xml 的响应体(无论是否 gzip 压缩)。
模板只是替你省去了接线工作(wiring),你完全可以在任何普通 Spider.parse() 里直接使用 LinkExtractor。模板的源码位于 scrapling/spiders/templates/,导出 CrawlSpider、CrawlRule、SitemapSpider、ShopifySpider、XMLFeedSpider、CSVFeedSpider 六个类。
CrawlSpider:基于声明式规则自动跟随链接
CrawlSpider 根据声明式的规则自动跟随链接。官方文档给出的最小可用示例:
from scrapling.spiders import CrawlSpider, CrawlRule, LinkExtractor
class QuotesSpider(CrawlSpider):
name = "blog"
start_urls = ["https://quotes.toscrape.com/"]
def rules(self):
return [
CrawlRule(LinkExtractor(allow=r"/author/"), callback=self.parse_author),
CrawlRule(LinkExtractor(allow=r"/page/\d+/")), # follow pagination, no callback
]
async def parse_author(self, response):
yield {
'.author-title': response.css('.author-title::text').get(),
"birthday": response.css('.author-born-date::text').get(),
"url": response.url,
}
result = QuotesSpider().start()
规则机制:从源码看默认 parse() 的行为
一条 CrawlRule 将 LinkExtractor 与三个可选字段配对(定义见 crawler.py):
callback:蜘蛛上的一个绑定方法,处理每个匹配 URL 的请求;priority:覆盖所派发Request的优先级;process_request:一个绑定方法,在Request被 yield 之前对其进行修改。
CrawlSpider 的默认 parse() 实现非常直白(crawler.py):遍历 rules() 返回的每条规则,用规则的 link_extractor.extract(response) 从当前响应中抽取 URL,逐个调用 response.follow(url, callback=rule.callback) 生成请求,若设置了 priority 则覆写请求优先级,若设置了 process_request 则在其返回的 request 上继续 yield。
两条值得注意的行为细节:
- 无 callback 的规则会落到默认
parse()。规则没有 callback 时,匹配到的 URL 会经由response.follow()继承原请求的callback(即None),引擎随后回落到蜘蛛的默认parse()。这对翻页非常方便:抽取下一页链接让爬取继续,无需单独写处理器。tests/spiders/test_templates.py 中的test_rule_with_no_callback_leaves_request_callback_none明确验证了这一点。 - 规则是叠加执行的。默认
parse()会把每条规则都作用于每个响应,每个匹配 URL 都会产出一个Request——多条规则之间不做互斥。这与SitemapSpider的「首个匹配即胜出」语义不同,写规则时要心中有数。
结合规则与自定义逻辑
重写 parse() 并调用 super().parse(response),就能同时获得规则行为和自己的产出:
class MySpider(CrawlSpider):
def rules(self):
return [CrawlRule(LinkExtractor(allow=r"/posts/"), callback=self.parse_post)]
async def parse(self, response):
yield {"page_url": response.url}
async for req in super().parse(response):
yield req
tests/spiders/test_templates.py 的 test_user_can_compose_super_parse 验证了这种组合:先产出自定义 item,再转发规则产生的请求。
用 process_request 修改请求
process_request 的签名是 (request, response) -> request,可以在 yield 前加请求头、改优先级,甚至整个替换请求:
def add_priority(self, request, response):
request.priority = 10
return request
def rules(self):
return [CrawlRule(
LinkExtractor(allow=r"/posts/"),
callback=self.parse_post,
process_request=self.add_priority,
)]
测试用例 test_process_request_invoked 与 test_process_request_can_replace_request 分别验证了「修改后返回」与「返回一个全新的 Request」两种用法都成立。
另外,response.follow() 会附带 referer 头,规则路径派发出的请求同样保留该头(见 test_referer_set_on_followed_requests);而带绑定方法 callback 的 Request 也通过 __getstate__ 机制把回调转换为方法名字符串,可安全参与 pickle 序列化,这对 checkpoint 断点续爬至关重要(见 test_pickle_request_with_bound_method_callback)。
SitemapSpider:从 sitemap.xml 播种的爬虫
SitemapSpider 以 sitemap.xml 中的 URL 作为爬取种子。它使用与 CrawlSpider 相同的 rules() API,心智模型完全共享:
from scrapling.spiders import SitemapSpider, CrawlRule, LinkExtractor
class MySitemap(SitemapSpider):
name = "sm"
sitemap_urls = ["https://example.com/sitemap.xml"]
def rules(self):
return [
CrawlRule(LinkExtractor(allow=r"/posts/"), callback=self.parse_post),
CrawlRule(LinkExtractor(allow=r"/products/"), callback=self.parse_product),
]
async def parse_post(self, response):
yield {"title": response.css("h1::text").get()}
async def parse_product(self, response):
yield {"sku": response.css(".sku::text").get()}
result = MySitemap().start()
URL 是如何被派发的
对 sitemap 中的每个 URL,SitemapSpider 按顺序检查每条规则的 LinkExtractor.matches(url),首个匹配的规则胜出,并 yield 一个携带该规则 callback 的 Request。若没有任何规则匹配且 rules() 非空,该 URL 被丢弃;若 rules() 返回空列表,则所有 URL 都路由到蜘蛛的 parse() 方法——而基类默认实现直接抛 NotImplementedError(除非你重写了它)。
这段调度逻辑对应 _dispatch 与 _parse_sitemap:rules 为空时直接 response.follow(url)(callback 为 None,落回 parse());否则逐条 matches() 命中即返回请求。tests/spiders/test_sitemap.py 的 test_urlset_dispatched_through_rules 和 test_no_rules_means_all_urls_fall_through 分别锁定了这两种行为:/about 这类未匹配 URL 会被丢弃,而空规则时所有 URL 都以 callback=None 派发。
Sitemap 索引(sitemap of sitemaps)
遇到 <sitemapindex> 时,蜘蛛会自动深入每个子 sitemap。若要筛选深入哪些子 sitemap,把 sitemap_follow 设为一个 LinkExtractor:
class MySitemap(SitemapSpider):
name = "sm"
sitemap_urls = ["https://example.com/sitemap.xml"]
sitemap_follow = LinkExtractor(allow=r"/posts-sitemap-\d+\.xml") # only post sitemaps
实现上,_sm_body 区分根元素类型:sitemapindex 收集子 sitemap 的 loc,urlset 则提取 URL 列表(见数据结构 SitemapResult);随后在 _parse_sitemap 中,每个子 sitemap URL 都先过一遍 sitemap_follow.matches()(sitemap_follow 为 None 时全部深入)。test_sitemap_follow_filters_child_sitemaps 验证了过滤器只放行 posts-sitemap.xml 的效果。
robots.txt 支持
直接把 robots.txt 的 URL 放进 sitemap_urls,蜘蛛会识别它、提取其中声明的所有 Sitemap 并逐一跟随:
class MySitemap(SitemapSpider):
name = "sm"
sitemap_urls = ["https://example.com/robots.txt"]
判断依据是响应 URL 的路径是否以 /robots.txt 结尾(sitemap.py),解析则由 _robots_body 借助 protego 库完成;解析失败时仅记录警告并返回空列表,不会中断爬取。
多语言(Alternate)URL
设置 sitemap_alternate_links = True 后,<xhtml:link rel="alternate" hreflang="..."> 声明的 URL 也会一并通过你的 rules() 派发。实现位于 _extract_urls:遍历 <url> 节点时,除 loc 外还收集开启了开关后的 link 子元素的 href。test_alternate_links_dispatched_when_enabled 验证了英文主 URL 与法语、德语 alternate URL 共三个 URL 全部进入调度。
gzip 与容错
Sitemap 体无论是以 gzip 魔数(\x1f\x8b)开头还是 content-type 声明 gzip,都会被 _decompress 自动解压;解压输出设置了 64 MiB 上限以防御 gzip 炸弹,超限抛 OSError 后被 _sm_body 捕获为警告。XML 解析失败(XMLSyntaxError)同样只记录警告并返回空的 SitemapResult,爬虫继续运行。
XMLFeedSpider:逐节点解析 XML 数据源
XMLFeedSpider 遍历 XML 数据源(RSS、Atom、商品 feed 等)的节点。把 itertag 设为你想迭代的节点名(默认 "item"),并重写 parse_node()——它会对每个匹配节点调用一次:
from scrapling.spiders import XMLFeedSpider
class RSSSpider(XMLFeedSpider):
name = "rss"
start_urls = ["https://example.com/feed.xml"]
itertag = "item"
async def parse_node(self, response, node):
yield {
"title": node.findtext("title"),
"link": node.findtext("link"),
"date": node.findtext("pubDate"),
}
result = RSSSpider().start()
和其他回调一样,parse_node() 也可以 yield Request 对象(例如 response.follow(node.findtext("link"), callback=self.parse_post)),从而深入数据源指向的页面。
节点如何匹配与解析
传给 parse_node() 的每个节点都是一个已剥离全部命名空间的 lxml 元素,因此 node.findtext("title")、node.find("thumbnail").get("url") 以及大小写敏感的 node.xpath(...) 在任何 feed 上都不需要命名空间映射即可工作。
匹配机制分两种(见 _wanted_tag 与 _iter_nodes):
- 普通
itertag(如"entry"):按局部名匹配,无视命名空间——这正是 Atom 和大多数带命名空间 feed 需要的行为; - 带前缀的
itertag:前缀必须在namespaces中定义成(prefix, uri)元组,否则抛ValueError。此时只匹配该命名空间下的节点:
class ThumbnailSpider(XMLFeedSpider):
name = "thumbs"
start_urls = ["https://example.com/feed.xml"]
itertag = "media:thumbnail"
namespaces = (("media", "http://search.yahoo.com/mrss/"),)
async def parse_node(self, response, node):
yield {"thumbnail": node.get("url")}
命名空间剥离由 _strip_namespaces 完成:深拷贝节点后把每个 tag 与属性名替换为 localname,再调用 etree.cleanup_namespaces。Gzipped feed(.xml.gz 或以 gzip content-type 传输)沿用 sitemap 同一套解压保护自动解压;畸形 XML 记录警告而非让爬取崩溃——feed.py 的 parse() 中对 OSError 和 XMLSyntaxError 都只 warning 后 return。测试文件 tests/spiders/test_feed.py 覆盖了默认 itertag 迭代、命名空间剥离、Atom entry 匹配等场景。
CSVFeedSpider:逐行解析 CSV 数据源
CSVFeedSpider 遍历 CSV feed 的每一行。重写 parse_row(),它接收的每行都是以列名为键的字典:
from scrapling.spiders import CSVFeedSpider
class PriceSpider(CSVFeedSpider):
name = "prices"
start_urls = ["https://example.com/products.csv"]
async def parse_row(self, response, row):
yield {"product": row["title"], "price": float(row["price"])}
result = PriceSpider().start()
相关类属性(定义见 feed.py):
headers:列名列表。默认不设置时,feed 的第一行用作表头;若 feed 没有表头行,需自行指定;delimiter:字段分隔符,默认",";quotechar:包裹特殊字符字段的引号字符,默认'"'。
class PriceSpider(CSVFeedSpider):
name = "prices"
start_urls = ["https://example.com/products.csv"]
headers = ["title", "price", "url"]
delimiter = ";"
实现上(parse),响应体经 _decompress 解压后按 response.encoding(缺省 utf-8,errors="replace")解码,交给 csv.DictReader 逐行产出并转发给 parse_row()。Gzipped feed 同样自动解压,保护机制与 XMLFeedSpider 相同。tests/spiders/test_feed.py 中专门准备了无表头 CSV 与分号分隔 CSV 的测试数据。
直接使用 LinkExtractor
你不必使用模板。LinkExtractor 在任何普通 Spider 里都能工作:
from scrapling.spiders import Spider, LinkExtractor
class CustomSpider(Spider):
name = "custom"
start_urls = ["https://example.com"]
def __init__(self):
super().__init__()
self._links = LinkExtractor(allow=r"/posts/", deny_domains="ads.example.com")
async def parse(self, response):
for url in self._links.extract(response):
yield response.follow(url, callback=self.parse_post)
async def parse_post(self, response):
yield {"title": response.css("h1::text").get()}
LinkExtractor 参数参考
完整参数语义(源码位于 links.py):
| 参数 | 默认值 | 说明 |
|---|---|---|
allow |
() |
要保留的 URL 模式。空表示「全匹配」。可为字符串、编译好的 Pattern,或二者的可迭代对象。 |
deny |
() |
要丢弃的 URL 模式。永远覆盖 allow。 |
allow_domains |
() |
要保留的主机名。子域自动匹配(example.com 匹配 api.example.com)。 |
deny_domains |
() |
要丢弃的主机名。 |
restrict_css |
() |
CSS 选择器,把 DOM 抽取限定到某个区域。 |
restrict_xpath |
() |
XPath 选择器,把 DOM 抽取限定到某个区域。 |
tags |
("a", "area") |
查找链接的元素标签。 |
attrs |
("href",) |
从这些标签读取 URL 的属性。 |
canonicalize |
True |
排序查询参数并规范化路径。 |
strip |
True |
去除抽取 URL 中的空白字符。 |
keep_fragment |
False |
规范化时是否保留 #fragment。 |
deny_extensions |
IGNORED_EXTENSIONS |
要丢弃的文件扩展名(pdf、zip、图片、视频等)。 |
process |
None |
可选的回调,在过滤前作用于每个抽取到的 URL。返回假值即丢弃该 URL。 |
LinkExtractor.extract(response) 返回一个 list[str]:绝对的、经过过滤的、去重后的 URL 列表;LinkExtractor.matches(url) 返回 bool,是纯 URL 过滤器(allow/deny/domain/extension),被 SitemapSpider 用于在没有 Response 的情况下按规则派发 sitemap URL。
从 _extract 与 _url_passes 的实现还可以确认几个细节:
- 抽取流程:若设置了
restrict_css/restrict_xpath,先圈定作用域(都没设则作用于整页),再用拼接的 XPath(如.//a/@href | .//area/@href)取原始 href,随后依次经过strip空白 →response.urljoin相对转绝对 →process回调 →canonicalize_url规范化 → 合法性校验 → 过滤;去重使用dict.fromkeys以保持链接出现顺序; - Schema 白名单:只放行
http、https、file三种协议,javascript:、mailto:等链接天然被排除(links.py); - 扩展名检查最优先:在 allow/deny 正则之前先做扩展名过滤,
IGNORED_EXTENSIONS内置了约 100 个扩展名,涵盖压缩包(zip、tar.gz等)、图片、音视频、Office 文档、css/pdf/exe/js等(links.py);匹配时按后缀逐级判断(tar.gz这类多段后缀也能命中); - 域名匹配规则:
host == d or host.endswith("." + d),即精确主机加任意子域,且两侧都转小写比较,避免Example.com与example.com的漏配。
小结:模板的选择路径
| 场景 | 选择 | 核心 API |
|---|---|---|
| 跟随符合正则模式的链接 | CrawlSpider |
rules() + CrawlRule |
| 以 sitemap.xml 为种子 | SitemapSpider |
sitemap_urls / sitemap_follow / rules() |
| 消费 robots.txt 声明的 sitemap | SitemapSpider |
sitemap_urls 直接放 robots.txt |
| 遍历 RSS/Atom/商品 XML 源 | XMLFeedSpider |
itertag + parse_node() |
| 遍历 CSV 数据源 | CSVFeedSpider |
headers / delimiter + parse_row() |
| 完全自定义链接策略 | 普通 Spider |
直接使用 LinkExtractor |
三者共同的底座是 LinkExtractor 的正则化 URL 过滤(scrapling/spiders/links.py),模板则在其上叠加了各自的调度语义:CrawlSpider 多规则叠加、SitemapSpider 首规则胜出。配合 CrawlRule 的 priority 与 process_request 钩子、response.follow() 的 referer 传递、以及 Request 的 pickle 友好设计,这套模板可以平滑接入 Scrapling 爬虫引擎的并发、限速与 checkpoint 能力,从单条请求到全量爬取都适用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00