集蜂云是一个可以让开发者在上面构建、部署、运行、发布采集器的数据采集云平台。加入到数百名开发者中,将你的采集器发布到市场,从而给你带来被动收入吧!
优秀的网页抓取工具必须能够高效地导航和操作页面的 HTML 结构,以提取相关数据。要使用 Rust 实现这一点,您需要专用的 Rust HTML 解析器。
目前有许多流行的 Rust HTML 解析器,每个解析器都有其独特的功能和能力。种类繁多,但也让您有权选择适合您项目的解析器。本文将回顾一些最好的 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 文件。
Html5ever是作为 Servo 项目的一部分开发的一款广泛使用的 Rust HTML 解析器。它能够根据 WHATWG 规范解析和序列化 HTML,因此成为一种普遍可靠的选择。
此工具本质上是一个 C HTML 解析器,但具有 Rust 的内置内存安全功能。这种独特的组合使 html5ever 具有 C 库所期望的高级性能,同时缓解了通常与该语言相关的安全问题。
与大多数构建 HTML 文档的 DOM 树表示的解析器不同,html5ever 使用回调来操作 DOM。这允许进行事件驱动的解析,其中回调函数由特定事件触发,例如 HTML 标记的关闭。这种解析类型节省内存,最终可提高性能。
Html5ever 也是此列表中最受欢迎的 Rust HTML 解析器,下载量超过 1200 万次。
👍 优点:
👎 缺点:
⚙️ 特点:
👨💻示例:
下面的代码展示了如何使用 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 文档分解为代表不同元素的标记。
结构体用于处理这些标记。当它遇到开始标记时,它会定位包含所需价格值的子节点并提取它。
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(())
}
与此列表中的大多数库不同,pulldown-cmark不是传统的 HTML 解析器,而是 CommonMark(标准 Markdown 版本)的拉式解析器。它以 Markdown 作为输入并呈现 HTML,但不会从 HTML 中提取数据。
那么,pulldown-cmark 为何会出现在这里?虽然它旨在解析 Markdown,但也可以配置为 HTML。最重要的是,它的拉式解析器架构使其成为一种有价值的工具,尤其是当内存对您的项目至关重要时。该工具使用的内存比推送解析器或基于树的解析器少得多。此外,它允许您仅在需要时解析所需的内容,这最终会带来更好的性能,尤其是对于大型文档。
👍 优点:
快速地。 节省内存。 完全符合CommonMark规范。 可选择支持解析脚注。 便于使用。 用纯 Rust 编写,没有不安全的块。
👎 缺点:
需要额外的配置和其他包来解析 HTML。 不支持所有 HTML 标签、属性和功能。 解析复杂的 HTML 可能很有挑战性。 由于某些不受支持的 HTML 功能,可能会丢失数据。 ⚙️ 特点:
👨💻示例:
以下代码使用外部 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(())
}
Select.rs是一个强大的 Rust 库,用于从 HTML 文档中提取数据。与 Scraper 一样,此库在底层使用 html5ever,但提供类似 jQuery 的界面。高级 API 允许您使用不同的方法(包括 XPath 和 CSS 选择器)选择特定元素。
此外,Select.rs 还提供了易于使用的遍历节点方法,让您可以快速浏览 HTML 结构。您还可以通过设置 HTML 属性、标签和文本来修改节点。该库支持多种格式的输出,包括 HTML 字符串、纯文本、YAML 数据和 JSON 数据。
👍 优点:
👎 缺点:
内存使用效率低下,尤其是在处理大型 HTML 文档时。 它在后台使用其他库,这会增加整体应用程序的大小。 ⚙️ 特点:
👨💻示例:
以下代码使用 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(())
}
最后一个解决方案是Kuchiki,这是一个用于 HTML/XML 树操作的强大 Rust 库。与此列表中的大多数工具一样,它在底层使用 html5ever。但是,它添加了其他功能,使操作 DOM 更加容易。
Kuchiki具有诸如Node、NodeRef、ElementData、DocumentData等结构,用于表示和处理类似 DOM 树中的节点,让您可以轻松遍历 DOM 并修改元素。此外,它还提供了易于使用的函数来使用 html5ever 解析 HTML。此外,其Selectors结构可用于熟悉的 CSS 选择器语法,因此可以轻松查找和提取数据。
然而,Kuchiki 并没有得到积极维护,因为所有者已于 2023 年 1 月将其存档。这意味着虽然该工具仍然可用,但您不应该期待任何更新或错误修复。
👍 优点:
👎 缺点:
⚙️ 特点:
👨💻示例:
下面的示例展示了如何使用 Kuchiki 解析 HTML。
它使用 解析 HTML 响应parse_html(),使用选择带有类的标签,并提取文本内容。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 就是合适的选择。
集蜂云是一个可以让开发者在上面构建、部署、运行、发布采集器的数据采集云平台。平台提供了海量任务调度、三方应用集成、数据存储、监控告警、运行日志查看等功能,能够提供稳定的数据采集环境。平台提供丰富的采集模板,简单配置就可以直接运行,快来试一下吧。