首页
/ Scrapy SEP-014 深度解析:CrawlSpider v2 的解耦设计——Matcher、Request Extractor 与 Processor 的分离思想

Scrapy SEP-014 深度解析:CrawlSpider v2 的解耦设计——Matcher、Request Extractor 与 Processor 的分离思想

2026-09-06 19:14:59作者:廉皓灿Ida

SEP-014 是 Scrapy 增强提案(Scrapy Enhancement Proposal,SEP)中专门针对 CrawlSpider 的一次重构方案,它针对规则爬虫三大痛点——回调难以持久化、链接抽取与处理紧耦合、无法从命令行直接爬取指定 URL——提出了把"匹配、抽取、处理、分发"四个职责彻底拆分的 v2 架构。虽然该提案在 r2632 版本中因使用率不足而被弃用,但其核心思想(规则即匹配条件、链接处理流水线化)最终以演化形态沉淀进了今天 Scrapy 的 CrawlSpider 实现。读完本文,你既能完整掌握 SEP-014 的 API 设计与实现草图,也能对照 scrapy/spiders/crawl.py 中的现行代码,理解 Scrapy 最终为何选择了另一条实现路线。

SEP-014 的来历与要解决的三个缺陷

SEP 存放于 sep/ 目录,该目录 README 说明这些提案大多从旧的 Trac Wiki 迁移而来。sep/sep-014.rst 的元信息表明:提案名为 CrawlSpider v2,由 Insophia Team 于 2010-01-22 创建,2010-02-04 更新,状态为 Final. Partially implemented but discarded because of lack of use in r2632(部分实现但因使用率低而在 r2632 被弃用)。

提案开篇列出了当时 CrawlSpider 的三大缺陷:

  1. Request 的 callback 难以持久化。回调以函数引用形式存在于请求对象中,队列序列化(如磁盘调度器)无法可靠保存它;
  2. Link Extractor 不灵活且难以维护,链接的处理/过滤逻辑(如 canonicalize 规范化)与抽取逻辑紧耦合在一起;
  3. 无法从命令行直接爬取一个 URL,因为 Spider 不知道该 URL 应该使用哪个回调。

提案给出的总体修改方向是四点 API 变更:

  • 分离 Rule-LinkExtractor-Callback 三者的功能;
  • 把 LinkExtractor 的功能拆分为 Request Extractor(返回 Request 对象,而不仅仅是链接 URL)与 Request Processor(对 Request 做过滤或改写);
  • 将"确定 response 回调"与"抽取新请求"两个过程解耦;
  • 回调由 Matcher 对象对 request/response 对象做匹配来确定,而不是像旧式设计那样由提取规则附带决定。

五大核心组件

SEP-014 把整个规则爬虫拆成五个组件,职责链条为:RulesManager(定回调)→ RequestGenerator(生成请求)→ 内部依次调用 RequestExtractor(抽链接)和 RequestProcessor(链式处理链接)。

Matcher 对象:匹配请求/响应

Matcher 负责判断给定的 request 或 response 是否满足任意条件,它直接拿到 request/response 对象,因此可以访问其全部属性。提案指出,在旧版 CrawlSpider 中,Rule 对象既负责确定 extractor 的回调、又携带 URL 正则,而新的 Matcher 只保留"模式/条件"这一份职责,决定哪个 request/response 应该执行某个动作。

Request Extractor:从 Response 产出 Request

Request Extractor 接收 response 对象,决定后续跟随哪些请求。提案将其定位为 LinkExtractor 的增强:LinkExtractor 返回的是 URL(链接),而 Request Extractor 直接返回 Request 对象,把"链接→请求"的构造提前到抽取阶段完成。

Request Processor:对请求链式加工

Request Processor 接收请求对象,可以对其执行任意动作,如过滤(filtering)或就地修改(modifying on the fly)。旧的 LinkExtractor 把 canonicalize 这类链接处理内置其中,而 Request Processor 可以复用、并且串联成流水线依次应用。

Request Generator:解耦 _request_to_follow()

Request Generator 是对 CrawlSpider_request_to_follow() 方法的解耦:它接收 response 对象,依次应用 Request Extractors 和 Request Processors,产出新的请求。

Rules Manager:用 Matcher 确定回调

新的 Rule 由 Matcher 对象加 callback 组成。旧的 Legacy Rules 用于执行链接抽取并给生成的 Request 挂上回调;新 Rules 只用于为给定 response 确定回调。提案认为这"打开了许多可能性",例如按 URL 确定回调,并且由于回调是在 response 与 Rules 匹配时就确定的,Request 队列因此可以被持久化(直接解决缺陷 1)。

使用示例

基本爬取

#!python
#
# Basic Crawling
#
class SampleSpider(CrawlSpider):
    rules = [
        # 分发器采用 first-match(首个匹配)策略
        Rule(UrlRegexMatch(r"product\.html\?id=\d+"), "parse_item", follow=False),
        # 若第一个参数是字符串,默认会被包装成 UrlRegexMatch
        Rule(r".+", "parse_page"),
    ]

    request_extractors = [
        # 爬取所有链接,寻找商品和图片
        SgmlRequestExtractor(),
    ]

    request_processors = [
        # 规范化所有请求的 URL
        Canonicalize(),
    ]

    def parse_item(self, response):
        # 从 response 中解析并抽取 item
        pass

    def parse_page(self, response):
        # 抽取所有页面的图片
        pass

三个类属性各司其职:rules 决定回调分发(首条匹配生效,follow=False 表示匹配到 parse_item 后不再继续抽取该页链接);request_extractors 决定从页面哪里抽链接;request_processors 决定链接抽取后如何被清洗/过滤。

自定义 Processor 与外部回调

#!python
#
# Using external callbacks
#

# 自定义 Processor
def filter_today_links(requests):
    # 只爬取今天的链接
    today = datetime.datetime.today().strftime("%Y-%m-%d")
    return [r for r in requests if today in r.url]


# 定义在 spider 之外的回调
def my_external_callback(response):
    # 处理 item
    pass


class SampleSpider(CrawlSpider):
    rules = [
        # 分发器采用 first-match 策略
        Rule(UrlRegexMatch(r"/news/(.+)/"), my_external_callback),
    ]

    request_extractors = [
        RegexRequestExtractor(r"/sections/.+"),
        RegexRequestExtractor(r"/news/.+"),
    ]

    request_processors = [
        # 规范化所有请求的 URL
        Canonicalize(),
        filter_today_links,
    ]

这个例子展示了提案的两个关键灵活性:回调可以是外部函数而不必是 spider 方法(对应缺陷 3 中"命令行直接爬取 URL"的场景);Processor 既可以是类(Canonicalize())也可以是普通函数(filter_today_links),且多个 processor 串行应用。

提案的包结构

提案将实现标记为 Work-in-progress,规划的包结构如下:

contrib_exp
    |- crawlspider/
        |- spider.py
            |- CrawlSpider
        |- rules.py
            |- Rule
            |- CompiledRule
            |- RulesManager
        |- reqgen.py
            |- RequestGenerator
        |- reqproc.py
            |- Canonicalize
            |- Unique
            |- ...
        |- reqext.py
            |- SgmlRequestExtractor
            |- RegexRequestExtractor
            |- ...
        |- matchers.py
            |- BaseMatcher
            |- UrlMatcher
            |- UrlRegexMatcher
            |- ...

放在 contrib_exp(contributor experimental)下,也说明它当时是以实验性扩展的形态推进的。

实现草图:Matcher、Extractor 与 Processor

Request/Response Matcher

#!python
"""
Request/Response Matchers

Perform evaluation to Request or Response attributes
"""


class BaseMatcher(object):
    """Base matcher. Returns True by default."""

    def matches_request(self, request):
        """Performs Request Matching"""
        return True

    def matches_response(self, response):
        """Performs Response Matching"""
        return True


class UrlMatcher(BaseMatcher):
    """Matches URL attribute"""

    def __init__(self, url):
        """Initialize url attribute"""
        self._url = url

    def matches_url(self, url):
        """Returns True if given url is equal to matcher's url"""
        return self._url == url

    def matches_request(self, request):
        """Returns True if Request's url matches initial url"""
        return self.matches_url(request.url)

    def matches_response(self, response):
        """Returns True if Response's url matches initial url"""
        return self.matches_url(response.url)


class UrlRegexMatcher(UrlMatcher):
    """Matches URL using regular expression"""

    def __init__(self, regex, flags=0):
        """Initialize regular expression"""
        self._regex = re.compile(regex, flags)

    def matches_url(self, url):
        """Returns True if url matches regular expression"""
        return self._regex.search(url) is not None

设计上注意两点:基类 BaseMatcher 默认返回 True,意味着任何 Matcher 都可以通过继承定制到任意请求/响应属性(meta、headers 等);UrlRegexMatcher 继承 UrlMatcher 并只重写 matches_url,体现了"URL 匹配是通用的单点扩展位"。

Request Extractor

#!python
#
# Requests Extractor
# Extractors receive response and return list of Requests
#


class BaseSgmlRequestExtractor(FixedSGMLParser):
    """Base SGML Request Extractor"""

    def __init__(self, tag="a", attr="href"):
        """Initialize attributes"""
        FixedSGMLParser.__init__(self)

        self.scan_tag = tag if callable(tag) else lambda t: t = tag
        self.scan_attr = attr if callable(attr) else lambda a: a = attr
        self.current_request = None

    def extract_requests(self, response):
        """Returns list of requests extracted from response"""
        return self._extract_requests(response.body, response.url, response.encoding)

    def _extract_requests(self, response_text, response_url, response_encoding):
        """Extract requests with absolute urls"""
        self.reset()
        self.feed(response_text)
        self.close()

        base_url = self.base_url if self.base_url else response_url
        self._make_absolute_urls(base_url, response_encoding)
        self._fix_link_text_encoding(response_encoding)

        return self.requests

    def _make_absolute_urls(self, base_url, encoding):
        """Makes all request's urls absolute"""
        for req in self.requests:
            url = req.url
            # make absolute url
            url = urljoin_rfc(base_url, url, encoding)
            url = safe_url_string(url, encoding)
            # replace in-place request's url
            req.url = url

    def _fix_link_text_encoding(self, encoding):
        """Convert link_text to unicode for each request"""
        for req in self.requests:
            req.meta.setdefault("link_text", "")
            req.meta["link_text"] = str_to_unicode(req.meta["link_text"], encoding)

    def reset(self):
        """Reset state"""
        FixedSGMLParser.reset(self)
        self.requests = []
        self.base_url = None

    def unknown_starttag(self, tag, attrs):
        """Process unknown start tag"""
        if "base" == tag:
            self.base_url = dict(attrs).get("href")

        if self.scan_tag(tag):
            for attr, value in attrs:
                if self.scan_attr(attr):
                    if value is not None:
                        req = Request(url=value)
                        self.requests.append(req)
                        self.current_request = req

    def unknown_endtag(self, tag):
        """Process unknown end tag"""
        self.current_request = None

    def handle_data(self, data):
        """Process data"""
        current = self.current_request
        if current and not "link_text" in current.meta:
            current.meta["link_text"] = data.strip()


class SgmlRequestExtractor(BaseSgmlRequestExtractor):
    """SGML Request Extractor"""

    def __init__(self, tags=None, attrs=None):
        """Initialize with custom tag & attribute function checkers"""
        # defaults
        tags = tuple(tags) if tags else ("a", "area")
        attrs = tuple(attrs) if attrs else ("href",)

        tag_func = lambda x: x in tags
        attr_func = lambda x: x in attrs
        BaseSgmlRequestExtractor.__init__(self, tag=tag_func, attr=attr_func)


class XPathRequestExtractor(SgmlRequestExtractor):
    """SGML Request Extractor with XPath restriction"""

    def __init__(self, restrict_xpaths, tags=None, attrs=None):
        """Initialize XPath restrictions"""
        self.restrict_xpaths = tuple(arg_to_iter(restrict_xpaths))
        SgmlRequestExtractor.__init__(self, tags, attrs)

    def extract_requests(self, response):
        """Restrict to XPath regions"""
        hxs = HtmlXPathSelector(response)
        fragments = (
            "".join(html_frag for html_frag in hxs.select(xpath).extract())
            for xpath in self.restrict_xpaths
        )
        html_slice = "".join(html_frag for html_frag in fragments)
        return self._extract_requests(html_slice, response.url, response.encoding)

实现上这是一个基于 SGML 事件流解析器的抽取器:unknown_starttag 中识别 <base> 标签获得 base URL,命中 scan_tag/scan_attr 就立即构造 Requesthandle_data 捕获链接文本写入 meta["link_text"];解析完成后 _make_absolute_urlsurljoin_rfc 就地改写为绝对 URL,并修正 link_text 编码。子类 SgmlRequestExtractortag/attr 泛化为集合匹配,默认扫描 ("a", "area")hrefXPathRequestExtractor 则先用 HtmlXPathSelector 把页面切片到指定 XPath 区域再抽取。

Request Processor

#!python
#
# Request Processors
# Processors receive list of requests and return list of requests
#
"""Request Processors"""


class Canonicalize(object):
    """Canonicalize Request Processor"""

    def __call__(self, requests):
        """Canonicalize all requests' urls"""
        for req in requests:
            # replace in-place
            req.url = canonicalize_url(req.url)
            yield req


class Unique(object):
    """Filter duplicate Requests"""

    def __init__(self, *attributes):
        """Initialize comparison attributes"""
        self._attributes = attributes or ["url"]

    def _requests_equal(self, req1, req2):
        """Attribute comparison helper"""
        for attr in self._attributes:
            if getattr(req1, attr) != getattr(req2, attr):
                return False
        # all attributes equal
        return True

    def _request_in(self, request, requests_seen):
        """Check if request is in given requests seen list"""
        for seen in requests_seen:
            if self._request_in(request, seen):
                return True
        # request not seen
        return False

    def __call__(self, requests):
        """Filter seen requests"""
        # per-call duplicates filter
        requests_seen = set()
        for req in requests:
            if not self._request_in(req, requests_seen):
                yield req
                # registry seen request
            requests_seen.add(req)


class FilterDomain(object):
    """Filter request's domain"""

    def __init__(self, allow=(), deny=()):
        """Initialize allow/deny attributes"""
        self.allow = tuple(arg_to_iter(allow))
        self.deny = tuple(arg_to_iter(deny))

    def __call__(self, requests):
        """Filter domains"""
        processed = (req for req in requests)

        if self.allow:
            processed = (
                req for req in requests if url_is_from_any_domain(req.url, self.allow)
            )
        if self.deny:
            processed = (
                req
                for req in requests
                if not url_is_from_any_domain(req.url, self.deny)
            )

        return processed


class FilterUrl(object):
    """Filter request's url"""

    def __init__(self, allow=(), deny=()):
        """Initialize allow/deny attributes"""
        _re_type = type(re.compile("", 0))

        self.allow_res = [
            x if isinstance(x, _re_type) else re.compile(x) for x in arg_to_iter(allow)
        ]
        self.deny_res = [
            x if isinstance(x, _re_type) else re.compile(x) for x in arg_to_iter(deny)
        ]

    def __call__(self, requests):
        """Filter request's url based on allow/deny rules"""
        # TODO: filter valid urls here?
        processed = (req for req in requests)

        if self.allow_res:
            processed = (
                req for req in requests if self._matches(req.url, self.allow_res)
            )
        if self.deny_res:
            processed = (
                req
                for req in requests
                if not self._matches(req.url, self.deny_res)
            )

        return processed

    def _matches(self, url, regexs):
        """Returns True if url matches any regex in given list"""
        return any(r.search(url) for r in regexs)

四个内置 Processor 的契约是"接收请求列表,返回请求列表"(生成器或迭代器),因此可以自由串联:Canonicalize 就地规范化 URL(对应旧 LinkExtractor 内建的 canonicalize);Unique 按可配置的属性去重,默认按 url 比较,且是单次调用内的去重;FilterDomainFilterUrl 分别按域名、URL 正则做 allow/deny 过滤——这两者的 allow/deny 参数形态,与今天 scrapy/linkextractors/lxmlhtml.pyLxmlLinkExtractorallow_domains/deny_domains/allow/deny 参数如出一辙,可以看出提案思路对现行抽取器设计的直接影响。值得强调的是,FilterDomain/FilterUrl 中调用的 url_is_from_any_domain 就是当前仓库 scrapy/utils/url.py 中真实存在的函数(第 23 行定义),说明提案引用的工具函数在今天的代码库中仍在服役。

Rule、CompiledRule 与 RulesManager

#!python
#
# Dispatch Rules classes
# Manage Rules (Matchers + Callbacks)
#
class Rule(object):
    """Crawler Rule"""

    def __init__(
        self, matcher, callback=None, cb_args=None, cb_kwargs=None, follow=True
    ):
        """Store attributes"""
        self.matcher = matcher
        self.callback = callback
        self.cb_args = cb_args if cb_args else ()
        self.cb_kwargs = cb_kwargs if cb_kwargs else {}
        self.follow = follow


#
# Rules Manager takes list of Rule objects and normalize matcher and callback
# into CompiledRule
#
class CompiledRule(object):
    """Compiled version of Rule"""

    def __init__(self, matcher, callback=None, follow=False):
        """Initialize attributes checking type"""
        assert isinstance(matcher, BaseMatcher)
        assert callback is None or callable(callback)
        assert isinstance(follow, bool)

        self.matcher = matcher
        self.callback = callback
        self.follow = follow
#!python
#
# Handles rules matcher/callbacks
# Resolve rule for given response
#
class RulesManager(object):
    """Rules Manager"""

    def __init__(self, rules, spider, default_matcher=UrlRegexMatcher):
        """Initialize rules using spider and default matcher"""
        self._rules = tuple()

        # compile absolute/relative-to-spider callbacks"""
        for rule in rules:
            # prepare matcher
            if isinstance(rule.matcher, BaseMatcher):
                matcher = rule.matcher
            else:
                # matcher not BaseMatcher, check for string
                if isinstance(rule.matcher, basestring):
                    # instance default matcher
                    matcher = default_matcher(rule.matcher)
                else:
                    raise ValueError(
                        "Not valid matcher given %r in %r" % (rule.matcher, rule)
                    )

            # prepare callback
            if callable(rule.callback):
                callback = rule.callback
            elif not rule.callback is None:
                # callback from spider
                callback = getattr(spider, rule.callback)

                if not callable(callback):
                    raise AttributeError(
                        "Invalid callback %r can not be resolved" % callback
                    )
            else:
                callback = None

            if rule.cb_args or rule.cb_kwargs:
                # build partial callback
                callback = partial(callback, *rule.cb_args, **rule.cb_kwargs)

            # append compiled rule to rules list
            crule = CompiledRule(matcher, callback, follow=rule.follow)
            self._rules += (crule,)

    def get_rule(self, response):
        """Returns first rule that matches response"""
        for rule in self._rules:
            if rule.matcher.matches_response(response):
                return rule

RulesManager.__init__ 完成了"编译"工作:字符串 matcher 被默认包装为 UrlRegexMatcher(这解释了基本示例中 Rule(r".+", ...) 的写法);字符串 callback 通过 getattr(spider, name) 解析为 spider 方法;cb_args/cb_kwargs 通过 functools.partial 预绑定。get_rule(response) 遍历规则表,返回第一条 matcher 命中的规则,即首条匹配策略。

RequestGenerator 与 CrawlSpider v2

#!python
#
# Request Generator
# Takes response and generate requests using extractors and processors
#
class RequestGenerator(object):
    def __init__(self, req_extractors, req_processors, callback):
        self._request_extractors = req_extractors
        self._request_processors = req_processors
        self.callback = callback

    def generate_requests(self, response):
        """
        Extract and process new requests from response
        """
        requests = []
        for ext in self._request_extractors:
            requests.extend(ext.extract_requests(response))

        for proc in self._request_processors:
            requests = proc(requests)

        for request in requests:
            yield request.replace(callback=self.callback)
#!python
#
# Spider
#
class CrawlSpider(InitSpider):
    """CrawlSpider v2"""

    request_extractors = []
    request_processors = []
    rules = []

    def __init__(self):
        """Initialize dispatcher"""
        super(CrawlSpider, self).__init__()

        # wrap rules
        self._rulesman = RulesManager(self.rules, spider=self)
        # generates new requests with given callback
        self._reqgen = RequestGenerator(
            self.request_extractors, self.request_processors, self.parse
        )

    def parse(self, response):
        """Dispatch callback and generate requests"""
        # get rule for response
        rule = self._rulesman.get_rule(response)
        if rule:
            # dispatch callback if set
            if rule.callback:
                output = iterate_spider_output(rule.callback(response))
                for req_or_item in output:
                    yield req_or_item

            if rule.follow:
                for req in self._reqgen.generate_requests(response):
                    yield req

整条数据流至此清晰:parse(response) 先经 RulesManager.get_rule 按 URL 匹配确定该响应的回调并执行,再按 rule.follow 决定是否调用 RequestGeneratorRequestGenerator 聚合所有 Extractor 的抽取结果,串行穿过 Processor 流水线,最后用 request.replace(callback=self.callback)回调重置为统一的 parse——这样被持久化的队列里只有 parse 这一稳定入口,真正的业务回调等到 response 回来时才由规则表重新确定。这正是对缺陷 1(回调难以持久化)的正面回答。

对照现行实现:SEP-014 的思想如何演化进今天的 CrawlSpider

Scrapy 仓库中最终采用的 CrawlSpiderscrapy/spiders/crawl.py)并没有实现 v2 的 Matcher 机制,而是走了另一条更简洁的路线,但两条路线的对应关系非常清晰,值得逐点对照。官方文档中的讲解见 docs/topics/spiders.rst 的 "Crawling rules" 小节。

Rule 语义的取舍。 现行 Rule 的签名为 Rule(link_extractor, callback, cb_kwargs, follow, process_links, process_request, errback)scrapy/spiders/crawl.py 第 63-95 行):

  • SEP 中的 Request Extractor 对应现行的 link_extractor:每条规则自带一个 LinkExtractor(若省略则用模块级默认实例 _default_link_extractor = LinkExtractor())。区别在于现行 LinkExtractor(实现于 scrapy/linkextractors/lxmlhtml.pyLxmlLinkExtractor)返回的是 Link 对象而非 Request,且把 allow/deny/allow_domains/deny_domains/deny_extensions/restrict_xpaths/restrict_css/restrict_text 等过滤选项内置其中——从源码结构看,这其实是把 SEP 中 RegexRequestExtractorFilterDomain/FilterUrl 的能力重新合并回抽取器,用更少的概念换取易用性;
  • SEP 中的 Request Processor 流水线对应现行的 process_linksprocess_request 两个钩子:process_links 接收整批链接做过滤(相当于批量 Processor),process_request 接收 (request, response) 返回改写后的 RequestNone(相当于单请求 Processor,返回 None 即过滤丢弃)。二者的默认值是恒等函数 _identity / _identity_process_request
  • SEP 中的 Matcher(按 response URL 匹配确定回调) 则被整体放弃,取而代之的是规则索引方案_build_request 在生成请求时写入 meta["rule"] = rule_indexscrapy/spiders/crawl.py 第 133-139 行),响应回来时 _callback 通过 self._rules[response.meta["rule"]] 找回原始规则再分发。这样回调归属在请求创建时就被规则固化(且 rule 索引可序列化),同样规避了"回调不可持久化"的问题,但语义从 SEP 的"按 URL 正则决定回调"变成了"按生成该请求的规则决定回调"。docs/topics/spiders.rst 也明确警告:rule meta 键被 CrawlSpider 依赖,把它从一个规则生成的请求复制到另一个规则生成的请求会导致响应分发到错误的回调;
  • 回调解析的"编译"逻辑一脉相承:SEP 的 RulesManagergetattr(spider, name) 把字符串回调解析为方法、用 partial 预绑定参数;现行的 Rule._compile(spider)_get_method 完成同样的字符串→方法解析(callback、errback、process_links、process_request 四项),并在 CrawlSpider.__init__ 中对 self.rules 逐条 copy.copy 后编译,避免污染类属性。

入口回调的演化。 SEP v2 依赖外部调用 parse 完成分发;现行实现则把 start_urls 的处理独立为 parse_start_url(response, **kwargs),由 _parseparse_with_rules 调用(scrapy/spiders/crawl.py 第 117-126 行)。parse_with_rules 是统一入口:先执行 callback(支持异步生成器与 Awaitable),再把结果交给可覆写的 process_results 后逐条 yield,最后按 follow 与全局开关 _follow_links 产出 _requests_to_follow 的结果。_requests_to_follow 内部用 seen 集合做跨规则的去重(第 141-154 行)——这一点比 SEP 中 Unique 的"单次调用内去重"更进一步,等价于把 SEP 示例里 Rule(r".+", ...)Rule(r"product\.html...", ...) 并存时可能产生的重复请求问题在实现层直接消解。

follow 的默认值差异。 现行 Rulefollow = follow if follow is not None else not callback:不给 callback 则默认继续跟随,给了 callback 则默认不跟随。这与 SEP v2 中 Rule.__init__follow=True 默认值不同,与 CompiledRulefollow=False 断言默认值也不同,属于最终落地的行为约定,写规则时应显式理解该默认值。

全局开关的废弃。 CrawlSpider.from_crawler 中读取 CRAWLSPIDER_FOLLOW_LINKS 设置并提示其已废弃,官方建议改用规则级的 follow=Falsescrapy/spiders/crawl.py 第 222-233 行)。从废弃信息看,Scrapy 团队的选择与 SEP-014 的精神一致:把行为下沉到规则本身,而非全局开关。

为什么 v2 被弃用,以及它对今天写爬虫的启示

SEP-014 的状态说明(Final,但 "Partially implemented but discarded because of lack of use in r2632")提示了它的结局:v2 的 Matcher + Request Extractor + Processor 三件套表达力确实更强(回调可按任意 response 属性匹配、处理器可任意串联、支持外部函数回调),但它要求用户管理 request_extractors/request_processors/rules 三个类属性并引入 Matcher 这一新抽象,学习成本明显高于"每条 Rule 自带一个 LinkExtractor + 两个钩子"的现行设计。从 scrapy/spiders/crawl.py 的现行结构看,Scrapy 把 v2 最有价值的两点——回调归属可序列化(meta["rule"]链接处理钩子化(process_links/process_request——以更小的 API 面保留了下来,而把 URL 过滤能力收编进 LinkExtractor 的参数体系(对照 scrapy/linkextractors/lxmlhtml.pyLxmlLinkExtractorallow/deny/allow_domains/deny_domains/deny_extensions 参数,其语义正是 SEP 中 FilterUrl/FilterDomain 的原型)。

对今天的开发者而言,SEP-014 仍有一份实际参考手册的价值:当你需要在 CrawlSpider 中做跨规则的链接去重、自定义 URL 规范化(对应 Canonicalize)或按属性批量过滤链接时,现行 Ruleprocess_links/process_request 钩子就是 SEP "Request Processor 流水线"的落点;而当你的回调确实需要按响应 URL 模式动态确定(而非按生成规则确定)时,v2 的 Matcher 思路提醒你:可以放弃 CrawlSpider 的规则分发,改用普通 Spider 在回调里自行按 response.url 路由,因为这才是 Matcher 机制在现行 API 约束下的等价替代。

最后需要说明版本边界:SEP-014 的代码草图基于 2010 年的 Python 2 / Twisted 时代 API(basestringHtmlXPathSelectorFixedSGMLParserurljoin_rfc 等),这些名称在今天的仓库中已不存在或已被替换,草图仅作为设计文档阅读;现行行为以 scrapy/spiders/crawl.pyscrapy/linkextractors/lxmlhtml.pydocs/topics/spiders.rst 为准。

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

项目优选

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