5 个最佳的 Rust HTML 解析器

11,173次阅读
没有评论

共计 9191 个字符,预计需要花费 23 分钟才能阅读完成。

集蜂云是一个可以让开发者在上面构建、部署、运行、发布采集器的数据采集云平台。加入到数百名开发者中,将你的采集器发布到市场,从而给你带来被动收入吧!

优秀的网页抓取工具必须能够高效地导航和操作页面的 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> {
    // 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 规范存在一些差异。

⚙️ 特点:

👨‍💻示例:

下面的代码展示了如何使用 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 

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

tag and the tag is a 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

tag and the tag is a 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

, , or 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

tag...                if self.in_price_tag {// ...and we're inside a tag...                    if self.in_span_tag {                        // ...add the text to the price string                        self.price = format!("price: {}", s);                    // ...and we're inside a 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> {    // 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::<: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 文档分解为代表不同元素的标记。

结构体用于处理这些标记。当它遇到

开始标记时,它会定位包含所需价格值的子节点并提取它。

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> {
    // 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::();
 
    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 功能,可能会丢失数据。⚙️ 特点:

👨‍💻示例:

以下代码使用外部 crate (html2d) 将获取的 HTML 转换为 markdown。然后,它会创建一个 Markdown 解析器并遍历每个事件以查找和提取价格。

#[tokio::main]
async fn main() -> Result> {
    // 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> {
    // 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 的易于使用的函数。

  • 可以与外部板条箱集成。

  • 详尽的文献资料。

👎 缺点:

⚙️ 特点:

  • HTML 解析和序列化

  • DOM 操作

  • 节点遍历与修改

  • CSS 选择器

  • 特质

👨‍💻示例:

下面的示例展示了如何使用 Kuchiki 解析 HTML。

它使用 解析 HTML 响应 parse_html(),使用选择

带有类的标签,并提取文本内容。pricedocument.select_first()

use kuchiki::traits::*;
use kuchiki::parse_html;
 
#[tokio::main]
async fn main() -> Result> {
    // 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 就是合适的选择。

集蜂云是一个可以让开发者在上面构建、部署、运行、发布采集器的数据采集云平台。平台提供了海量任务调度、三方应用集成、数据存储、监控告警、运行日志查看等功能,能够提供稳定的数据采集环境。平台提供丰富的采集模板,简单配置就可以直接运行,快来试一下吧。

原文地址: 5 个最佳的 Rust HTML 解析器

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于2024-11-04发表,共计9191字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)