首页
/ Scrapling 通用爬虫模板实战:CrawlSpider、SitemapSpider 与 LinkExtractor 深度解析

Scrapling 通用爬虫模板实战:CrawlSpider、SitemapSpider 与 LinkExtractor 深度解析

2026-09-06 09:01:19作者:伍希望

绝大多数站点抓取都逃不出两种模式:「跟随符合某种模式的链接」和「遍历站点 sitemap 中列出的每个 URL」。Scrapling 的通用爬虫模板(Generic Spider Templates)正是为此而生——它把这两类最常见的 parse() 样板代码封装成了 CrawlSpiderSitemapSpider,并提供 XMLFeedSpiderCSVFeedSpider 处理数据源场景。读完本篇,你将掌握如何用最少的代码声明式地驱动一场完整爬取,并理解每个模板底层的调度逻辑与 LinkExtractor 的完整参数语义。

模板总览:它们省掉了什么

所有模板都构建在 LinkExtractor 之上——这个原语负责从 Response 中抽取 URL(或者通过 matches() 对单个 URL 做过滤判断)。SitemapSpider 还会在内部解析 sitemap.xml / sitemap_index.xml 的响应体(无论是否 gzip 压缩)。

模板只是替你省去了接线工作(wiring),你完全可以在任何普通 Spider.parse() 里直接使用 LinkExtractor。模板的源码位于 scrapling/spiders/templates/,导出 CrawlSpiderCrawlRuleSitemapSpiderShopifySpiderXMLFeedSpiderCSVFeedSpider 六个类。

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() 的行为

一条 CrawlRuleLinkExtractor 与三个可选字段配对(定义见 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。

两条值得注意的行为细节:

  1. 无 callback 的规则会落到默认 parse()。规则没有 callback 时,匹配到的 URL 会经由 response.follow() 继承原请求的 callback(即 None),引擎随后回落到蜘蛛的默认 parse()。这对翻页非常方便:抽取下一页链接让爬取继续,无需单独写处理器。tests/spiders/test_templates.py 中的 test_rule_with_no_callback_leaves_request_callback_none 明确验证了这一点。
  2. 规则是叠加执行的。默认 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.pytest_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_invokedtest_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_sitemaprules 为空时直接 response.follow(url)(callback 为 None,落回 parse());否则逐条 matches() 命中即返回请求。tests/spiders/test_sitemap.pytest_urlset_dispatched_through_rulestest_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 的 locurlset 则提取 URL 列表(见数据结构 SitemapResult);随后在 _parse_sitemap 中,每个子 sitemap URL 都先过一遍 sitemap_follow.matches()sitemap_followNone 时全部深入)。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 子元素的 hreftest_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.pyparse() 中对 OSErrorXMLSyntaxError 都只 warningreturn。测试文件 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-8errors="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 白名单:只放行 httphttpsfile 三种协议,javascript:mailto: 等链接天然被排除(links.py);
  • 扩展名检查最优先:在 allow/deny 正则之前先做扩展名过滤,IGNORED_EXTENSIONS 内置了约 100 个扩展名,涵盖压缩包(ziptar.gz 等)、图片、音视频、Office 文档、css/pdf/exe/js 等(links.py);匹配时按后缀逐级判断(tar.gz 这类多段后缀也能命中);
  • 域名匹配规则host == d or host.endswith("." + d),即精确主机加任意子域,且两侧都转小写比较,避免 Example.comexample.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 首规则胜出。配合 CrawlRulepriorityprocess_request 钩子、response.follow() 的 referer 传递、以及 Request 的 pickle 友好设计,这套模板可以平滑接入 Scrapling 爬虫引擎的并发、限速与 checkpoint 能力,从单条请求到全量爬取都适用。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391