news 2026/9/5 16:23:40

Scrapling 自适应抓取(Adaptive Scraping):当网站改版后,让选择器自动找回元素

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Scrapling 自适应抓取(Adaptive Scraping):当网站改版后,让选择器自动找回元素

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 class="container"> <section class="products"> <article class="product" id="p1"> <h3>Product 1</h3> <p class="description">Description 1</p> </article> <article class="product" id="p2"> <h3>Product 2</h3> <p class="description">Description 2</p> </article> </section> </div>

你想抓取第一个商品(id="p1"),自然会选择:

page.css('#p1')

但网站运营方一旦实施结构性改版,比如变成这样:

<div class="new-container"> <div class="product-wrapper"> <section class="products"> <article class="product new-class">from scrapling import Selector, Fetcher # 改动发生之前 page = Selector(page_source, adaptive=True, url='example.com') # 或者 Fetcher.adaptive = True page = Fetcher.get('https://example.com') # 然后 element = page.css('#p1', auto_save=True) if not element: # 某一天网站改版了? element = page.css('#p1', adaptive=True) # 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_domain='stackoverflow.com') page = Fetcher.get(old_url, timeout=30) element1 = page.css(selector, auto_save=True)[0] # 同一个选择器,用在改版后的网站上 page = Fetcher.get(new_url) element2 = page.css(selector, adaptive=True)[0] if element1.text == element2.text: print('Scrapling found the same element in the old and new designs!') # Spoiler: 它确实做到了

这里引入了一个文档中的新参数adaptive_domain,它存在的原因值得展开:

  • 对 Scrapling 而言,archive.orgstackoverflow.com两个不同域名,自适应数据会按域名隔离存储;
  • 通过Fetcher.configure(adaptive_domain='stackoverflow.com')显式指定统一域名,Scrapling 就会把两次抓取的数据归到同一个“网站”下,而不是相互隔离;
  • 该参数的设计初衷还覆盖了一种更常见的真实场景:网站改版的同时更换了域名。此时可以用adaptive_domain让新旧 URL 共用同一份已存储的自适应数据,否则 Scrapling 会把新域名当作全新网站,旧数据被废弃。

在真实生产场景中代码是一样的,只是两次请求使用同一个 URL,因此不需要adaptive_domain。上述示例同时演示了Selector类与Fetcher类两种入口,两者的自适应逻辑完全相同。

三、工作原理:保存阶段与匹配阶段

自适应抓取分为两个阶段:

  1. 保存阶段(Save Phase):存储元素的唯一属性;
  2. 匹配阶段(Match Phase):之后据此寻找属性相似的元素。

整体流程(文档中的“少技术细节版”描述)是:

  1. 通过下文的任一方式告诉 Scrapling 保存某元素的唯一属性;

  2. Scrapling 使用其配置的数据库(默认 SQLite)保存每个元素的唯一属性;

  3. 由于网站运营方可以修改或删除元素的一切内容,元素上没有任何一项可以单独充当数据库唯一键。为此存储系统依赖两样东西组合定位:

    • 当前网站的域名(使用Selector类时需通过url参数传入;使用抓取器时会自动从 URL 取得);
    • 一个用于查询该元素属性的identifier(不需要时它会由选择器自动生成)。

    两者共同用于之后从数据库中检索元素的唯一属性。

  4. 当网站结构变化后,你启用adaptive让 Scrapling 查找元素。它取出该元素的唯一属性,用页面上所有元素逐一与之比对,计算相似度得分;

  5. 返回与目标元素相似度得分最高者。

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)。如果初始化时没有传 URL,default会代替 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, adaptive=True) # 或者 Fetcher.adaptive = True page = Fetcher.get('https://example.com')

若使用Selector类,务必通过url参数传入网站 URL,Scrapling 会按域名隔离每个元素保存的属性(前文已说明不传 URL 时用default兜底的后果)。

除了这两个参数,Selector还支持storagestorage_args:前者传入存储类(默认是库内置的 SQLite 实现),后者是传给存储类的参数字典。除非你想编写自己的存储系统,否则一般用不到默认配置即可——自定义存储的完整教程见仓库中的 Writing your retrieval system。

全局启用后,有两种主要使用方式。

4.2 auto_save 与 adaptive 参数

选择当前存在的元素时,用auto_save保存其特征:

element = page.css('#p1', auto_save=True)

当元素消失后,用同一个选择器adaptive参数,库会替你找到它:

element = page.css('#p1', adaptive=True)

看似简单,但源码中发生的事很多。先看参数如何落库:identifier 默认就是你传入的选择器本身(见 css 方法:identifier or selector),无需手动指定;你也可以显式传入identifier参数自行命名——这在“以后要换一个选择器却想找回同一元素”的场景中非常有用。

css方法的内部实现细节(对使用者是透明但对排障很有价值):

  • css并不直接执行 CSS,而是经_css_to_xpath转换后委托给 xpath 方法;
  • 组合选择器(用逗号合并多个选择器)会被拆分,每个子选择器单独执行,且各自的 identifier 取各自的规范化选择器——这解释了后文“已知问题”中组合选择器不受“只存第一个元素”限制的原因;
  • xpath 方法的执行顺序是:先尝试常规 XPath 命中;命中且auto_save=True时保存elements[0](第一个元素);未命中且adaptive=True才调用retrieve取出指纹、再走relocate重定位,若重定位成功且auto_save=True,会把重定位到的元素再次保存(用新页面结构刷新指纹);
  • 若初始化时未启用adaptiveadaptiveauto_save参数会被忽略并记录 warning(源码中两处log.warning可作行为佐证)。

所有选择方法都额外支持percentage参数,取值范围 0–100,默认 40:它是重定位的最低可接受相似度得分。文档同时提醒:得分计算完全取决于页面结构本身,“除非你确切知道自己在做什么,否则别动这个数字”。

五、使用方式二:手动保存与重定位

手动方式允许你保存任何方式找到的任何元素,再重定位它,全部发生在adaptive特性之内。例如你通过文本找到了一个元素:

element = page.find_by_text('Tipping the Velvet', first_match=True)

可以用save方法保存其唯一属性,但此时identifier 必须自己设置——示例中使用my_special_element;文档建议像命名变量一样使用有意义的 identifier:

page.save(element, 'my_special_element')

之后要取出并在页面中重新定位:

>>> element_dict = page.retrieve('my_special_element') >>> page.relocate(element_dict, selector_type=True) [<data='<a href="catalogue/tipping-the-velvet_99...' parent='<h3><a href="catalogue/tipping-the-velve...'>] >>> page.relocate(element_dict, selector_type=True).css('::text').getall() ['Tipping the Velvet']

retrieve取出指纹字典、relocate在当前页面中重定位。关于返回类型:

  • selector_type=True时,结果转换为Selectors对象,可继续链式使用.css()等选择方法(如上例提取文本);
  • 省略selector_type(默认False)时,返回原始的lxml.etree元素列表:
>>> page.relocate(element_dict) [<Element a at 0x105a2a7b0>]

5.1 relocate 的源码逻辑

relocate 方法 的工作过程:

  1. 入参可以是字典、HtmlElementSelector,内部统一经element_to_dict转成指纹字典;
  2. 用预编译的 XPath.//*遍历页面全部节点,对每个候选节点调用__calculate_similarity_score计算得分(源码注释说明:即使某个元素已达 100% 也不提前终止,因为页面上可能存在同分的其他元素,全部要收进score_table);
  3. 取得最高分后,只有最高分 ≥ percentage(默认 40)时才返回该得分档的全部节点;否则记录 warning(提示“top score 是多少,如果这就是你要的元素可以调低 percentage”)并返回空列表;
  4. 在 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', adaptive=True, identifier='old_selector') # 3. 用新的 identifier 重新保存 products = page.css('.new-product', auto_save=True, identifier='new_identifier')

排查思路:先用retrieve确认该 identifier 下确实有指纹数据;若数据是旧结构留下的,可尝试指向旧保存记录的 identifier;最后以当前结构重新auto_save建立新基准。

6.2 匹配到了错误的元素(Wrong Elements Matched)

# 使用更具体的选择器 products = page.css('.product-list .product', auto_save=True) # 或者保存时携带更多上下文 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, adaptive=True, url=...)Fetcher.adaptive = True/Fetcher.configure(adaptive=True, adaptive_domain=...)
  • 选择器流auto_save=True首次保存(identifier 默认为选择器),元素消失后adaptive=True自动重定位,最低相似度阈值由percentage控制(默认 40);
  • 手动流save(element, identifier)retrieve(identifier)relocate(element_dict, selector_type=True),可作用于任意选择方式找到的元素;
  • 存储:默认 SQLite(WAL 模式、线程安全),按“域名 + 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 16:21:35

LLM基准审计工具BenchMIRT:破解评测数据污染与模型记忆难题

这次我们来看 Hugging Face 与 Ai2 联合推出的一个专门做“基准审计”的工具&#xff1a;BenchMIRT。它解决的问题非常具体&#xff1a;当大家都在用 MMLU、GPQA 这类 benchmark 给大模型打分时&#xff0c;模型刷的到底是“推理能力”&#xff0c;还是“题目记忆”&#xff1f…

作者头像 李华
网站建设 2026/9/5 16:20:19

禁闭求生2恶意盛宴获取教程:神秘人营地触发条件与起源荆棘全流程攻略

如果你在《禁闭求生2》里已经推进到“由内而外畸形藤蔓”事件&#xff0c;却一直没见到神秘人布莱顿布莱尔&#xff0c;那很可能是同一个原因&#xff1a;你把地图上的世界创伤处理得太干净了。神秘人营地不会无限期刷在地图上&#xff0c;他的出现和“由内而外”的存活状态有直…

作者头像 李华
网站建设 2026/9/5 16:17:55

Simulink风力发电机仿真建模:从气动计算到控制策略实现

简介&#xff1a;本资源是一套基于MATLAB 2013a开发的风力发电机系统级Simulink仿真模型&#xff0c;面向新能源方向本科生、研究生及风电控制工程师&#xff0c;用于快速理解风能转换原理、验证控制策略并开展动态性能分析。压缩包共1432个文件&#xff0c;含252个.slx主模型文…

作者头像 李华