首页 前端知识 5 个最佳的 Rust HTML 解析器

5 个最佳的 Rust HTML 解析器

2024-11-02 10:11:46 前端知识 前端哥 586 14 我要收藏

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

优秀的网页抓取工具必须能够高效地导航和操作页面的 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 就是合适的选择。

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

转载请注明出处或者链接地址:https://www.qianduange.cn//article/19784.html
标签
rust
评论
发布的文章

JQuery中的load()、$

2024-05-10 08:05:15

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!