:当网站改版后,让选择器自动找回元素)
Scrapling 自适应抓取Adaptive Scraping当网站改版后让选择器自动找回元素【免费下载链接】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 官方文档docs/parsing/adaptive.md及其配套源码完整讲解自适应抓取adaptive scraping早期称为 automatch这一核心特性它如何让爬虫在网站结构或设计改版后无需人工维护选择器即可重新定位到同一元素。读完后你将掌握auto_save/adaptive/identifier等参数用法、save/retrieve/relocate手动定位流程、相似度评分算法的源码原理以及 SQLite 存储系统的隔离机制与自定义存储方案。一、为什么需要自适应抓取爬虫最脆弱的地方在于你写死的选择器往往绑定着网页当前的具体结构。假设你正在抓取这样一页商品列表div classcontainer section classproducts article classproduct idp1 h3Product 1/h3 p classdescriptionDescription 1/p /article article classproduct idp2 h3Product 2/h3 p classdescriptionDescription 2/p /article /section /div你想抓取第一个商品idp1自然会选择page.css(#p1)但网站运营方一旦实施结构性改版比如变成这样div classnew-container div classproduct-wrapper section classproducts article classproduct new-class>from scrapling import Selector, Fetcher # 改动发生之前 page Selector(page_source, adaptiveTrue, urlexample.com) # 或者 Fetcher.adaptive True page Fetcher.get(https://example.com) # 然后 element page.css(#p1, auto_saveTrue) if not element: # 某一天网站改版了 element page.css(#p1, adaptiveTrue) # Scrapling 依然能找到它 # 你的其余代码……需要注意的是自适应逻辑对所有选择方式都生效不只是 CSS/XPath 选择。二、实战验证同一选择器横跨 2010 年与现在官方文档给出的真实场景是找一个即将改版的网站几乎不可能于是作者利用互联网档案馆Wayback Machine保存的 StackOverflow 2010 年快照https://web.archive.org/web/20100102003420/http://stackoverflow.com/与当前站点对比——用同一个选择器提取两个时代设计中的同一个按钮。从 2010 版设计中提取 Questions 按钮可以写一个像#hmenus div:nth-child(1) ul li:nth-child(1) a这样非常具体的选择器它是由 Chrome 开发工具自动生成的典型产物from scrapling import Fetcher selector #hmenus div:nth-child(1) ul li:nth-child(1) a old_url https://web.archive.org/web/20100102003420/http://stackoverflow.com/ new_url https://stackoverflow.com/ Fetcher.configure(adaptive True, adaptive_domainstackoverflow.com) page Fetcher.get(old_url, timeout30) element1 page.css(selector, auto_saveTrue)[0] # 同一个选择器用在改版后的网站上 page Fetcher.get(new_url) element2 page.css(selector, adaptiveTrue)[0] if element1.text element2.text: print(Scrapling found the same element in the old and new designs!) # Spoiler: 它确实做到了这里引入了一个文档中的新参数adaptive_domain它存在的原因值得展开对 Scrapling 而言archive.org与stackoverflow.com是两个不同域名自适应数据会按域名隔离存储通过Fetcher.configure(adaptive_domainstackoverflow.com)显式指定统一域名Scrapling 就会把两次抓取的数据归到同一个“网站”下而不是相互隔离该参数的设计初衷还覆盖了一种更常见的真实场景网站改版的同时更换了域名。此时可以用adaptive_domain让新旧 URL 共用同一份已存储的自适应数据否则 Scrapling 会把新域名当作全新网站旧数据被废弃。在真实生产场景中代码是一样的只是两次请求使用同一个 URL因此不需要adaptive_domain。上述示例同时演示了Selector类与Fetcher类两种入口两者的自适应逻辑完全相同。三、工作原理保存阶段与匹配阶段自适应抓取分为两个阶段保存阶段Save Phase存储元素的唯一属性匹配阶段Match Phase之后据此寻找属性相似的元素。整体流程文档中的“少技术细节版”描述是通过下文的任一方式告诉 Scrapling 保存某元素的唯一属性Scrapling 使用其配置的数据库默认 SQLite保存每个元素的唯一属性由于网站运营方可以修改或删除元素的一切内容元素上没有任何一项可以单独充当数据库唯一键。为此存储系统依赖两样东西组合定位当前网站的域名使用Selector类时需通过url参数传入使用抓取器时会自动从 URL 取得一个用于查询该元素属性的identifier不需要时它会由选择器自动生成。两者共同用于之后从数据库中检索元素的唯一属性。当网站结构变化后你启用adaptive让 Scrapling 查找元素。它取出该元素的唯一属性用页面上所有元素逐一与之比对计算相似度得分返回与目标元素相似度得分最高者。3.1 元素指纹到底包含什么文档强调元素的所有属性都可能被改比较不是精确匹配而是“这些值有多相似”。从源码看指纹的生成在 element_to_dict_StorageTools工具类中保存的字段为字段含义tag元素标签名attributes元素的属性名与值会剔除空值text元素直接文本path从根节点到该元素的标签名路径仅标签名parent_name父元素标签名parent_attribs父元素的属性名与值parent_text父元素的直接文本siblings兄弟元素列表仅标签名按顺序children子元素列表仅标签名按顺序这与文档“唯一属性”一节完全对应元素自身的标签名、文本、属性名值、兄弟仅标签名、路径仅标签名加上父元素的标签名、属性、文本。此外文档特别提醒连值的顺序也参与比较——例如 class 名此前的书写顺序与现在的书写顺序——这一点在源码中体现为兄弟/子元素以有序元组保存、以及后文评分算法对属性名/值分别做序列比对。3.2 默认存储按域名 identifier 隔离的 SQLite默认存储在 SQLiteStorageSystem 中实现几个源码级细节值得注意表结构为storage (id, url, identifier, element_data, UNIQUE (url, identifier))——域名与 identifier 的组合是唯一定位键保存是覆盖写ON CONFLICT更新所以adaptive特性永远只使用最新一次保存的属性域名提取由StorageSystemMixin._get_base_url()storage.py完成它基于tld库从 URL 中解析出可注册域fld/domain。如果初始化时没有传 URLdefault会代替 URL 字段入库——这正是文档警告的隐患若你在不同网站间复用了同一个 identifier 却从未传过url保存过程会互相覆盖数据默认数据库文件由 parser.py 中的__DEFAULT_DB_FILE__指定类使用RLock加锁并开启 SQLite 的 WAL 日志模式官方注释说明其面向 Scrapy 等线程化框架设计是线程安全的。四、使用方式一CSS/XPath 选择方式4.1 启用自适应首先必须全局启用该特性——两种方式任选其一from scrapling import Selector, Fetcher page Selector(html_doc, adaptiveTrue) # 或者 Fetcher.adaptive True page Fetcher.get(https://example.com)若使用Selector类务必通过url参数传入网站 URLScrapling 会按域名隔离每个元素保存的属性前文已说明不传 URL 时用default兜底的后果。除了这两个参数Selector还支持storage与storage_args前者传入存储类默认是库内置的 SQLite 实现后者是传给存储类的参数字典。除非你想编写自己的存储系统否则一般用不到默认配置即可——自定义存储的完整教程见仓库中的 Writing your retrieval system。全局启用后有两种主要使用方式。4.2 auto_save 与 adaptive 参数选择当前存在的元素时用auto_save保存其特征element page.css(#p1, auto_saveTrue)当元素消失后用同一个选择器加adaptive参数库会替你找到它element page.css(#p1, adaptiveTrue)看似简单但源码中发生的事很多。先看参数如何落库identifier 默认就是你传入的选择器本身见 css 方法identifier or selector无需手动指定你也可以显式传入identifier参数自行命名——这在“以后要换一个选择器却想找回同一元素”的场景中非常有用。css方法的内部实现细节对使用者是透明但对排障很有价值css并不直接执行 CSS而是经_css_to_xpath转换后委托给 xpath 方法组合选择器用逗号合并多个选择器会被拆分每个子选择器单独执行且各自的 identifier 取各自的规范化选择器——这解释了后文“已知问题”中组合选择器不受“只存第一个元素”限制的原因xpath 方法的执行顺序是先尝试常规 XPath 命中命中且auto_saveTrue时保存elements[0]第一个元素未命中且adaptiveTrue时才调用retrieve取出指纹、再走relocate重定位若重定位成功且auto_saveTrue会把重定位到的元素再次保存用新页面结构刷新指纹若初始化时未启用adaptiveadaptive或auto_save参数会被忽略并记录 warning源码中两处log.warning可作行为佐证。所有选择方法都额外支持percentage参数取值范围 0–100默认 40它是重定位的最低可接受相似度得分。文档同时提醒得分计算完全取决于页面结构本身“除非你确切知道自己在做什么否则别动这个数字”。五、使用方式二手动保存与重定位手动方式允许你保存任何方式找到的任何元素再重定位它全部发生在adaptive特性之内。例如你通过文本找到了一个元素element page.find_by_text(Tipping the Velvet, first_matchTrue)可以用save方法保存其唯一属性但此时identifier 必须自己设置——示例中使用my_special_element文档建议像命名变量一样使用有意义的 identifierpage.save(element, my_special_element)之后要取出并在页面中重新定位 element_dict page.retrieve(my_special_element) page.relocate(element_dict, selector_typeTrue) [dataa hrefcatalogue/tipping-the-velvet_99... parenth3a hrefcatalogue/tipping-the-velve...] page.relocate(element_dict, selector_typeTrue).css(::text).getall() [Tipping the Velvet]即retrieve取出指纹字典、relocate在当前页面中重定位。关于返回类型selector_typeTrue时结果转换为Selectors对象可继续链式使用.css()等选择方法如上例提取文本省略selector_type默认False时返回原始的lxml.etree元素列表 page.relocate(element_dict) [Element a at 0x105a2a7b0]5.1 relocate 的源码逻辑relocate 方法 的工作过程入参可以是字典、HtmlElement或Selector内部统一经element_to_dict转成指纹字典用预编译的 XPath.//*遍历页面全部节点对每个候选节点调用__calculate_similarity_score计算得分源码注释说明即使某个元素已达 100% 也不提前终止因为页面上可能存在同分的其他元素全部要收进score_table取得最高分后只有最高分 ≥ percentage默认 40时才返回该得分档的全部节点否则记录 warning提示“top score 是多少如果这就是你要的元素可以调低 percentage”并返回空列表在 DEBUG 日志级别下会额外打印 Top 5 得分节点方便排查匹配偏差。5.2 相似度评分算法__calculate_similarity_score 是“什么都被考虑”这一说法的落点。它对候选元素逐项打分score并累计检查项数checks最终得分为round(score / checks * 100, 2)。逐项看比较项计分方式标签名完全相等计 1 分文本原文与候选文本的SequenceMatcher相似度比值仅当原文有文本时属性整体__calculate_dict_diff属性键序列比值 × 0.5 属性值序列比值 × 0.5所以顺序参与比较双方都没有属性也计 1 分class/id/href/src这四个关键属性各自单独再做一次序列相似度比较注释说明单独测试有助于应对“完全的结构化变更”标签路径路径字符串序列的SequenceMatcher比值父元素标签名 / 父属性 / 父文本有父级信息时分别做序列相似度比较父属性同样走 dict diff兄弟元素兄弟标签有序元组的序列比值可以看出比较是逐项加权平均而非一票否决标签、文本、属性、路径、父级、兄弟共同决定得分。这也解释了官方示例中为什么 2010 版与当代 StackOverflow 的同一按钮仍能超过默认 40% 阈值被找回——即使 class 全变了标签、文本、路径结构与父级上下文的相似项仍然足够多。save/retrieve本身在 parser.py 中有两个值得注意的边界行为若adaptive未在全局启用就调用它们会直接抛出RuntimeError提示需新建实例save时若传入的是文本节点会自动取其父元素再保存。六、故障排查Troubleshooting文档提供了两类典型故障的排查路径。6.1 找不到匹配No Matches Found# 1. 检查数据是否被保存 element_data page.retrieve(identifier) if not element_data: print(No data saved for this identifier) # 2. 尝试使用不同的 identifier products page.css(.product, adaptiveTrue, identifierold_selector) # 3. 用新的 identifier 重新保存 products page.css(.new-product, auto_saveTrue, identifiernew_identifier)排查思路先用retrieve确认该 identifier 下确实有指纹数据若数据是旧结构留下的可尝试指向旧保存记录的 identifier最后以当前结构重新auto_save建立新基准。6.2 匹配到了错误的元素Wrong Elements Matched# 使用更具体的选择器 products page.css(.product-list .product, auto_saveTrue) # 或者保存时携带更多上下文 product page.find_by_text(Product Name).parent page.save(product, specific_product)即提高被保存元素的“上下文浓度”更深层的选择器、带父级上下文让指纹中携带更多区分度信息。七、已知限制文档明确指出一条需要牢记的限制在adaptive的保存过程中只保存选择结果中第一个元素的唯一属性。如果你使用的选择器在页面不同位置选中了多个元素之后重定位时只会返回第一个元素。唯一例外是组合 CSS 选择器用逗号合并多个选择器——因为此类选择器会被拆分、逐个单独执行对应css方法中的split_selectors逻辑每个子选择器分别保存与重定位。结合源码可确认这一点auto_save触发时执行的是self.save(elements[0], identifier or selector)即只取结果集首元素。测试覆盖见 tests/parser/test_adaptive.py。八、总结Scrapling 的自适应抓取用“保存阶段 匹配阶段”的指纹比对模型把“网站改版导致选择器失效”这一爬虫维护中最常见的问题转化成了运行时的一次相似度检索启用Selector(html, adaptiveTrue, url...)或Fetcher.adaptive True/Fetcher.configure(adaptiveTrue, adaptive_domain...)选择器流auto_saveTrue首次保存identifier 默认为选择器元素消失后adaptiveTrue自动重定位最低相似度阈值由percentage控制默认 40;手动流save(element, identifier)→retrieve(identifier)→relocate(element_dict, selector_typeTrue)可作用于任意选择方式找到的元素存储默认 SQLiteWAL 模式、线程安全按“域名 identifier”唯一约束隔离与覆盖域名缺省记为default跨域名/跨存档场景用adaptive_domain统一也可通过storage/storage_args参数接入自定义存储系统继承StorageSystemMixin参考 自定义存储教程。掌握这套机制后配合 选择器文档 与 核心类文档 中的元素查询能力即可构建出对网站结构变更具有韧性的抓取管道。【免费下载链接】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),仅供参考