bigywater.tech
Scrapy · 14

ItemLoader:输入输出处理器

ItemLoader 把“提取”和“清洗”拆成输入处理器和输出处理器。它们什么时候被调用?TakeFirst、MapCompose 对 None、空串、0 各怎么处理?多处声明的处理器谁优先?这篇用真实的 itemloaders 逐项验证。

2 次 vs 1 次
name 字段 add_value 两次时,输入处理器调用 2 次,输出处理器只调用 1 次
0
TakeFirst 对 [0, False, 5] 的返回值(0 算有值)
KeyError
往 scrapy.Item(没声明字段)的 ItemLoader 里放值的结果

输入与输出

实测 · 本机执行输出
  add_value 之后,collected: {'name': ['a', 'b', 'c'], 'price': ['9.9']}
  调用记录(add_value 阶段): [('输入', [' a ']), ('输入', [' b ', ' c ']), ('输入', ['9.9'])]
  load_item 之后的 item: {'name': ['a', 'b', 'c'], 'price': ['9.9']}
  调用记录(完整): [('输入', [' a ']), ('输入', [' b ', ' c ']), ('输入', ['9.9']), ('输出', ['a', 'b', 'c']), ('输出', ['9.9'])]
  不设处理器(Identity): {'name': ['x', 'y']} ;get_output_value = ['x', 'y']

输入处理器在 add_value / add_css / add_xpath 时对每次提取到的值调用;输出处理器在 load_item() 时对每个字段已收集的全部值调用一次。默认都是 Identity,值是列表。

常用处理器

实测 · 本机执行输出
  Identity()                                    ['a', 'b']                        → ['a', 'b']
  TakeFirst()                                   [None, '', 'a', 'b']              → 'a'
  TakeFirst()(全空)                               [None, '']                        → None
  TakeFirst()(0 和 False)                        [0, False, 5]                     → 0
  Join()                                        ['a', 'b', 'c']                   → 'a b c'
  Join(', ')                                    ['a', 'b', 'c']                   → 'a, b, c'
  MapCompose(str.strip)                         [' a ', ' b']                     → ['a', 'b']
  MapCompose(str.strip, str.upper)              [' a ', ' b']                     → ['A', 'B']
  MapCompose(lambda x: None 当 x 为空)             ['a', '', 'b']                    → ['a', 'b']
  MapCompose(lambda x: x.split(","))(返回列表→展平)   ['a,b', 'c']                      → ['a', 'b', 'c']
  MapCompose(int)(遇到非数字)                        ['1', 'x']                        → 抛 ValueError: Error in MapCompose with <class 'int'> value=['1', 'x'] error='ValueError: invalid literal for int() with base 10: 'x''
  Compose(lambda v: v[0])                       ['a', 'b']                        → 'a'
  Compose(lambda v: v[0])(空列表)                  []                                → 抛 ValueError: Error in Compose with <function <lambda> at 0x…> value=[] error='IndexError: list index out of range'
  Compose(…, stop_on_none=False)(空列表)           []                                → 抛 ValueError: Error in Compose with <method 'upper' of 'str' objects> value=None error='TypeError: descriptor 'upper' for 'str' objects doesn't apply to a 'NoneType' object'
  Compose(…)(默认 stop_on_none=True,遇到 None 就停)   []                                → None

TakeFirst 跳过 None 和空串;MapCompose 逐个值套函数并展平、丢掉 None;Compose 整体处理;Join 拼接字符串。出错会包装成 ValueError。

优先级

实测 · 本机执行输出
  name  (loader 的 name_in + Field 元数据 + default): loader:1|loader:2
  tag   (只有 Field 元数据的 input_processor + default 输出): meta:1+meta:2
  other (什么都没有 → 全用 default_*): default:1+default:2
  item = {'name': 'loader:1|loader:2', 'tag': 'meta:1+meta:2', 'other': 'default:1+default:2'}

Loader 子类里的 字段名_in/_out 优先于 Item 字段元数据,再优先于 default_input_processor / default_output_processor。

选择器与上下文

实测 · 本机执行输出
  loader_context 里的 rate=2 传给了 price_in: {'title': 'Python 入门', 'price': 119.8, 'tags': 'a,b', 'src': ['http://x/']}
  nested_css("#p") 里再 add_css: {'title': 'Python 入门', 'tags': 'a,b'}
  loader.context 的键: ['item', 'rate', 'response', 'selector']

构造 loader 时传 response 就能 add_css/add_xpath;处理器多声明一个 loader_context 参数就能拿到构造时传入的任意关键字参数。

空值

实测 · 本机执行输出
  add_value(None / "" / [] / [None, ""] / 0 / False) → collected: {'a': [], 'b': [''], 'c': [], 'd': [None, ''], 'e': [0], 'f': [False]}
  load_item(): {'b': [''], 'd': [None, ''], 'e': [0], 'f': [False]}
  replace_value 之后: {'x': ['new'], 'y': ['123']} ;add_value(re=…): ['123']
  get_value("a b", re="b"): ['b']

None 和空列表不会被收集,所以对应字段不会出现在 item 里;空串、0、False 会被收集。

item 类

实测 · 本机执行输出
  SL()(默认 scrapy.Item,没有任何字段)add_value("title"):抛 KeyError 'title'
  SL(item=Declared()) 里 add_value("other")(没声明):抛 KeyError 'other'
  声明了 title 的 item: {'title': ['x']} Declared
  default_item_class = dict: {'anything': ['z']} dict

scrapy.loader.ItemLoader 默认的 item 类是 scrapy.Item,没声明的字段会 KeyError;设 default_item_class = dict 则不限制字段。

分步演示

ItemLoader
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Scrapy 2.19.0 / Twisted 26.4.0 / lxml 6.1.3 / parsel 1.12.1(临时虚拟环境);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、重定向、压缩、断连等行为,并记录收到的原始请求;每个实验在临时目录里新建 Scrapy 项目或单文件 spider,命令输出来自真实的 scrapy 进程;计时用 time.perf_counter,数字每次运行会有小幅抖动(itemloaders 1.5.0、itemadapter 0.13.1)。实验直接在虚拟环境里构造 Loader、HtmlResponse,不经过网络
    • 没有验证:SelectJmes、MapCompose 配 w3lib 的 remove_tags 等其它常见处理器;add_jmes(JSON 响应);ItemLoader 与 dataclass / attrs item 的搭配;loader 用在 pipeline 之后(把已有 item 重新装载)的情形;处理器抛异常时的栈信息(只看了错误文本)
    京ICP备2026067502号-1京公网安备11010502063487号