Scrapy Item 完全指南:scrapy.item 中 Item、Field 元数据与多种 Item 类型的实现原理
本文基于 Scrapy 仓库官方文档 docs/topics/items.rst 及其对应源码实现,系统讲解 Scrapy 的结构化数据载体 Item:五种受支持的 Item 类型(dict、Item、dataclass、attrs、Pydantic)、Field 字段元数据机制、Item 的日常操作 API、子类扩展方式,以及如何用 ItemAdapter 编写兼容所有 Item 类型的管道与中间件代码。读完后你可以理解 Item 背后的元类工作原理,并能在项目中灵活选择和建模抓取数据。
一、什么是 Item:从非结构化网页到结构化数据
爬取的核心目标是从非结构化来源(通常是网页)中提取结构化数据。Spider(见 docs/topics/spiders.rst)可以返回提取出的数据作为 item——即定义键值对的 Python 对象。
Scrapy 支持多种 item 类型,创建 item 时你可以任选其一;而编写接收 item 的代码(item 管道、Spider 中间件等)时,应当使用 itemadapter 库的 ItemAdapter,使代码对任意受支持的 item 类型都能工作。
二、Scrapy 支持的 5 种 Item 类型
通过 itemadapter 库,Scrapy 支持以下五类 item:
1. 字典(dict)
作为 item 类型,dict 最熟悉也最便捷,无需任何额外声明,直接返回 {"name": ..., "price": ...} 即可。
2. Item 对象
Item(scrapy/item.py)提供 dict 风格的 API,并具备额外特性,是功能最完整的 item 类型。Item 对象复制了标准 dict API(包括 __init__ 方法),并允许声明字段名,从而带来两个关键好处:
- 使用未声明的字段名时抛出
KeyError,防止拼写错误被默默放过; - item 导出器(exporters)默认导出所有已声明字段,即使第一个被抓取到的对象并没有填充全部字段。
Item 还支持定义字段元数据,可用于自定义序列化行为(见 docs/topics/exporters.rst)。
from scrapy.item import Item, Field
class CustomItem(Item):
one_field = Field()
another_field = Field()
从源码结构看,Item 还混入了 object_ref 基类(scrapy/item.py 第 72 行:class Item(MutableMapping[str, Any], object_ref, metaclass=ItemMeta)),用于追踪 Item 实例以帮助定位内存泄漏(详见下文第五节)。
3. dataclass 对象
:func:dataclasses.dataclass`` 允许声明带字段名的 item 类,导出器因此也能默认导出全部字段。此外 dataclass 类型的 item 还能:
- 定义每个字段的类型和默认值;
- 通过
dataclasses.field定义自定义字段元数据,用于自定义序列化。
from dataclasses import dataclass
@dataclass
class CustomItem:
one_field: str
another_field: int
注意:字段类型注解不会在运行时强制校验。
4. attrs 对象
attr.s 同样允许声明带字段名的 item 类,导出器也能默认导出全部字段。它还可以:
- 定义每个字段的类型和默认值;
- 通过
attr.ib定义自定义字段元数据(metadata),用于自定义序列化。
使用该类型需要安装 attrs 包。
import attr
@attr.s
class CustomItem:
one_field = attr.ib()
another_field = attr.ib()
5. Pydantic 模型
Pydantic 模型允许声明带字段名的 item 类,并且还能:
- 定义每个字段的类型、默认值,并在运行时进行类型校验;
- 通过
pydantic.Field定义自定义字段元数据,用于自定义序列化; - 享受基于类型注解的自动数据校验与类型转换。
使用该类型需要安装 pydantic 包。
from pydantic import BaseModel, Field
class CustomItem(BaseModel):
one_field: str = Field(default="", description="First field")
another_field: int = Field(default=0, description="Second field")
与其他 item 类型不同,Pydantic 模型在运行时强制字段类型,非法数据类型会触发校验错误。
五类 item 的核心差异可以概括为:
| 类型 | 声明字段名(防拼写错误/导出全部字段) | 类型注解 | 运行时类型强制 | 默认值 | 自定义序列化元数据 | 额外依赖 |
|---|---|---|---|---|---|---|
| dict | 否 | 无 | 无 | 无 | 无 | 无 |
| Item | 是(Field) |
无 | 无 | 可放入 Field 字典 |
Field(...) 键值对 |
无 |
| dataclass | 是 | 有(不强制) | 无 | 支持 | dataclasses.field |
无 |
| attrs | 是 | 有 | 无 | 支持 | attr.ib(metadata=...) |
attrs |
| Pydantic | 是 | 有(强制) | 有 | 支持 | pydantic.Field |
pydantic |
三、声明 Item 子类
Item 子类使用简单的类定义语法加上 Field 对象声明。官方文档给出的经典示例是:
import scrapy
class Product(scrapy.Item):
name = scrapy.Field()
price = scrapy.Field()
stock = scrapy.Field()
tags = scrapy.Field()
last_updated = scrapy.Field(serializer=str)
熟悉 Django 的读者会注意到 Scrapy Item 的声明方式与 Django Model 相似,但 Scrapy Item 简单得多——它没有不同字段类型的概念。
四、Field:一个"纯 dict"的字段元数据容器
Field 对象用于指定每个字段的元数据,例如上文 last_updated 字段上的 serializer 序列化函数。
关键要点(与源码一致):
- 元数据没有固定 schema。
Field对象接受任意键值,没有任何限制;因此也不存在"全部可用元数据键"的官方清单。Field中定义的每个键都可能被某个组件使用,只有那些组件知道它。你可以在自己的项目中定义并使用其他任意键。Field的主要目标就是把所有字段元数据集中定义在一处——行为依赖字段元数据的组件,会用特定字段键来配置自身行为,具体键名需查阅各组件文档。 - 声明用的 Field 对象不会保留为类属性。它们被元类收集后通过
Item.fields属性访问,例如Product.fields["name"]返回该字段的元数据字典。
从源码看(scrapy/item.py),Field 就是内建 dict 的一个子类,不提供任何额外功能:
class Field(dict[str, Any]):
"""Container of field metadata"""
单独的类只是为了支持基于类属性的 item 声明语法。
ItemMeta 元类如何工作
真正完成"把类属性里的 Field 收集进 fields 字典"这一动作的是 ItemMeta 元类(scrapy/item.py 第 47–69 行):
ItemMeta.__new__创建临时类后,按_ordered_field_names()遍历 MRO,把每个Field实例从类属性搬入fields字典(第 60–62 行);- 随后生成一个新类字典,剔除非 Field 属性中的
Field值(第 63 行new_attrs = {n: v for n, v in attrs.items() if not isinstance(v, Field)}),并注入fields与_class属性; - 这就是为什么
Product.name拿不到值、必须用Product.fields["name"]的原因,也解释了in product检查"字段是否有值"、in product.fields检查"字段是否被声明"的语义差异。
_ordered_field_names()(第 28–44 行)保证了字段顺序规则:基类字段在前(按最顶层基类到最派生类排列),各保持定义顺序;子类中重新定义的字段保留其首次定义的位置。源码注释中明确记载这一顺序在 2.17.0 版本起取代了此前的字母序(versionchanged 2.17.0),测试用例 tests/test_item.py 中的 test_fields_order 与 test_fields_order_inheritance 分别覆盖了单层与继承场景下的顺序断言,多继承(含菱形继承)场景由 test_metaclass_multiple_inheritance_diamond 等用例保障。
属性访问被刻意禁用
源码中 Item.__getattr__ 与 Item.__setattr__(第 126–134 行)会主动抛出 AttributeError,并提示 "Use item['name'] to get field value"。这强制用户以字典方式读写字段,避免属性访问与字段名冲突。tests/test_item.py 的 test_raise_getattr / test_raise_setattr 用例验证了这一行为;而以下划线开头的私有属性(如 i._private = "test")仍被允许,见 test_private_attr。
五、Item 日常操作 API
以第三节声明的 Product 为例,API 与 dict 非常相似。
创建 item
>>> product = Product(name="Desktop PC", price=1000)
>>> print(product)
{'name': 'Desktop PC', 'price': 1000}
源码中 Item.__init__ 接受任意位置/关键字参数并逐一 self[k] = v 写入(scrapy/item.py 第 108–112 行),因此 Product({"name": "Laptop PC", "price": 1500})、Product(existing_item) 都合法。
读取字段值
>>> product["name"]
Desktop PC
>>> product.get("name")
Desktop PC
>>> product["price"]
1000
>>> product["last_updated"]
KeyError: 'last_updated'
>>> product.get("last_updated", "not set")
not set
>>> product["lala"] # 获取未声明字段
KeyError: 'lala'
>>> product.get("lala", "unknown field")
'unknown field'
>>> "name" in product # name 字段是否有值?
True
>>> "last_updated" in product # last_updated 是否有值?
False
>>> "last_updated" in product.fields # last_updated 是否为声明字段?
True
>>> "lala" in product.fields # lala 是否为声明字段?
False
in product 判断"该字段当前是否有值",in product.fields 判断"该字段是否被声明",这是 Item 相对 dict 最重要的语义区分。
写入字段值
>>> product["last_updated"] = "today"
>>> product["last_updated"]
today
>>> product["lala"] = "test" # 设置未声明字段
KeyError: 'Product does not support field: lala'
这个精确的错误信息来自 Item.__setitem__(scrapy/item.py 第 117–121 行):
def __setitem__(self, key: str, value: Any) -> None:
if key in self.fields:
self._values[key] = value
else:
raise KeyError(f"{self.__class__.__name__} does not support field: {key}")
tests/test_item.py::test_invalid_field 验证了未声明 Item(无 Field 的类)写入任意字段都会抛 KeyError。
访问所有已填充的值
使用标准 dict API 即可:
>>> product.keys()
['price', 'name']
>>> product.items()
[('price', 1000), ('name', 'Desktop PC')]
复制 item:浅拷贝与深拷贝
复制前先决定要浅拷贝还是深拷贝。如果 item 包含列表、字典等可变值,浅拷贝会在所有副本间共享这些可变对象:
- 若 item 有一个 tags 列表,浅拷贝后原 item 与副本持有同一个列表,往其中任何一个追加标签,另一个也会变;
- 若这不是期望行为,请使用深拷贝。
具体操作:
- 浅拷贝:调用
item.copy()(product2 = product.copy()),或用已有 item 实例化你的 item 类(product2 = Product(product))。源码中copy的实现即return self.__class__(self)(scrapy/item.py第 150–151 行); - 深拷贝:调用
item.deepcopy()(product2 = product.deepcopy()),内部使用标准库copy.deepcopy(第 153–155 行)。
tests/test_item.py::test_copy 验证浅拷贝修改副本不影响原 item(对不可变值而言),test_deepcopy 验证深拷贝后 item["tags"].append("tag2") 不会传染到副本的 tags。更多细节可参考标准库 copy 模块文档。
其他常见任务
从 item 创建 dict:
>>> dict(product) # 从所有已填充的值创建 dict
{'price': 1000, 'name': 'Desktop PC'}
从 dict 创建 item:
>>> Product({"name": "Laptop PC", "price": 1500})
{'name': 'Laptop PC', 'price': 1500}
>>> Product({"name": "Laptop PC", "lala": 1500}) # 警告:dict 中包含未声明字段
KeyError: 'Product does not support field: lala'
注意最后一行:__init__ 会逐键走 __setitem__,所以 dict 构造同样受未声明字段检查约束,tests/test_item.py::test_init 对此有专门断言。
六、扩展 Item 子类
可以通过声明原 Item 的子类来扩展 Item(增加字段,或修改某些字段的元数据):
class DiscountedProduct(Product):
discount_percent = scrapy.Field(serializer=str)
discount_expiration_date = scrapy.Field()
还可以基于父类已有的元数据来扩展字段元数据——保留原有值,追加或覆盖新值:
class SpecificProduct(Product):
name = scrapy.Field(Product.fields["name"], serializer=my_serializer)
这会给 name 字段添加(或替换)serializer 元数据键,同时保留此前已有的全部元数据值。
对应源码机制:ItemMeta.__new__ 中 fields = getattr(_class, "fields", {}) 先继承父类的 fields 字典,再遍历当前类新声明的 Field 覆盖同名键(scrapy/item.py 第 60–62 行)。tests/test_item.py::test_metaclass_multiple_inheritance_diamond 用菱形继承精确验证了这一合并优先级(例如 D.fields == {"save": {"default": "C"}, "load": {"default": "D"}, "update": {"default": "D"}}),以及非 Item 基类中的 Field 不会被纳入(test_metaclass_multiple_inheritance_without_metaclass)。
七、编写支持所有 Item 类型的代码:ItemAdapter
在接收 item 的代码中(item 管道、Spider 中间件等),良好的实践是使用 itemadapter.ItemAdapter 类编写兼容所有受支持 item 类型的代码。Scrapy 自身的实现正是这一范式的示范:
- 导出器
scrapy/exporters.py通过ItemAdapter(item).field_names()获取全部声明字段名(例如 JSONItemExporter 在首个 item 到达时用ItemAdapter(item).field_names()初始化fields_to_export,从而"即使第一个对象字段不全也导出所有字段"),遍历字段值时用for key, value in ItemAdapter(item).items(); - 序列化工具
scrapy/utils/serialize.py的ScrapyJSONEncoder.default中,凡is_item(o)为真即返回ItemAdapter(o).asdict(),使 Item 能被标准 JSON 编码; - 媒体管道
scrapy/pipelines/files.py、scrapy/pipelines/images.py,合同scrapy/contracts/default.py,以及scrapy/commands/parse.py同样依赖ItemAdapter访问字段。
新项目的脚手架模板(如 scrapy/templates/project/module/pipelines.py.tmpl、scrapy/templates/project/module/middlewares.py.tmpl)中给出的管道/中间件骨架也导入并使用 ItemAdapter,docs/topics/item-pipeline.rst 与 docs/topics/spider-middleware.rst 描述了它们的运行位置。
八、Item 实例追踪:定位内存泄漏
文档特别指出:scrapy.utils.trackref 模块会追踪 Item 对象以帮助查找内存泄漏。结合源码(scrapy/utils/trackref.py):
object_ref.__new__在每次实例化时把对象注册进全局live_refs(一个WeakKeyDictionary嵌套结构),并记录monotonic_ns()时间戳;由于Item混入了object_ref,每个 Item 实例都被自动追踪;format_live_refs(ignore=NoneType)输出按类名分组、带存活数量与最老实例年龄的表格报告;get_oldest(class_name)返回某类中最老的对象实例,iter_all(class_name)返回某类所有存活实例,便于逐一排查谁持有了引用。
文档同时提醒:PyPy 使用追踪式垃圾回收器,对象可能在 live_refs 中停留得比预期更久,必要时需显式触发 GC 或调用 trackref.live_refs.clear()。相关使用场景见 docs/topics/leaks.rst。
九、小结与延伸阅读
本文覆盖的 scrapy.item 模块由三个核心符号构成:Item(基于 MutableMapping 的数据容器)、Field(纯 dict 元数据容器)与 ItemMeta(负责字段收集、继承合并与顺序维护的元类),完整实现见 item 实现,行为验证见 item 测试。
延伸阅读(均为仓库内相对路径):
- items 官方文档:本文骨架来源;
- exporters 文档 与 exporters 源码:字段元数据(如
serializer)如何影响导出与字段序列化; - item pipeline 文档:item 流转的下一站;
- 内存泄漏文档:结合 trackref 排查 Item 堆积;
- trackref 源码:对象追踪的具体实现。
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