首页
/ Scrapy Item 完全指南:scrapy.item 中 Item、Field 元数据与多种 Item 类型的实现原理

Scrapy Item 完全指南:scrapy.item 中 Item、Field 元数据与多种 Item 类型的实现原理

2026-09-04 23:01:53作者:宣利权Counsellor

本文基于 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 对象

Itemscrapy/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 序列化函数。

关键要点(与源码一致):

  • 元数据没有固定 schemaField 对象接受任意键值,没有任何限制;因此也不存在"全部可用元数据键"的官方清单。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_ordertest_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.pytest_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.pyScrapyJSONEncoder.default 中,凡 is_item(o) 为真即返回 ItemAdapter(o).asdict(),使 Item 能被标准 JSON 编码;
  • 媒体管道 scrapy/pipelines/files.pyscrapy/pipelines/images.py,合同 scrapy/contracts/default.py,以及 scrapy/commands/parse.py 同样依赖 ItemAdapter 访问字段。

新项目的脚手架模板(如 scrapy/templates/project/module/pipelines.py.tmplscrapy/templates/project/module/middlewares.py.tmpl)中给出的管道/中间件骨架也导入并使用 ItemAdapterdocs/topics/item-pipeline.rstdocs/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 测试

延伸阅读(均为仓库内相对路径):

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384