bigywater.tech
Scrapy · 48

Item 类型:dict、Item、dataclass 与 ItemAdapter

Scrapy 里的 item 可以是 dict、scrapy.Item、dataclass、attrs。它们在字段检查、元数据、导出、速度和内存上差在哪里?ItemAdapter 又怎样把它们统一起来?这篇逐项量。

13.5 倍
scrapy.Item 创建一个 3 字段 item 比 dict 慢的倍数(17.8 vs 1.3 ms/万次)
436 字节
一个 3 字段 scrapy.Item 的内存(dict 192、dataclass(slots) 64)
KeyError
给 scrapy.Item 赋值未声明字段的结果

统一接口

实测 · 本机执行输出
  类型            is_item  field_names()                     asdict()                                  get("price")  get("zzz", "默认")
  dict          True     ['name', 'price']                 {'name': 'a', 'price': 1.5}               1.5         默认
  scrapy.Item   True     ['name', 'price']                 {'name': 'a', 'price': 1.5}               1.5         默认
  dataclass     True     ['name', 'price', 'extra']        {'name': 'a', 'price': 1.5, 'extra': []}  1.5         默认
  attrs         True     ['name', 'price']                 {'name': 'a', 'price': 1.5}               1.5         默认
  字段元数据 get_field_meta("price"):scrapy.Item={'serializer': <function SItem.<lambda> at 0x…>}; dataclass={}; attrs={'serializer': <class 'str'>}
  不是 item 的东西:is_item(42)= False  is_item("s")= False  is_item([1])= False  is_item(object())= False

ItemAdapter 给 dict、scrapy.Item、dataclass、attrs 提供相同的读写、asdict()、字段元数据接口,pipeline、导出器都通过它处理 item。

严格程度

实测 · 本机执行输出
  scrapy.Item 赋值未声明字段                         → 抛 KeyError: 'SItem does not support field: zzz'
  scrapy.Item item["zzz"] = 1                 → 抛 KeyError: 'SItem does not support field: zzz'
  scrapy.Item 读未赋值的字段 item["name"]            → 抛 KeyError: 'name'
  scrapy.Item item.get("name")                → 通过,结果 None
  dataclass 传未声明字段 DItem(zzz=1)               → 抛 TypeError: DItem.__init__() got an unexpected keyword argument 'zzz'
  dataclass 给 name 赋整数(类型标注 str)              → 通过,结果 DItem(name=123)
  dict 随便加字段                                  → 通过,结果 None
  ItemAdapter(SItem())["zzz"] = 1  → 抛 KeyError: 'SItem does not support field: zzz'

scrapy.Item 不允许未声明的字段(KeyError);dataclass 不允许未声明的构造参数但不检查类型;dict 随意。

导出

实测 · 本机执行输出
  o.jsonl:
    {"name": "字典", "price": 1.5}
    {"name": "Item", "price": "¥2.5"}
    {"name": "dataclass", "price": 3.5, "extra": ["x"]}
    {"name": "attrs", "price": 4.5}
  o.csv:
    name,price
    字典,1.5
    Item,¥2.5
    dataclass,3.5
    attrs,4.5
  item_scraped_count=4(四个类型都被接受)
  说明:csv 表头取自第一个 item(字典)的键 name,price;scrapy.Item 的 serializer 在 jsonlines 里生效(price 变成 “¥2.5”),dataclass 的 extra 字段在 csv 里被丢掉(列头里没有)

FEEDS 对四种类型都能导出;scrapy.Item 的 Field(serializer=…) 会在导出时使用;csv 的列由第一个 item 决定。

成本

实测 · 本机执行输出
  dict                         1.3 ms / 万次    1.0× dict
  scrapy.Item                 17.8 ms / 万次   13.5× dict
  dataclass                    3.0 ms / 万次    2.3× dict
  dataclass(slots=True)        2.4 ms / 万次    1.8× dict
  attrs                        2.6 ms / 万次    2.0× dict
  attrs(slots=True)            2.5 ms / 万次    1.9× dict
  每个对象占的内存(tracemalloc,10 万个实例的平均字节):
    dict                        192 字节/个
    scrapy.Item                 436 字节/个
    dataclass                   104 字节/个
    dataclass(slots=True)        64 字节/个
    attrs                       104 字节/个
    attrs(slots=True)            80 字节/个

scrapy.Item 的创建和内存开销明显更大;大量 item 时 dataclass / attrs(尤其 slots=True)更省。

分步演示

Item 类型
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动(itemadapter 0.13.1、attrs 26.1.0)。前三节是直接对类型做的最小实验;导出一节是单文件 spider 往 FEEDS 写 jsonlines 与 csv;耗时取 10 万次创建循环 5 次的中位数,内存用 tracemalloc 量 10 万个存活实例
    • 没有验证:pydantic 模型作为 item(没装 pydantic,ItemAdapter 默认也不内置支持);ItemLoader 与 dataclass 的搭配;嵌套 item 的导出;item 在 pipeline 里被修改的可变性;Python 版本变化对 slots dataclass 内存的影响
    京ICP备2026067502号-1京公网安备11010502063487号