ItemLoader 把“提取”和“清洗”拆成输入处理器和输出处理器。它们什么时候被调用?TakeFirst、MapCompose 对 None、空串、0 各怎么处理?多处声明的处理器谁优先?这篇用真实的 itemloaders 逐项验证。
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 就停) [] → NoneTakeFirst 跳过 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 会被收集。
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']} dictscrapy.loader.ItemLoader 默认的 item 类是 scrapy.Item,没声明的字段会 KeyError;设 default_item_class = dict 则不限制字段。