Scrapling:以自适应与智能选择器替代 AI 的高成本网页抓取方案
本文基于 Scrapling 仓库中的教程 replacing_ai.md 展开,系统讲解网页抓取中反复出现的三类痛点——站点结构快速变更、选择器不稳定、反爬机制升级——以及广义抓取(Broad/Generic Scraping)下的网站多样性与分页问题;读完你将掌握如何用 Scrapling 的 adaptive 自适应重定位、非选择器查询方法和双浏览器抓取器,在几乎零 API 成本下解决这些曾经需要付费 AI 工具才能应对的问题。
一、网页抓取的持久痛点:从定向到广义
Web scraping 一直是数据抽取、索引构建和训练数据准备的核心工具,但经验用户会不断遇到同一批顽固问题。教程将其归纳为三类定向抓取(Targeted Scraping)痛点:
- 网站结构快速变更:站点频繁更新 DOM 结构,静态 XPath/CSS 选择器随之失效;
- 不稳定的选择器:class/id 经常改名,或干脆是随机生成的值,使抓取器失效;
- 日益复杂的反爬措施:CAPTCHA、浏览器指纹识别、行为分析让传统抓取越来越难。
如果你只针对少数已知站点做定向抓取,还能"一站一写"地硬编码。但一旦目标是广义抓取(处理无数互不共享技术的站点),上述问题会进一步放大,并新增三类挑战:
- 极端的网站多样性:HTML 结构、CSS 用法、JS 框架、后端技术千差万别;
- 如何识别相关数据:面对一个从未见过的页面,抓取器凭什么知道哪个数据才是重要的?
- 分页方式各异:无限滚动、传统翻页、"Load More" 按钮,各需不同处理手段。
这些问题的共同本质是:页面理解成本随站点数量线性甚至超线性增长。人工维护选择器做不到规模化,于是出现了 AI 方案。
二、AI 方案的引入与真实代价
AI 确实能"看懂"页面源码、自动定位字段或生成选择器——前提是你已经用别的工具解决了反爬问题。这条路线的代价在于:多数页面内容量巨大,要让模型理解页面就必须把 HTML 传进去,token 消耗会快速累积成高昂的 API/订阅费用。教程原文对此的判断很直接:如果钱不是问题另当别论,否则就应该寻找更便宜的替代路径——这正是 Scrapling 的定位。
三、Scrapling 的逐项替代方案
Scrapling 是一个自适应的 Web Scraping 框架,覆盖从单次请求到完整爬虫系统的需求。它对上述每个痛点都有对应机制,下面逐一拆解,并结合仓库源码验证其实现细节。
3.1 痛点 T1:自适应(adaptive)特性抵御结构变更
核心思想:抓取时启用 adaptive 特性并保存目标元素的"唯一属性";当网站改版导致原选择器失效时,Scrapling 会检索库中所有元素,返回与已存属性相似度得分最高的那个元素——全程不使用 AI。
最直观的演示见官方教程 adaptive 详解:用 Wayback Machine 上 2010 年旧版 StackOverflow 的页面,以 Chrome 生成的超长选择器 #hmenus > div:nth-child(1) > ul > li:nth-child(1) > a 选中"Questions"按钮并 auto_save;再对当前新版站点使用同一个选择器配合 adaptive=True,Scrapling 在新设计中找到了同一个按钮。
from scrapling import Fetcher
selector = '#hmenus > div:nth-child(1) > ul > li:nth-child(1) > a'
Fetcher.configure(adaptive=True, adaptive_domain='stackoverflow.com')
page = Fetcher.get('https://web.archive.org/web/20100102003420/http://stackoverflow.com/', timeout=30)
element1 = page.css(selector, auto_save=True)[0]
# 同一个选择器,用于改版后的站点
page = Fetcher.get('https://stackoverflow.com/')
element2 = page.css(selector, adaptive=True)[0]
if element1.text == element2.text:
print('Scrapling found the same element in the old and new designs!')
其中 adaptive_domain 参数的作用值得注意:对 Scrapling 来说 archive.org 与 stackoverflow.com 是两个域,adaptive 数据会被隔离;传入统一的自定义域名即可让两次抓取共享同一份属性数据。它同样适用于"网站改版时连 URL 都换了"的真实场景——否则旧数据会被当作新站点丢弃。
两种启用与使用方式
启用方式(对应 主类文档 中 Selector 的参数):
from scrapling import Selector, Fetcher
page = Selector(html_doc, adaptive=True, url='example.com')
# 或对 fetcher 全局配置
Fetcher.adaptive = True
page = Fetcher.get('https://example.com')
注意:使用 Selector 类时需传 url 参数,否则存储时以 default 作为域字段占位。
方式一:CSS/XPath 选择时自动绑定。css/xpath 方法接受四个 adaptive 相关参数,见 parser.py 的 css 实现:
identifier:存取属性数据用的标识符。若不传,默认以选择器字符串本身作为 identifier(逗号组合选择器会被拆分,逐个单独执行);auto_save:元素命中时自动把唯一属性存入数据库;adaptive:元素未命中时触发重定位;percentage:可接受的最低相似度百分比,源码默认值为 40(见 relocate 实现)。
element = page.css('#p1', auto_save=True) # 元素存在时:保存
element = page.css('#p1', adaptive=True) # 元素消失时:重定位
方式二:手动保存/检索/重定位,可与任意查找方式(文本、正则、过滤器)配合:
element = page.find_by_text('Tipping the Velvet', first_match=True)
page.save(element, 'my_special_element') # 手动保存,identifier 需自拟
element_dict = page.retrieve('my_special_element')
page.relocate(element_dict, selector_type=True) # 返回 Selectors 对象
page.relocate(element_dict, selector_type=True).css('::text').getall()
# ['Tipping the Velvet']
源码视角:数据如何存、分数如何算
存储层位于 storage.py。默认的 SQLiteStorageSystem 是线程安全的(WAL 日志模式 + RLock,注释中明确其为适配 Scrapy 等线程框架而优化),数据表结构为 (id, url, identifier, element_data),并以 UNIQUE (url, identifier) 约束隔离不同站点、不同标识符的记录。identifier 经 SHA-256 哈希(附加长度信息降低碰撞概率)后落库。
所谓"唯一属性"具体包括(见 adaptive 文档):元素的标签名、文本、属性(名与值)、兄弟元素(仅标签名)、路径(仅标签名),以及父元素的标签名、属性与文本。比较并非精确匹配,而是相似度计算,甚至包括 class 名书写顺序这类细节。
重定位逻辑(relocate)遍历页面全部元素计算相似度分数,即使遇到 100% 分数也不提前停止,以收集所有同分候选;只有最高分不低于 percentage 阈值时才返回,否则给出告警并建议调整阈值。
排障与已知限制
官方文档给出了两段实用的排障代码:
# 无匹配时:先确认数据是否保存过
element_data = page.retrieve('identifier')
if not element_data:
print("No data saved for this identifier")
# 或换用旧的 identifier 重定位
products = page.css('.product', adaptive=True, identifier='old_selector')
若匹配到了错误元素,应使用更具体的选择器保存(如 '.product-list .product'),或选取带更多上下文的元素(如 page.find_by_text('Product Name').parent)后再 page.save(...)。
已知限制:auto_save 只保存选择结果中第一个元素的属性,因此同一选择器若指向页面多处不同元素,重定位时只会返回第一个(逗号组合选择器除外,它们被拆分后各自独立处理)。此外,storage / storage_args 参数允许接入自研存储系统,扩展方式在 adaptive_storage_system 文档 中有说明。
3.2 痛点 T2:不依赖稳定选择器的三种查询方法
面对没有 id/class、或 class 全随机的"裸 HTML"站点,CSS/XPath 选择器并不总是最优解。Scrapling 在 选择器文档 中提供了三条替代路线:
① 按文本内容选择:find_by_text / find_by_regex
两个方法共享一组参数(可从 源码签名 核对默认值):
| 参数 | 默认值 | 说明 |
|---|---|---|
first_match |
True |
True 时返回第一个匹配元素;False 时返回 Selectors 列表 |
case_sensitive |
False |
是否区分大小写 |
clean_match |
True |
匹配前把空白与连续空格归一为单个空格 |
partial(仅 find_by_text) |
False |
True 时退化为"包含"匹配而非精确匹配 |
find_by_regex 的第一参数既可以是普通字符串也可以是编译后的 re.Pattern,源码会按输入类型分别处理:
page = Fetcher.get('https://books.toscrape.com/index.html')
# 精确匹配标题文本
page.find_by_text('Tipping the Velvet')
# <data='<a href="catalogue/tipping-the-velvet_99...' ...>
# 拼接相对链接为完整 URL
page.urljoin(page.find_by_text('Tipping the Velvet').attrib['href'])
# 部分匹配、限定小写
results = page.find_by_text('the', partial=True, first_match=False, case_sensitive=True)
# 正则匹配价格(字符串或编译对象均可)
page.find_by_regex(r'£[\d\.]+').text # '£51.77'
② 找相似元素:find_similar
这是 Scrapling 的招牌特性之一(灵感来自 AutoScraper,但可作用于任何查找方式得到的元素)。其工作流程在 find_similar 实现 中清晰可见:
- 找出页面中与该元素同 DOM 深度的所有节点(XPath 实现为
[count(ancestor::*) = {depth}]); - 过滤掉标签名、父标签名、祖父标签名不一致的节点——此时正确率已接近 99%;
- 用模糊匹配比较属性,剔除属性相似度不足的元素。
参数说明:
similarity_threshold(默认0.2):第 3 步的属性相似度下限;设为0即关闭属性检查。官方建议除非默认值拿不到想要的元素,否则不要随意调整;ignore_attributes(默认('href', 'src')):比较时忽略的属性名,因为 URL 在不同元素间差异过大、不可靠;match_text(默认False):是否把文本内容纳入比较,一般场景不推荐开启。
实战示例——从一个商品链接出发批量提取整页商品(返回结果不含自身,所以 20 个商品会得到 19 个相似项):
element = page.find_by_text('Tipping the Velvet')
for product in element.parent.parent.find_similar():
print({
"name": product.css('h3 a::text').get(),
"price": product.css('.price_color')[0].re_first(r'[\d\.]+'),
"stock": product.css('.availability::text').getall()[-1].clean()
})
③ 基于过滤器的搜索:find / find_all
受 BeautifulSoup find_all 启发,参数按类型自动解释为过滤器,并按"标签名 → 属性 → 正则 → 函数"的瀑布顺序逐级筛选(参数书写顺序无关紧要):
page = Fetcher.get('https://quotes.toscrape.com/')
page.find_all('div', class_='quote') # 标签 + 属性关键字
page.find_all({'class': 'quote'}) # 仅属性字典
page.find_all('div', {'class': 'quote'},
lambda e: 'world' in e.css('.text::text').get()) # 标签+属性+函数
page.find_all('span', re.compile(r'world')) # 标签+正则
page.find_all({'href$': 'Einstein'}) # 属性后缀匹配
属性字典还支持 CSS 风格的匹配符($ 结尾、* 包含),例如 page.find_all({'href*': '/author/'}) 可直接找出所有作者链接。
补充能力:反向生成选择器与正则抽取
无论元素是哪种方式找到的,都能通过属性访问生成可复用的选择器(实现位于 mixins.py):
url_element = page.find({'href*': '/author/'})
url_element.generate_css_selector # 优先短选择器(找 id 等唯一点作为终止)
url_element.generate_full_css_selector # 从页面根部开始的完整 CSS 选择器
url_element.generate_xpath_selector
url_element.generate_full_xpath_selector
这与 Scrapy/Parsel 一致,re / re_first 也可直接在元素或选择结果上调用(如 page.css('.price_color')[0].re_first(r'[\d\.]+') → '51.77'),进一步减少对脆弱选择器的依赖。
3.3 痛点 T3:反爬对抗——DynamicFetcher 与 StealthyFetcher
教程指出,构造难以被识别的爬虫不止需要住宅/移动代理与拟人化行为,还需要难以被检测的浏览器,Scrapling 提供两级方案:
- DynamicFetcher:灵活的浏览器自动化,配置项丰富,内置少量底层反检测增强,适合中小强度防护;
- StealthyFetcher:基于"隐身浏览器"(一个几乎绕过主流反爬保护的 DynamicFetcher 版本)构建,提供处理剩余防护的工具,并自动通过 Cloudflare Turnstile/Interstitial 各类挑战。
两者与纯 HTTP 的 Fetcher 的官方对比见 Fetchers 概览:
| 维度 | Fetcher | DynamicFetcher | StealthyFetcher |
|---|---|---|---|
| 速度 | 最快 | 中等 | 中等 |
| 隐身/反爬能力 | 低 | 中 | 最高 |
| JavaScript 渲染 | 不支持 | 支持 | 支持 |
| 内存占用 | 低 | 较高 | 较高 |
| 适用场景 | 纯 HTTP 能解决的场景 | 动态加载、小型自动化、中小防护 | 动态加载、复杂防护 |
三者均基于 Chromium/Google Chrome(浏览器版使用 Playwright),v0.3 起每个 fetcher 都有独立的 Session 类以保持会话/浏览器常驻。fetcher 的解析器配置(adaptive、adaptive_domain、huge_tree、keep_comments、keep_cdata、storage、storage_args)统一由类方法 configure 设置(见 toolbelt/custom.py),也可通过 selector_config 参数按单次请求覆盖。
3.4 痛点 B1 & B2:在"陌生站点"上定位关键数据(真实案例)
教程用了一个真实的"去 AI 化"案例:一位用户用 AI 从各电商站点提取价格与标题,作者告诉他完全可以用确定性代码替代。
第一步:用正则定位含价格的元素:
price_element = page.find_by_regex(r'£[\d\.,]+', first_match=True) # 例如匹配到 "£10.50"
# 若想要包含价格元素的容器:
price_element_container = price_element.parent \
or price_element.find_ancestor(lambda ancestor: ancestor.has_class('product'))
# 还可以反向生成选择器固化下来
target_element_selector = price_element_container.generate_css_selector \
or price_element_container.generate_full_css_selector # 或 xpath
第二步:用户抛出反例——货币符号与数字被拆成两个 span:
<span class='currency'> $ </span> <span class='a-price'> 45,000 </span>
解法:放宽正则到数字本身,取父容器后合并全部文本:
price_element_container = page.find_by_regex(r'[\d,]+', first_match=True).parent
full_price_data = price_element_container.get_all_text(strip=True) # 返回 '$45,000'
这个案例给出的通用方法论是:按常见程度为模式建立"正则级联"——先用覆盖最常见版式的正则(货币符号+数字),未命中时依次降级到更宽泛的正则(纯数字、带千分位等)。它会略显枯燥,但成本远低于把整页 HTML 喂给 LLM。正如教程总结的:并非每个难题都需要 AI,有时创造性地组合 Scrapling 的查询方法就能省下大量费用。
3.5 痛点 B3:分页差异的应对模式
教程明确说明:Scrapling 目前没有自动抽取分页 URL 的直接方法(作者表示将在后续版本中补充)。在此之前,可按常见模式人工匹配,命中率已覆盖大多数站点:
# 文本模式
page.find_by_text('Next')['href'] # 或 page.find_by_text('Next').attrib['href']
page.find_by_text('load more')['href']
# 选择器模式(属性包含匹配)
page.css('a[href*="?page="]')
page.css('a[href*="/page/"]')
思路是统一的:优先尝试文本入口(find_by_text 的 partial 匹配可容忍文案差异),再退到 href 结构特征(查询参数、路径段),无限滚动类站点则交给 3.3 节的浏览器 fetcher 处理滚动行为。
四、成本对比:Scrapling vs AI 类工具
教程给出的对比表(AI 工具价格为 Browse AI 与 Oxylabs 公开定价页数据,2019 年后的 SaaS 定价可能已调整,请以厂商官网为准):
| 维度 | Scrapling | AI 类工具(如 Browse AI、Oxylabs) |
|---|---|---|
| 成本结构 | 基本免费或低成本,无按次计费 | 起步约 $19/月至 $49/月,随用量增长 |
| 上手门槛 | 需要少量技术背景与手动配置 | 常为无代码 GUI,对非技术用户更友好 |
| 使用方式 | 代码、终端(CLI/Shell)或 MCP server | GUI 或 API,取决于厂商提供形式 |
| 扩展性 | 取决于用户自身实现 | 内建托管的大规模服务 |
| 适应性 | adaptive 与多种非选择器查询带来高适应性 |
AI 自动适应强,但频繁变更时费用高 |
需要客观说明:AI 类工具的"无代码、托管"优势对非技术用户仍然成立;Scrapling 的胜算在于成本与自主可控——数据不出本地、无 token 账单、行为可复现。此外 Scrapling 的三种使用形态(Python 代码、交互式 Shell/CLI、MCP server)也意味着它并非只能以库的形式存在。
五、结论与落地建议
AI 的能力毋庸置疑,但把整页 HTML 持续送入模型的费用对多数抓取任务是难以接受的。Scrapling 提供的组合拳——adaptive 自适应重定位(SQLite 存储 + 相似度评分 + 阈值控制)、find_by_text/find_by_regex/find_similar/find_all 四类非选择器查询、DynamicFetcher/StealthyFetcher 两级反爬对抗,外加可反向生成的选择器——能够覆盖定向抓取与广义抓取中的绝大多数现实问题,且全部机制在 tests/parser 与 tests/fetchers 下有对应测试用例可验证行为。
落地时的建议路径:先用 find_by_text/find_by_regex 定位关键元素并 generate_css_selector 固化为选择器;在 auto_save 与 adaptive 的帮助下为高频目标元素建立"保险";反爬强度不足时按 Fetcher → DynamicFetcher → StealthyFetcher 逐级升级;对未见过的新站点,采用 3.4 节的"正则级联 + 容器合并文本"策略替代 AI 语义理解。这套以确定性代码为核心的方案,正是 Scrapling 作为"免费 AI 替代"的完整技术图景。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00