从 BeautifulSoup 迁移到 Scrapling:完整的 API 映射与源码级迁移指南
本篇指南面向已经熟悉 BeautifulSoup(BS4)、希望把既有解析代码切换到 Scrapling 的开发者。我们将以仓库中的官方迁移参考文档为骨架,逐行对照 BS4 与 Scrapling 的 API 差异,并结合 scrapling/parser.py 等源码实现,说明每个等价方法背后的真实行为(如选择器如何编译、正则过滤如何叠加、未命中时的返回约定),帮助你在保持代码风格的前提下完成一次平滑且可验证的迁移。
一、迁移前的三个核心认知
在动手改代码之前,先明确 BS4 与 Scrapling 的三点根本差异(源自 迁移参考文档,并与源码逐一印证):
-
解析引擎不同。BeautifulSoup 允许你选择
html.parser、lxml、html5lib等解析后端;Scrapling 出于性能考虑只使用lxml。从 Selector 的构造函数 可以看到,内部固定使用lxml.html.HTMLParser,并开启了recover=True(容错解析残缺 HTML)、remove_blank_text=True、huge_tree=True等参数:# scrapling/parser.py(节选) _parser_kwargs: Dict[str, Any] = dict( recover=True, remove_blank_text=True, remove_comments=(not keep_comments), encoding=encoding, compact=True, huge_tree=huge_tree, default_doctype=True, strip_cdata=(not keep_cdata), ) parser = HTMLParser(**_parser_kwargs)这意味着 BS4 代码中的
BeautifulSoup(html, 'html.parser')迁移后不需要也不应该再传解析器参数。 -
元素类型不同。BS4 的节点是
Tag对象,Scrapling 的节点是Selector对象,但两者提供高度相似的导航与提取方法。Selector是对lxml.html.HtmlElement的轻量封装(定义见 parser.py L64),源码注释说明了不直接继承HtmlElement的原因:lxml 元素代理对象不可 pickle,直接继承会导致大量引用场景抛AssertionError。 -
只读设计。BS4 支持解析后修改、重组 DOM;Scrapling 刻意不提供这些能力——它被优化为"快速读取并提取",提取完成后由你的业务代码处理数据。参考文档的结论是:这是两种定位不同的工具,其中一种专精于网页抓取本身。
解析器导入与创建
# BeautifulSoup
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
# Scrapling
from scrapling.parser import Selector
page = Selector(html)
Selector 的构造参数(源码 L80-L94)还包括:
url:随 HTML 数据一起存储的来源 URL,供后续urljoin()解析相对链接使用;encoding:解析编码,默认utf-8;huge_tree:默认True,解析大型 HTML 文档时应保持开启(控制 libxml2 防止内存耗尽的解析限制);keep_comments/keep_cdata:默认均为False,解析时会丢弃注释与 CDATA 以获得更干净的 HTML;adaptive:自适应定位功能的总开关(后文单独说明)。
二、完整 API 对照表
以下对照表完整继承了官方迁移文档的核心内容,覆盖了抓取网页时最常见的操作。每一行都是"同一任务、两套写法":
| 任务 | BeautifulSoup 代码 | Scrapling 代码 |
|---|---|---|
| 导入解析器 | from bs4 import BeautifulSoup |
from scrapling.parser import Selector |
| 从字符串解析 HTML | soup = BeautifulSoup(html, 'html.parser') |
page = Selector(html) |
| 查找单个元素 | element = soup.find('div', class_='example') |
element = page.find('div', class_='example') |
| 查找多个元素 | elements = soup.find_all('div', class_='example') |
elements = page.find_all('div', class_='example') |
| 查找单个元素(字典属性写法) | element = soup.find('div', attrs={"class": "example"}) |
element = page.find('div', {"class": "example"}) |
| 查找单个元素(正则匹配文本) | element = soup.find(re.compile("^b")) |
element = page.find(re.compile("^b")) 或 element = page.find_by_regex(r"^b") |
| 查找单个元素(Lambda 过滤) | element = soup.find(lambda e: len(list(e.children)) > 0) |
element = page.find(lambda e: len(e.children) > 0) |
| 查找单个元素(多标签列表) | element = soup.find(["a", "b"]) |
element = page.find(["a", "b"]) |
| 按文本内容查找元素 | element = soup.find(text="some text") |
element = page.find_by_text("some text", partial=False) |
| CSS 选择器找第一个匹配 | element = soup.select_one('div.example') |
element = page.css('div.example').first |
| CSS 选择器找全部匹配 | elements = soup.select('div.example') |
elements = page.css('div.example') |
| 获取格式化后的源码 | prettified = soup.prettify() |
prettified = page.prettify() |
| 获取非格式化源码 | source = str(soup) |
source = page.html_content |
| 获取元素标签名 | name = element.name |
name = element.tag |
| 提取元素文本 | string = element.string |
string = element.text |
| 提取文档/标签下全部文本 | text = soup.get_text(strip=True) |
text = page.get_all_text(strip=True) |
| 获取属性字典 | attrs = element.attrs |
attrs = element.attrib |
| 提取单个属性 | attr = element['href'] |
attr = element['href'] |
| 访问父元素 | parent = element.parent |
parent = element.parent |
| 获取所有祖先 | parents = list(element.parents) |
parents = list(element.iterancestors()) |
| 在祖先中查找元素 | target_parent = element.find_parent("a") |
target_parent = element.find_ancestor(lambda p: p.tag == 'a') |
| 获取所有兄弟元素 | N/A | siblings = element.siblings |
| 获取下一个兄弟 | next_element = element.next_sibling |
next_element = element.next |
| 在兄弟中查找单个元素 | target = element.find_next_sibling("a") / find_previous_sibling("a") |
target = element.siblings.search(lambda s: s.tag == 'a') |
| 在兄弟中查找多个元素 | target = element.find_next_siblings("a") / find_previous_siblings("a") |
target = element.siblings.filter(lambda s: s.tag == 'a') |
| 在后续元素中查找单个 | target = element.find_next("a") |
target = element.below_elements.search(lambda p: p.tag == 'a') |
| 在后续元素中查找多个 | target = element.find_all_next("a") |
target = element.below_elements.filter(lambda p: p.tag == 'a') |
| 在祖先中查找单个 ¹ | target = element.find_previous("a") ¹ |
target = element.path.search(lambda p: p.tag == 'a') |
| 在祖先中查找多个 ¹ | target = element.find_all_previous("a") ¹ |
target = element.path.filter(lambda p: p.tag == 'a') |
| 获取上一个兄弟 | prev_element = element.previous_sibling |
prev_element = element.previous |
| 访问子元素 | children = list(element.children) |
children = element.children |
| 获取全部后代 | children = list(element.descendants) |
children = element.below_elements |
| 过滤元素组 | group = soup.find('p', 'story').css.filter('a') |
group = page.find_all('p', 'story').filter(lambda p: p.tag == 'a') |
¹ 注意:BS4 的
find_previous/find_all_previous按文档顺序搜索所有前置元素;Scrapling 的path只返回祖先链(父级链路)。两者并非完全等价的替代,但祖先查找覆盖了绝大多数常见用例。
会注意到 Scrapling 缺少部分 BS4 的"捷径"方法(如 find_parent("a") 的直接标签名查找)。参考文档的解释是:当一个功能可以用一行短代码表达时,没有必要为它牺牲性能——Scrapling 通过 search/filter 这类统一的谓词接口替代了成对的方向性快捷方法。
三、元素查找:find 与 find_all 的实现原理
对照表中最接近 BS4 习惯的是 find / find_all。阅读 find_all 的源码 后,可以确认它的执行策略是"先构建 CSS 选择器,再叠加过滤":
# scrapling/parser.py(find_all 核心逻辑节选)
for arg in args:
if isinstance(arg, str):
tags.add(arg) # 位置字符串参数 = 标签名
elif type(arg) in (list, tuple, set):
tags.update(set(arg)) # 标签名列表
elif isinstance(arg, dict):
attributes.update(arg) # 属性字典
elif isinstance(arg, re_Pattern):
patterns.add(arg) # 正则:作用于元素文本
elif callable(arg):
functions.append(arg) # Lambda:对 Selector 求值
# 属性经 kwargs 传入,class_ 会被映射回 class
for attribute_name, value in kwargs.items():
attribute_name = _whitelisted.get(attribute_name, attribute_name)
attributes[attribute_name] = value
# 组装为 CSS:如 "div[class=\"example\"]",多个标签用逗号合并
selector += '[{}="{}"]'.format(key, value)
results = self.css(", ".join(selectors))
# 再依次应用正则与 Lambda 过滤
for pattern in patterns:
results = results.filter(lambda e: e.text.re(pattern, check_match=True))
for function in functions:
results = results.filter(function)
由此得到几条迁移时的实操要点(均有源码依据):
- 位置参数只接受标签名(字符串、标签名列表、正则、Lambda、属性字典五类)。
page.find('div', class_='example')中的class_='example'走 kwargs 分支;源码中的_whitelisted把class_自动映射为class(for_→for),与 BS4 的命名习惯保持一致,因此这类调用可以原样照抄。 - 正则参数的语义:BS4 中
soup.find(re.compile("^b"))用正则匹配标签名;Scrapling 中find(re.compile(...))的正则作用于元素文本内容(e.text.re(pattern, check_match=True))。如果确实需要按正则找标签名,对照表给出的官方等价方式是find_by_regex(r"^b")——但需注意其实现(L1160-L1197)同样是遍历"有文本内容的元素"并匹配其文本。迁移涉及正则查找的行时,建议先跑一遍测试用例确认命中对象是否符合预期。 - Lambda 的签名要求:传入的可调用对象必须接受至少一个参数(接收
Selector对象),否则抛出TypeError,见 L739-L745。 find是find_all的"取第一个"包装(L790-L803),无匹配时返回None,与 BS4 一致。
按文本内容查找:find_by_text
BS4 的 soup.find(text="some text") 对应的官方写法是 page.find_by_text("some text", partial=False)。find_by_text 的签名提供了比 BS4 更精细的控制:
def find_by_text(
self,
text: str,
first_match: bool = True, # 默认只返回第一个匹配
partial: bool = False, # False=精确匹配文本,True=包含匹配
case_sensitive: bool = False, # 默认不区分大小写
clean_match: bool = True, # 默认清洗空白/连续空格后再比较
)
partial=False 时是整段文本相等比较,partial=True 则是"文本包含"。clean_match 默认开启会先调用 TextHandler.clean() 去除多余空白,这对处理 HTML 中大量换行缩进的场景非常实用。同样风格的 API 还有 find_by_regex(query, first_match, case_sensitive, clean_match)(L1160),二者在 first_match=True(默认)时返回单个 Selector,first_match=False 时返回 Selectors 列表——这一返回值约定在迁移时比 BS4 的"总是列表"更省心。
四、CSS 选择器与 Selectors 容器
BS4 的 select / select_one 迁移到 Scrapling 后是 css() 方法,且它返回的 Selectors 是一个可继续链式查询的容器(Selectors 类定义见 parser.py L1200):
# 等价于 soup.select('div.example')
elements = page.css('div.example')
# 等价于 soup.select_one('div.example')
element = page.css('div.example').first # 无匹配时返回 None
# Scrapling 独有能力:CSS 属性伪元素直接取属性值
hrefs = page.css('a::attr(href)') # TextHandlers 列表
text = page.css('.quote .text::text') # 取文本内容
# 链式查询:对列表中每个元素执行 css,结果自动展平
author = page.css('.quote').css('.author::text')
与 BS4 不同的关键行为约定:
page.css('.foo')无匹配时返回空Selectors列表而非None;Selectors.first/Selectors.last是属性,空列表时返回None(L1359-L1367),因此page.css('.foo').first是"安全取第一个匹配"的惯用写法;Selectors是list[Selector]的子类,天然支持索引、切片、len(),另有get(default=None)、getall()、search(func)、filter(func)四个补充方法。其中search/filter就是对照表中所有"在兄弟/后代/祖先里按条件查找"的统一实现(L1321-L1336)。
此外 css() 与 xpath() 在开启自适应后还支持 identifier、adaptive、auto_save、percentage 参数(css 签名 L566-L624),属于 BS4 完全没有的能力域:auto_save 会把元素的结构指纹存入 SQLite(存储实现见 scrapling/core/storage.py),页面改版后按相似度分数(relocate,L507-L564)自动重定位元素。这是迁移时可选的"白送"升级,不启用则行为与普通选择器一致。
五、属性、文本与源码输出
这一组 API 的迁移几乎是逐字替换,源码实现都很直接:
| 需求 | Scrapling API | 源码位置 |
|---|---|---|
| 标签名 | element.tag(文本节点返回 #text) |
parser.py L259-L266 |
| 元素自身文本 | element.text(返回 TextHandler) |
parser.py L268-L277 |
| 子树全部文本 | page.get_all_text(separator="\n", strip=False, ignore_tags=("script","style")) |
parser.py L279-L327 |
| 属性字典 | element.attrib(返回 AttributesHandler) |
parser.py L333-L340 |
| 单个属性 | element['href'],配合 in 判断存在性 |
parser.py L183-L191 |
| 原始 HTML | element.html_content |
parser.py L342-L350 |
| 美化输出 | page.prettify() |
parser.py L359-L372 |
两点比 BS4 更方便的地方值得注意:
get_all_text默认通过ignore_tags排除script/style节点,而 BS4 的get_text()会把它们一并取出来,需要手动剔除;- 所有文本返回值都是
TextHandler(str的子类,实现见 scrapling/core/custom_types.py),除了字符串全部方法外,还提供clean()(移除各类空白与连续空格,L104-L109)、re()/re_first()(在提取结果上直接跑正则)、json()(响应体可解析时转 dict)等方法。BS4 迁移代码里常见的re.search(r'\d+', text)后处理,在 Scrapling 中可以写成page.css('.price').get().re(r"\d+")。
六、DOM 树导航
BS4 的导航属性(parent、parents、next_sibling、children、descendants…)在 Scrapling 中有对应实现,且部分属性名与 lxml 习惯一致:
- 父级:
element.parent(L383-L387,无父级返回None);全部祖先用element.iterancestors()生成器(L415-L420),按条件找祖先用element.find_ancestor(lambda p: p.tag == 'a')(L422-L430); - 祖先路径:
element.path返回从根到该元素的完整祖先链Selectors(L432-L436),配合.search/.filter完成对照表 ¹ 标注的祖先搜索; - 兄弟:
element.next/element.previous(L438-L460)返回直接相邻兄弟,实现中会自动跳过注释等"被禁止"的节点类型;element.siblings(L408-L413)返回除自己外的全部兄弟——这是 BS4 没有的 N/A 项; - 子级/后代:
element.children(L397-L406)直接返回Selectors,无需list()包裹;全部后代是element.below_elements(L389-L395)。
与 BS4 最大的语义差别在于方向性快捷方法被取消了:find_next、find_all_next、find_previous、find_next_sibling 等不再有对应名称,统一改写为"关系集合 + search/filter"两段式。好处是语义无歧义——below_elements 明确是"当前元素之下的所有元素",不会像 BS4 那样依赖文档序遍历。若你的代码大量使用方向性导航,建议迁移后为每个目标页面补一个断言型测试用例,用 tests/parser/test_ancestor_navigation.py、tests/parser/test_selectors_filter.py 这类仓库内置测试的写法核对导航结果。
七、完整实战:把"抓取所有链接"从两步变一步
官方迁移文档给出的端到端示例是:BS4 版需要 requests + BeautifulSoup 两个库、先请求再解析;Scrapling 版则把抓取和解析合并在一步完成。
BeautifulSoup 版本:
import requests
from bs4 import BeautifulSoup
url = 'https://example.com'
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
links = soup.find_all('a')
for link in links:
print(link['href'])
Scrapling 版本:
from scrapling import Fetcher
url = 'https://example.com'
page = Fetcher.get(url)
links = page.css('a::attr(href)')
for link in links:
print(link)
几个可以确认的实现事实:
from scrapling import Fetcher能直接工作,因为包顶层通过惰性导入映射暴露了Fetcher、Selector等名字(scrapling/__init__.py L14-L24);Fetcher本身来自 scrapling/fetchers/requests.py,Fetcher.get(url)返回的Response对象本身就是Selector的扩展,因此css()、find_all()等解析方法可直接调用;a::attr(href)这类 CSS 属性伪元素经由内部翻译器(scrapling/core/translator.py 的css_to_xpath)编译为 XPath 执行,所以取href不需要逐个节点访问link['href'];- 相对链接可以直接用
link_source.urljoin('/relative/path')拼成绝对地址(urljoin 实现 L329-L331),前提是构造Selector时传入了url参数——Fetcher.get返回的响应已自动携带来源 URL。
如果页面是 JS 渲染的,只需把 Fetcher 换成 DynamicFetcher/StealthyFetcher(scrapling/fetchers/__init__.py 中同样惰性导出),解析侧代码一行不改——这也是迁移文档标题所述"抓取与解析合并"的完整含义。
八、迁移时必须记住的行为差异
- DOM 只读。Scrapling 不提供
append/insert/replace之类的节点修改 API(append_text仅用于get_all_text内部收集文本,L309-L312)。所有"先改页面再提取"的 BS4 思路都要改为"提取后在 Python 数据结构中处理"。 - 未命中返回值约定一致但容器不同。
find()与 BS4 一样返回None;css()返回空Selectors。访问结果前先判空或判len(),是两份代码共同的纪律。 - 对象不可 pickle。
Selector/Selectors的__getstate__显式抛出TypeError(L250-L252、L1374-L1376),因为 lxml 元素代理不支持序列化。多线程/多进程共享页面时,应在各进程中重新解析原始 HTML,而不是分发Selector对象。 find_all的位置参数是标签名。page.find_all('p', 'story')中两个字符串都被视为标签名(源码 L720-L727);要按属性过滤请用 kwargs(class_=)或属性字典({'class': 'story'})。- 版本与运行前提。当前仓库代码版本为 0.4.13(scrapling/__init__.py L2),SKILL.md 声明要求 Python 3.10+,安装方式为
pip install "scrapling[all]>=0.4.13"(含浏览器依赖需再执行scrapling install)。仅使用Selector做纯解析迁移时,标准scrapling包即可满足。
九、迁移检查清单
按以下顺序改造既有 BS4 代码,即可覆盖绝大多数场景:
- 替换导入:
from scrapling.parser import Selector(或用from scrapling import Selector); BeautifulSoup(html, ...)→Selector(html),需要相对链接解析时补url=...;find/find_all/find(text=...)/select/select_one按第二节对照表逐行替换,注意select_one要写成.css(...).first;.string/.get_text(strip=True)/.attrs分别改为.text/.get_all_text(strip=True)/.attrib;- 方向性导航(
find_parent、find_next_sibling等)改写为iterancestors/siblings/below_elements/path+search/filter的两段式写法; - 删除所有 DOM 修改逻辑,改为提取后处理;
- 为每条关键选择补测试,确认
None/空列表分支的行为符合预期; - (可选)把
clean()、find_by_text(partial=...)、::attr()伪元素用起来,替换 BS4 时代的正则后处理,让迁移后的代码比原来更短。
完成以上步骤后,你的代码在保留 BeautifulSoup 时代全部解析习惯的同时,获得了 Scrapling 的 lxml 解析性能、统一的谓词过滤接口,以及抓取与解析一体化(Fetcher/DynamicFetcher/StealthyFetcher)带来的扩展空间——这些能力域的完整文档可进一步参见仓库内的 解析 API 参考 与 解析器主类文档。
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 StartedRust0627
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