ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Scrapy SEP-001:Item 字段填充 API 的设计之争——ItemForm 与 ItemBuilder 对比及其对 Item Loader 的深远影响

Scrapy SEP-001:Item 字段填充 API 的设计之争——ItemForm 与 ItemBuilder 对比及其对 Item Loader 的深远影响 Scrapy SEP-001Item 字段填充 API 的设计之争——ItemForm 与 ItemBuilder 对比及其对 Item Loader 的深远影响【免费下载链接】scrapyScrapy, a fast high-level web crawling scraping framework for Python.项目地址: https://gitcode.com/GitHub_Trending/sc/scrapySEP-001 是 Scrapy 增强提案Scrapy Enhancement Proposal中关于Item 字段填充 API的历史设计文档通过七个典型使用场景系统对比了 ItemForm 与 ItemBuilder 两种候选方案的 API 形态、优劣势与适用边界。阅读本文你将理解 Scrapy 早期在如何优雅地用选择器填充 Item这一问题上的设计权衡过程并能顺着提案的演进脉络看懂当前scrapy.loader.ItemLoaderAPIadd_value/replace_value/load_item的历史由来与设计基因。一、SEP-001 的定位一场 API 选型之争SEP-001 由 Ismael Carnales、Pablo Hoffman、Daniel Grana 于 2009-07-19 提出状态标记为Obsoleted by SEP-008见 sep/sep-001.rst 头部元数据。它要解决的问题非常具体Scrapy 早期使用已废弃的!RobustItemAPI需要为其选择一个替代方案在 Scrapy 0.7 中作为推荐并受支持的Item 字段填充机制。提案给出了两个候选ItemForm表单式__setitem__风格与ItemBuilder构建器式显式方法风格。整个仓库的sep/目录收录了从 Trac 迁移过来的全部提案见 sep/README.rstSEP-001 是其中Item 填充 API系列讨论的起点后续的 SEP-002、SEP-003、SEP-005 围绕同一主题继续讨论最终被 SEP-008Item Loaders 统一终结。二、三个候选 API 的形态SEP-001 首先列出了三个候选的完整 API 签名这是全文技术讨论的基础2.1 RobustItem旧 API已废弃attribute(field_name, selector_or_value, **modifiers_and_adaptor_args)提案中明确指出其缺陷attribute()的修饰符如addTrue不得不和 adaptor 参数混在一起以关键字参数传入作者评价这种方式 this is ugly。2.2 ItemForm表单式方法职责__init__(response, itemNone, **adaptor_args)用预定义的 adaptor 参数实例化可传入既有 item 实例__setitem__(field_name, selector_or_value)设置字段值__getitem__(field_name)返回字段的计算后值即最终会写入 item 的值未设置时返回Noneget_item()返回已填充数据的 item2.3 ItemBuilder构建器式方法职责__init__(response, itemNone, **adaptor_args)用预定义的 adaptor 参数实例化add_value(field_name, selector_or_value, **adaptor_args)向字段追加值replace_value(field_name, selector_or_value, **adaptor_args)替换字段已有值get_value(field_name)返回字段的计算后值未设置时返回Noneget_item()返回已填充数据的 item两者结构高度对称核心分歧点在于赋值语义是用ia[field] value的字典风格表达还是用ib.add_value(field, value)/ib.replace_value(field, value)的显式方法表达。三、优劣对比提案如何权衡提案对两个候选的优缺点做了明确列表值得逐条理解其设计含义ItemForm优点与 Item 本身使用的 API 保持一致Item 就是字典风格见 docs/topics/items.rst一部分开发者认为 setitem API 比方法式 API 更优雅。缺点赋值时无法向 adaptor 传递运行期参数。如果某个 spider 需要对 adaptor 传入特定参数只能为该 spider 覆写 adaptor带来额外负担。中性结论用标准的__add__与list.append()机制解决了addTrue的问题即ia[field] value天然表示追加。ItemBuilder优点允许在赋值时向 adaptor 传递运行期参数add_value(..., keyvalue)。缺点与 ItemForm 的优点互为镜像——认为 setitem 更优雅的人会觉得方法式啰嗦。中性结论通过不同动作对应不同方法add_value追加 /replace_value替换的方式解决了addTrue问题。从源码结构看这一权衡的关键变量是adaptor后来的 processor是否需要按字段、按调用点传参。若 adaptor 参数在类定义期就能确定两种方案等价只有运行期传参需求ItemBuilder 才体现优势。四、七个使用场景逐一对比SEP-001 的精华在于用同一组业务场景新闻页抓取让两个候选各写一遍让差异在具体代码中可见。以下完整保留原文档示例adaptor 即后来 Item Loader 中 input/output processor 的前身。4.1 定义 adaptor类声明期ItemFormclass NewsForm(ItemForm): item_class NewsItem url adaptor(extract, remove_tags(), unquote(), strip) headline adaptor(extract, remove_tags(), unquote(), strip)ItemBuilderclass NewsBuilder(ItemBuilder): item_class NewsItem url adaptor(extract, remove_tags(), unquote(), strip) headline adaptor(extract, remove_tags(), unquote(), strip)此场景下两者完全等价——adaptor 以类属性方式声明与响应无关这正是后来 Item Loader 中name_in/name_out类属性声明方式的雏形见 sep/sep-008.rst 中name_in parsers.MapConcat(...)、price_out parsers.TakeFirst()的声明风格。4.2 创建一个 ItemItemFormx为选择器对象ia NewsForm(response) ia[url] response.url ia[headline] x.x(//h1[classheadline]) # 向同一字段追加一个值 ia[headline] x.x(//h1[classheadline2]) # 用新值替换该字段 ia[headline] x.x(//h1[classheadline3]) return ia.get_item()ItemBuilderil NewsBuilder(response) il.add_value(url, response.url) il.add_value(headline, x.x(//h1[classheadline])) # 向同一字段追加一个值 il.add_value(headline, x.x(//h1[classheadline2])) # 用新值替换该字段 il.replace_value(headline, x.x(//h1[classheadline3])) return il.get_item()注意语义映射关系__setitem__一个表达式身兼替换与首次设置两职追加依赖而 ItemBuilder 把追加/替换拆成两个动词方法语义在方法名上显式化。4.3 不同 Spider/站点使用不同 adaptor当不同站点的日期格式不同如需要to_date(%d.%m.%Y)时# ItemForm class SiteNewsFrom(NewsForm): published adaptor(HtmlNewsForm.published, to_date(%d.%m.%Y)) # ItemBuilder class SiteNewsBuilder(NewsBuilder): published adaptor(HtmlNewsBuilder.published, to_date(%d.%m.%Y))两种方案都通过子类覆写类属性解决——这验证了adaptor 参数类定义期可确定时两者等价的判断。4.4 检查正在抽取中的字段值回退逻辑# ItemForm ia NewsForm(response) ia[headline] x.x(//h1[classheadline]) if not ia[headline]: ia[headline] x.x(//h1[classtitle]) # ItemBuilder il NewsBuilder(response) il.add_value(headline, x.x(//h1[classheadline])) if not il.get_value(headline): il.add_value(headline, x.x(//h1[classtitle]))这是抽取失败时换选择器重试的经典爬虫模式。ItemForm 用__getitem__读回计算后值ItemBuilder 用get_value()。值得注意的是get_value()返回的是经 adaptor 计算后的值而非原始存储值这个语义直接延续到了现代 Item Loader 的get_output_value()。4.5 向列表字段追加值# ItemForm依赖 __add__ ia[headline] x.x(//h1[classheadline]) # ItemBuilderadd_value 本身即追加语义 il.add_value(headline, x.x(//h1[classheadline]))这是两种方案最直观的语法差异点ItemForm 的追加需要读者知道背后的约定ItemBuilder 的方法名自解释。4.6 向 adaptor 传递运行期参数核心分歧场景# ItemForm只能在实例化时传参 ia NewsForm(response, default_unitcm) ia[width] x.x(//p[classwidth]) # ItemBuilder可在每次赋值时传参 il.add_value(width, x.x(//p[classwidth]), default_unitcm) # 更高效的替代实例化时传参一次生效 il NewsBuilder(response, default_unitcm) il.add_value(width, x.x(//p[classwidth]))这是 ItemBuilder 唯一具有实质技术优势的场景同一响应中不同字段需要不同参数时ItemForm 无解除非继承覆写ItemBuilder 可以逐调用点指定。4.7 同名参数的多字段区分# ItemForm通过子类绑定不同参数值 class MySiteForm(ItemForm): width adaptor(ItemForm.width, default_unitcm) volume adaptor(ItemForm.width, default_unitlt) ia[width] x.x(//p[classwidth]) ia[volume] x.x(//p[classvolume]) # 另一示例实例化时传参 ia NewsForm(response, encodingutf-8) ia[name] x.x(//p[classname]) # ItemBuilder直接逐调用点传参 il.add_value(width, x.x(//p[classwidth]), default_unitcm) il.add_value(volume, x.x(//p[classvolume]), default_unitlt)此场景是上一节的极端化两个字段复用同一 adaptor 但需要不同单位。ItemForm 被迫引入子类 类属性绑定ItemBuilder 两个add_value调用即完成——这也是提案中 ItemBuilder Pros 一栏的直接论据。五、结果验证从 ItemBuilder 到现代 ItemLoader历史走向与提案预判一致最终落地的 API 继承了ItemBuilder 的方法式形态而非 ItemForm 的 setitem 形态。证据链清晰可查SEP-008状态为 Final (implemented with variations)明确 Obsoletes sep-001, sep-002, sep-003, sep-005即终结了 SEP-001 开启的整场 API 之争。SEP-008 定下的公共 API 为add_value()/replace_value()/populate_item()后更名load_item()并引入get_output_value()、get_stored_values()等读取方法——与 SEP-001 中 ItemBuilder 的add_value/replace_value/get_value一脉相承只是把get_item()重命名为load_item()。当前仓库的实现scrapy/loader/init.py 中ItemLoader继承自独立的itemloaders库版本约束见 pyproject.toml 中itemloaders1.0.1依赖项并扩展了 Scrapy 特有能力构造时接受item/selector/response/parent及任意**context关键字参数写入加载器上下文——对应文档中__init__(response, itemNone, **adaptor_args)的实例化时传参通道即 ItemForm/ItemBuilder 共同的**adaptor_args入口。测试用例印证tests/test_loader.py 中大量用例围绕add_value/load_item展开覆盖单值/列表的四种组合test_add_value_singlevalue_singlevalue等、未知字段告警test_add_value_on_unknown_field等验证了值先收集、后统一处理的数据流收集值内部以列表存储最终由 output processor 归约这正是 SEP-001 中addTrue追加语义的最终实现形态。官方文档 docs/topics/loaders.rst 则说明了 Item Loader 与 Item 的分工items 提供 scraped data 的容器Item Loaders 提供填充该容器的机制——这句话恰好概括了 SEP-001 从诞生起要解决的全部问题。六、对现代开发者的实践启示虽然 SEP-001 本身已被废弃其设计结论已固化在今天的scrapy.loader.ItemLoader中但理解这场争论有三点实用价值理解add_*/replace_*的语义分界add_xpath/add_css/add_value是追加到收集列表replace_*是清空后替换。这套双轨命名不是随意的而是 ItemBuilder 提案不同动作对应不同方法原则的直接遗产避免了 RobustItem 时代addTrue参数混用的丑陋。理解default_*参数与字段级处理器的分层SEP-001 中实例化时传参与赋值时传参两种模式在现代 API 中分别对应构造器的**context写入 ItemLoader.context与default_input_processor/default_output_processor及*field*_in/*field*_out类属性——分层解决参数何时确定的问题。理解 Item 与 ItemLoader 的边界Item 保持字典风格SEP-001 中 ItemForm 与 Item 同 API的优点被保留给了 Item 本身而填充这一动作剥离到 ItemLoader 中以方法式 API 承载——两种候选 API 的优点在最终架构中被拆分安放到了不同组件这是比二选一更成熟的收尾方式。七、小结SEP-001 作为一份API 对比型提案其价值不在任何单一结论而在于用七个对等场景把 setitem 风格与方法式风格的取舍空间完全展开语法优雅性ItemForm与运行期传参能力ItemBuilder之争最终以 SEP-008 的 Item Loaders 方案收束并在当前仓库的 scrapy/loader/init.py 与 tests/test_loader.py 中可完整验证。阅读历史提案是理解现有 API 设计为什么长这样的最短路径。【免费下载链接】scrapyScrapy, a fast high-level web crawling scraping framework for Python.项目地址: https://gitcode.com/GitHub_Trending/sc/scrapy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表