首页
/ 从 BeautifulSoup 迁移到 Scrapling:API 对照、源码级解析与实战迁移指南

从 BeautifulSoup 迁移到 Scrapling:API 对照、源码级解析与实战迁移指南

2026-09-06 12:07:01作者:鲍丁臣Ursa

如果你已经熟悉 BeautifulSoup 的 find/select/get_text 等 API,Scrapling 提供的 Selector 解析器可以让你的解析代码几乎"平移"迁移,同时把底层解析引擎换成 lxml 并获得更快的执行速度。本文基于仓库中的迁移文档 migrating_from_beautifulsoup.md 展开,逐一核对仓库源码(核心实现在 scrapling/parser.py),给出完整的 BS 与 Scrapling 操作对照表、每个 API 的底层实现原理,以及一个可直接运行的完整迁移示例,帮助你把现有的 BeautifulSoup 爬虫代码系统性替换为 Scrapling 写法。

迁移前的总体认知:两个库的定位差异

原文档在结尾给出了一个必须记住的关键点:BeautifulSoup 提供解析后修改、操作页面结构的能力(如 insertreplace_with 等 DOM 改写操作);Scrapling 专注于更快地抓取并提取数据,提取之后你可以对数据做任何想做的事。两者都可用于 Web 抓取,但 Scrapling 专精于"抓取"这一侧。这意味着迁移时如果代码中存在"解析后回写 HTML"的用法,需要改为"提取数据、自行拼装"的思路,而不是寻找对应的 DOM 修改 API。

这个定位在源码中体现得很直接:Selector 内部包装的是 lxml.html.HtmlElement,但刻意没有继承它(因为 lxml 的 Element 对象不可 pickle,会破坏很多序列化场景),而是通过 __slots__ 封装并只暴露"读取/检索"接口,源码注释明确写道 "Scrapling focuses more on scraping the page faster for you"。相关实现可参见 Selector 类定义

核心对照表:最常用的操作逐行迁移

下表完整覆盖抓取网页时最常见的操作(完整继承自原文档对照表),每一行展示 BeautifulSoup 写法与 Scrapling 的对应方法:

任务 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')
查找单个元素(示例 2,属性字典) element = soup.find('div', attrs={"class": "example"}) element = page.find('div', {"class": "example"})
查找单个元素(示例 3,正则) element = soup.find(re.compile("^b")) element = page.find(re.compile("^b"))
element = page.find_by_regex(r"^b")
查找单个元素(示例 4,函数过滤) element = soup.find(lambda e: len(list(e.children)) > 0) element = page.find(lambda e: len(e.children) > 0)
查找单个元素(示例 5,标签列表) 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 选择器查找第一个匹配元素 elements = soup.select_one('div.example') elements = 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_sibling = element.find_next_sibling("a")
target_sibling = element.find_previous_sibling("a")
target_sibling = element.siblings.search(lambda s: s.tag == 'a')
在兄弟中查找多个元素 target_sibling = element.find_next_siblings("a")
target_sibling = element.find_previous_siblings("a")
target_sibling = element.siblings.filter(lambda s: s.tag == 'a')
在元素之后的后代中查找 target_parent = element.find_next("a") target_parent = element.below_elements.search(lambda p: p.tag == 'a')
在元素之后的后代中查找多个 target_parent = element.find_all_next("a") target_parent = element.below_elements.filter(lambda p: p.tag == 'a')
在祖先中查找元素 target_parent = element.find_previous("a") ¹ target_parent = element.path.search(lambda p: p.tag == 'a')
在祖先中查找多个元素 target_parent = element.find_all_previous("a") ¹ target_parent = 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 只返回祖先链(父元素链)。两者不是严格等价,但"在祖先中查找"覆盖了绝大多数实际用例。

值得注意的几点,均可在源码中得到印证:

  • 函数过滤的写法有细微差别:BS 的 lambda 接收的是带 .children 迭代器属性的 Tag,而 Scrapling 的 children 是返回 Selectors 容器的属性find() 内部会校验传入的可调用对象必须接受至少一个参数(见 find_all 参数解析),过滤逻辑统一委托给 Selectors.filter实现位置)。
  • 正则查找有两条路径page.find(re.compile("^b"))find_all 内建的正则分支(对每个候选元素的文本调用 text.re(pattern, check_match=True),见 源码);更专门的文本正则查找则用 find_by_regex(r"^b")实现位置),它还额外提供 first_matchcase_sensitiveclean_match 三个控制参数。
  • 按文本查找用 find_by_text:对应 BS 的 find(text="some text")partial=False 表示精确匹配元素直接文本,partial=True 表示包含匹配;默认 first_match=True 只返回第一个匹配(返回单个 Selector),设为 False 则返回全部(返回 Selectors),见 find_by_text 实现
  • class_ 等 Python 保留字属性:Scrapling 与 BS 一样允许 class_for_ 写法。源码中有一个白名单映射,在解析 find/find_all 的关键字参数时把 class_ 还原为真实的 class 属性名(白名单定义kwargs 处理逻辑)。
  • 兄弟元素是 BS 没有的能力:BS4 没有"获取全部兄弟"的内置属性,Scrapling 的 element.siblings 直接返回除自身之外的所有兄弟(实现位置)。在兄弟中做单发/批量查找,分别用 siblings.search(lambda)siblings.filter(lambda)
  • "后代搜索"用 below_elements:对应 BS 的 find_next/find_all_nextbelow_elements 返回当前元素之下所有后代(内部使用预编译的 XPath .//*实现位置),再配合 Selectorssearch/filter 完成条件过滤。

为什么 Scrapling 比 BeautifulSoup 快

原文档指出:你会注意到 Scrapling 缺少一些 BeautifulSoup 的"快捷方式",而这正是 BS 更慢的原因之一——如果一个功能本可以用一行短代码表达,就没必要为了缩短这一行而牺牲性能。从源码结构看,Scrapling 的速度来源包括:

  1. 统一使用 lxml(见下文"不同解析器"一节),解析与 XPath 求值都走 C 扩展;
  2. Selector 使用 __slots__ 并刻意不继承 lxml.html.HtmlElement,避免 lxml Element 代理的序列化问题(源码注释);
  3. tagtextattrib 等属性是惰性求值的——源码注释解释,把这些做成属性而非初始化时立即计算的变量,让"只在首次需要时执行并缓存","性能测试因此快了好几倍"(属性实现);
  4. find/find_all 尽量构造选择器而非遍历整棵树:源码注释写道 "It's easier and faster to build a selector than traversing the tree",即把标签 + 属性过滤拼接成 CSS 选择器一次求值(实现位置)。

解析器导入与 HTML 解析

导入方式有两种等价写法。原文档对照表使用的是显式模块导入:

from scrapling.parser import Selector

也可以从包顶层懒加载导入——scrapling/init.py 中维护了一张懒导入映射表,SelectorSelectorsFetcherAsyncFetcherStealthyFetcherDynamicFetcher 都通过 __getattr__ 按需加载,避免导入未使用的引擎模块:

from scrapling import Selector
page = Selector(html)

解析行为与 BS 的关键差异在于:Scrapling 不提供解析器引擎选择(BS 可以在 html.parserlxmlhtml5lib 之间切换),而是固定使用 lxml 以保证性能。从 Selector.init 可以看到实际构造的 HTMLParser 参数:

_parser_kwargs = dict(
    recover=True,             # 容错模式,能修复不完整 HTML
    remove_blank_text=True,   # 移除空白文本节点
    remove_comments=(not keep_comments),
    encoding=encoding,        # 默认 utf-8
    compact=True,
    huge_tree=huge_tree,     # 默认 True,解析超大文档必须开启
    default_doctype=True,
    strip_cdata=(not keep_cdata),
)

由此得到几个可落地的默认值与取值范围说明:

  • content:str 或 bytes,必填(否则抛出 ValueError);
  • url:默认 "",可随 HTML 保存来源 URL,之后用 selector.urljoin('/path') 把相对链接补全为绝对链接(urljoin 实现);
  • encoding:默认 "utf-8"
  • huge_tree:默认 True,控制 libxml2 对大文档的内存保护,解析大型 HTML 时建议保持开启;
  • keep_comments / keep_cdata:默认 False,即解析时丢弃注释与 CDATA,得到更干净的 HTML。

解析后得到的页面是 Selector 对象;BS 中的 Tag 对象在 Scrapling 中对应 Selector,两者提供相似的导航与提取方法,但 Scrapling 额外提供了 find_by_textfind_by_regexsiblings 等 BS 没有的能力,以及 Selector 之外的 Selectors 结果容器(本质是 list 的子类,另提供 searchfilterfirstlastgetgetall 等方法,实现位置)。

文本与属性提取的源码级细节

对照表中与文本相关的四行(tag/text/get_all_text/attrib)在源码中的行为值得展开:

  • element.tag:返回标签名字符串;若元素是文本节点(如 CSS 的 ::text 结果)则返回 "#text"实现位置)。
  • element.text:返回元素的直接文本element.string 的对应物),被包装为 TextHandler——一个 str 子类,因此可以直接当普通字符串用,同时还能链式调用 clean()re()json() 等增强方法(实现位置)。
  • get_all_text(separator="\n", strip=False, ignore_tags=("script", "style"), valid_values=True):对应 BS 的 get_text(),但默认自动忽略 <script><style> 标签,这点对抓取正文时避免混入 JS 代码非常实用(实现位置)。
  • element.attrib:返回 AttributesHandlerdict 的增强子类);element['href'] 这种下标访问依然可用(getitem 实现)。

TextHandler 的增强能力在原文档"额外说明"中被点到:clean() 可以去除多余空白、连续空格等。源码实现是先用 str.translate 去除空白字符、可选地替换 HTML 实体,再用正则把连续空格收敛为一个并 strip()实现位置):

def clean(self, remove_entities=False):
    data = self.translate(__CLEANING_TABLE__)
    if remove_entities:
        data = _replace_entities(data)
    return self.__class__(__CONSECUTIVE_SPACES_REGEX__.sub(" ", data).strip())

page.get_all_text().clean()element.text.clean() 这类写法可以直接用于规整脏文本。

选择器:css() 与 xpath()

Scrapling 的选择器方法返回的是 Selectors 容器(而不是 BS 的 ResultSet/列表),因此 page.css('div.example') 对应 soup.select('div.example')page.css('div.example').first 对应 soup.select_one('div.example')first 属性在列表为空时安全地返回 None实现位置),这正是原文档"错误处理"一节推荐的安全取法。

从源码结构看,CSS 选择器最终都会翻译成 XPath 再执行css 方法 内部调用 css_to_xpath(翻译器位于 scrapling/core/translator.py),然后委托给 xpath 方法(实现位置)。这意味着:

  • CSS 的伪元素扩展同样可用,例如 a::attr(href) 直接提取属性值字符串(下文完整示例就用到了它);
  • 你也可以完全绕过 CSS,直接用 page.xpath('//div[@class="example"]'),且 XPath 的额外关键字参数会作为 XPath 变量传入表达式。

css/xpath 还接受 identifieradaptiveauto_savepercentage 参数(默认 percentage=40),用于 Scrapling 的自适应定位(adaptive)特性——当页面结构变化时按相似度重新定位元素。该特性与本文迁移主题正交,完整说明见 adaptive.md,BS 迁移过来时可以先忽略这些参数,只使用位置参数。

把一切串起来:完整的链接抓取迁移示例

原文档给出了一个"抓取页面并提取全部链接"的最小完整示例,完整保留如下。

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)

如原文档所述,Scrapling 通过把"抓取"和"解析"合并成一步简化了流程:Fetcher.get(url) 直接返回一个已经解析好的页面对象(其本质就是 Selector),你可以立即在它上面调用 cssfind_all 等所有解析方法。

源码印证:

  • Fetcher 基于 curl_cffi 实现基础 HTTP 请求(GET/POST/PUT/DELETE),见 Fetcher 类;它同时会把类级的解析器配置合并进每次请求的 selector_config合并逻辑)。
  • page.css('a::attr(href)') 返回的是 Selectors 容器,其中每个元素是 ::attr(href) 产生的文本节点 Selector;遍历时每个 linkstr(link) 即链接字符串,因此循环体直接 print(link) 即可,无需再写 link['href']
  • 若你只想拿第一个链接,可以写 page.css('a::attr(href)').first;取多个字符串值时也可用 .getall()(对应 Scrapy 风格的 extract,两者互为别名,见 getall/extract 定义)。

如果只需要解析本地/已有的 HTML 而不发起网络请求,把示例换成 page = Selector(html) 即可,两种入口共享完全相同的解析 API。

额外说明(原文档附加注释,结合源码核实)

原文档末尾的 "Additional Notes" 包含四条重要约定,逐条核实后总结如下:

  1. 不同解析器:BeautifulSoup 允许设置解析引擎(其中之一是 lxml);Scrapling 不做选择,出于性能原因默认使用 lxml。这已由 Selector 初始化中的 lxml HTMLParser 构造 证实。
  2. 元素类型:BeautifulSoup 的元素是 Tag 对象,Scrapling 中是 Selector 对象,但两者提供相似的导航与数据提取方法/属性(对照表即是映射关系)。
  3. 错误处理:两个库在找不到元素时都返回 Nonesoup.find() / page.find()——find 实现 确实遍历 find_all 结果并返回首个或 None)。在 Scrapling 中,page.css() 无匹配时返回空的 Selectors 列表(而不是 None),可用 page.css('.foo').first 安全地拿到第一个匹配或 None。为避免报错,访问属性前应检查 None 或空结果。
  4. 文本提取:Scrapling 通过 TextHandler 提供额外的文本处理方法,例如 clean(),可帮助去除多余空白、连续空格或不需要的字符。完整方法列表见 custom_types.py 中的 TextHandler 类(含 cleanre/re_firstjsonupperlowerreplace 等,且多数字符串方法会保持返回 TextHandler 以支持链式调用)。

迁移检查清单与适用前提

按以下顺序操作可以让迁移过程平滑(依据原文档结论与源码事实整理):

  1. 替换导入from bs4 import BeautifulSoupfrom scrapling.parser import Selector(或 from scrapling import Selector);如果请求部分也从 requests 迁出,用 from scrapling import Fetcher 一步到位。
  2. 替换解析入口BeautifulSoup(text, 'html.parser')Selector(text);解析器引擎参数直接删掉。
  3. 按对照表替换方法名find/find_all 签名基本兼容(标签、属性字典、正则、可调用对象、标签列表均可作为位置参数,属性可作为关键字参数,见 find_all 参数类型分支);select_onecss(...).firstselectcss(...)get_text(strip=True)get_all_text(strip=True)element.stringelement.textelement.attrselement.attribelement.nameelement.tag
  4. 重写导航类调用:BS 的 find_parent/find_next_sibling/find_previous 等"按方向 + 标签查找"API 在 Scrapling 中统一为"方向容器 + 谓词"模式——parentpath(祖先)、siblingsbelow_elements(后代)、next/previous,再链式调用 search(lambda)(取第一个)或 filter(lambda)(取全部)。注意原文档脚注 ¹ 的语义差异:path 只含祖先链。
  5. 移除 DOM 修改代码:任何"解析后修改 HTML 树"的逻辑需改写为"提取数据后自行处理",Scrapling 不提供此类能力。
  6. 防御性判空:对 find()/css(...).first 的结果判 None;对 css(...)/find_all(...) 结果判空列表。

适用前提与限制(以当前仓库实际内容为准):

  • 当前仓库版本为 0.4.13(见 scrapling/init.py),本文描述的方法签名均基于该版本源码。
  • Selector/Selectors 对象不可 pickle__getstate__ 直接抛出 TypeError源码),需要序列化时应序列化提取出的数据而不是元素对象本身。
  • 解析行为默认丢弃注释与 CDATA(keep_comments=Falsekeep_cdata=False);如果你的 BS 代码依赖注释节点,需要显式打开这两个开关。
  • find_all 要求至少传入一个查询条件(标签、属性、正则、函数之一),否则抛出 TypeError源码);正则只匹配元素的直接文本,函数过滤接收 Selector 参数。

更深入的方法签名、参数说明与更多场景示例,可继续查阅仓库文档:解析主类说明 main_classes.md、选择器专题 selection.md、静态抓取器参数 static.md;解析行为的自动化验证可参考测试目录 tests/parser/(如 test_general.pytest_selectors_filter.py)。

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