Scrapling 实战:网页改版后,自适应定位能恢复多少数据?
用 Scrapling 0.5 对比固定选择器与自适应定位,复现列表漏采、包装层误匹配和干扰卡片命中,并用业务字段验证结果。
自适应定位可以帮助你重新寻找元素,但返回了节点,并不意味着整个列表已经恢复。
这次使用 Scrapling 0.5,在同一组已知商品数据上改变页面结构。原版采集完整;改类名后,自适应结果只有第一件商品;增加包装层时,虽然返回三个节点,但商品 ID 全部为空。
这组 2026 年 10 月 29 日的本地结果,适合用来理解适用边界,不能据此推断所有站点的恢复效果。整个实验没有调用 LLM。
先定义正确的业务数据
实验只采集前三件商品,价格按页面显示保留为字符串。
| ID | 名称 | 价格 |
|---|---|---|
| p1 | Atlas | 19.50 |
| p2 | Beacon | 24.00 |
| p3 | Compass | 31.25 |
结果正确必须同时满足:没有漏 ID、没有额外 ID、名称和价格对应正确。只比较数量,无法发现错层节点和推荐卡片。
示例源码仓库为 16yun-cn/www-16yun-cn-v3,完整实验位于 scripts/blog-ai-crawling/scrapling_adaptive.py,页面由同目录 serve.py 提供。
五种页面变体改变了什么
每种变体都从原版保存一份独立的定位信息,避免前一轮结果影响后一轮。
| 变体 | 页面变化 | 要观察的问题 |
|---|---|---|
| original | 保持 .product-card | 基线是否完整 |
| renamed | 商品改为 .item-tile | 原选择器失效后能恢复几条 |
| wrapped | 改类名,并增加 section 包装层 | 返回的是否仍是商品节点 |
| reordered | 改类名,并反转商品顺序 | 组合变化下恢复哪些商品 |
| distractor | 商品改类名,推荐卡片沿用旧类名 | 原选择器有命中时是否触发自适应 |
reordered 同时改变类名和顺序,不是单独的排序实验。wrapped 也是组合变化。解读结果时必须保留这个条件。
获取与本文一致的示例版本
先准备 Git 和已能访问公开仓库的 GitHub CLI。下面使用仓库标识获取固定版本示例。
gh repo clone 16yun-cn/www-16yun-cn-v3 ai-crawling-examples -- --filter=blob:none --sparse
cd ai-crawling-examples
git checkout --detach bc6828e62c417b1a3459199811fd4e2164137a5f
git sparse-checkout set scripts/blog-ai-crawling这个固定提交包含本文运行过的脚本和依赖锁;稀疏检出只取运行示例需要的目录及仓库根文件,不要求启动官网。
后文安装命令从这个 checkout 的根目录执行。代码摘录按完整示例的文件和行号标注,省略的 import、变量初始化及清理步骤仍在原文件中。
你可以先阅读代码摘录理解行为,再执行完整脚本;不要将多段摘录当作一个可直接粘贴运行的独立文件。
固定环境并运行
本次使用 Python 3.12.13、Scrapling 0.5。依赖锁文件与同系列 Crawl4AI 示例共享,环境独立于网站。
LAB_ENV="$(mktemp -d)"
uv venv --python 3.12 "$LAB_ENV/venv"
uv pip sync --python "$LAB_ENV/venv/bin/python" \
scripts/blog-ai-crawling/requirements.lock
"$LAB_ENV/venv/bin/python" \
scripts/blog-ai-crawling/scrapling_adaptive.py \
--output "$LAB_ENV/results"脚本自动启动本地 HTTP 服务,使用 Fetcher 读取页面,不需要运行浏览器或配置模型。
报告写入 scrapling-report.json,包含每种变体的固定结果、自适应结果、完整正确标记,以及缺失和意外 ID。
保存原版定位信息
下面是脚本中保存基线的核心调用。storage 为每种变体分配独立临时数据库。
对应示例:scripts/blog-ai-crawling/scrapling_adaptive.py:25-28。以下仅摘录相关语句,已为阅读展开换行。
original = Fetcher.get(base + "/products?variant=original")
saved = Selector(
original.html_content,
url=base + "/products",
adaptive=True,
storage_args=storage,
)
baseline = records(
saved.css(".product-card", auto_save=True, identifier="cards")
)
assert baseline == expected初始化时的 adaptive=True 允许保存和恢复信息;查询时的参数决定本次是否尝试重新定位。两处作用不同,不能漏掉保存步骤后就期待恢复。
实验保持 Selector 的 URL 和 identifier 不变,以明确复用哪份定位信息。
对同一改版页面比较两种查询
页面重新获取后,固定查询与自适应查询都使用相同的原始选择器。
对应示例:scripts/blog-ai-crawling/scrapling_adaptive.py:30-32。以下仅摘录相关语句,已为阅读展开换行。
changed = Selector(
response.html_content,
url=base + "/products",
adaptive=True,
storage_args=storage,
)
fixed = records(changed.css(".product-card"))
adaptive = records(
changed.css(".product-card", adaptive=True, identifier="cards")
)这里的 records 读取节点自身的 data-id,再读取内部名称和价格。若节点层级不对,ID 就会暴露问题,而不是被静默补成猜测值。
为什么读取节点自身的 ID
记录转换对应 scripts/blog-ai-crawling/scrapling_adaptive.py:14-15。下面展开同一实现,便于看清 ID 与子字段的来源。
def records(elements):
return [
{
"id": item.attrib.get("data-id"),
"name": item.css("h2::text").get(),
"price": item.css(".price::text").get(),
}
for item in elements
]名称与价格来自后代节点,ID 则来自当前节点的属性。这个差别使包装层错误能够被发现:外层 section 包含商品文字,却不拥有商品身份。
如果为了得到一个非空 ID,临时改成在任意后代寻找 data-id,就改变了本次检查的含义。你需要先确认新节点结构,再决定新的提取规则。
实际结果:列表完整性没有自动恢复
本次完整输出如下。
| 变体 | 固定查询 | 自适应查询 | 业务结果 |
|---|---|---|---|
| original | p1、p2、p3 | p1、p2、p3 | 完整且字段正确 |
| renamed | 空 | p1 | 漏 p2、p3 |
| wrapped | 空 | 三个 ID 为空的节点 | 返回错误层级 |
| reordered | 空 | p1 | 漏 p2、p3 |
| distractor | ad1 | ad1 | 采到推荐卡片,漏全部目标商品 |
包装层变体尤其容易误判:名称和价格仍然能从后代节点读取,但 data-id 在内部 article 上,返回节点自身没有这个属性。
如果只看“返回三条”,这一轮会被错误标记为成功。
报告如何表示漏采
下面摘录 scrapling-report.json 的 renamed 结果,只保留自适应结果及相关判定字段。
{
"adaptive": [
{"id": "p1", "name": "Atlas", "price": "19.50"}
],
"adaptive_complete_and_correct": false,
"missing_ids": ["p2", "p3"],
"unexpected_ids": []
}虽然唯一返回的 p1 字段正确,完整性仍然是 false。没有额外记录,也不能抵消两条目标记录缺失。
这份报告用于观察行为,脚本不会因为自适应查询的返回值非空,就把这一变体改写为成功。
从源码解释两个边界
核对 D4Vinci/Scrapling 提交 0336400 的 scrapling/parser.py:662-680,可以看到两个与本次结果直接相关的分支。
原选择器有结果时,会直接返回。启用自动保存时保存的是 elements[0]。只有原选择器没有命中,且启用了自适应查询,才会读取保存信息并重新定位。
因此,用列表选择器保存信息,不应理解为保存了一份完整的列表恢复契约。第一条锚点的相似度匹配,不能保证找回所有兄弟记录。
同样,推荐卡片沿用旧类名时,查询已经有结果,自适应分支不会被触发。框架并不知道业务上你要的是“可售商品”,而不是“任何保留旧类名的卡片”。
Issue #259 报告过旧版 0.4.7 中列表只恢复第一条的现象。本次在 0.5 的受控页面中观察到相关行为;我们没有把该 Issue 的版本或站点条件直接当成本次测试结果。
用业务字段检查,而不是只看节点数量
示例将记录排序后与已知预期比较,顺序变化不会被误判为漏采。
对应示例:scripts/blog-ai-crawling/scrapling_adaptive.py:34-41。以下是便于说明的等价或精简写法。
canonical = lambda rows: sorted(rows, key=lambda row: row["id"] or "")
complete_and_correct = canonical(adaptive) == canonical(expected)
missing_ids = sorted(
{row["id"] for row in expected}
- {row["id"] for row in adaptive}
)固定测试站拥有完整答案,所以可以逐项比较。真实业务通常需要组合检查:必填字段、身份唯一性、允许的取值,以及与已知分页总量或历史数据的核对。
类型正确也不够。999.00 是合法的价格字符串,但推荐卡片 ad1 并不是目标商品。
不要在缺 ID 时自动生成一个新 ID 来让校验通过;那会把定位错误变成看似有效的数据。
明确修复当前选择器
示例页面仍保留稳定的商品属性和容器,可以直接修复查询。
对应示例:scripts/blog-ai-crawling/scrapling_adaptive.py:45-47。以下仅摘录相关语句,已为阅读展开换行。
renamed = Fetcher.get(base + "/products?variant=renamed")
stable = records(renamed.css("main#products > article[data-id]"))
assert stable == expected在 renamed 变体中,这个选择器得到完整的 p1、p2、p3,以及正确的名称、价格。
它没有被证明能覆盖所有变体。增加包装层后,直接子元素关系也会变化,需要再次检查页面契约。
选择器修复是针对已知页面的明确修改,不应被包装成自适应算法已经成功修复了页面。
按错误类型选择处理方式
不同的失败需要不同处理,不能都归结为“调一下自适应阈值”。
| 可能看到的现象 | 意味着什么 | 下一步 |
|---|---|---|
| 原查询空,自适应也空 | 当前条件未恢复目标 | 保留页面,检查保存信息及页面变化 |
| 只返回 p1 | 找回一个候选,列表不完整 | 重新核对列表定位与记录范围 |
| 返回三个节点但 ID 为空 | 节点层级违反提取约定 | 检查外层包装与内部商品节点 |
| 返回 ad1 | 匹配到了错误业务对象 | 修正作用区域和业务属性条件 |
| ID 正确但字段不一致 | 记录存在,字段提取仍有错误 | 核对字段定位及页面原值 |
首行和末行是排查时需要考虑的情况,不是本次已观察到的输出。本次实验实际发现的是漏采、错层和干扰节点,没有声称这些变体覆盖了全部错误类型。
修复 renamed 后,应先确认原版仍然正确,再检查修复声称覆盖的变体。若你进一步修改包装层规则,需要单独验证它,不应沿用 renamed 的通过结果。
什么时候停止自动输出
测试夹具知道所有预期商品,可以直接比较完整记录。真实采集若没有总量答案,也应先定义哪些错误需要停止导出,哪些可以保留为待处理记录。
例如,身份字段缺失时,你无法可靠去重或更新已有商品。此时应暴露错误,而不是猜测身份;推荐卡片混入时,也不能仅按价格格式有效就接受。
这些决定属于你的数据契约。自适应定位不会自动替你判断错误数据进入下游后的影响。
不要在误匹配后立即保存新锚点并继续运行,否则后续排查会更难区分原始基线与错误匹配。本例为每个变体使用独立存储,保留了这种可比较性。
如何放进实际采集任务
稳定的业务属性可作为优先定位依据;自适应结果可以用于辅助找回候选元素。候选结果仍应通过字段和完整性检查,异常时保留页面和报告,修正定位规则。
Scrapling 还提供浏览器获取、Spider 调度和代理相关能力,但本次没有验证暂停恢复、代理轮换、大规模并发或反爬效果,不能从这组选择器实验推导这些能力的表现。
如果页面需要点击和翻页,可继续阅读 Stagehand 配置与实现。该篇把实际跑通的确定性流程,与供你配置的模型调用明确分开。
需要企业代理方案?
我们可根据目标站点、并发规模与稳定性目标提供定制方案。