logo

5 个最佳的 Rust HTML 解析器

2024-06-27 23:04
本文介绍了5 个最佳的 Rust HTML 解析器,并推荐使用集蜂云管理来爬虫。

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

优秀的网页抓取工具必须能够高效地导航和操作页面的 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 文档分解为代表不同元素的标记。

结构体用于处理这些标记。当它遇到开始标记时,它会定位包含所需价格值的子节点并提取它。

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(),使用选择带有类的标签,并提取文本内容。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 就是合适的选择。

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

导航目录