首页
/ 从 BeautifulSoup 迁移到 Scrapling:完整的 API 映射与源码级迁移指南

从 BeautifulSoup 迁移到 Scrapling:完整的 API 映射与源码级迁移指南

2026-09-06 12:49:38作者:龚格成

本篇指南面向已经熟悉 BeautifulSoup(BS4)、希望把既有解析代码切换到 Scrapling 的开发者。我们将以仓库中的官方迁移参考文档为骨架,逐行对照 BS4 与 Scrapling 的 API 差异,并结合 scrapling/parser.py 等源码实现,说明每个等价方法背后的真实行为(如选择器如何编译、正则过滤如何叠加、未命中时的返回约定),帮助你在保持代码风格的前提下完成一次平滑且可验证的迁移。

一、迁移前的三个核心认知

在动手改代码之前,先明确 BS4 与 Scrapling 的三点根本差异(源自 迁移参考文档,并与源码逐一印证):

  1. 解析引擎不同。BeautifulSoup 允许你选择 html.parserlxmlhtml5lib 等解析后端;Scrapling 出于性能考虑只使用 lxml。从 Selector 的构造函数 可以看到,内部固定使用 lxml.html.HTMLParser,并开启了 recover=True(容错解析残缺 HTML)、remove_blank_text=Truehuge_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') 迁移后不需要也不应该再传解析器参数

  2. 元素类型不同。BS4 的节点是 Tag 对象,Scrapling 的节点是 Selector 对象,但两者提供高度相似的导航与提取方法。Selector 是对 lxml.html.HtmlElement 的轻量封装(定义见 parser.py L64),源码注释说明了不直接继承 HtmlElement 的原因:lxml 元素代理对象不可 pickle,直接继承会导致大量引用场景抛 AssertionError

  3. 只读设计。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 这类统一的谓词接口替代了成对的方向性快捷方法。

三、元素查找:findfind_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 分支;源码中的 _whitelistedclass_ 自动映射为 classfor_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
  • findfind_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(默认)时返回单个 Selectorfirst_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 是属性,空列表时返回 NoneL1359-L1367),因此 page.css('.foo').first 是"安全取第一个匹配"的惯用写法;
  • Selectorslist[Selector] 的子类,天然支持索引、切片、len(),另有 get(default=None)getall()search(func)filter(func) 四个补充方法。其中 search/filter 就是对照表中所有"在兄弟/后代/祖先里按条件查找"的统一实现(L1321-L1336)。

此外 css()xpath() 在开启自适应后还支持 identifieradaptiveauto_savepercentage 参数(css 签名 L566-L624),属于 BS4 完全没有的能力域:auto_save 会把元素的结构指纹存入 SQLite(存储实现见 scrapling/core/storage.py),页面改版后按相似度分数(relocateL507-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 更方便的地方值得注意:

  1. get_all_text 默认通过 ignore_tags 排除 script/style 节点,而 BS4 的 get_text() 会把它们一并取出来,需要手动剔除;
  2. 所有文本返回值都是 TextHandlerstr 的子类,实现见 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 的导航属性(parentparentsnext_siblingchildrendescendants…)在 Scrapling 中有对应实现,且部分属性名与 lxml 习惯一致:

  • 父级element.parentL383-L387,无父级返回 None);全部祖先用 element.iterancestors() 生成器(L415-L420),按条件找祖先用 element.find_ancestor(lambda p: p.tag == 'a')L422-L430);
  • 祖先路径element.path 返回从根到该元素的完整祖先链 SelectorsL432-L436),配合 .search / .filter 完成对照表 ¹ 标注的祖先搜索;
  • 兄弟element.next / element.previousL438-L460)返回直接相邻兄弟,实现中会自动跳过注释等"被禁止"的节点类型;element.siblingsL408-L413)返回除自己外的全部兄弟——这是 BS4 没有的 N/A 项;
  • 子级/后代element.childrenL397-L406)直接返回 Selectors,无需 list() 包裹;全部后代是 element.below_elementsL389-L395)。

与 BS4 最大的语义差别在于方向性快捷方法被取消了find_nextfind_all_nextfind_previousfind_next_sibling 等不再有对应名称,统一改写为"关系集合 + search/filter"两段式。好处是语义无歧义——below_elements 明确是"当前元素之下的所有元素",不会像 BS4 那样依赖文档序遍历。若你的代码大量使用方向性导航,建议迁移后为每个目标页面补一个断言型测试用例,用 tests/parser/test_ancestor_navigation.pytests/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 能直接工作,因为包顶层通过惰性导入映射暴露了 FetcherSelector 等名字(scrapling/__init__.py L14-L24);Fetcher 本身来自 scrapling/fetchers/requests.pyFetcher.get(url) 返回的 Response 对象本身就是 Selector 的扩展,因此 css()find_all() 等解析方法可直接调用;
  • a::attr(href) 这类 CSS 属性伪元素经由内部翻译器(scrapling/core/translator.pycss_to_xpath)编译为 XPath 执行,所以取 href 不需要逐个节点访问 link['href']
  • 相对链接可以直接用 link_source.urljoin('/relative/path') 拼成绝对地址(urljoin 实现 L329-L331),前提是构造 Selector 时传入了 url 参数——Fetcher.get 返回的响应已自动携带来源 URL。

如果页面是 JS 渲染的,只需把 Fetcher 换成 DynamicFetcher/StealthyFetcherscrapling/fetchers/__init__.py 中同样惰性导出),解析侧代码一行不改——这也是迁移文档标题所述"抓取与解析合并"的完整含义。

八、迁移时必须记住的行为差异

  1. DOM 只读。Scrapling 不提供 append/insert/replace 之类的节点修改 API(append_text 仅用于 get_all_text 内部收集文本,L309-L312)。所有"先改页面再提取"的 BS4 思路都要改为"提取后在 Python 数据结构中处理"。
  2. 未命中返回值约定一致但容器不同find() 与 BS4 一样返回 Nonecss() 返回空 Selectors。访问结果前先判空或判 len(),是两份代码共同的纪律。
  3. 对象不可 pickleSelector/Selectors__getstate__ 显式抛出 TypeErrorL250-L252L1374-L1376),因为 lxml 元素代理不支持序列化。多线程/多进程共享页面时,应在各进程中重新解析原始 HTML,而不是分发 Selector 对象。
  4. find_all 的位置参数是标签名page.find_all('p', 'story') 中两个字符串都被视为标签名(源码 L720-L727);要按属性过滤请用 kwargs(class_=)或属性字典({'class': 'story'})。
  5. 版本与运行前提。当前仓库代码版本为 0.4.13(scrapling/__init__.py L2),SKILL.md 声明要求 Python 3.10+,安装方式为 pip install "scrapling[all]>=0.4.13"(含浏览器依赖需再执行 scrapling install)。仅使用 Selector 做纯解析迁移时,标准 scrapling 包即可满足。

九、迁移检查清单

按以下顺序改造既有 BS4 代码,即可覆盖绝大多数场景:

  1. 替换导入:from scrapling.parser import Selector(或用 from scrapling import Selector);
  2. BeautifulSoup(html, ...)Selector(html),需要相对链接解析时补 url=...
  3. find/find_all/find(text=...)/select/select_one 按第二节对照表逐行替换,注意 select_one 要写成 .css(...).first
  4. .string/.get_text(strip=True)/.attrs 分别改为 .text/.get_all_text(strip=True)/.attrib
  5. 方向性导航(find_parentfind_next_sibling 等)改写为 iterancestors/siblings/below_elements/path + search/filter 的两段式写法;
  6. 删除所有 DOM 修改逻辑,改为提取后处理;
  7. 为每条关键选择补测试,确认 None/空列表分支的行为符合预期;
  8. (可选)把 clean()find_by_text(partial=...)::attr() 伪元素用起来,替换 BS4 时代的正则后处理,让迁移后的代码比原来更短。

完成以上步骤后,你的代码在保留 BeautifulSoup 时代全部解析习惯的同时,获得了 Scrapling 的 lxml 解析性能、统一的谓词过滤接口,以及抓取与解析一体化(Fetcher/DynamicFetcher/StealthyFetcher)带来的扩展空间——这些能力域的完整文档可进一步参见仓库内的 解析 API 参考解析器主类文档

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