
Scrapy SEP-020 深度解读Bulk Item Loader —— 用模式识别自动填充 Item Loader 的提案与实践【免费下载链接】scrapyScrapy, a fast high-level web crawling scraping framework for Python.项目地址: https://gitcode.com/GitHub_Trending/sc/scrapy导读本文围绕 Scrapy 官方 Enhancement ProposalSEP体系中的SEP-020: Bulk Item Loader展开。它提出既然 Item Loader 解决了如何填充 Item的便捷性问题那么同样可以有一个批量层面的工具自动识别页面中结构规整的标记模式如dl定义列表、table表格等把字段名 字段值一次性地灌入 Item Loader从而免去逐字段硬编码 XPath。读完本文你将完整理解该提案的设计动机、工作方式、核心代码实现与扩展思路同时掌握它与现代 Scrapyscrapy.loader之间的 API 映射关系。背景说明SEP 目录sep/存放 Scrapy Enhancement ProposalsScrapy 增强提案其中多数由旧 Trac Wiki 迁移而来参见 sep/README.rst。SEP-020 标注为Draft草案状态作者 Steven Almeroth创建于 2012-02-24并在页首注明This SEP has been migrated from the Wiki。因此文中代码属于历史提案示例需结合当时与当前的 API 差异理解。一、提案的立足点Item Loader 之上再加一层批量SEP-020 的出发点非常明确。官方文档在 docs/topics/loaders.rst 中写道Item Loaders provide a convenient mechanism for populating scraped itemsItem Loader 为填充爬取到的 Item 提供了便捷机制。Item 提供装数据的容器而 Item Loader 提供填充这个容器的机制详见 docs/topics/loaders.rst。SEP-020 顺着这一层抽象继续向上延伸提出Bulk Item Loader批量 Item LoaderJust as Item Loaders provide a convenient mechanism for populating scraped Items, the Bulk Item Loader provides a convenient mechanism for populating Item Loaders.也就是说当面对成批的、同构的字段名/字段值结构时程序员不应该手写几十行add_xpath而应当让 Loader 自己看懂页面的标记结构并自动填充。二、设计动机Rationale哪些 HTML 模式适合自动解析现实中存在许多天然适合自动化解析的标记模式提案点名了两类table表格tr代表一行tr内的td代表一个个字段——天然对应一条数据库记录。定义列表dl这是提案认为尤其适合自动填充 Item Loader 的模式。dtdefinition term放字段名紧随其后的dddefinition description放字段值。提案给出了经典的 W3C HTML 4.01 风格示例div classgeeks dl dt hacker dd a clever programmer dt nerd dd technically bright but socially inept person /dl /div对应地W3C 的 The ingredients配料、The procedure步骤类页面结构正好是一组组dt/dd键值对这正是本提案意欲自动化处理的典型场景。该示例页即是提案示例 Spider 抓取的http://www.w3.org/TR/html401/struct/lists.html详见下文示例 Spider部分。在这一结构里每个dt承载字段name它后面紧跟的dd承载字段value。三、工作方式从逐个硬编码到给定一个种子点SEP 明确指出没有批量加载器时程序员需要把所有条目逐一硬编码有了批量加载器只需提供一个种子点seed pointLoader 即可根据模式自动完成余下的工作。3.1 Before手工逐字段 XPath传统写法需要针对每个字段拼接 XPath 并单独调用一次add_xpathxpath //div[classgeeks]/dl/dt[contains(text(),%s)]/following-sibling::dd[1]//text() gl XPathItemLoader(responseresponse, itemdict()) gl.default_output_processor Compose(TakeFirst(), lambda v: v.strip()) gl.add_xpath(hacker, xpath % hacker) gl.add_xpath(nerd, xpath % nerd)这段代码的问题很明显字段每多一个就要多写一行几乎一样的调用而且 XPath 本身还是用%字符串格式化拼出来的维护性差、易出错。3.2 After一行触发批量解析在 BulkItemLoader 的帮助下同样的逻辑被压缩成两步bil BulkItemLoader(responseresponse) bil.parse_dl(//div[classgeeks]/dl)只要给出定义列表的 XPath 位置即种子点parse_dl就会自动遍历其中的所有dt/dd键值对并填充 Loader。四、核心代码提案逐行拆解这是 SEP-020 给出的仅覆盖基础功能的可运行示例也是整个提案的代码骨架。注意其中 import 的是历史路径scrapy.contrib.loader旧版模块位置在现代 Scrapy 中对应scrapy.loaderfrom scrapy.contrib.loader import XPathItemLoader from scrapy.contrib.loader.processor import MapCompose class BulkItemLoader(XPathItemLoader): Item loader based on specified pattern recognition default_item_class dict base_xpath //body ignore () def _get_label(self, entity): Pull the text label out of selected markup :param entity: Found markup :type entity: Selector label .join(entity.xpath(.//text()).extract()) label label.encode(ascii, xmlcharrefreplace) if label else label label.strip(#160;) if #160; in label else label label label.strip(:) if : in label else label label label.strip() return label def _get_entities(self, xpath): Retrieve the list of selectors for a given sub-pattern :param xpath: The xpath to select :type xpath: String :return: The list of selectors :rtype: list return self.selector.xpath(self.base_xpath xpath) def parse_dl(self, xpath//dl): Look for the specified definition list pattern and store all found values for the enclosed terms and descriptions. :param xpath: The xpath to select :type xpath: String for term in self._get_entities(xpath /dt): label self._get_label(term) if label and label not in self.ignore: value term.xpath(following-sibling::dd[1]//text()) if value: self.add_value( label, value.extract(), MapCompose(lambda v: v.strip()) )4.1 类级配置三个可调参数属性默认值作用default_item_classdict当实例化时未显式传入 item 时用该工厂自动创建容器。这里直接用dict即最终得到一个普通字典而非 Scrapy Itembase_xpath//body所有传入的 XPath 都会先拼接此前缀。相当于给后续所有parse_*方法设一个根作用域便于整体限定在页面某区域ignore()一个标签元组黑名单_get_label得到的标签若命中该集合则被跳过不会写入 Loader这种子类级默认配置 方法级参数覆盖的设计与现代 Item Loader 的处理器声明方式异曲同工现代的 Item Loader 通过default_input_processor、default_output_processor、default_selector_class等类属性定制行为参见 scrapy/loader/init.py 中ItemLoader的 docstring 与类属性定义default_item_class: type Item、default_selector_class Selector。4.2_get_label(entity)从标记中提取干净的字段名这是字段名清洗的核心方法按顺序执行一系列处理 .join(entity.xpath(.//text()).extract())取出该节点下所有文本并拼接——字段名内部可能被多个文本节点如内联标签、换行切碎需要先合并。label.encode(ascii, xmlcharrefreplace)若文本含非 ASCII 字符将其转成#NNN;形式的 XML 字符引用如#160;表示不间断空格从而保证字段名是纯 ASCII、可安全用作字典键。若label为空则不转换。若包含#160;则用strip(#160;)剥掉首尾的字符引用。若包含:则用strip(:)剥掉冒号如 W3C 示例页中的The ingredients:→The ingredients。最后再做一次strip()清理空白。4.3_get_entities(xpath)批量取出子模式的选择器列表return self.selector.xpath(self.base_xpath xpath)把类级base_xpath前缀与方法传入的 XPath 拼接后执行查询返回 Selector 列表。self.selector来自 Item Loader 的selector/response机制详见下文现代实现映射。4.4parse_dl(xpath//dl)核心批量解析入口工作流程是清晰的三步循环先查xpath /dt拿到所有dt节点列表对每个dt用_get_label提取并清洗出字段名label过滤label非空 且 不在ignore黑名单中用相对 XPathfollowing-sibling::dd[1]//text()取该dt紧随其后的第一个dd内的全部文本节点若取到值就调用self.add_value(label, value.extract(), MapCompose(lambda v: v.strip()))——把字段名作为 key、dd内所有文本节点作为原始值列表写入 Loader并由输入处理器MapCompose(lambda v: v.strip())对每个值做strip()清洗extract()返回列表正好对应 Item Loader 内部以列表收集数据的语义见 docs/topics/loaders.rst 中Collected data is internally stored as lists的说明。这里复用 Item Loader 的add_value意味着所有已有的字段处理器、上下文机制都被天然继承这是该提案能站在 Item Loader 肩膀上的根本原因。五、示例 Spider抓取 W3C 列表页面提案给出了一个配套示例 Spider运行后可以验证parse_dl在真实页面上的输出。5.1 Spider 代码from scrapy.spider import BaseSpider from scrapy.contrib.loader.bulk import BulkItemLoader class W3cSpider(BaseSpider): name w3c allowed_domains [w3.org] start_urls (http://www.w3.org/TR/html401/struct/lists.html,) def parse(self, response): el BulkItemLoader(responseresponse) el.parse_dl(//dl[2]) item el.load_item() from pprint import pprint pprint(item)注意示例中的from scrapy.contrib.loader.bulk import BulkItemLoader表明该 BulkItemLoader 原计划作为一个独立的bulk子模块对外暴露对应scrapy.contrib.loader.bulk.py文件。示例里调用el.parse_dl(//dl[2])只指定了页面中的第二个dl作为种子点最后用load_item()产出最终字典。关于示例用到的旧版 API 与现代对应关系提案写于 2012 年彼时 Scrapy 0.17提案中的历史 API现代 Scrapy 中的对应scrapy.spider.BaseSpiderscrapy.Spiderscrapy.contrib.loader.XPathItemLoaderscrapy.loader.ItemLoaderXPath/CSS 方法已合并见下方说明scrapy.contrib.loader.processor.MapComposeitemloaders.processors.MapCompose以及TakeFirst、Compose、Join等.extract().getall()单值可用.get()现代scrapy.loader.ItemLoader源码见 scrapy/loader/init.py继承自itemloaders.ItemLoader其 docstring 明确说明当以response实例化时会用default_selector_class默认Selector自动构造选择器无需显式创建XPathItemLoader这类子类——因为现代ItemLoader同时提供add_xpath、add_css、add_value等方法。若按现代写法改写上述 Spider 的实例化部分等价于from scrapy.loader import ItemLoader el ItemLoader(responseresponse) # 自动以 response 构造 Selector由于本次目标是示范parse_dl这类自定义方法最贴合的做法是让自定义的BulkItemLoader继承scrapy.loader.ItemLoader而非历史同名类这也体现了提案思想在现代代码库中落地时只需替换基类即可。5.2 实际运行日志与输出提案贴出了 2012-11-19 的运行日志完整展示了当时 Scrapy 0.17.0 的启动过程依次启用的 extensionsLogStats、TelnetConsole、CloseSpider、WebService、CoreStats、SpiderState、downloader middlewaresHttpAuthMiddleware、DownloadTimeoutMiddleware、UserAgentMiddleware、RetryMiddleware 等、spider middlewares最终请求http://www.w3.org/TR/html401/struct/lists.html返回 200 并缓存命中。去除噪声后的核心结果是页面中dl[2]内的键值对全部被自动解析成了一个字典{Notes: [uThe recipe may be improved by adding raisins.], The ingredients: [u, u100 g. flour, u, u10 g. sugar, u, u1 cup water, u, u2 eggs, u, usalt, pepper, u], The procedure: [u, uMix dry ingredients thoroughly., u, uPour in wet ingredients., u, uMix for 10 minutes., u, uBake for one hour at 300 degrees., u]}这份输出清晰印证了几个实现细节字段名中的冒号被_get_label正确剥掉页面中原文是The ingredients:、The procedure:、Notes:输出中已无冒号每个字段的值保持列表形态如The ingredients含 10 个元素其中空字符串来自dd内text()多节点 换行空白这正符合 Item Loader内部以列表收集、输出处理器负责合并的约定空字符串元素尚未被过滤说明若要更干净的结果可在输出处理器层追加Compose(TakeFirst(), ...)之类过滤步骤对应提案 Before 示例中的default_output_processor用法。六、Notes可扩展的其他解析器与嵌入式智能SEP-020 在文末明确指出parse_dl只是起点同样的思路可以轻易扩展出更多解析器parse_table()配合哪一列是 key、哪一列是 value的列位指定将table/tr/td结构批量入库parse_ul()配合 key/value 分隔符的指定解析无序列表parse_ol()同parse_ul针对有序列表parse()直接指定 key/value 对应的标签做通用解析。提案更进一步抛出embedded intelligence嵌入式智能的概念如果再做一点什么内容该放哪里的引导bootstrapping甚至可以让一个通用解析器直接走出去把所有上述结构都抓下来——即让框架具备对页面语义结构的自适应识别能力而不再依赖逐字段编程。这正是该提案在思想层面最有价值的遗产把结构模式识别从业务代码中抽离成可复用、可组合的 Loader 策略。SEP 中提到的parse_table/parse_ul等在未来往往不再是硬编码字段的 XPath而是声明式的模式 命名规则。七、与当前仓库的结合现代实现映射与落地建议7.1 提案代码与现代scrapy.loader的对应关系当前仓库的 Item Loader 实现在 scrapy/loader/init.py要点如下可直接用于验证或迁移 SEP-020 的代码class ItemLoader(itemloaders.ItemLoader)Scrapy 的 Loader 是对第三方库 itemloaders仓库说明见 docs/topics/loaders.rst的封装叠加了对 ScrapyResponse/Selector的自动支持__init__中若只传response而未传selector会用default_selector_class(response)自动构造 Selector见 scrapy/loader/init.py这正是 SEP 示例中BulkItemLoader(responseresponse)能直接用self.selector的原因类属性default_item_class Item、default_selector_class Selector见 scrapy/loader/init.py与 SEP 中default_item_class dict的用意一致现代默认产出Item而 SEP 示例选dict以求最简输出。7.2 处理器从何而来SEP 提案中的MapCompose、Compose、TakeFirst在旧版路径scrapy.contrib.loader.processor下现代用法是在 Loader 定义或 Item Field 元数据中声明_in/_out后缀属性处理器从itemloaders.processors导入例如from itemloaders.processors import TakeFirst, MapCompose, Join详见 docs/topics/loaders.rst 中 Input and Output processors 一节处理器接收迭代对象、Loader 内部按列表收集数据、输入处理器逐元素 map、输出处理器作用于整体。SEP-020 的parse_dl把MapCompose(lambda v: v.strip())作为第三个参数传给add_value等价于为本次收集动态指定了一个内联输入处理器——现代 Item Loader 的add_value同样接受该参数因此该方法在现代代码中依旧成立。7.3 在 2.0 语法下重写的可运行版参考结合上述映射把 SEP-020 的核心类迁移到当前仓库 API 后大致如下继承 scrapy/loader/init.py 中的ItemLoader并把extract()换为getall()、XPathItemLoader换为ItemLoaderfrom scrapy.loader import ItemLoader from itemloaders.processors import MapCompose class BulkItemLoader(ItemLoader): Item loader based on specified pattern recognition default_item_class dict # 或以自定义 Item 类替换 base_xpath //body ignore () def _get_label(self, entity): label .join(entity.xpath(.//text()).getall()) label label.encode(ascii, xmlcharrefreplace) if label else label label.strip(#160;) if #160; in label else label label label.strip(:) if : in label else label return label.strip() def _get_entities(self, xpath): return self.selector.xpath(self.base_xpath xpath) def parse_dl(self, xpath//dl): for term in self._get_entities(xpath /dt): label self._get_label(term) if label and label not in self.ignore: value term.xpath(following-sibling::dd[1]//text()) if value: self.add_value( label, value.getall(), MapCompose(lambda v: v.strip()) )用的时候依然是两个调用bil BulkItemLoader(responseresponse) bil.parse_dl(//div[classgeeks]/dl) # 或 //body//dl 等任意种子点 item bil.load_item()7.4 需要警惕的两个边界从代码与 SEP 状态出发落地时有两点须注意SEP 仍为草案SEP-020 状态是DraftBulkItemLoader 并未以独立内置模块的形式进入现代 Scrapy 主包当前仓库scrapy/loader/下不存在bulk.py因此它应被视作设计模式参考/用户侧扩展模板而非官方开箱功能。使用时应按 §7.3 的方式在项目内自行实现。结果需要后处理如 §5.2 所示parse_dl的原始输出是包含空串的列表值字段名清洗也依赖页面格式冒号、#160;等。在实际爬虫中通常要配合default_output_processor Compose(TakeFirst(), lambda v: v.strip())即 SEP 中 Before 示例的组合或自定义输出处理器才能得到干净的单值字段——这正好再次印证了 Item Loader 体系中输入处理器负责清洗逐条值、输出处理器负责汇总的分工。八、小结一份值得借鉴的结构自动化设计蓝图SEP-020 虽以Draft收场且代码停留在 2012 年的 API 快照上但它的设计思想在今日依然成立且具有直接的工程参考价值抽象层级递进Item → Item Loader → Bulk Item Loader每一层都在减少重复劳动parse_dl用一个种子点 一条模式规则替代 N 条add_xpath是约定优于配置在抓取层的朴素实践。复用而非重写批量解析完全建立在 Item Loader 的selector、add_value、处理器与上下文机制之上没有另起炉灶这是该方案能被现代scrapy.loader.ItemLoader平滑承接的关键。面向未来的嵌入式智能parse_table/parse_ul/parse与embedded intelligence的设想本质是把结构化标记 → 键值对的识别从手写 XPath 中解放出来——今天你可以用同一个BulkItemLoader模式把内容站点中形形色色的dl、table列表页一次解析处处复用。若你的爬虫正反复面对结构统一的键值对型页面规格表、属性列表、词汇表、数据字典等不妨以 SEP-020 为蓝图在 scrapy/loader/init.py 提供的ItemLoader之上扩展一个属于自己的BulkItemLoader再配合 docs/topics/loaders.rst 中的输入/输出处理器约定完成清洗——你会发现距离给一个种子点整页结构化数据自动到手并不遥远。【免费下载链接】scrapyScrapy, a fast high-level web crawling scraping framework for Python.项目地址: https://gitcode.com/GitHub_Trending/sc/scrapy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考