使用 Rust 进行网络抓取的新手指南

Mihai Maxim,2022 年 10 月 17 日

Rust 适合网络搜索吗?

Rust 是一种专为提高速度和效率而设计的编程语言。与 C 或 C++ 不同,Rust 具有集成的软件包管理器和构建工具。它还拥有出色的文档和友好的编译器,并提供有用的错误信息。熟悉语法确实需要一段时间。但一旦习惯了,你就会发现只需几行代码就能编写出复杂的功能。使用 Rust 进行网络搜刮是一种令人振奋的体验。你可以访问功能强大的搜索库,它们会为你完成大部分繁重的工作。因此,你可以把更多时间花在有趣的地方,比如设计新功能。在本文中,我将带你了解使用 Rust 构建网络搜索器的过程。 

如何安装 Rust

安装 Rust 的过程非常简单。访问 "安装 Rust - Rust 编程语言"(rust-lang.org),并按照推荐的教程安装您的操作系统。该页面会根据你使用的操作系统显示不同的内容。安装结束后,确保打开一个全新的终端,运行 rustc --version 命令。如果一切顺利,你应该能看到已安装 Rust 编译器的版本号。

Since we will be building a web scraper, let’s create a Rust project with Cargo. Cargo is Rust’s build system and package manager. If you used the official installers provided by rust-lang.org, Cargo should be already installed. Check whether Cargo is installed by entering the following into your terminal:  cargo --version.  If you see a version number, you have it! If you see an error, such as command not found, look at the documentation for your method of installation to determine how to install Cargo separately. To create a project, navigate to the desired project location and run cargo new <project name>.

这是默认的项目结构:

用 Rust 构建网络搜索器

现在,让我们来看看如何使用 Rust 构建刮板。第一步是明确目的。我想提取什么?其次是决定如何存储搜索到的数据。大多数人将其保存为 .json,但一般来说,你应该考虑更适合你个人需求的格式。弄清了这两个要求,你就可以放心地实施任何刮擦工具了。为了更好地说明这一过程,我建议我们建立一个小型工具,从COVID Live - Coronavirus Statistics - Worldometer (worldometers.info)网站提取 Covid 数据。它应解析报告病例表并将数据存储为 .json。我们将在接下来的章节中一起创建这个 scraper。

通过 HTTP 请求获取 HTML

要提取表格,首先需要获取网页中的 HTML。我们将使用 "reqwest "板块/库从网站获取原始 HTML。

首先,在 Cargo.toml 文件中将其添加为依赖项:

reqwest = { version = "0.11", features = ["blocking", "json"] }

然后定义目标网址并发送请求:

let url = "https://www.worldometers.info/coronavirus/";
let response = reqwest::blocking::get(url).expect("Could not load url.");

阻塞 "功能确保请求是同步的。因此,程序会等待请求完成,然后继续执行其他指令。 

let raw_html_string = response.text().unwrap();

使用 CSS 选择器定位数据

您已获得所有必要的原始数据。现在,你必须找到一种方法来定位报告的案例表。最常用的 Rust 库被称为 "scraper"。它可以使用 CSS 选择器进行 HTML 解析和查询。

将此依赖关系添加到 Cargo.toml 文件中:

scraper = "0.13.0"

将这些模块添加到 main.rs 文件中。

使用 scraper::Selector;
使用 scraper::Html;

现在使用原始 HTML 字符串创建 HTML 片段:

let html_fragment = Html::parse_fragment(&raw_html_string);

我们将选择显示今天、昨天和两天前报告案例的表格。

博客图片

打开开发人员控制台并识别表 id:

博客图片

在撰写本文时,今天的 ID 是"main_table_countries_today"。

另外两个表的 id 分别是
"main_table_countries_yesterday "和 "main_table_countries_yesterday2"。

现在,让我们定义一些选择器:

让 table_selector_string = "#main_table_countries_today, #main_table_countries_yesterday, #main_table_countries_yesterday2";

让 table_selector = Selector::parse(table_selector_string).unwrap();

let head_elements_selector = Selector::parse("thead>tr>th").unwrap();

let row_elements_selector = Selector::parse("tbody>tr").unwrap();

let row_element_data_selector = Selector::parse("td, th").unwrap();

将 table_selector_string 传递给 html_fragment select 方法,以获取所有表格的引用:

let all_tables = html_fragment.select(&table_selector);

使用表格引用,创建一个循环,解析每个表格中的数据。

for table in all_tables{
let head_elements = table.select(&head_elements_selector);
for head_element in head_elements{
//parse the header elements
}

let head_elements = table.select(&head_elements_selector);
for row_element in row_elements{
for td_element in row_element.select(&row_element_data_selector){
//parse the individual row elements
}
}
}

解析数据

存储数据的格式决定了解析数据的方式。本项目使用的是 .json。因此,我们需要将表数据放在键值对中。我们可以使用表头名称作为键,使用表行作为值。 

使用 .text() 函数提取标题并将其存储在一个向量中:

//for table in tables loop
let mut head:Vec<String> = Vec::new();

let head_elements = table.select(&head_elements_selector);

for head_element in head_elements{
let mut element = head_element.text().collect::<Vec<_>>().join(" ");
element = element.trim().replace("\n", " ");
head.push(element);
}


//head
["#", "Country, Other", "Total Cases", "New Cases", "Total Deaths", ...]

以类似方式提取行值:

//for table in tables loop
let mut rows:Vec<Vec<String>> = Vec::new();

let row_elements = table.select(&row_elements_selector);

for row_element in row_elements{
let mut row = Vec::new();
for td_element in row_element.select(&row_element_data_selector){
let mut element = td_element.text().collect::<Vec<_>>().join(" ");
element = element.trim().replace("\n", " ");
row.push(element);
}
rows.push(row)

}
//rows
[...
["", "World", "625,032,352", "+142,183", "6,555,767", ...]
...
["2", "India", "44,604,463", "", "528,745", ...]
...]

使用 zip() 函数在标题和行值之间建立匹配:

for row in rows {
let zipped_array = head.iter().zip(row.iter()).map(|(a, b)|
(a,b)).collect::<Vec<_>>();
}

//zipped_array
[
...
[("#", ""), ("Country, Other", "World"), ("Total Cases", "625,032,352"), ("New Cases", "+142,183"), ("Total Deaths", "6,555,767"), ...]
...
]

现在,将 zipped_array(键、值)对存储到 IndexMap 中:

serde = {version="1.0.0",features = ["derive"]}

indexmap = {version="1.9.1", features = ["serde"]} (添加这些依赖项)
use indexmap::IndexMap;

//use this to store all the IndexMaps
let mut table_data:Vec<IndexMap<String, String>> = Vec::new();
for row in rows {
let zipped_array = head.iter().zip(row.iter()).map(|(a, b)|
(a,b)).collect::<Vec<_>>();
let mut item_hash:IndexMap<String, String> = IndexMap::new();
for pair in zipped_array{
//we only want the non empty values
if !pair.1.to_string().is_empty(){
item_hash.insert(pair.0.to_string(), pair.1.to_string());
}
}
table_data.push(item_hash);

//table_data
[
...
{"Country, Other": "North America", "Total Cases": "116,665,220", "Total Deaths": "1,542,172", "Total Recovered": "111,708,347", "New Recovered": "+2,623", "Active Cases": "3,414,701", "Serious, Critical": "7,937", "Continent": "North America"}
,
{"Country, Other": "Asia", "Total Cases": "190,530,469", "New Cases": "+109,009", "Total Deaths": "1,481,406", "New Deaths": "+177", "Total Recovered": "184,705,387", "New Recovered": "+84,214", "Active Cases": "4,343,676", "Serious, Critical": "10,640", "Continent": "Asia"}
...
]

IndexMap 是存储表数据的最佳选择,因为它保留了(键、值)对的插入顺序。

数据序列化

现在,您可以用表格数据创建类似 json 的对象,是时候将它们序列化为 .json 了。在开始之前,请确保您已经安装了所有这些依赖项:

serde = {version="1.0.0",features = ["derive"]}
serde_json = "1.0.85"
indexmap = {version="1.9.1", features = ["serde"]}

将每个表数据存储在一个 tables_data 向量中:

let mut tables_data: Vec<Vec<IndexMap<String, String>>> = Vec::new();

For each table:
//fill table_data (see previous chapter)
tables_data.push(table_data);

为 tables_data 定义一个结构容器:

 #[derive(Serialize)]
struct FinalTableObject {
tables: IndexMap<String, Vec<IndexMap<String, String>>>,
}

实例化结构体:

 let final_table_object = FinalTableObject{tables: tables_data};

将结构体序列化为 .json 字符串:

let serialized = serde_json::to_string_pretty(&final_table_object).unwrap();

将序列化的 .json 字符串写入 .json 文件:

use std::fs::File;
use std::io::{Write};

let path = "out.json";

let mut output = File::create(path).unwrap();

let result = output.write_all(serialized.as_bytes());

match result {

Ok(()) => println!("Successfully wrote to {}", path),

Err(e) => println!("Failed to write to file: {}", e),

}

然后,就大功告成了。如果一切顺利,输出的 .json 应该是这样的:

{
"tables": [
[ //table data for #main_table_countries_today
{
"Country, Other": "North America",
"Total Cases": "116,665,220",
"Total Deaths": "1,542,172",
"Total Recovered": "111,708,347",
"New Recovered": "+2,623",
"Active Cases": "3,414,701",
"Serious, Critical": "7,937",
"Continent": "North America"
},
...
],
[...table data for #main_table_countries_yesterday...],
[...table data for #main_table_countries_yesterday2...],
]
}

You can find the whole code for the project at [Rust][A simple <table> scraper] (github.com)

进行调整以适应其他使用情况

如果你跟随我走了这么远,你可能已经意识到,你可以在其他网站上使用这个搜刮工具。该搜索器不受限于特定的表格列数或命名约定。此外,它也不依赖于许多 CSS 选择器。因此,要让它适用于其他表格,应该不需要太多的调整,对吗?让我们来验证一下这个理论。

博客图片

We need a selector for the <table> tag.

博客图片

如果 class="wikitable sortable jquery-tablesorter",可以将 table_selector 改为:

let table_selector_string = ".wikitable.sortable.jquery-tablesorter";
let table_selector = Selector::parse(table_selector_string).unwrap();

This table has the same <thead> <tbody> structure, so there is no reason to change the other selectors.

刮板现在应该可以工作了。让我们试运行一下:

{
"tables": []
}

用 Rust 进行网络抓取很有趣,不是吗? 

怎么会失败呢? 

让我们再深入一点:

最简单的方法就是查看 GET 请求返回的 HTML,从而找出问题所在:

let url = "https://en.wikipedia.org/wiki/List_of_countries_by_population_in_2010";


let response = reqwest::blocking::get(url).expect("Could not load url.");

et raw_html_string = response.text().unwrap();

let path = "debug.html";


let mut output = File::create(path).unwrap();

let result = output.write_all(raw_html_string.as_bytes());
博客图片

GET 请求返回的 HTML 与我们在实际网站上看到的不同。浏览器为 Javascript 提供了一个运行环境,以改变页面布局。在我们的刮擦程序中,我们得到的是未经修改的版本。

Our table_selector did not work because the “jquery-tablesorter” class is injected dynamically by Javascript. Also, you can see that the <table> structure is different. The <thead> tag is missing. The table head elements are now found in the first <tr> of the <tbody>. Thus, they will be picked up by the row_elements_selector.

Removing “jquery-tablesorter” from the table_selector is not enough, we also need to handle the missing <tbody> case:

let table_selector_string = ".wikitable.sortable";
 if head.is_empty() {
head=rows[0].clone();
rows.remove(0);
}// take the first row values as head if there is no <thead>

现在,让我们再转一转:

{
"tables": [
[
{
"Rank": "--",
"Country / territory": "World",
"Population 2010 (OECD estimate)": "6,843,522,711"
},
{
"Rank": "1",
"Country / territory": "China",
"Population 2010 (OECD estimate)": "1,339,724,852",
"Area (km 2 ) [1]": "9,596,961",
"Population density (people per km 2 )": "140"
},
{
"Rank": "2",
"Country / territory": "India",
"Population 2010 (OECD estimate)": "1,182,105,564",
"Area (km 2 ) [1]": "3,287,263",
"Population density (people per km 2 )": "360"
},
...
]
]

这样好多了!

摘要

我希望这篇文章能为使用 Rust 进行网络搜刮提供一个很好的参考点。尽管 Rust 丰富的类型系统和所有权模型可能会让人有点不知所措,但它绝不是不适合网络搜刮的。你会得到一个友好的编译器,它不断为你指出正确的方向。此外,你还会遇到大量精心编写的文档:Rust 编程语言 - Rust 编程语言 (rust-lang.org)

构建网络搜刮工具并不总是一个简单的过程。您将面临 Javascript 渲染、IP 屏蔽、验证码和许多其他问题。在 WebScraping API,我们为您提供所有必要的工具来解决这些常见问题。您是否想知道它是如何工作的?您可以登录WebScrapingAPI - 产品免费试用我们的产品。您也可以通过WebScrapingAPI - 联系我们联系我们。我们非常乐意回答您的所有问题!

新闻和更新

订阅我们的时事通讯,了解最新的网络搜索指南和新闻。

We care about the protection of your data. Read our <l>Privacy Policy</l>.Privacy Policy.

相关文章

缩图
网络抓取科学Scrapy 与 Selenium:选择最佳网络抓取工具综合指南

探索 Scrapy 和 Selenium 在网络刮擦方面的深入比较。从大规模数据采集到处理动态内容,了解两者的优缺点和独特功能。了解如何根据项目需求和规模选择最佳框架。

WebscrapingAPI
作者头像
WebscrapingAPI
14 分钟阅读
缩图
指南Scrapy Splash 教程:掌握使用 Scrapy 和 Splash 抓取 JavaScript 渲染的网站的艺术

学习如何使用 Scrapy 和 Splash 抓取 JavaScript 渲染的动态网站。从安装到编写 spider、处理分页和管理 Splash 响应,本综合指南为初学者和专家提供了循序渐进的指导。

Ștefan Răcila
作者头像
Ștefan Răcila
6 分钟阅读
缩图
使用案例在金融领域利用网络抓取另类数据:投资者综合指南

探索网络搜索在金融领域的变革力量。从产品数据到情感分析,本指南深入介绍了可用于投资决策的各类网络数据。

米赫内亚-奥克塔维安-马诺拉什
作者头像
米赫内亚-奥克塔维安-马诺拉什
13 分钟阅读