尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Scrapling 解析核心类详解:Selector、Selectors 与 TextHandler、AttributesHandler 的完整工作流

Scrapling 解析核心类详解:Selector、Selectors 与 TextHandler、AttributesHandler 的完整工作流 Scrapling 解析核心类详解Selector、Selectors 与 TextHandler、AttributesHandler 的完整工作流【免费下载链接】Scrapling️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!项目地址: https://gitcode.com/GitHub_Trending/sc/Scrapling本文以 Scrapling 仓库中的解析主类参考文档为核心系统讲解 HTML 解析后的对象模型Selector、Selectors、TextHandler、TextHandlers、AttributesHandler并结合 解析器源码 与 自定义类型源码 逐条印证参数默认值、惰性加载机制与返回值约定帮助你从会调用进阶到知其所以然地掌握 Scrapling 的解析层。读完本文你将能够正确初始化Selector并理解encoding、keep_comments、keep_cdata、adaptive等参数的实际作用熟练使用get_all_text、get/getall、re/re_first、DOM 遍历与选择器生成等核心能力弄清 Scrapling 为什么几乎处处返回TextHandler而非原生str以及只读AttributesHandler背后的性能动机。1. 解析对象模型总览在 Scrapling 中只要你传入 HTML 源码或抓取了一个网页随后打交道的主对象就是Selector。任何选择、导航操作只要结果是页面中的元素返回的都是Selector或Selectors对象。整套对象模型由五个类构成类继承自角色SelectorSelectorsGenerationMixin核心解析引擎包装单个元素或整个页面SelectorsList[Selector]元素集合可链式执行 CSS/XPath/正则TextHandlerstr字符串增强类型支持re、json、clean等TextHandlersList[TextHandler]字符串集合新增re/re_firstAttributesHandlerMapping[str, TextHandler]只读属性字典底层MappingProxyType从 解析器源码 可以看到Selector刻意没有继承lxml.html.HtmlElement。源码 docstring 给出的理由很直接HtmlElement不可 pickle会导致许多引用型任务抛出AssertionError: invalid Element proxy因此 Scrapling 选择包装而非继承在保持更简单接口的同时扩展 lxml 能力。导入方式有两种均可from scrapling import Selector from scrapling.parser import Selector从 包入口 的懒加载映射_LAZY_IMPORTS可以看到from scrapling import Selector实际上也是从scrapling.parser模块中取类的两者指向同一实现当前仓库版本为0.4.13。2. Selector构造参数与解析器底层行为2.1 参数详解content是最重要的参数用于传入待解析的 HTML接受str或bytespage Selector( html.../html, urlhttps://example.com ) # 然后按喜好选择元素 elements page.css(.product)url、adaptive、storage、storage_args是 adaptive自适应定位特性相关的设置详见 adaptive 特性文档 与官方文档 adaptive 章节。解析调整类参数对照 源码构造函数 的签名encoding解析 HTML 使用的编码默认utf-8。它直接传给 lxml 的HTMLParserkeep_comments是否在解析时保留 HTML 注释默认False。源码中对应remove_comments(not keep_comments)parser.py#L142-L151注释默认被丢弃是因为它们可能以多种方式干扰抓取keep_cdata与注释同理cdata 默认被移除以获得更干净的 HTML对应参数strip_cdata(not keep_cdata)。此外源码中还可见两个文档标注为高级特性的参数huge_tree默认True控制 libxml2 对超大文档的保护机制解析大型 HTML 时应保持启用和root内部用于直接传递 etree 对象优先级最高非内部场景不建议使用。当root与content都为None时构造函数会直接抛出ValueErrorparser.py#L118-L119也就是说Selector必须拿着东西才能工作。值得注意的性能设计源码注释parser.py#L254-L258说明tag、text、attrib、html_content等属性被做成了惰性属性——首次访问时才计算并缓存。作者备注这样做让性能测试快了数倍因为避免在初始化大量实例时白白执行这些逻辑。这就是页面及其元素的绝大多数属性都是惰性加载的这一说法的源码级依据。2.2 内容提取text、get_all_text 与 JSON以下面这个页面作为贯穿示例html head titleSome page/title /head body div classproduct-list article classproduct>from scrapling import Selector page Selector(html_doc)递归获取页面全部文本 page.get_all_text() Some page\n\n \n\n \nProduct 1\nThis is product 1\n$10.99\nIn stock: 5\nProduct 2\nThis is product 2\n$20.99\nIn stock: 3\nProduct 3\nThis is product 3\n$15.99\nOut of stock取第一个article后文沿用该示例article page.find(article)同一逻辑递归获取元素全部文本 article.get_all_text() Product 1\nThis is product 1\n$10.99\nIn stock: 5但直接文本article.text为空因为该元素没有直接文本节点 article.text 对照 源码实现get_all_text先用预编译的 XPath.//text()收集所有文本节点再通过ignore_tags过滤掉默认script/style及其全部子孙的不可见文本。其四个可选参数的默认值与文档一致separator拼接分隔符默认\nstrip拼接前是否strip各字符串默认关闭ignore_tags要忽略的标签名元组默认(script, style,)valid_values是否只收集有真实值的文本节点默认开启。返回的文本是TextHandler而非标准字符串。若内容可 JSON 序列化直接调用.json() script page.find(script) script.json() {lastUpdated: 2024-09-22T10:30:00Z, totalProducts: 3}Selector.json()的回退链在 源码 中非常清晰先尝试原始响应体_raw_body根页面保留的原始内容副本再退回text最后退回get_all_text(stripTrue)。这正是后文对 JSON 响应可直接page.json()的底层原因。2.3 标签、属性与 HTML 内容 article.tag article作用于整页时等价于操作根html元素 page.tag html获取元素属性 print(article.attrib) {class: product, data-id: 1}三种等价方式访问单个属性article[class]为 v0.3 新增article.attrib[class] article.attrib.get(class) article[class] # new in v0.3判断属性是否存在class in article.attrib class in article # new in v0.3__getitem__/__contains__在源码 parser.py#L183-L191 中实现且对文本节点做了保护文本节点执行[]会抛TypeErrorin判断返回False。获取元素的 HTML 内容与格式化版本 article.html_content article classproduct>print(article.prettify())article classproduct> page.body html\n head\n titleSome page/title\n /head\n ...2.4 DOM 路径与选择器生成获取元素在 DOM 树中的全部祖先 article.path [datadiv classproduct-list article clas... parentbody div classproduct-list artic..., databody div classproduct-list artic... parenthtmlheadtitleSome page/title/he..., datahtmlheadtitleSome page/title/he...]在可能时生成 CSS 简化解或生成完整选择器 article.generate_css_selector body div article article.generate_full_css_selector body div articleXPath 同理 article.generate_xpath_selector //body/div/article article.generate_full_xpath_selector //body/div/article生成逻辑位于 SelectorsGeneration Mixin它从当前元素向上回溯优先用id提前截断短选择器否则按父级中同标签第 N 个补充:nth-of-type(n)定位注释中还提到作者刻意不再使用 class因为很多网站在不同元素间复用相同的 class。这解释了为何示例里article本身带productclass生成的却是body div article。3. SelectorDOM 遍历页面是一棵树html是根head与body是它的子节点互为兄弟html是它们的父节点。访问父节点 article.parent datadiv classproduct-list article clas... parentbody div classproduct-list artic... article.parent.tag div与其他同类属性一样支持链式调用 article.parent.parent.tag body获取子节点 article.children [datah3Product 1/h3 parentarticle classproduct> article.below_elements [datah3Product 1/h3 parentarticle classproduct> products_list page.css(.product-list)[0] products_list.children # 仅 3 个 article products_list.below_elements # 3 个 article 所有内部 h3/p/span/div ...获取兄弟节点 article.siblings [dataarticle classproduct> article.next dataarticle classproduct> article.previous # 第一个子节点没有前驱 second_article page.css(.product[data-id2])[0] second_article.previous dataarticle classproduct> article.has_class(product) True遍历整个祖先树for ancestor in article.iterancestors(): # do something with it...搜索满足条件的特定祖先——传入一个接收Selector并返回True/False的函数 article.find_ancestor(lambda ancestor: ancestor.has_class(product-list)) datadiv classproduct-list article clas... parentbody div classproduct-list artic... article.find_ancestor(lambda ancestor: ancestor.css(.product-list)) # 同结果不同思路 datadiv classproduct-list article clas... parentbody div classproduct-list artic...4. Selectors元素集合容器Selectors是Selector的List 版本。它继承 Python 标准List源码 parser.py#L1200-L1205因此拥有全部列表方法与属性同时扩展了面向选择实例的便捷方法。在Selector类中所有应返回一组元素的方法/属性返回的都是Selectors实例。v0.4 的重要约定所有选择方法现在一致地返回Selector/Selectors对象包括文本节点与属性值。文本节点通过::text、/text()、::attr()、/attr选出被包装为Selector其tag为#texttext属性返回文本值其余属性优雅地返回空/默认值page.css(a::text) # - Selectors (of text node Selectors) page.xpath(//a/text()) # - Selectors page.css(a::text).get() # - TextHandler (the first text value) page.css(a::text).getall() # - TextHandlers (all text values) page.css(a::attr(href)) # - Selectors page.xpath(//a/href) # - Selectors page.css(.price_color) # - Selectors这一约定在源码中可印证_is_text_node静态方法parser.py#L194-L204通过判断 lxml 的_ElementUnicodeResult子类型识别文本/属性节点并在tag属性中统一返回#textparser.py#L260-L266。4.1 数据提取get / getall / extract / extract_first自 v0.4 起Selector与Selectors都提供get()、getall()及其别名extract_first、extract沿用 Scrapy 惯例旧的get_all()已移除。在Selector对象上源码实现get()返回TextHandler文本节点选择器返回文本值HTML 元素选择器返回序列化后的外部 HTMLgetall()返回包含单个序列化字符串的TextHandlers列表extract_first是get()的别名extract是getall()的别名。 page.css(h3)[0].get() # 元素的外部 HTML h3Product 1/h3 page.css(h3::text)[0].get() # 文本节点的文本值 Product 1在Selectors对象上源码实现get(defaultNone)返回第一个元素的序列化字符串空列表时返回defaultgetall()序列化所有元素并返回TextHandlers列表别名规则与上相同。 page.css(.price::text).get() # 第一个价格文本 $10.99 page.css(.price::text).getall() # 全部价格文本 [$10.99, $20.99, $15.99] page.css(.price::text).get() # 带默认值 $10.99这些方法与所有选择方式CSS、XPath、find等无缝配合是 Scrapy 风格提取文本与属性值的推荐做法。从 tests/parser 测试目录 的测试用例如 test_general.py也可以看到get/getall/get_all_text等提取路径是被持续回归覆盖的。4.2 链式选择与集合内查询CSS 与 XPath 选择器可直接在Selectors实例上执行返回类型与Selector的css/xpath相同。参数类似但没有adaptive参数从 源码 可见Selectors.css/xpath对每个内部元素调用时强制传入adaptiveFalse并展平结果。这让链式调用非常自然 page.css(.product_pod a) # 直接在页面上执行 page.css(.product_pod).css(a) # 等价结果re与re_first可直接运行参数与Selector类相同。在该类中re_first对内部每个Selector执行re并返回第一个有结果的项re则返回合并所有匹配的TextHandlers page.css(.price_color).re(r[\d\.]) [51.77, 53.74, 50.10, 47.82, 54.23, ...] page.css(.product_pod h3 a::attr(href)).re(rcatalogue/(.*)/index.html) [a-light-in-the-attic_1000, tipping-the-velvet_999, soumission_998, ...]search方法在可用实例中查找传入的函数必须接收Selector并返回 True/False返回第一个匹配项或None# 找出价格为 54.23 的商品 search_function lambda p: float(p.css(.price_color).re_first(r[\d\.])) 54.23 page.css(.product_pod).search(search_function) dataarticle classproduct_poddiv class... parentli classcol-xs-6 col-sm-4 col-md-3 co...filter方法接收类似search的函数但返回包含所有匹配实例的Selectors# 找出所有价格高于 50 的商品 filtering_function lambda p: float(p.css(.price_color).re_first(r[\d\.])) 50 page.css(.product_pod).filter(filtering_function) [dataarticle classproduct_poddiv class... ..., ...]安全的头/尾元素访问不会抛出索引错误 page.css(.product).first # 第一个 Selector 或 None dataarticle classproduct>page.css(.product_pod).length等价于len(page.css(.product_pod))5. TextHandler处处可链的增强字符串所有返回字符串的方法/属性返回TextHandler返回字符串列表的返回TextHandlers。TextHandler是标准 Pythonstr的子类因此所有标准字符串操作均可用同时它是整个库中几乎所有字符串返回点的统一类型使得方法链与更简洁的代码成为可能也可以直接导入用于任意字符串。from scrapling import TextHandler由于切片、索引等所有操作与方法split、replace、strip等都返回TextHandler可以无限链式调用——自定义类型源码 中可以看到split、strip、replace、join、upper、lower等方法都被重写为返回TextHandler/TextHandlers。re与re_first同样存在于Selector、Selectors、TextHandlers中参数一致re接收字符串或已编译正则返回全部匹配组成的TextHandlersre_first参数相同只返回首个匹配TextHandler。附加参数replace_entities默认开启将字符实体引用如amp;还原为对应字符实现上借助w3lib.html.replace_entities见 custom_types.py#L179-L182clean_match默认关闭。开启后匹配前忽略所有空白与连续空格case_sensitive默认开启。关闭后编译正则时忽略字母大小写。返回值是TextHandlers因为走的是re方法 page.css(.price_color).re(r[\d\.]) [51.77, 53.74, 50.10, 47.82, 54.23, ...] page.css(.product_pod h3 a::attr(href)).re(rcatalogue/(.*)/index.html) [a-light-in-the-attic_1000, tipping-the-velvet_999, soumission_998, ...]用自定义字符串演示其余参数 from scrapling import TextHandler test_string TextHandler(hi there) # 注意两个空格 test_string.re(hi there) test_string.re(hi there, clean_matchTrue) # clean_match 会在匹配前清洗字符串 [hi there] test_string2 TextHandler(Oh, Hi Mark) test_string2.re_first(oh, hi Mark) test_string2.re_first(oh, hi Mark, case_sensitiveFalse) # 关闭大小写敏感 Oh, Hi Mark # 混合参数 test_string.re(hi there, clean_matchTrue, case_sensitiveFalse) [hi There]由于html_content返回TextHandler正则可以直接作用于 HTML 内容 page.html_content.re(div class.*(.*)/div) [In stock: 5, In stock: 3, Out of stock].json()在内容可 JSON 化时转换为 JSON 对象否则抛错 page.css(#page-data::text).get() \n {\n lastUpdated: 2024-09-22T10:30:00Z,\n totalProducts: 3\n }\n page.css(#page-data::text).get().json() {lastUpdated: 2024-09-22T10:30:00Z, totalProducts: 3}若选择元素时未指定文本节点则自动选择其文本内容 page.css(#page-data)[0].json() {lastUpdated: 2024-09-22T10:30:00Z, totalProducts: 3}Selector类在此之上有额外行为。给定如下页面html body div script idpage-data typeapplication/json { lastUpdated: 2024-09-22T10:30:00Z, totalProducts: 3 } /script /div /body /htmlSelector的get_all_text方法返回TextHandler。例如 page.css(div::text).get().json()这会抛错因为div标签没有直接文本内容。get_all_text正是处理这类场景的 page.css(div)[0].get_all_text(ignore_tags[]).json() {lastUpdated: 2024-09-22T10:30:00Z, totalProducts: 3}这里显式传ignore_tags[]因为其默认值为(script, style,)会把 script 文本过滤掉。处理纯 JSON 响应时 page Selector({some_key: some_value})Selector为 HTML 优化会把它当作破损 HTML 并包装起来html_content显示 page.html_content htmlbodyp{some_key: some_value}/p/body/html而json方法可直接使用 page.json() {some_key: some_value}对于 JSON 响应Selector会保留一份收到的原始内容副本。调用.json()时优先检查该原始副本并转换若原始副本不可用例如子元素场景则先检查当前元素文本再回退到get_all_text——与 源码中的回退链 完全对应。.clean()去除所有空白与连续空格返回新的TextHandler TextHandler(\n wonderful idea, \reh?).clean() wonderful idea, eh?remove_entities参数会让clean将 HTML 实体替换为对应字符。.sort()按字符排序 TextHandler(acb).sort() abc或反向 TextHandler(acb).sort(reverseTrue) cba额外值得一提TextHandler还内置了get/getall/extract/extract_first的直通实现custom_types.py#L111-L119 注释写明是为了从 Scrapy/parsel 代码复制粘贴时更省事这让 Scrapy 风格代码几乎可以无缝迁移到 Scrapling。6. TextHandlers字符串集合TextHandlers继承标准List[TextHandler]新增re与re_first两个方法源码。re_first对每个TextHandler执行re并返回第一个结果无结果时返回None或default。与Selectors一样切片索引会返回同类型实例保持链式能力。7. AttributesHandler只读且省资源的属性容器AttributesHandler是 Python 标准dict的只读版本唯一用途就是存储每个元素/Selector实例的属性 print(page.find(script).attrib) {id: page-data, type: application/json} type(page.find(script).attrib).__name__ AttributesHandler由于只读它的资源消耗低于标准字典。实现上它继承collections.abc.Mapping内部使用MappingProxyType包装custom_types.py#L285-L306并因此省去了所有允许修改/覆盖数据的方法其余字典方法与属性保持不变。字符串属性值在存入时会自动转为TextHandler所以属性值同样支持re、clean等链式调用。它额外提供两个简单方法search_values方法按值而非键搜索当前属性返回每个匹配项组成的字典生成器。简单示例 for i in page.find(script).attrib.search_values(page-data): print(i) {id: page-data}它还支持partial参数允许按值的一部分搜索 for i in page.find(script).attrib.search_values(page, partialTrue): print(i) {id: page-data}更实际的用法是配合find_all找出属性中带有特定值的所有元素 page.find_all(lambda element: list(element.attrib.search_values(product))) [dataarticle classproduct> page.find(script).attrib.json_string b{id:page-data,type:application/json}8. 小结解析层的核心约定元素即 Selector集合即 Selectors任何选择/导航的结果只要仍是元素就保持这两种类型保证任意深度链式调用字符串即 TextHandler所有文本出口统一为可链式、可正则、可 JSON 化的增强字符串Scrapy 风格的get/getall/extract全部兼容属性即只读 MappingAttributesHandler用MappingProxyType换取资源与安全性并提供按值搜索等扩展能力性能靠惰性tag/text/attrib/html_content等属性首次访问才计算并缓存见 parser.py 注释这是 Scrapling 解析速度的关键设计之一v0.4 的文本节点统一::text//text()/::attr()//attr一律返回Selector/Selectorstag为#text配合get/getall取值选择行为完全一致。在此基础上元素定位CSS/XPath/文本/正则/相似元素与自适应adaptive特性分别由 元素查询文档 与 adaptive 特性文档 覆盖更完整的 API 说明可参考 解析 API 参考 与 自定义类型参考实现细节集中在 scrapling/parser.py、scrapling/core/custom_types.py 与 scrapling/core/mixins.py回归测试位于 tests/parser 目录。【免费下载链接】Scrapling️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!项目地址: https://gitcode.com/GitHub_Trending/sc/Scrapling创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表