首页
/ Scrapling:以自适应与智能选择器替代 AI 的高成本网页抓取方案

Scrapling:以自适应与智能选择器替代 AI 的高成本网页抓取方案

2026-09-06 16:06:48作者:伍霜盼Ellen

本文基于 Scrapling 仓库中的教程 replacing_ai.md 展开,系统讲解网页抓取中反复出现的三类痛点——站点结构快速变更、选择器不稳定、反爬机制升级——以及广义抓取(Broad/Generic Scraping)下的网站多样性与分页问题;读完你将掌握如何用 Scrapling 的 adaptive 自适应重定位、非选择器查询方法和双浏览器抓取器,在几乎零 API 成本下解决这些曾经需要付费 AI 工具才能应对的问题。

一、网页抓取的持久痛点:从定向到广义

Web scraping 一直是数据抽取、索引构建和训练数据准备的核心工具,但经验用户会不断遇到同一批顽固问题。教程将其归纳为三类定向抓取(Targeted Scraping)痛点:

  1. 网站结构快速变更:站点频繁更新 DOM 结构,静态 XPath/CSS 选择器随之失效;
  2. 不稳定的选择器:class/id 经常改名,或干脆是随机生成的值,使抓取器失效;
  3. 日益复杂的反爬措施:CAPTCHA、浏览器指纹识别、行为分析让传统抓取越来越难。

如果你只针对少数已知站点做定向抓取,还能"一站一写"地硬编码。但一旦目标是广义抓取(处理无数互不共享技术的站点),上述问题会进一步放大,并新增三类挑战:

  1. 极端的网站多样性:HTML 结构、CSS 用法、JS 框架、后端技术千差万别;
  2. 如何识别相关数据:面对一个从未见过的页面,抓取器凭什么知道哪个数据才是重要的?
  3. 分页方式各异:无限滚动、传统翻页、"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.orgstackoverflow.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 实现 中清晰可见:

  1. 找出页面中与该元素同 DOM 深度的所有节点(XPath 实现为 [count(ancestor::*) = {depth}]);
  2. 过滤掉标签名、父标签名、祖父标签名不一致的节点——此时正确率已接近 99%;
  3. 用模糊匹配比较属性,剔除属性相似度不足的元素。

参数说明:

  • 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 提供两级方案:

  1. DynamicFetcher:灵活的浏览器自动化,配置项丰富,内置少量底层反检测增强,适合中小强度防护;
  2. StealthyFetcher:基于"隐身浏览器"(一个几乎绕过主流反爬保护的 DynamicFetcher 版本)构建,提供处理剩余防护的工具,并自动通过 Cloudflare Turnstile/Interstitial 各类挑战

两者与纯 HTTP 的 Fetcher 的官方对比见 Fetchers 概览

维度 Fetcher DynamicFetcher StealthyFetcher
速度 最快 中等 中等
隐身/反爬能力 最高
JavaScript 渲染 不支持 支持 支持
内存占用 较高 较高
适用场景 纯 HTTP 能解决的场景 动态加载、小型自动化、中小防护 动态加载、复杂防护

三者均基于 Chromium/Google Chrome(浏览器版使用 Playwright),v0.3 起每个 fetcher 都有独立的 Session 类以保持会话/浏览器常驻。fetcher 的解析器配置(adaptiveadaptive_domainhuge_treekeep_commentskeep_cdatastoragestorage_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_textpartial 匹配可容忍文案差异),再退到 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/CLIMCP server)也意味着它并非只能以库的形式存在。

五、结论与落地建议

AI 的能力毋庸置疑,但把整页 HTML 持续送入模型的费用对多数抓取任务是难以接受的。Scrapling 提供的组合拳——adaptive 自适应重定位(SQLite 存储 + 相似度评分 + 阈值控制)、find_by_text/find_by_regex/find_similar/find_all 四类非选择器查询、DynamicFetcher/StealthyFetcher 两级反爬对抗,外加可反向生成的选择器——能够覆盖定向抓取与广义抓取中的绝大多数现实问题,且全部机制在 tests/parsertests/fetchers 下有对应测试用例可验证行为。

落地时的建议路径:先用 find_by_text/find_by_regex 定位关键元素并 generate_css_selector 固化为选择器;在 auto_saveadaptive 的帮助下为高频目标元素建立"保险";反爬强度不足时按 Fetcher → DynamicFetcher → StealthyFetcher 逐级升级;对未见过的新站点,采用 3.4 节的"正则级联 + 容器合并文本"策略替代 AI 语义理解。这套以确定性代码为核心的方案,正是 Scrapling 作为"免费 AI 替代"的完整技术图景。

登录后查看全文
热门项目推荐
相关项目推荐