首页
/ Scrapy SEP-007 复盘:ItemLoader 处理器库提案与它在 Scrapy 中的最终归宿

Scrapy SEP-007 复盘:ItemLoader 处理器库提案与它在 Scrapy 中的最终归宿

2026-09-04 23:04:52作者:晏闻田Solitary

本文基于 Scrapy 仓库中的增强提案 sep/sep-007.rst(SEP-007: ItemLoader processors library,2009 年 8 月由 Ismael Carnales 提出,状态 Draft)展开,逐一复盘该提案设计的 10 个 ItemLoader 字段处理器(processors)的命运:哪些已废弃、哪些功能被 Selector 或 reducers 取代、哪些最终在 scrapy/utils/python.py 和 w3lib 中落地。读完后,你将理解 Scrapy Item Loader 处理器的完整体系(输入/输出处理器、优先级、内置 reducers),并能将提案中的历史 API 映射到当前代码库中真正可用的等价实现。

SEP-007 是什么

SEP 是 Scrapy Enhancement Proposal(增强提案),存放在仓库的 sep/README.rst 所述目录中。SEP-007 的原始诉求非常朴素:为 Scrapy 附带一个 ItemLoader 处理器函数库,让开发者在给 Item 填充数据时,能对提取出的原始值做标准化加工——去标签、清洗空白、统一编码、正则匹配等。

提案按当时计划的模块组织,共提出 10 个处理器,分布在 4 个假想文件中:

  • date.pyto_date
  • extraction.pyextractExtractImageLinks
  • markup.pyremove_tagsremove_rootreplace_escapeunquote
  • misc.pyto_unicodeclean_spacesdrop_emptydelistRegex

提案中每个条目都带有 Decision 字段,标记了评审结论。结合当前仓库代码,可以精确还原每个提案处理器的最终归宿。

提案处理器清单与归宿总表

提案处理器 提案用途 SEP 决策 当前代码库中的对应实现
to_date 将日期字符串转为 YYYY-MM-DDDateField 使用 已废弃DateField 已不存在 无对应物;DateField 属于早期 Django-Item 时代产物,现代 Scrapy 用 item 字段元数据 + 自定义函数替代
extract 尝试从给定位置提取数据,XPathSelector 会被提取,其他数据原样加入结果 已废弃:功能并入 XpathLoader Selector.extract() 及 Item Loader 的 add_xpath/add_css 方法覆盖,见 scrapy/selector/unified.py
ExtractImageLinks 接收指向图片 URL 位置的 XPathSelector 或 XPath 表达式列表,提取图片链接 XXX(未决) 无专用处理器;等价做法是对 <img> 节点做 XPath 提取
remove_tags 工厂函数,移除值中 tags 参数指定的标签(未指定则移除全部标签) XXX(未决) 由依赖库 w3lib 提供 w3lib.html.remove_tags,可作输入处理器(见 docs/topics/loaders.rst 中的官方示例)
remove_root 移除字符串根标签 XXX(未决) 可用 w3lib 的 strip_html5_whitespacecollapse_whitespace 等组合替代
replace_escape 移除/替换值中 wich_ones 参数指定的转义字符 XXX(未决) 由 w3lib 提供 w3lib.html.replace_escape_chars(旧名 remove_escape_chars 已在历史版本中被移除,见 docs/news.rst 中的变更记录)
unquote 移除字符串中全部 CDATA 和 HTML 实体(可经 keep 参数保留部分) XXX(未决) Selector.get_all() / extract() 本身即返回已解析文本,实体解析由 lxml 层完成
to_unicode 按指定编码(默认 utf-8)将字节串转换为 unicode XXX(未决) 已落地scrapy/utils/python.py 中的 to_unicode,且是 Scrapy 内部通用的编码工具
clean_spaces 将字符串中的多空格压缩为单空格 XXX(未决) 可用 re.sub(r"\s+", " ", value) 或 w3lib 的 collapse_whitespace 实现
drop_empty 从可迭代对象中移除所有求值为 None 的项 已废弃:功能并入 reducers 由 itemloaders 的 Join()TakeFirst() 等内置 reducers 覆盖
delist 工厂函数,用指定分隔符连接可迭代对象 已废弃:功能并入 reducers 由 itemloaders 的 Join(delimiter) 覆盖,官方文档示例直接使用 Join()
Regex 接收字符串列表或 XPathSelector,用必选的关键字参数 regex 做正则匹配并返回匹配列表 XXX(未决) 无内置处理器;用 Python re 模块或 Selector.extract(xpath, search=...) 在 Spider 中自行实现

这张表的结论是:SEP-007 没有被整体实现为独立的处理器库——它的功能被三条路径吸收:废弃、并入 Selector/Loader 核心能力、以及由 itemloaders 与 w3lib 两个生态库承接。

唯一直接落地的处理器:to_unicode

提案 misc.py 中的 to_unicode 虽然 Decision 标记为 XXX,但其核心语义最终以更通用的形式活在 Scrapy 代码库中。当前实现位于 scrapy/utils/python.py

def to_unicode(
    text: str | bytes, encoding: str | None = None, errors: str = "strict"
) -> str:
    """Return the unicode representation of a bytes object ``text``. If
    ``text`` is already an unicode object, return it as-is."""
    if isinstance(text, str):
        return text
    if not isinstance(text, (bytes, str)):
        raise TypeError(
            f"to_unicode must receive a bytes or str object, got {type(text).__name__}"
        )
    if encoding is None:
        encoding = "utf-8"
    return text.decode(encoding, errors)

对照提案可以看出两点演进:

  1. 提案示例to_unicode('it costs 20\xe2\x82\xac, or 30\xc2\xa3') 演示了把 latin-1 风格的字节串解码为含 £ 的 unicode 文本,语义与当前实现完全一致;
  2. 当前实现比提案更严谨:显式校验输入必须是 bytesstr 否则抛出 TypeError,新增 errors 参数(默认 strict)控制解码错误策略,且在 Python 3 语境下 str 输入直接原样返回(提案写于 Python 2 时代,unicode 是独立类型,如今 str 即 unicode)。

to_unicode 如今更多承担 Scrapy 内部工具职责(如 scrapy/http/response/text.py 的文本解码链路),而 Spider 开发者在 Item Loader 处理器中处理编码时,通常直接写 lambda v: v.decode('utf-8') 或组合 MapCompose(str.strip, to_unicode)

已废弃的两类处理器:被 Selector 与 reducers 取代

extract:功能并入 Selector 体系

提案 extract 的设计目标是"尝试从给定的位置提取数据:其中的 XPathSelector 会被提取,其余数据原样加入结果"。其 Decision 注明:已废弃,功能并入 XpathLoader。从源码结构看,这一归宿的承载者已经演化为统一的 Selector 类(scrapy/selector/init.pyscrapy.selector.unified 导出 SelectorSelectorList),而 Item Loader 对 Selector 的原生支持体现在 scrapy/loader/init.py 中:ItemLoader.__init__ 接受 selectorresponse 参数,后者会用 default_selector_class(即 Selector)构造选择器并放入 loader context。今天等价于 extract 的写法就是:

def parse(self, response):
    l = ItemLoader(item=Product(), response=response)
    l.add_xpath("name", '//div[@class="product_name"]')
    l.add_xpath("name", '//div[@class="product_title"]')
    l.add_css("stock", "p#stock")
    l.add_value("last_updated", "today")
    return l.load_item()

这是 docs/topics/loaders.rst 中的标准用法:多来源提取、按字段聚合,正是 extract 当年想做的事,如今由 add_xpath/add_css/add_value 三个方法原生完成。

drop_empty 与 delist:功能并入 reducers

这两个处理器被标记为"Obsolete. Functionality included in reducers"。所谓 reducers,即 itemloaders 库提供的内置处理器(TakeFirstJoinMapCompose 等,作为 scrapy/loader/init.py 底层依赖 itemloaders>=1.0.1pyproject.toml 中声明)。它们分别对应:

  • drop_empty(剔除 None 项):Item Loader 内部将字段值收集为列表,输出处理器 Join() 拼接时天然只输出有值的内容;若需严格过滤,可在输入处理器中 MapCompose(lambda v: v if v is not None else [])
  • delist(用分隔符连接列表):直接对应 Join(delimiter)。官方文档 docs/topics/loaders.rst 的示例即使用 Join() 作为输出处理器,将 name 字段收集到的多个片段合并为一个字符串。

官方文档中一个可直接运行的完整示例(声明在字段元数据中的处理器):

from dataclasses import dataclass, field

from itemloaders.processors import Join, MapCompose, TakeFirst
from w3lib.html import remove_tags


def filter_price(value):
    if value.isdigit():
        return value


@dataclass
class Product:
    name: str | None = field(
        default=None,
        metadata={
            "input_processor": MapCompose(remove_tags),
            "output_processor": Join(),
        },
    )
    price: str | None = field(
        default=None,
        metadata={
            "input_processor": MapCompose(remove_tags, filter_price),
            "output_processor": TakeFirst(),
        },
    )
>>> from scrapy.loader import ItemLoader
>>> il = ItemLoader(item=Product())
>>> il.add_value("name", ["Welcome to my", "<strong>website</strong>"])
>>> il.add_value("price", ["&euro;", "<span>1000</span>"])
>>> il.load_item()
Product(name='Welcome to my website', price='1000')

未决(XXX)处理器与今天的替代方案

提案中六个标记为 XXX 的条目没有正式评审结论,但从当前代码库可以找到明确的事实性替代路径。

remove_tags:w3lib.html.remove_tags

提案中的 remove_tags 是"工厂函数,返回一个移除 tags 参数中每个标签的适配器;不指定则移除所有标签"。今天的官方推荐路径是 w3lib 的 remove_tags(w3lib 同为 Scrapy 声明依赖,pyproject.tomlw3lib>=2.1.1),它支持按 tags 参数选择要移除的标签,语义与提案高度一致,并且被官方文档直接用作输入处理器示例(MapCompose(remove_tags),见上文 Product 示例)。

replace_escape:w3lib.html.replace_escape_chars

提案的 replace_escape 用于移除/替换转义字符(注意提案原文把参数拼成了 wich_ones)。对应实现是 w3lib 的 replace_escape_charsdocs/news.rst 中有一条变更记录可作佐证:旧函数 scrapy.utils.markup.remove_escape_chars 已被移除,改用 scrapy.utils.markup.replace_escape_chars——这印证了该功能经历了从"Scrapy 内置"到"随生态库迁移"的演变。

unquote:Selector 的实体解析能力

提案的 unquote 要移除 CDATA 与 HTML 实体(keep 参数可保留部分)。今天无需手写:Selector.extract()/get() 基于 lxml 解析,返回的文本已经是实体解码后的结果,MapCompose(remove_tags) 再配合即可覆盖提案意图。

remove_root 与 clean_spaces

两者在仓库中均无同名实现(在全部 .py 源码中检索 remove_rootclean_spaces 无任何命中,仅出现在 sep 文档与文档正文的历史叙述中)。实践中的等价写法:

import re

remove_root = lambda v: re.sub(r"^<[^>]+>(.*)</[^>]+>$", r"\1", v, flags=re.S)
clean_spaces = lambda v: re.sub(r"\s+", " ", v).strip()

或使用 w3lib 的 collapse_whitespace 完成多空格压缩。

Regex 与 ExtractImageLinks

  • Regex 处理器(必选 regex 关键字参数,对字符串列表或 XPathSelector 做匹配)至今没有内置版本,实践中在 Spider 内用 re 模块或 Selector.extract(xpath, search=pattern) 完成。
  • ExtractImageLinks 的直接等价是 XPath 提取:response.xpath('//img/@src').getall(),无需专用适配器。

SEP-007 的设计意图为何值得今天的开发者关心

尽管 SEP-007 是 2009 年的 Draft,它揭示的"ItemLoader 处理器"思想在今天的 Scrapy 中是一等公民,且机制比提案时代更完整。理解这套机制是读懂提案归宿的前提,关键事实如下:

  1. 处理器体系:每个 item 字段有且仅有一个输入处理器和一个输出处理器。输入处理器在 add_xpath/add_css/add_value 收集数据时立即作用于提取出的数据;输出处理器在 load_item() 时接收收集好的列表并产出最终字段值。处理器只需接受一个可迭代位置参数,任何函数都可以充当处理器。
  2. 处理器声明位置与优先级(从高到低,见 docs/topics/loaders.rst):
    • Item Loader 字段专属属性:field_in / field_out
    • 字段元数据:input_processor / output_processor
    • Item Loader 默认值:default_input_processor / default_output_processor(见 scrapy/loader/init.py 中的属性文档)。
  3. 内置 reducersTakeFirstJoinMapCompose 等来自 itemloaders 库,取代了提案中 drop_emptydelist 的历史角色。
  4. 数据容器与填充机制分离Itemscrapy/item.py)只负责"装"——声明 Field、校验字段名(未声明字段写入会抛 KeyError);ItemLoader 负责"填"。这正是 SEP-007 所处的分层:处理器只应关心值的转换,不关心值的来源。

小结与迁移指引

SEP-007 的历史意义在于它定义了 ItemLoader 处理器库的需求清单;而当前 Scrapy 的答案是:不内置大而全的处理器包,而是以 Selector 提取 + itemloaders reducers + w3lib HTML 工具的组合承接。对照提案迁移时可直接遵循:

你原来想要的 现在应该用
to_date 已废弃,无替代物;日期标准化用自定义函数
extract ItemLoader.add_xpath / add_css + Selector.extract()
remove_tags w3lib.html.remove_tagsMapCompose(remove_tags) 作输入处理器)
replace_escape w3lib.html.replace_escape_chars
unquote Selector 提取结果天然已解码实体
clean_spaces w3lib.html.collapse_whitespace 或正则
remove_root 正则或 w3lib.html 工具组合
to_unicode scrapy/utils/python.pyto_unicode
drop_empty / delist itemloaders 的 Join() / TakeFirst()
Regex re 模块或 Selector.extract(xpath, search=...)

需要说明的适用前提:以上结论基于当前仓库(sep/sep-007.rst 状态为 Draft,提案未整体通过);to_unicode 行为以 scrapy/utils/python.py 源码为准(默认编码 utf-8、errors 默认 strict);w3lib/itemloaders 的具体 API 以其各自库为准,Scrapy 仓库中仅通过 pyproject.toml 声明了版本下限依赖。

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