Scrapy 爬虫命名机制:SEP-012 中 `name` 标识符与 `allowed_domains` 的设计与源码实现
SEP-012 是 Scrapy 早期最关键的架构提案之一:它把爬虫(Spider)从“以 domain_name 作为身份”转变为“以 name 作为唯一标识符”,并将 domain_name 与 extra_domain_names 合并为 allowed_domains。这篇指南以该提案为骨架,结合当前仓库中 Spider 基类、SpiderLoader、OffsiteMiddleware 与 genspider 命令 的真实实现,完整讲清这一设计是如何落地的、为什么这样设计,以及你在写爬虫和匹配 URL 时应如何正确使用 name 与 allowed_domains。
一、提案背景:为什么爬虫需要独立的 name 标识符
SEP-012 的核心出发点,是旧设计下爬虫以 domain_name 属性被引用所带来的两个根本性缺陷(见 sep-012.rst):
- 无法创建两个爬取同一域的爬虫。旧方案中域名即身份,如果你想写两个都针对
example.com的爬虫(比如一个抓首页列表、一个抓商品详情),就必须使用“给其中一个任意伪造domain_name、把真实域名塞进extra_domain_names”这类规避手段; - 多域爬虫要在两处重复声明域名:主域名写在
domain_name,其余域名写在extra_domain_names,同一份信息分散在两个属性里,既冗余又容易不一致。
为此,提案给出了两条修改:
- 给爬虫新增
name属性,并将其作为爬虫的唯一标识符; - 将
domain_name与extra_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。 -
爬虫的加载与索引也全部基于
name。SpiderLoader 在初始化时递归遍历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 list、scrapy crawl <spider>、scrapy genspider 的存在性检查等操作,最终都归结为按 name 在 _spiders 字典中查找。
这里可以明确一条实操结论:同一个项目中,不同爬虫的 name 必须互不相同;类名(MySpider)与文件名只是代码组织方式,框架身份完全由 name 决定。官方文档 docs/topics/spiders.rst 对 name 属性的说明也与此一致:“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/filtered与offsite/domains统计项;对每个离站域名只在第一次记录 DEBUG 日志,保持日志可读(见 offsite.py 第 79–137 行)。
提案末尾还有一处值得注意的附注:spider_allowed_domains 成为可选项,因为它只剩 OffsiteMiddleware 一个使用者。也就是说,allowed_domains 不再参与“爬虫身份判定”(那是 name 的职责),只参与下载阶段的域名过滤——这正是提案解耦“身份”与“域名”后的直接结果。
另外两个与 allowed_domains 相关的细节:
- 若某请求确需访问域外站点,可通过
request.meta["allow_offsite"] = True或dont_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.py 的
syntax()为[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>"。其执行流程:
- 校验恰好两个位置参数
name、url;verify_url_scheme会为缺省协议的 URL 补上https; sanitize_module_name(name)将名字转成合法模块名:-和.替换为_,首字符不是字母时前缀a;- 通过
_spider_exists(name)检查是否已有同名爬虫(借助 SpiderLoader 按name查找),存在则拒绝覆盖(除非--force)——再次印证name是全局唯一身份; - 渲染模板文件到
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.tmpl、csvfeed.tmpl、xmlfeed.tmpl 等模板,可用 scrapy genspider --list 查看,用 -t/--template 指定。
五、命名实践建议与易错点
综合提案与源码实现,可以提炼出如下使用规则:
name必填且全局唯一。缺失会在实例化时抛ValueError,重名会在加载期产生UserWarning并导致后加载者覆盖先加载者(self._spiders[spcls.name] = spcls是字典赋值,后者胜出)。- 命名习惯:官方文档 docs/topics/spiders.rst 建议单域爬虫直接用域名作名字(如
name = "example.com"),多域爬虫用可描述业务的语义化名称。用域名作名字的好处是url_is_from_spider能顺带匹配该域名的 URL,便于fetch/shell等命令的自动定位。 name与allowed_domains各司其职:name决定“我是谁”(日志、命令、加载器),allowed_domains只决定“我允许下载器去哪些域”(OffsiteMiddleware过滤)。不写allowed_domains不会报错,只会失去离站过滤。- 注意
name的“隐形域名匹配”副作用:由于_spider_domains会把name当域匹配,如果你给爬虫起名叫blog,而某台主机名恰好是my.blog,url_is_from_spider也会返回 True。从源码结构看这是子域匹配规则的自然结果;若你的 URL 匹配场景对精确性敏感,可以推断应尽量避免让name与真实域名的子域形式冲突。 allowed_domains应保持为普通类属性:utils/url.py 第 31–44 行 明确警告——若把它定义成property,类级别的 URL 匹配(shell、fetch、parse 命令)无法求值并会发出UserWarning,应改为普通类属性。
六、小结
SEP-012 用一个属性解决了身份问题、用一个列表解决了域名问题:name 让任意多个爬虫可以服务于同一域名而不必互相伪造身份;allowed_domains 让多域声明收敛到单一位置,并成为 OffsiteMiddleware 唯一消费的过滤依据。这一设计历经十余年仍是 Scrapy 的骨架——Spider 强制 name、SpiderLoader 按名索引并检测重名、OffsiteMiddleware 按 allowed_domains 构造子域正则、genspider 以 <name> <domain> 生成双属性爬虫——从提案文本到当前仓库源码,可以清晰地看到“身份归 name、域名归 allowed_domains”这一解耦原则被完整实现并贯穿始终。
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00