首页
/ Scrapy Item Loader 深入解析:从字段采集、输入输出处理器到嵌套加载器的完整机制

Scrapy Item Loader 深入解析:从字段采集、输入输出处理器到嵌套加载器的完整机制

2026-09-04 20:54:46作者:裘旻烁

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.pydefault_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.pytest_init_method_errors 对六种情况都做了断言。另外,对未声明字段调用 add_value 会抛 KeyErrortests/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 中使用的 NameDataClassname: 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)
  1. xpath1 的数据被提取,经过 name 字段的输入处理器,结果被收集(但尚未赋给 Item);
  2. xpath2 的数据经过同一个输入处理器,结果追加到 (1) 收集的数据之后;
  3. CSS 选择器提取的数据同样经过该输入处理器,再追加;
  4. 字面量 test 不经过选择器提取,但依然经过输入处理器——由于输入处理器总是接收可迭代对象,单个值会先被包装成单元素可迭代对象;
  5. 步骤 (1)–(4) 收集到的全部数据进入输出处理器,其结果赋给 name 字段。

两个关键约束:

  • 处理器就是可调用对象,唯一要求是接受恰好一个位置参数,且该参数必须是可迭代对象(输出可以是任意类型);
  • 输入处理器的返回值会被追加进 Loader 内部列表;输出处理器的返回值才是最终值。

itemloaders 库自带一批常用内置处理器(TakeFirstMapComposeComposeJoin 等)。测试代码 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.pyTestFunctionProcessor 演示了 FunctionProcessorItemfoo = 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", ["&euro;", "<span>1000</span>"])
>>> il.load_item()
Product(name='Welcome to my website', price='1000')

nameJoin() 把两个片段拼成一句完整文字;pricefilter_price 会丢弃非纯数字值(返回 None 的值被过滤),TakeFirst() 取剩下的第一个,得到 "1000"

优先级规则(输入与输出处理器相同,从高到低):

  1. Item Loader 上的字段级属性:field_in / field_out(最高);
  2. 字段元数据中的 input_processor / output_processor
  3. 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

修改上下文的三种途径:

  1. 运行时修改当前 Loader 的 context 属性
loader = ItemLoader(product)
loader.context["unit"] = "cm"
  1. 实例化时通过关键字参数传入__init__ 的多余关键字参数全部存入 context,与源码 scrapy/loader/init.py**contextcontext.update(response=response) 行为一致):
loader = ItemLoader(product, unit="cm")
  1. 声明时直接写死(对支持构造时传入 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_replacenl1.nested_xpath("a") 二级嵌套);
  • nested_xpathnested_css 都可用,且 XPath 与 CSS 可以在嵌套链路中混用(test_nested_xpathnl 同时使用了 add_xpathadd_css);
  • 子 Loader 的值写入与父 Loader 共享同一份内部状态:l.get_output_value(...)nl.get_output_value(...) 返回相同结果;load_item() 只在根 Loader 上调一次,且 item is l.item is nl1.item is nl2.itemtests/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.rsttests/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.rstscrapy/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.pypyproject.toml

Item Loader 的价值在于把「提取规则」和「清洗规则」分离到两个正交维度(选择器 vs 处理器)上,再用类继承解决站点差异。理解了输入/输出处理器的触发时机、优先级与 context 传递机制,你就可以在任意 Scrapy 项目中按站点、按源格式构建可维护的解析体系。

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