如何用 Rust Reqwest 写一个Web 爬虫?

用 Rust Reqwest 编写 Web 爬虫

在这里插入图片描述

您是否曾考虑过建立自己的 潜在业务数据库,用于潜在客户开发或产品价格数据,以便您可以毫不费力地以最便宜的价格获得产品?网络爬虫可以让您无需亲自执行任何手动工作即可做到这一点。Rust通过允许显式地处理错误和并发地运行任务,让您可以做一些事情,比如将web服务 路由 到爬虫 或输出数据的Discord bot。

在本 Rust 网络爬虫指南中,将编写一个 Rust 网络爬虫,它将抓取 Amazon 上的 Raspberry Pi 产品并获取其价格,然后将它们存储在 PostgresQL 数据库中以供进一步处理。

可以在此处找到本文的 Github 代码库。

入门

让我们使用 cargo shuttle init 创建一个新项目。对于这个项目,我们将简单地将其称为 webscraper - 您需要框架的 none 选项,这将生成一个添加了 shuttle-runtime 的新 Cargo 项目(由于我们当前没有使用 Web 框架,因此我们不需要选择任何其他选项)。

使用以下代码来安装依赖项:

cargo add chrono reqwest scraper tracing shuttle-shared-db sqlx --features shuttle-shared-db/postgres,sqlx/runtime-tokio-native-tls,sqlx/postgres

还需要安装 sqlx-cli ,它是管理 SQL 迁移的有用工具。可以通过运行以下命令来安装它:

cargo install sqlx-cli

如果在项目文件夹中使用 sqlx migrate add schema ,将获得 SQL 迁移文件,该文件可以在 migrations 文件夹中找到!该文件的格式将包含创建迁移的日期和时间,然后是为其指定的名称(在本例中为 schema )。出于我们的目的,以下是将使用的迁移脚本:

-- migrations/schema.sql
CREATE TABLE IF NOT EXISTS products (id SERIAL PRIMARY KEY,name VARCHAR NOT NULL,price VARCHAR NOT NULL,old_price VARCHAR,link VARCHAR,scraped_at DATE NOT NULL DEFAULT CURRENT_DATE   
);

在开始之前,需要创建一个实现 shuttle_runtime::Service 的结构,这是一个异步trait。还需要设置user agent,以减少被拦截的可能性。值得庆幸的是,可以通过在主函数中返回一个结构来完成所有这一切,如下所示:

// src/main.rs
use reqwest::Client;
use tracing::error;
use sqlx::PgPool;struct CustomService {ctx: Client,db: PgPool
}// Set up our user agent
const USER_AGENT: &str = "Mozilla/5.0 (Linux x86_64; rv:115.0) Gecko/20100101 Firefox/115.0";// note that we add our Database as an annotation here so we can easily get it provisioned to us
#[shuttle_runtime::main]
async fn main(#[shuttle_shared_db::Postgres] db: PgPool
) -> Result<CustomService, shuttle_runtime::Error> {
// automatically attempt to do migrations
// we only create the table if it doesn't exist which prevents data wipingsqlx::migrate!().run(&db).await.expect("Migrations failed");
// initialise Reqwest client here so we can add it in later onlet ctx = Client::builder().user_agent(USER_AGENT).build().unwrap();Ok(CustomService { ctx, db })
}#[shuttle_runtime::async_trait]
impl shuttle_runtime::Service for CustomService {async fn bind(mut self, _addr: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> {scrape(self.ctx, self.db).await.expect("scraping should not finish");error!("The web scraper loop shouldn't finish!");Ok(())}
}

现在我们已经完成了,可以开始在 Rust 中进行网页抓取了!

完成网络爬虫

制作网络抓取工具的第一部分是向目标 URL 发出请求,以便可以获取响应正文进行处理。值得庆幸的是,亚马逊的 URL 语法非常简单,因此我们可以通过添加我们想要查找的搜索词的名称来轻松自定义 URL 查询参数。由于亚马逊返回多页结果,我们还希望能够将页码设置为可变动态变量,每次请求成功时该变量都会增加 1。

// src/main.rs
use chrono::NaiveDate;#[derive(Clone, Debug)]
struct Product {name: String,price: String,old_price: Option<String>,link: String,
}async fn scrape(ctx: Client) -> Result<(), String> {let mut pagenum = 1;let mut retry_attempts = 0;let url = format!("https://www.amazon.com/s?k=raspberry+pi&page={pagenum}");let res = match ctx.get(url).send().await { Ok(res) => res,Err(e) => {error!("Error while attempting to send HTTP request: {e}");break}};let res = match res.text().await {Ok(res) => res,Err(e) => {error!("Error while attempting to get the HTTP body: {e}");break}};
}

您可能已经注意到,上边代码添加了一个名为 retry_attempts 的变量。这是因为有时当抓取时,亚马逊(或任何其他网站)可能会给我们一个 503 服务不可用的消息,这意味着抓取将失败。有时这可能是由于服务器过载或抓取太快造成的,因此可以像这样对错误处理进行建模:

// src/main.rsuse reqwest::StatusCode;
use std::thread::sleep as std_sleep;
use tokio::time::Duration;let mut retry_attempts = 0;if res.status() == StatusCode::SERVICE_UNAVAILABLE {error!("Amazon returned a 503 at page {pagenum}");retry_attempts += 1;if retry_attempts >= 10 {// take a break if too many retry attemptserror!("It looks like Amazon is blocking us! We will rest for an hour.");// sleep for an hour then retry on current iterationstd_sleep(Duration::from_secs(3600));continue;} else {std_sleep(Duration::from_secs(15));continue;}
}retry_attempts = 0;

假设 HTTP 请求成功,我们将获得一个可以使用 scraper crate 解析的 HTML body。

如果您在浏览器中访问亚马逊并搜索“raspberry pi”,您将收到一份产品列表。您可以使用浏览器上的开发工具功能检查此产品列表(在本例中,它是 Firefox 中的检查功能,但您也可以使用 Chrome Devtools、Microsoft Edge DevTools 等…)。它应该如下所示:

Devtools preview of webpage analysis for web scraping in a browser

注意到 div 元素具有 data-component-type 的数据属性,其值为 s-search-result 。这很有帮助,因为除了想要抓取的页面组件之外,没有其他页面组件具有该属性!因此,可以通过选择它作为 CSS 选择器来抓取数据(有关更多信息,请参阅下文)。需要确保通过将 HTML 解析为 HTML 片段来准备 HTML,然后可以声明初始 scraper::Selector

// src/main.rs
use scraper::{Html, Selector};let html = Html::parse_fragment(&res);
let selector = Selector::parse("div[data-component-type='s-search-result']").unwrap();

如您所见, Selector 使用 CSS 选择器来解析 HTML。在本例中,我们专门尝试搜索具有名为“data-component-type”且值为“s-search-result”的数据属性的 HTML div 元素。

如果现在尝试运行我程序并按照 scraper 文档 html.select(&selector) 运行,您将看到它返回一个 HTML 元素的迭代器。然而,因为迭代计数在技术上也可以为零,所以需要确保实际上有我们可以迭代的东西 - 所以让通过添加一个 if 语句来检查迭代器计数来确保我们覆盖了这一点:

// src/main.rs
if html.select(&selector).count() == 0 {error!("There's nothing to parse here!");break
};

在应用程序的最终迭代中,这应该会退出循环,因为这通常表明没有更多的产品可以检索,因为在第一种情况下应该始终有产品结果。

现在已经完成了各自的错误处理,可以迭代条目并创建一个产品,然后将其附加到我们的产品向量中。

// src/main.rs
for entry in html.select(&selector) {
// declaring more Selectors to use on each entrylet price_selector = Selector::parse("span.a-price > span.a-offscreen").unwrap();let productname_selector = Selector::parse("h2 > a").unwrap();let name = entry.select(&productname_selector).next().expect("Couldn't find the product name").text.next().unwrap().to_string();// Amazon products can have two prices: a current price, and an "old price". We iterate through both of these and map them to a Vec<String>.let price_text = entry.select(&price_selector).map(|x| x.text().next().unwrap().to_string()).collect::<Vec<String>>();// get local date from chrono for database storage purposeslet scraped_at = Local::now().date_naive();// here we find the anchor element and find the value of the href attribute - this should always exist so we can safely unwraplet link = entry.select(&productname_selector).map(|link| {format!("https://amazon.co.uk{}", link.value().attr("href").unwrap())}).collect::<String>();vec.push(Product {name,   price: price_text[0].clone(),old_price: Some(price_text[1].clone()),link,                    scraped_at,});
}pagenum += 1;
std_sleep(Duration::from_secs(20));

请注意,在上面的代码块中,我们使用标准库中的 sleep - 如果尝试使用 tokio::time::sleep ,编译器将返回一个关于在等待点上持有非 Send future 的错误。

现在已经编写了用于处理从网页收集的数据的代码,可以将到目前为止编写的内容包装在循环中,移动 Vec<Product>pagenumdb.commit 来完成。检查下面的代码:

// src/main.rs
let transaction = db.begin().await.unwrap();for product in vec {if let Err(e) = sqlx::query("INSERT INTO         products       (name, price, old_price, link, scraped_at)       VALUES       ($1, $2, $3, $4, $5)      ").bind(product.name).bind(product.price).bind(product.old_price).bind(product.link).bind(product.scraped_at).execute(&db).await.unwrap() {error!("There was an error: {e}");error!("This web scraper will now shut down.");transaction.rollback().await.unwrap();break}
}
transaction.commit().await.unwrap();

在这里所做的只是对已抓取的产品列表运行一个 for 循环,并将它们全部插入数据库,然后在最后提交以完成它。

现在理想情况下,希望 爬虫 休息一段时间,以便页面有时间更新 - 否则,如果一直抓取页面,您很可能会得到大量重复数据。假设我们想让它休息到午夜:

// src/main.rs
use tokio::time::{sleep as tokio_sleep, Duration};// get the local time, add a day then get the NaiveDate and set a time of 00:00 to it
let tomorrow_midnight = Local::now().checked_add_days(Days::new(1)).unwrap().date_naive()
.and_hms_opt(0, 0, 0)
.unwrap();// get the local time now
let now = Local::now().naive_local();// check the amount of time between now and midnight tomorrow
let duration_to_midnight =  tomorrow_midnight.signed_duration_since(now).to_std().unwrap();// sleep for the required time
tokio_sleep(Duration::from_secs(duration_to_midnight.as_secs())).await;

现在已经完成了!

你的最终抓取函数应该如下所示:

// src/main.rs
async fn scrape(ctx: Client, db: PgPool) -> Result<(), String> {debug!("Starting scraper...");loop {let mut vec: Vec<Product> = Vec::new();let mut pagenum = 1;let mut retry_attempts = 0;loop {let url = format!("https://www.amazon.com/s?k=raspberry+pi&page={pagenum}");let res = match ctx.get(url).send().await {Ok(res) => res,Err(e) => {error!("Something went wrong while fetching from url: {e}");StdSleep(StdDuration::from_secs(15));continue;}};if res.status() == StatusCode::SERVICE_UNAVAILABLE {error!("Amazon returned a 503 at page {pagenum}");retry_attempts += 1;if retry_attempts >= 10 {error!("It looks like Amazon is blocking us! We will rest for an hour.");StdSleep(StdDuration::from_secs(3600));continue;} else {StdSleep(StdDuration::from_secs(15));continue;}}let body = match res.text().await {Ok(res) => res,Err(e) => {error!("Something went wrong while turning data to text: {e}");StdSleep(StdDuration::from_secs(15));continue;}};debug!("Page {pagenum} was scraped");let html = Html::parse_fragment(&body);let selector =Selector::parse("div[data-component-type= ' s-search-result ' ]").unwrap();if html.select(&selector).count() == 0 {break;};for entry in html.select(&selector) {let price_selector = Selector::parse("span.a-price > span.a-offscreen").unwrap();let productname_selector = Selector::parse("h2 > a").unwrap();let price_text = entry.select(&price_selector).map(|x| x.text().next().unwrap().to_string()).collect::<Vec<String>>();vec.push(Product {name: entry.select(&productname_selector).next().expect("Couldn't find the product name!").text().next().unwrap().to_string(),price: price_text[0].clone(),old_price: Some(price_text[1].clone()),link: entry.select(&productname_selector).map(|link| {format!("https://amazon.co.uk{}", link.value().attr("href").unwrap())}).collect::<String>(),});}pagenum += 1;retry_attempts = 0;StdSleep(StdDuration::from_secs(15));}let transaction = db.begin().await.unwrap();for product in vec {if let Err(e) = sqlx::query("INSERT INTO products               (name, price, old_price, link, scraped_at)               VALUES               ($1, $2, $3, $4, $5)").bind(product.name).bind(product.price).bind(product.old_price).bind(product.link).execute(&db).await{error!("There was an error: {e}");error!("This web scraper will now shut down.");break;}}transaction.commit().await.unwrap();// get the local time, add a day then get the NaiveDate and set a time of 00:00 to itlet tomorrow_midnight = Local::now().checked_add_days(Days::new(1)).unwrap().date_naive().and_hms_opt(0, 0, 0).unwrap();// get the local time nowlet now = Local::now().naive_local();// check the amount of time between now and midnight tomorrowlet duration_to_midnight = tomorrow_midnight.signed_duration_since(now).to_std().unwrap();// sleep for the required timeTokioSleep(TokioDuration::from_secs(duration_to_midnight.as_secs())).await;}Ok(())
}

搞定!

部署

如果您在 Shuttle 服务器上初始化项目,则可以使用 cargo shuttle deploy 开始(如果在脏 Git 分支上,则添加 --allow-dirty )。如果没有,您将需要使用 cargo shuttle project start --idle-minutes 0 来启动并运行您的项目。

尾声

感谢您阅读这篇文章!我希望您能够更全面地了解如何使用 Rust Reqwest 和 scraper crate 在 Rust 中开始网页抓取。


原文地址:Writing a Web Scraper in Rust using Reqwest

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/279727.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MD5算法:密码学中的传奇

title: MD5算法&#xff1a;密码学中的传奇 date: 2024/3/15 20:08:07 updated: 2024/3/15 20:08:07 tags: MD5起源算法原理安全分析优缺点比较技术改进示例代码应用趋势 MD5算法起源&#xff1a; MD5&#xff08;Message Digest Algorithm 5&#xff09;算法是由MIT的计算机…

【数据结构】哈希表与哈希桶

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》《算法》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负 目录 前言 1.概念 2.哈希冲突…

Vulnhub靶机渗透:DC-7打靶记录

前言 自信自强&#xff0c;来自于不怕苦、不怕难的积淀。宝剑锋从磨砺出&#xff0c;梅花香自苦寒来&#xff1b;任何美好理想&#xff0c;都离不开筚路蓝缕、手胼足胝的艰苦奋斗&#xff01; 靶场介绍 DC-7是一个初中级的靶场&#xff0c;需要具备以下前置知识&#xff1a;…

DevEco Studio 项目创建

安装DevEco Studio后开始使用&#xff0c;双击桌面DevEco Studio 快捷方式弹出界面&#xff1a; 选择Application —> Empty Ability&#xff0c;点击Next 项目配置 Project name&#xff1a;工程的名称&#xff0c;可以自定义&#xff0c;由大小写字母、数字和下划线组成。…

『运维心得』BPC-EPM-AddIn专家看过来

目录 系统版本问题 安装顺序问题 framework问题 vstor_redis问题 dll问题 一个小彩蛋 总结 最近在搞BPC&#xff0c;安装Office所需的EPM-AddIn的过程中&#xff0c;碰到了一些奇怪的问题。 查了BPC专家提供的安装说明文档&#xff0c;文档里要么没有提到我们碰到的问题…

简介:使用TensorFlow实现python简版神经网络模型

如果你想进一步深入AI编程的魔法世界&#xff0c;那么TensorFlow和PyTorch这两个深度学习框架将是你的不二之选。它们可以帮助你构建更加复杂的神经网络模型&#xff0c;实现图像识别、语音识别等高级功能。 模型原理&#xff1a;神经网络是一种模拟人脑神经元结构的计算模型&a…

osgEarth学习笔记3-第二个Osg QT程序

原文链接 打开QT Creator&#xff0c;新建一个窗口项目。 QT版本如下&#xff1a; 修改pro文件 QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c11 DEFINES QT_DEPRECATED_WARNINGS SOURCES \main.cpp \mainwindow.cpp HEADERS \mainwindow…

长安链团队论文入选国际顶会Usenix Security 2024

零知识证明是区块链扩容和隐私保护的关键前沿技术&#xff0c;其天然具备完备性、可靠性和零知识性的特点&#xff0c;是提升区块链交易吞吐量与可扩展性、在验证用户身份的同时保护用户数据隐私&#xff0c;实现复杂计算不可或缺的关键技术。基于零知识证明技术实现高兼容性、…

Linux——进程信号(二)

目录 1、阻塞信号 1.1、信号其他相关常见概念 1.2、在内核中的表示 1.3、sigset_t 1.4、信号集操作函数 2、捕捉信号 2.1、内核如何捕捉信号 5.2、sigaction 1、阻塞信号 1.1、信号其他相关常见概念 实际执行信号的处理动作被称为信号递达&#xff08;Delivery&#x…

迈向容错新时代!PASQAL发布最新技术路线图

内容来源&#xff1a;量子前哨&#xff08;ID&#xff1a;Qforepost&#xff09; 编辑丨慕一 编译/排版丨沛贤 深度好文&#xff1a;1200字丨8分钟阅读 近日&#xff0c;法国中性原子量子计算公司PASQAL发布了最新技术路线图&#xff0c;概述了其在硬件、业务场景用例及进一…

v3-admin-vite 整合pont

需求 目前后端的Admin模板使用的是v3-admin-vite&#xff0c;需要整合pont接口&#xff0c;方便前后端统一一体化开发 安装PONT 按照官方的文档&#xff0c;将pont engine安装好&#xff0c;然后在项目根目录执行pont start。注意生成代码路径要修改一下&#xff0c;因为v3-a…

【区间、栈】算法例题

目录 六、区间 48. 汇总区间 ① 49. 合并区间 ② 50. 插入区间 ② 51. 用最少数量的箭引爆气球 ② 七、栈 52. 有效的括号 ① 53. 简化路径 ② 54. 最小栈 ② 55. 逆波兰表达式求值 ② √- 56. 基本计算器 ③ 六、区间 48. 汇总区间 ① 给定一个 无重复元素 的 …

外键约束

目录 外键约束 对数据表进行初期设计&#xff0c;暂时不使用外键 验证限制三 验证级联删除 设置级联更新 Oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 外键约束 外键约束主要是在父子表关系中体现的一种约束操作。…

matlab 基于小波变换的油气管道泄露信号检测

1、内容简介 略 71-可以交流、咨询、答疑 基于小波变换的油气管道泄露信号检测 去噪、小波变换、油气管道泄露、信号检测 2、内容说明 摘 要&#xff1a; 油气管道泄漏会造成严重危害&#xff0c;因此&#xff0c;亟需寻找一种能快速检测油气管道信号的技术。传统的 傅里…

Java多线程实战-CompletableFuture异步编程优化查询接口响应速度

&#x1f3f7;️个人主页&#xff1a;牵着猫散步的鼠鼠 &#x1f3f7;️系列专栏&#xff1a;Java全栈-专栏 &#x1f3f7;️本系列源码仓库&#xff1a;多线程并发编程学习的多个代码片段(github) &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正…

远程桌面安卓版下载 安卓远程控制免费版

远程桌面安卓版下载与安卓远程控制免费版的应用解析 随着移动互联网的快速发展&#xff0c;远程桌面应用逐渐成为了许多用户、特别是技术爱好者和商务人士的必备工具。它们不仅可以在电脑上实现远程控制&#xff0c;还能将这种功能延伸到移动设备上&#xff0c;如安卓手机和平…

R语言中的常用基础绘图函数 直方图,箱线图,条形图,散点图

目录 R语言中的绘图参数 绘图函数 1.plot函数绘制散点图 2.hist函数绘制直方图 如何修饰直方图? 如何在直方图上标注各组频数&#xff1f; 使用text函数把某些信息标注在直方图上 如何在直方图上添加概率密度曲线&#xff1f; 3.boxplot函数绘制箱线图 4.barplot函数…

一、MySQL基础学习

目录 1、MySQL启动2、MySQL客户端连接3、SQL3.1、SQL语句分类3.2、DDL&#xff08;数据库定义语言&#xff09;3.2.1、操作数据库3.2.2、操作数据表 3.3、DML&#xff08;数据库操作语言&#xff09;3.3.1、增加 insert into3.3.2、删除 delete3.3.3、修改 update 3.4、DQL&…

idea error java:compilation failed:internal java compiler error

idea中编译运行maven项目报错如下 idea error java:compilation failed:internal java compiler error 尝试如下操作 注意&#xff1a;jdk8 需要设置4个地方 1.首先打开File->Project Structure中的Project&#xff0c;将SDK和language level都设置一致&#xff0c;如下…

Jackson 2.x 系列【4】对象映射器 ObjectMapper

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Jackson 版本 2.0.0 源码地址&#xff1a;https://gitee.com/pearl-organization/study-seata-demo 文章目录 1. 概述2. 案例演示2.1 创建对象2.2 写入2.3 读取 3. 泛型擦除 1. 概述 在前两篇文…