Scrapy Item Loader 深入解析:从字段采集、输入输出处理器到嵌套加载器的完整机制
Item Loader 是 Scrapy 中用于向 Item 填充抓取数据的抽象层。它通过 add_xpath / add_css / add_value 三种采集方法、每字段独立的输入/输出处理器,以及嵌套 Loader 机制,把「提取 → 清洗 → 合并」的过程标准化。读完本文,你能掌握 Item Loader 的声明语法、处理器优先级、上下文(context)传递方式与嵌套加载器的使用边界,并理解 Scrapy 对底层 itemloaders 库的扩展实现。
一、Item Loader 解决什么问题
Item(docs/topics/items.rst 章节定义的数据容器)解决的是「数据放哪里」,而 Item Loader 解决的是「数据怎么放进容器」:它在把原始抓取值写入 Item 之前,自动完成诸如去标签、去空白、多值合并等常见清洗工作。
如 docs/topics/loaders.rst 所述,Item Loaders 的设计目标是提供一种灵活、高效且易于维护的机制,让你可以按 Spider 或按源格式(HTML、XML 等)扩展和覆盖不同字段的解析规则,而不必把项目变成维护噩梦。
从源码结构看,Scrapy 的 ItemLoader 是对外部库 itemloaders 中同名类的直接继承——scrapy/loader/init.py 中的类定义只有一行 class ItemLoader(itemloaders.ItemLoader),Scrapy 在其之上增加了对 response 对象的支持(这是文档中 note 强调的"对 itemloaders 库的扩展")。依赖版本约束见 pyproject.toml 第 16 行的 itemloaders>=1.0.1。
二、基本用法:在 Spider 中填充 Item
使用 Item Loader 的第一步是实例化它。你可以传入一个已存在的 Item 对象,也可以不传——此时 Loader 会在 __init__ 中根据 default_item_class 属性指定的 Item 类自动创建实例(Scrapy 的默认值是 Item,见 scrapy/loader/init.py 的 default_item_class: type = Item;全局层面还有 DEFAULT_ITEM_CLASS = "scrapy.item.Item" 这一默认设置,见 scrapy/settings/default_settings.py)。
典型的 Spider 用法如下(继承自官方文档示例):
from scrapy.loader import ItemLoader
from myproject.items import Product
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_xpath("price", '//p[@id="price"]')
l.add_css("stock", "p#stock")
l.add_value("last_updated", "today") # 也可以直接写字面量
return l.load_item()
这段代码中:name 字段从两个不同的 XPath 位置提取数据(add_xpath 可多次调用同一字段);price 同样用 XPath 提取;stock 改用 CSS 选择器(add_css);last_updated 则通过 add_value 直接赋予字面量 today。最后调用 load_item(),返回填充完毕的 Item。
内部存储规则:采集到的数据在 Loader 内部以列表形式存储,因此同一个字段可以追加多个值,之后由输出处理器决定如何"合并"。文档特别指出:如果创建 Loader 时传入了 item 参数,Item 中已有的值会按原样保留(本身是迭代器则直接保留,单值则包一层列表)。这一点在测试用例 tests/test_loader.py 中有系统验证:例如 test_add_value_singlevalue_list 断言初始值 "foo" 与后加的 ["item", "loader"] 最终合并为 ["foo", "item", "loader"];test_values_single / test_values_list 则确认初始 Item 的值会被写入 Loader 内部的 _values 字典。
关于初始化参数,源码 scrapy/loader/init.py 的签名是:
def __init__(
self,
item: Any = None,
selector: Selector | None = None,
response: TextResponse | None = None,
parent: itemloaders.ItemLoader | None = None,
**context: Any,
):
其中:
- 若给了
response而未给selector,会用default_selector_class(默认是scrapy.selector.Selector,见 scrapy/loader/init.py)从 response 构造 selector; - item、selector、response 以及其余所有关键字参数(
**context)都会被存入 Loader 的上下文(context属性)。
一个容易踩坑的细节:如果 Loader 既没有 selector 也没有 response 时调用 add_xpath / replace_xpath / add_css / get_css 等依赖选择器的方法,会抛出 RuntimeError——tests/test_loader.py 的 test_init_method_errors 对六种情况都做了断言。另外,对未声明字段调用 add_value 会抛 KeyError(tests/test_loader.py)。
三、与 dataclass Item 配合
默认的 dataclass Item 要求创建时传齐所有字段,这与 Item Loader「逐步填充」的工作方式天然冲突。官方文档给出的解决方案是给每个字段用 dataclasses.field 指定 default:
from dataclasses import dataclass, field
@dataclass
class InventoryItem:
name: str | None = field(default=None)
price: float | None = field(default=None)
stock: int | None = field(default=None)
这样 Loader 在 __init__ 中就能自动构造一个空的 Item,随后通过 add_xpath / add_css / add_value 增量填充。测试文件 tests/test_loader.py 中使用的 NameDataClass(name: list[str] = dataclasses.field(default_factory=list))同样依赖默认值机制,且 TestInitializationFromDataClass 证明 dict、传统 Item、attrs、dataclass 四种容器类型在"初始值保留 + 追加"语义上行为一致。
四、输入处理器与输出处理器
每个 Item 字段对应一个输入处理器和一个输出处理器,二者职责截然不同:
- 输入处理器(input processor):在数据被采集的当下(
add_xpath/add_css/add_value调用时)立即处理,处理结果被追加到该字段的内部列表里; - 输出处理器(output processor):在
load_item()被调用时才执行,接收的是该字段已收集的全部(经输入处理器处理过的)数据,其返回值就是最终赋给 Item 的值。
用一个完整的字段示例看调用时序(继承自官方文档):
l = ItemLoader(Product(), some_selector)
l.add_xpath("name", xpath1) # (1)
l.add_xpath("name", xpath2) # (2)
l.add_css("name", css) # (3)
l.add_value("name", "test") # (4)
return l.load_item() # (5)
xpath1的数据被提取,经过name字段的输入处理器,结果被收集(但尚未赋给 Item);xpath2的数据经过同一个输入处理器,结果追加到 (1) 收集的数据之后;- CSS 选择器提取的数据同样经过该输入处理器,再追加;
- 字面量
test不经过选择器提取,但依然经过输入处理器——由于输入处理器总是接收可迭代对象,单个值会先被包装成单元素可迭代对象; - 步骤 (1)–(4) 收集到的全部数据进入输出处理器,其结果赋给
name字段。
两个关键约束:
- 处理器就是可调用对象,唯一要求是接受恰好一个位置参数,且该参数必须是可迭代对象(输出可以是任意类型);
- 输入处理器的返回值会被追加进 Loader 内部列表;输出处理器的返回值才是最终值。
itemloaders 库自带一批常用内置处理器(TakeFirst、MapCompose、Compose、Join 等)。测试代码 tests/test_loader.py 展示了它们的使用:name_in = MapCompose(lambda v: v.title()) 让 add_value("name", "marta") 最终得到 "Marta"(tests/test_loader.py);Title 字段的 Compose(TakeFirst()) 配合 default_input_processor = Identity() 则验证了多处理器组合(tests/test_loader.py)。
此外还可以直接用普通函数充当处理器:只要函数签名是 def my_proc(iterable) 即可。tests/test_loader.py 的 TestFunctionProcessor 演示了 FunctionProcessorItem 中 foo = Field(input_processor=..., output_processor=...) 的函数式写法," bar " 经 strip 后与 "asdf"、"qwerty" 合并并统一大写,得到 ["BAR", "ASDF", "QWERTY"]。
五、声明 Item Loader 类与处理器优先级
Item Loader 用类定义语法声明。输入处理器以 _in 后缀命名,输出处理器以 _out 后缀命名,另可用 default_input_processor / default_output_processor 声明全局默认:
from itemloaders.processors import TakeFirst, MapCompose, Join
from scrapy.loader import ItemLoader
class ProductLoader(ItemLoader):
default_output_processor = TakeFirst()
name_in = MapCompose(str.title)
name_out = Join()
price_in = MapCompose(str.strip)
# ...
处理器还可以声明在 Item Field 的元数据中。官方文档展示了 dataclass Item 通过 field(metadata=...) 挂载 input_processor / output_processor 的方式:
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", ["€", "<span>1000</span>"])
>>> il.load_item()
Product(name='Welcome to my website', price='1000')
name 的 Join() 把两个片段拼成一句完整文字;price 的 filter_price 会丢弃非纯数字值(返回 None 的值被过滤),TakeFirst() 取剩下的第一个,得到 "1000"。
优先级规则(输入与输出处理器相同,从高到低):
- Item Loader 上的字段级属性:
field_in/field_out(最高); - 字段元数据中的
input_processor/output_processor; - Item Loader 默认值:
default_input_processor/default_output_processor(最低)。
测试代码 tests/test_loader.py 还展示了传统 Item 类上通过 Field() 关键字参数直接挂处理器的写法,与元数据方式等价。
六、Item Loader Context(上下文)
Item Loader Context 是一个共享的键值字典,被同一 Loader 内所有输入/输出处理器共享,用于修改处理器的行为。处理器函数如果声明了 loader_context 参数,Loader 就会在调用时把当前上下文传进去:
def parse_length(text, loader_context):
unit = loader_context.get("unit", "m")
# ... 长度解析逻辑 ...
return parsed_length
修改上下文的三种途径:
- 运行时修改当前 Loader 的
context属性:
loader = ItemLoader(product)
loader.context["unit"] = "cm"
- 实例化时通过关键字参数传入(
__init__的多余关键字参数全部存入 context,与源码 scrapy/loader/init.py 的**context及context.update(response=response)行为一致):
loader = ItemLoader(product, unit="cm")
- 声明时直接写死(对支持构造时传入 context 的处理器,如
MapCompose):
class ProductLoader(ItemLoader):
length_out = MapCompose(parse_length, unit="cm")
七、嵌套 Loader(Nested Loaders)
当你要从文档的某个子区域(如页面页脚)集中提取相关字段时,嵌套 Loader 可以避免每条 XPath 都重复书写完整前缀。示例场景是一个页脚:
<footer>
<a class="social" href="https://facebook.com/whatever">Like Us</a>
<a class="social" href="https://twitter.com/whatever">Follow Us</a>
<a class="email" href="mailto:whatever@example.com">Email Us</a>
</footer>
不用嵌套 Loader 时,每条规则都要写全路径:
loader = ItemLoader(item=Item())
loader.add_xpath("social", '//footer/a[@class = "social"]/@href')
loader.add_xpath("email", '//footer/a[@class = "email"]/@href')
loader.load_item()
用嵌套 Loader 后,相对选择器以 //footer 为作用域:
loader = ItemLoader(item=Item())
footer_loader = loader.nested_xpath("//footer")
footer_loader.add_xpath("social", 'a[@class = "social"]/@href')
footer_loader.add_xpath("email", 'a[@class = "email"]/@href')
# 不需要调用 footer_loader.load_item()
loader.load_item()
要点(与测试行为一致,见 tests/test_loader.py):
- 嵌套 Loader 可以任意层嵌套(
test_nested_replace中nl1.nested_xpath("a")二级嵌套); nested_xpath与nested_css都可用,且 XPath 与 CSS 可以在嵌套链路中混用(test_nested_xpath里nl同时使用了add_xpath和add_css);- 子 Loader 的值写入与父 Loader 共享同一份内部状态:
l.get_output_value(...)与nl.get_output_value(...)返回相同结果;load_item()只在根 Loader 上调一次,且item is l.item is nl1.item is nl2.item(tests/test_loader.py); - 追加顺序按调用顺序保留,父/子 Loader 混排时值按调用先后排列(
test_nested_ordering)。
文档同时给出使用建议:当嵌套能让代码更简洁时用,但不要过度嵌套,否则解析器会变得难以阅读。
八、复用与继承 Item Loader
项目变大、Spider 变多之后,各站点解析规则千差万别却又共享大量公共处理器,此时传统的 Python 类继承就成了组织解析规则的手段。官方文档给了两个典型例子。
例 1:某站点商品名被三短横线包裹(---Plasma TV---),需要剥掉短横线但复用父类其余清洗逻辑:
from itemloaders.processors import MapCompose
from myproject.ItemLoaders import ProductLoader
def strip_dashes(x):
return x.strip("-")
class SiteSpecificLoader(ProductLoader):
name_in = MapCompose(strip_dashes, ProductLoader.name_in)
MapCompose 会按顺序把每个值先过 strip_dashes 再过父类原有的 name_in,实现"在公共规则前插入一条站点特有规则"。
例 2:同一站点有 XML 和 HTML 两种源格式,XML 版需要额外去除 CDATA:
from itemloaders.processors import MapCompose
from myproject.ItemLoaders import ProductLoader
from myproject.utils.xml import remove_cdata
class XmlProductLoader(ProductLoader):
name_in = MapCompose(remove_cdata, ProductLoader.name_in)
文档的总结性建议:输入处理器通常依赖具体站点的解析规则,适合在 Loader 子类中扩展;输出处理器通常只依赖字段本身,更适合声明在字段元数据中(参见上文优先级一节)。Scrapy 只提供继承机制,不强制 Loader 的组织方式——不同层级、不同项目可以有完全不同的 Loader 体系。
九、小结:关键行为速查
| 行为 | 说明 | 依据 |
|---|---|---|
同字段多次 add_* |
值按调用顺序追加到内部列表 | docs/topics/loaders.rst、tests/test_loader.py |
| 传入已有 Item | 初始值保留:单值包成列表,列表原样保留 | tests/test_loader.py |
| 无 selector/response 时调用选择器方法 | 抛 RuntimeError |
tests/test_loader.py |
未声明字段 add_value |
抛 KeyError |
tests/test_loader.py |
非迭代值传入 add_value |
先包装为单元素可迭代再进输入处理器 | docs/topics/loaders.rst |
| 处理器函数签名 | 恰好一个位置参数(可迭代);声明 loader_context 参数可接收上下文 |
docs/topics/loaders.rst、scrapy/loader/init.py |
| 处理器优先级 | field_in/field_out > 字段元数据 > default_*_processor |
docs/topics/loaders.rst |
| 嵌套 Loader | 任意层级、XPath/CSS 混用、只需在根 Loader 调 load_item() |
tests/test_loader.py |
| Scrapy 与 itemloaders 的关系 | scrapy.loader.ItemLoader 继承外部库同名类,增加 response 支持 |
scrapy/loader/init.py、pyproject.toml |
Item Loader 的价值在于把「提取规则」和「清洗规则」分离到两个正交维度(选择器 vs 处理器)上,再用类继承解决站点差异。理解了输入/输出处理器的触发时机、优先级与 context 传递机制,你就可以在任意 Scrapy 项目中按站点、按源格式构建可维护的解析体系。
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 StartedRust0623
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