集蜂云是一个可以让开发者在上面构建、部署、运行、发布采集器的数据采集云平台。加入到数百名开发者中,将你的采集器发布到市场,从而给你带来被动收入吧!
优秀的网页抓取工具必须能够高效地导航和操作页面的 HTML 结构,以提取相关数据。要使用 Rust 实现这一点,您需要专用的 Rust HTML 解析器。
目前有许多流行的 Rust HTML 解析器,每个解析器都有其独特的功能和能力。种类繁多,但也让您有权选择适合您项目的解析器。本文将回顾一些最好的 HTML 解析器并重点介绍它们的主要用例。
最好的 Rust HTML 解析器是什么?
下面,您可以找到一个比较表,其中概述了最受欢迎选项的特征。它将帮助您根据优先级比较解析器。
现在,让我们详细研究每个 HTML 解析器。您还将探索它们在解析真实 HTML 时的表现。下面,您可以找到一个检索 Web 内容的示例 Rust 脚本,它将用于测试每个解析器。
// define async main function using Tokio
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// make GET request to target URL and retrieve response
let resp = reqwest::get("https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/")
.await?
.text()
.await?;
println!("{resp:#?}");
Ok(())
}
此代码片段用于Tokio定义主要异步函数。然后,它GET向目标 URL发送请求并检索其原始 HTML 文件。
1. Html5ever:高性能 HTML5 解析
Html5ever是作为 Servo 项目的一部分开发的一款广泛使用的 Rust HTML 解析器。它能够根据 WHATWG 规范解析和序列化 HTML,因此成为一种普遍可靠的选择。
此工具本质上是一个 C HTML 解析器,但具有 Rust 的内置内存安全功能。这种独特的组合使 html5ever 具有 C 库所期望的高级性能,同时缓解了通常与该语言相关的安全问题。
与大多数构建 HTML 文档的 DOM 树表示的解析器不同,html5ever 使用回调来操作 DOM。这允许进行事件驱动的解析,其中回调函数由特定事件触发,例如 HTML 标记的关闭。这种解析类型节省内存,最终可提高性能。
Html5ever 也是此列表中最受欢迎的 Rust HTML 解析器,下载量超过 1200 万次。
👍 优点:
-
遵守 WHATWG 规范。
-
使用回调来操作 DOM。
-
设计为与 Rust 的官方稳定版本兼容。
-
通过所有 HTML5 标记器测试。
-
提供生产环境的 Web 浏览器所需的所有钩子,例如,document.write
-
拥有庞大的用户群、活跃的社区和全面的文档。
👎 缺点:
-
不提供 HTML 文档的 DOM 树表示。
-
使用标记器,这可能会导致冗长的代码,尤其是在大规模解析期间。
-
由于 HTML 元素被分成标记,因此解析和查询会变得复杂。
-
一些 html5ever 优化仅支持夜间版本。
-
承认其当前实际行为与 WHATWG 规范存在一些差异。
⚙️ 特点:
-
UTF-8 字符串表示
-
基于回调的 DOM 操作
-
符合 WHATWG 规范
-
HTML 解析和序列化
👨💻示例:
下面的代码展示了如何使用 html5ever 解析 HTML。
// import necessary crates
extern crate html5ever;
extern crate reqwest;
use std::default::Default;
// import necessary modules from html5ever
use html5ever::tendril::*;
use html5ever::tokenizer::BufferQueue;
use html5ever::tokenizer::{TagToken, StartTag, EndTag};
use html5ever::tokenizer::{Token, TokenSink, TokenSinkResult, Tokenizer, TokenizerOpts,};
use html5ever::tokenizer::CharacterTokens;
// define a struct to hold the state of the parser
struct TokenPrinter {
// define flags to track token location.
in_price_tag: bool,
in_span_tag: bool,
in_bdi_tag: bool,
price: String, // string to hold the price
}
// implement the TokenSink trait for TokenPrinter
impl TokenSink for TokenPrinter {
type Handle = ();
// define function to process each token in the HTML document
fn process_token(&mut self, token: Token, _line_number: u64) -> TokenSinkResult<()> {
match token {
TagToken(tag) => {
// if the token is a start tag...
if tag.kind == StartTag {
// ...and the tag is a <p> tag with class "price"...
if tag.name.to_string() == "p" {
for attr in tag.attrs {
if attr.name.local.to_string() == "class" && attr.value.to_string() == "price" {
// ...set the in_price_tag flag to true
self.in_price_tag = true;
}
}
// if we're inside a <p class="price"> tag and the tag is a <span> tag...
} else if self.in_price_tag && tag.name.to_string() == "span" {
// ...set the in_span_tag flag to true
self.in_span_tag = true;
// if we're inside a <p class="price"> tag and the tag is a <bdi> tag...
} else if self.in_price_tag && tag.name.to_string() == "bdi" {
// ...set the in_bdi_tag flag to true
self.in_bdi_tag = true;
}
// if the token is an end tag...
} else if tag.kind == EndTag {
// ...and the tag is a <p>, <span>, or <bdi> tag...
if tag.name.to_string() == "p" {
// ...set the corresponding flag to false
self.in_price_tag = false;
} else if tag.name.to_string() == "span" {
self.in_span_tag = false;
} else if tag.name.to_string() == "bdi" {
self.in_bdi_tag = false;
}
}
},
// if the token is a character token (i.e., text)...
CharacterTokens(s) => {
// ...and we're inside a <p class="price"> tag...
if self.in_price_tag {
// ...and we're inside a <span> tag...
if self.in_span_tag {
// ...add the text to the price string
self.price = format!("price: {}", s);
// ...and we're inside a <bdi> tag...
} else if self.in_bdi_tag {
// ...append the text to the price string and print it
self.price = format!("{}{}", self.price, s);
println!("{}", self.price);
// clear the price string for the next price
self.price.clear();
}
}
},
// ignore all other tokens
_ => {},
}
// continue processing tokens
TokenSinkResult::Continue
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// initialize the TokenPrinter
let sink = TokenPrinter { in_price_tag: false, in_span_tag: false, in_bdi_tag: false, price: String::new() };
// retrieve HTML content from target website
//... let resp = reqwest::get("https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/").await?.text().await?;
// convert the HTML content to a ByteTendril
let chunk = ByteTendril::from(resp.as_bytes());
let mut input = BufferQueue::new();
input.push_back(chunk.try_reinterpret::<fmt::UTF8>().unwrap());
// initialize the Tokenizer with the TokenPrinter
let mut tok = Tokenizer::new(
sink,
TokenizerOpts::default(),
);
// feed the HTML content to the Tokenizer
let _ = tok.feed(&mut input);
assert!(input.is_empty());
// end tokenization
tok.end();
Ok(())
}
代码创建了一个结构体,该结构体以 的形式实现TokenSink。它还创建了一个新的标记器,其中结构体是接收器,然后将获取的 HTML 馈送到标记器中,以将 HTML 文档分解为代表不同元素的标记。
结构体用于处理这些标记。当它遇到<p>开始标记时,它会定位包含所需价格值的子节点并提取它。
2. Scraper:快速网页抓取
Scraper是一个流行的 Rust 库,用于解析 HTML 并从目标网页中提取相关数据。它建立在另外两个 Rust 包和之上,html5ever这selectors两个包是 Servo 项目的一部分。
这两个库使Scraper 能够实现浏览器级的解析和查询。换句话说,它是为处理现实世界的 HTML 而设计的,而 HTML 并不总是符合标准。
该库在底层使用 html5ever,并提供高级 API 来创建 HTML 文档的 DOM 树表示。它还允许您使用 CSS 选择器来查找和操作元素。
👍 优点:
提供浏览器级的解析和查询。 可以处理格式错误的 HTML。 用途html5ever和selectors引擎盖下。 创建 HTML 文档的 DOM 树表示。 允许您使用 CSS 选择器遍历和操作 DOM。 拥有活跃的社区和丰富的文档。
👎 缺点:
创建大型 HTML 文档的 DOM 树表示时会占用大量内存。 依赖于外部板条箱。 ⚙️ 特点:
DOM 树表示 CSS 选择器 HTML 解析和序列化 高级 API 外部集成 👨💻示例:
以下代码将获取的 HTML 响应解析为片段,创建Selector与类匹配的元素price,使用查找目标元素Selector并提取产品价格。
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// make GET request to target URL and retrieve response
//...
// create an HTML parser
let fragment = scraper::Html::parse_fragment(&resp);
// define CSS selector for the price element
let price_selector = scraper::Selector::parse(".price").unwrap();
// extract the price using the CSS selector
let price_element = fragment.select(&price_selector).next().unwrap();
let price = price_element.text().collect::<String>();
println!("Price: {}", price);
Ok(())
}
3. Pulldown-cmark:支持 HTML 的 Markdown 解析
与此列表中的大多数库不同,pulldown-cmark不是传统的 HTML 解析器,而是 CommonMark(标准 Markdown 版本)的拉式解析器。它以 Markdown 作为输入并呈现 HTML,但不会从 HTML 中提取数据。
那么,pulldown-cmark 为何会出现在这里?虽然它旨在解析 Markdown,但也可以配置为 HTML。最重要的是,它的拉式解析器架构使其成为一种有价值的工具,尤其是当内存对您的项目至关重要时。该工具使用的内存比推送解析器或基于树的解析器少得多。此外,它允许您仅在需要时解析所需的内容,这最终会带来更好的性能,尤其是对于大型文档。
👍 优点:
快速地。 节省内存。 完全符合CommonMark规范。 可选择支持解析脚注。 便于使用。 用纯 Rust 编写,没有不安全的块。
👎 缺点:
需要额外的配置和其他包来解析 HTML。 不支持所有 HTML 标签、属性和功能。 解析复杂的 HTML 可能很有挑战性。 由于某些不受支持的 HTML 功能,可能会丢失数据。 ⚙️ 特点:
-
拉解析器架构
-
CommonMark 规范合规性
-
支持外部集成
-
防锈安全
👨💻示例:
以下代码使用外部 crate (html2d) 将获取的 HTML 转换为 markdown。然后,它会创建一个 Markdown 解析器并遍历每个事件以查找和提取价格。
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// make GET request to target URL and retrieve response
//...
// convert HTML to Markdown
let md = html2md::parse_html(&resp);
// create a Markdown parser
let parser = pulldown_cmark::Parser::new(&md);
// iterate over the events in the parser
for event in parser {
match event {
pulldown_cmark::Event::Text(text) => {
// check if the text contains the price
if text.contains("$") {
println!("Price: {}", text);
}
},
_ => {},
}
}
Ok(())
}
4. Select: 综合 HTML 解析
Select.rs是一个强大的 Rust 库,用于从 HTML 文档中提取数据。与 Scraper 一样,此库在底层使用 html5ever,但提供类似 jQuery 的界面。高级 API 允许您使用不同的方法(包括 XPath 和 CSS 选择器)选择特定元素。
此外,Select.rs 还提供了易于使用的遍历节点方法,让您可以快速浏览 HTML 结构。您还可以通过设置 HTML 属性、标签和文本来修改节点。该库支持多种格式的输出,包括 HTML 字符串、纯文本、YAML 数据和 JSON 数据。
👍 优点:
-
便于使用。
-
功能丰富的库。
-
支持 XPath 和 CSS 选择器。
-
多种输出格式,包括 YAML 和 JSON。
-
类似 jQuey 的界面。
-
符合HTML5规范。
-
支持内存缓存。
-
有详尽的文献资料。
👎 缺点:
内存使用效率低下,尤其是在处理大型 HTML 文档时。 它在后台使用其他库,这会增加整体应用程序的大小。 ⚙️ 特点:
-
HTML 解析和序列化
-
节点遍历与修改
-
XPath 和 CSS 选择器
-
支持多种输出格式
-
支持外部集成
👨💻示例:
以下代码使用 select.rs 将 HTML 响应解析为文档。然后,它使用 HTML 标签和类查找并提取价格。
use select::document::Document;
use select::predicate::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// make GET request to target URL and retrieve response
//...
// parse the response text into a Document
let document = Document::from(resp.as_str());
// find the price element using its HTML tag and class
if let Some(price_node) = document.find(Name("p").and(Class("price"))).next() {
println!("Price: {}", price_node.text());
}
Ok(())
}
5. Kuchiki:高效的 XML 和 HTML 解析
最后一个解决方案是Kuchiki,这是一个用于 HTML/XML 树操作的强大 Rust 库。与此列表中的大多数工具一样,它在底层使用 html5ever。但是,它添加了其他功能,使操作 DOM 更加容易。
Kuchiki具有诸如Node、NodeRef、ElementData、DocumentData等结构,用于表示和处理类似 DOM 树中的节点,让您可以轻松遍历 DOM 并修改元素。此外,它还提供了易于使用的函数来使用 html5ever 解析 HTML。此外,其Selectors结构可用于熟悉的 CSS 选择器语法,因此可以轻松查找和提取数据。
然而,Kuchiki 并没有得到积极维护,因为所有者已于 2023 年 1 月将其存档。这意味着虽然该工具仍然可用,但您不应该期待任何更新或错误修复。
👍 优点:
-
HTML/XML 树操作。
-
便于使用。
-
支持 CSS 选择器。
-
类似 DOM 的树结构。
-
它提供了使用 html5ever 解析 HTML 的易于使用的函数。
-
可以与外部板条箱集成。
-
详尽的文献资料。
👎 缺点:
-
依赖于外部板条箱。
-
自 2023 年起未进行积极维护。
⚙️ 特点:
-
HTML 解析和序列化
-
DOM 操作
-
节点遍历与修改
-
CSS 选择器
-
特质
👨💻示例:
下面的示例展示了如何使用 Kuchiki 解析 HTML。
它使用 解析 HTML 响应parse_html(),使用选择<p>带有类的标签,并提取文本内容。pricedocument.select_first()
use kuchiki::traits::*;
use kuchiki::parse_html;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// make GET request to target URL and retrieve response
//...
let document = parse_html().one(resp);
// select the "p" element with class "price"
if let Some(price_node) = document.select_first("p.price").ok() {
let price = price_node.text_contents();
println!("Price: {}", price);
} else {
println!("Price not found");
}
Ok(())
}
上面介绍的所有 Rust HTML 解析器都可让您访问和操作 HTML 文档。每个解析器都有自己的一组功能,在为您的项目选择最佳工具之前,您应该仔细检查这些功能。
虽然 html5ever、scraper、select.rs 和 Kuchiki 的性能效率相似,但它们各自都有独特的优势。例如,如果您需要专门为网页抓取而设计的库,Scraper 和 Select.rs 是最佳选择。如果您使用 Markdown 并且内存是一个关键因素,请选择 pulldown-cmark。如果操作 HTML 树是您的主要要求,请选择 Kuchiki。最后,如果您需要高性能 Rust HTML 解析器并且冗长的代码不是问题,那么 html5ever 就是合适的选择。
集蜂云是一个可以让开发者在上面构建、部署、运行、发布采集器的数据采集云平台。平台提供了海量任务调度、三方应用集成、数据存储、监控告警、运行日志查看等功能,能够提供稳定的数据采集环境。平台提供丰富的采集模板,简单配置就可以直接运行,快来试一下吧。