首页
/ Scrapy 爬虫命名机制:SEP-012 中 `name` 标识符与 `allowed_domains` 的设计与源码实现

Scrapy 爬虫命名机制:SEP-012 中 `name` 标识符与 `allowed_domains` 的设计与源码实现

2026-09-06 14:43:46作者:裘晴惠Vivianne

SEP-012 是 Scrapy 早期最关键的架构提案之一:它把爬虫(Spider)从“以 domain_name 作为身份”转变为“以 name 作为唯一标识符”,并将 domain_nameextra_domain_names 合并为 allowed_domains。这篇指南以该提案为骨架,结合当前仓库中 Spider 基类SpiderLoaderOffsiteMiddlewaregenspider 命令 的真实实现,完整讲清这一设计是如何落地的、为什么这样设计,以及你在写爬虫和匹配 URL 时应如何正确使用 nameallowed_domains

一、提案背景:为什么爬虫需要独立的 name 标识符

SEP-012 的核心出发点,是旧设计下爬虫以 domain_name 属性被引用所带来的两个根本性缺陷(见 sep-012.rst):

  1. 无法创建两个爬取同一域的爬虫。旧方案中域名即身份,如果你想写两个都针对 example.com 的爬虫(比如一个抓首页列表、一个抓商品详情),就必须使用“给其中一个任意伪造 domain_name、把真实域名塞进 extra_domain_names”这类规避手段;
  2. 多域爬虫要在两处重复声明域名:主域名写在 domain_name,其余域名写在 extra_domain_names,同一份信息分散在两个属性里,既冗余又容易不一致。

为此,提案给出了两条修改:

  1. 给爬虫新增 name 属性,并将其作为爬虫的唯一标识符
  2. domain_nameextra_domain_names 合并为单一列表 allowed_domains,域名只描述“该爬虫可以抓取哪些域”,不再承担身份职责。

这两条修改在今天的 Scrapy 代码库中已经完全生效。下文按提案的“影响面”逐一对照源码。

二、总体影响:所有 spider.domain_name 替换为 spider.name

提案写道:“In general, all references to spider.domain_name will be replaced by spider.name。”在当前仓库中这一点体现得非常彻底:

  • Spider 基类 声明了类级类型标注 name: str,其构造函数对 name 做了硬性校验:
def __init__(self, name: str | None = None, **kwargs: Any):
    if name is not None:
        self.name: str = name
    elif not getattr(self, "name", None):
        raise ValueError(f"{type(self).__name__} must have a name")

也就是说,每个爬虫子类必须定义 name 属性,否则实例化时直接抛出 ValueError。这正是提案“name 是爬虫唯一标识符”的强制实现——没有名字的爬虫根本无法被框架加载。

  • name 同时是日志身份的载体:Spider.logger 属性用 logging.getLogger(self.name) 创建按爬虫名区分的 logger(见 spiders/init.py 第 56–62 行logger 的实现,位于 scrapy/spiders/init.py),因此在日志中你看到的爬虫名就是 name

  • 爬虫的加载与索引也全部基于 nameSpiderLoader 在初始化时递归遍历 SPIDER_MODULES 设置指定的模块,把每个爬虫类以 spcls.name 为键存入 self._spiders 字典:

def _load_spiders(self, module: ModuleType) -> None:
    for spcls in iter_spider_classes(module):
        self._found[spcls.name].append((module.__name__, spcls.__name__))
        self._spiders[spcls.name] = spcls

由于“一个名字只能对应一个爬虫类”,加载器还内置了重名检测 _check_name_duplicates():当发现多个模块中存在同名爬虫时,会发出 UserWarning 警告 “There are several spiders with the same name … This can cause unexpected behavior”(见 spiderloader.py 第 64–82 行)。scrapy listscrapy crawl <spider>scrapy genspider 的存在性检查等操作,最终都归结为按 name_spiders 字典中查找。

这里可以明确一条实操结论:同一个项目中,不同爬虫的 name 必须互不相同;类名(MySpider)与文件名只是代码组织方式,框架身份完全由 name 决定。官方文档 docs/topics/spiders.rstname 属性的说明也与此一致:“A string which defines the name for this spider. The spider name is how Scrapy identifies it.”

三、域名归属:allowed_domains 取代双属性设计

提案的第二条修改——合并出单一 allowed_domains 列表——同样已经落地。今天爬虫只需一处声明所有可抓取的域名:

import scrapy

class GooglecomSpider(scrapy.Spider):
    name = "google"
    allowed_domains = ["google.com"]
    start_urls = ["https://www.google.com/"]

这与提案中 genspider 示例生成的爬虫结构(name = "google"allowed_domains = ["google.com"])完全相同。

OffsiteMiddleware 基于 allowed_domains 过滤离站请求

提案明确指出:“OffsiteMiddleware will use spider.allowed_domains for determining the domain names of a spider”。当前 OffsiteMiddleware 的实现忠实执行了这一职责:

  • spider_opened 信号触发 _update_host_regex(spider),读取 spider.allowed_domains 并缓存(offsite.py 第 70–77 行);
  • 若爬虫未定义 allowed_domains 或列表为空,返回空正则 re.compile(""),即默认放行所有请求get_host_regex 中的注释 “allow all by default”);
  • 定义了 allowed_domains 时,构造成匹配正则 ^(.*\.)?(domain1|domain2|...)$:即请求主机名等于某允许域、或是其子域(www.example.org 允许 bob.www.example.org)时放行,而 www2.example.org 这类“把允许域当中缀”的域名不被允许;
  • 被过滤的请求抛出 IgnoreRequest,并累加 offsite/filteredoffsite/domains 统计项;对每个离站域名只在第一次记录 DEBUG 日志,保持日志可读(见 offsite.py 第 79–137 行)。

提案末尾还有一处值得注意的附注:spider_allowed_domains 成为可选项,因为它只剩 OffsiteMiddleware 一个使用者。也就是说,allowed_domains 不再参与“爬虫身份判定”(那是 name 的职责),只参与下载阶段的域名过滤——这正是提案解耦“身份”与“域名”后的直接结果。

另外两个与 allowed_domains 相关的细节:

  • 若某请求确需访问域外站点,可通过 request.meta["allow_offsite"] = Truedont_filter=True 绕过过滤;
  • 想改变默认策略(例如允许域名但不允许子域),子类化并覆写 should_follow() 即可,文档中给出的最小示例是 return urlparse_cached(request).hostname in spider.allowed_domains

name 也参与 URL 与爬虫的匹配

提案中 name 取代 domain_name 的身份作用,在“给定一个 URL,找出该由哪个爬虫处理”这一场景中尤为关键。scrapy/utils/url.py 中的实现值得细看:

def _spider_domains(spider: type[Spider]) -> Iterable[str]:
    yield spider.name
    allowed_domains = getattr(spider, "allowed_domains", None)
    ...
    if allowed_domains:
        yield from allowed_domains

def url_is_from_spider(url: UrlT, spider: type[Spider]) -> bool:
    """Return True if the url belongs to the given spider"""
    return url_is_from_any_domain(url, _spider_domains(spider))

爬虫的 name 被一并当作一个可匹配的域名。这意味着如果你的 name 恰好是一个域名字符串(如 name = "example.com"),URL https://example.com/... 也能匹配到它——这是一种向后兼容的便利设计(旧代码常把 domain_name 直接当名字用),但也带来了命名上的权衡。从 url_is_from_any_domain 的匹配规则看,匹配条件是“主机名等于该域、或以 .{域} 结尾”,同样允许子域。

这套匹配逻辑服务于 Spider.handles_request 类方法,进而被 SpiderLoader.find_by_request 使用,返回所有能处理某请求的爬虫名列表。

四、命令侧影响:crawl 与 genspider

crawl:按 name 运行爬虫

提案给出 crawl [options] <spider|url> ... 的新语法,并说明当无法从 URL 唯一确定爬虫时需通过 --spider 选项显式指定。当前仓库的实现可以从源码结构看演进为:

  • scrapy/commands/crawl.pysyntax()[options] <spider>,即以爬虫名运行:scrapy crawl <name>,且明确不支持一次传多个爬虫(“running 'scrapy crawl' with more than one spider is not supported”)。爬虫名经 _create_crawler(spname) 走 SpiderLoader 按 name 加载;
  • “URL → 自动匹配爬虫”的能力则保留在 fetch/shell/parse 一类命令中。以 fetch 命令 为例,其逻辑与提案完全吻合:若用户通过 --spider 指定了爬虫,直接 spider_loader.load(opts.spider) 加载;否则调用 spidercls_for_request(spider_loader, request, DefaultSpider) 依据 URL 自动匹配,匹配不到才退回 DefaultSpider

genspider:genspider [options] <name> <domain>

提案规定的 genspider 签名(参数依次为爬虫名与域名)在今天的 scrapy/commands/genspider.py 中原样保留——syntax() 返回 "[options] <name> <domain>"。其执行流程:

  1. 校验恰好两个位置参数 nameurlverify_url_scheme 会为缺省协议的 URL 补上 https
  2. sanitize_module_name(name) 将名字转成合法模块名:-. 替换为 _,首字符不是字母时前缀 a
  3. 通过 _spider_exists(name) 检查是否已有同名爬虫(借助 SpiderLoader 按 name 查找),存在则拒绝覆盖(除非 --force)——再次印证 name 是全局唯一身份;
  4. 渲染模板文件到 NEWSPIDER_MODULE 指定的 spiders 目录,生成 {module}.py

内置的 basic.tmpl 就是提案示例的最终形态:

import scrapy


class $classname(scrapy.Spider):
    name = "$name"
    allowed_domains = ["$domain"]
    start_urls = ["$url"]

    def parse(self, response):
        pass

对应提案中的完整示例,在今天的 Scrapy 中等价为:

$ scrapy genspider google google.com

生成的 project/spiders/google.py 内容即 name = "google"allowed_domains = ["google.com"] 的 Spider 子类。除默认模板外,仓库还内置 crawl.tmplcsvfeed.tmplxmlfeed.tmpl 等模板,可用 scrapy genspider --list 查看,用 -t/--template 指定。

五、命名实践建议与易错点

综合提案与源码实现,可以提炼出如下使用规则:

  1. name 必填且全局唯一。缺失会在实例化时抛 ValueError,重名会在加载期产生 UserWarning 并导致后加载者覆盖先加载者(self._spiders[spcls.name] = spcls 是字典赋值,后者胜出)。
  2. 命名习惯:官方文档 docs/topics/spiders.rst 建议单域爬虫直接用域名作名字(如 name = "example.com"),多域爬虫用可描述业务的语义化名称。用域名作名字的好处是 url_is_from_spider 能顺带匹配该域名的 URL,便于 fetch/shell 等命令的自动定位。
  3. nameallowed_domains 各司其职name 决定“我是谁”(日志、命令、加载器),allowed_domains 只决定“我允许下载器去哪些域”(OffsiteMiddleware 过滤)。不写 allowed_domains 不会报错,只会失去离站过滤。
  4. 注意 name 的“隐形域名匹配”副作用:由于 _spider_domains 会把 name 当域匹配,如果你给爬虫起名叫 blog,而某台主机名恰好是 my.blogurl_is_from_spider 也会返回 True。从源码结构看这是子域匹配规则的自然结果;若你的 URL 匹配场景对精确性敏感,可以推断应尽量避免让 name 与真实域名的子域形式冲突。
  5. allowed_domains 应保持为普通类属性utils/url.py 第 31–44 行 明确警告——若把它定义成 property,类级别的 URL 匹配(shell、fetch、parse 命令)无法求值并会发出 UserWarning,应改为普通类属性。

六、小结

SEP-012 用一个属性解决了身份问题、用一个列表解决了域名问题:name 让任意多个爬虫可以服务于同一域名而不必互相伪造身份;allowed_domains 让多域声明收敛到单一位置,并成为 OffsiteMiddleware 唯一消费的过滤依据。这一设计历经十余年仍是 Scrapy 的骨架——Spider 强制 nameSpiderLoader 按名索引并检测重名、OffsiteMiddlewareallowed_domains 构造子域正则、genspider<name> <domain> 生成双属性爬虫——从提案文本到当前仓库源码,可以清晰地看到“身份归 name、域名归 allowed_domains”这一解耦原则被完整实现并贯穿始终。

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