in

使用 Selenium 进行 Rust 网络抓取

使用 Selenium 进行 Rust 网络抓取

虽然像 Python 和 JavaScript 这样的动态脚本语言是进行网络抓取项目的最常见选择,但使用像 Rust 这样的静态类型和编译语言也能提供额外的安全保证和更高的性能。此外,用一种新语言进行网络抓取练习也很有趣!

本教程将向你展示如何使用 thirtyfour(Rust 的浏览器自动化库,由 Selenium 驱动)在 Rust 中抓取动态页面。


什么是 Rust,以及为什么要使用 Rust 进行网络抓取?

Rust 是一种通用编程语言,特别适用于需要最大化性能的低级任务。

它还拥有丰富的类型系统,通过structsenums, and traits,可以编码复杂的关系。虽然网络抓取通常是在动态类型语言中完成的,但你可以利用 Rust 的类型系统,确保生成的数据符合预期。


什么是 Selenium?

Selenium 是一组用于浏览器自动化的开源项目。通过 Selenium 及其 WebDriver API,你可以远程控制浏览器,并使其执行几乎任何用户可以执行的操作。

虽然 Selenium(及其替代品)主要用于测试,但它们也可以用于抓取仅在浏览器加载时才显示信息的动态网页。


使用 Rust 和 Selenium 抓取报价的教程

在本教程中,你将学习如何使用 thirtyfour 库抓取 Scraping Sandbox 上的mock quote app。它将向你展示如何执行诸如启动浏览器、定位元素、点击、输入和滚动等操作。

设置

确保你已安装 Rust

首先,你需要在计算机上安装 Rust。如果你还没有安装,可以按照官方说明进行安装。

下载 ChromeDriver 并运行

接下来,你还需要下载一个与你计算机上 Chrome 版本匹配的 ChromeDriver(你可以在设置 -> 关于 Chrome 中查找版本)。下载后,使用终端运行它。

这将创建一个 thirtyfour 将使用的 WebDriver 进程。

设置一个 Rust 项目

最后,使用以下命令设置一个新的 Rust 项目:

cargo new rust_scraper

移动到项目的目录并打开Cargo.toml文件。

将以下库添加到依赖项列表中:

[dependencies]

thirtyfour = "0.31.0"
tokio = "1.26.0"

Thirtyfour 是你将用于抓取的库。Tokio 是 Rust 的异步运行时。

设置好 Rust 项目后,在代码编辑器中打开 src/main.rs 文件。

抓取

首先,这里有一个使用 Thirtyfour 的示例 Rust 程序。它会打开我们要抓取的quote网站,等待一会儿,然后关闭浏览器。

要试试这个程序,将代码粘贴到 main.rs 中,并在终端中运行 cargo run 命令。

use thirtyfour::prelude::*;
use tokio::time::Duration;

#[tokio::main]
async fn main() -> WebDriverResult<()> {
    let caps = DesiredCapabilities::chrome();
    let driver = WebDriver::new("http://localhost:9515", caps).await?;

    driver.goto("http://quotes.toscrape.com/scroll").await?;

    tokio::time::sleep(Duration::from_secs(5)).await;

    driver.quit().await?;

    Ok(())
}

让我们一步一步地了解它的作用。

  • 首先,它告诉库你期望驱动的浏览器是带有 DesiredCapabilities 的 Chrome。
  • 然后,它连接到运行在机器上 9515 端口的 WebDriver(这是 ChromeDriver 在运行时默认使用的端口)。
  • 之后,它使用 driver.goto() 打开一个页面并等待五秒钟。
  • 最后,它关闭浏览器。

如果代码在你的机器上运行成功,那么你就可以开始抓取网站了。

定位元素

在本节中,你将学习如何抓取页面中显示的quote。

从脚本中删除以下这一行:

tokio::time::sleep(Duration::from_secs(5)).await;

在这一行的位置,你将放置抓取数据的代码。

要抓取页面中的quote,首先需要定位包含报价的所有框,然后从中提取文本。

rust 1.png

你可以使用driver.find_all() 和 driver.find() 函数来定位页面中的元素。它们接受多种选择器,例如 CSS selectorsXPath 等。

下面是一个定位网站中所有“quote boxes”的函数。

let quote_elems = driver.find_all(By::Css(".quote")).await?;

它通过查找所有具有 quote CSS 类的元素来实现这一点。

选择框后,你可以遍历它们以收集信息。

下面的代码获取每个框,找到其中具有 “text” 和 “author” 类的元素,并提取这些元素的文本。然后,它将它们作为元组存储在一个向量中。

let mut quotes = Vec::new();

   for quote_elem in quote_elems {
       let quote_text = quote_elem.find(By::Css(".text")).await?.text().await?;
       let author = quote_elem.find(By::Css(".author")).await?.text().await?;
       let quote = (quote_text, author);
       quotes.push(quote);
   }

最后,你可以在控制台中打印quotes:

for quote in quotes {
       println!("{} -- {}", quote.0, quote.1)
   }

下面是完整的代码:

use thirtyfour::prelude::*;

#[tokio::main]
async fn main() -> WebDriverResult<()> {
    let caps = DesiredCapabilities::chrome();
    let driver = WebDriver::new("http://localhost:9515", caps).await?;

    driver.goto("http://quotes.toscrape.com/scroll").await?;

    let quote_elems = driver.find_all(By::Css(".quote")).await?;

    let mut quotes = Vec::new();

    for quote_elem in quote_elems {
        let quote_text = quote_elem.find(By::Css(".text")).await?.text().await?;
        let author = quote_elem.find(By::Css(".author")).await?.text().await?;
        let quote = (quote_text, author);
        quotes.push(quote);
    }

    for quote in quotes {
        println!("{} -- {}", quote.0, quote.1)
    }

    driver.quit().await?;

    Ok(())
}

滚动页面

你可能已经注意到,该页面具有无限滚动功能:当你向下滚动时,会出现新的名言。这时,你正在自动化一个真实的浏览器而不仅仅是一个 HTML 解析库的事实会派上用场。

使用 scroll_into_view() 方法,你可以将页面滚动到最后一条名言,从而加载并抓取更多的名言。

例如,如果你想向下滚动五次,可以使用以下循环来实现:

let mut quote_elems: Vec<WebElement> = Vec::new();

   for _n in 1..5 {
       quote_elems = driver.find_all(By::Css(".quote")).await?;
       let last = quote_elems.last().unwrap();
       last.scroll_into_view().await?;
       tokio::time::sleep(Duration::from_secs(1)).await;
   }

代码非常简单:它找到所有的名言元素,滚动到最后一个元素,然后稍等片刻以加载新元素。

结合我们已有的代码,它看起来是这样的:

use thirtyfour::prelude::*;
use tokio::time::Duration;

#[tokio::main]
async fn main() -> WebDriverResult<()> {
    let caps = DesiredCapabilities::chrome();
    let driver = WebDriver::new("http://localhost:9515", caps).await?;

    driver.goto("http://quotes.toscrape.com/scroll").await?;

    let mut quote_elems: Vec<WebElement> = Vec::new();

    for _n in 1..5 {
        quote_elems = driver.find_all(By::Css(".quote")).await?;
        let last = quote_elems.last().unwrap();
        last.scroll_into_view().await?;
        tokio::time::sleep(Duration::from_secs(1)).await;
    }

    let mut quotes = Vec::new();

    for quote_elem in quote_elems {
        let quote_text = quote_elem.find(By::Css(".text")).await?.text().await?;
        let author = quote_elem.find(By::Css(".author")).await?.text().await?;
        let quote = (quote_text, author);
        quotes.push(quote);
    }

    for quote in quotes {
        println!("{} -- {}", quote.0, quote.1)
    }

    driver.quit().await?;

    Ok(())
}

与网页上的元素交互

Selenium 还可以让你与网站的其他元素进行交互。

例如,网站有一个登录页面。由于 Selenium 模拟的是一个真实的浏览器,你实际上可以使用它来登录!

首先,你需要选择页面上的登录按钮。这最简单的方法是使用查询元素文本的 XPath 选择器。

let login_button = driver
        .find(By::XPath("//a[contains(text(), 'Login')]"))
        .await?;

上面的代码选择了一个包含文本“Login”的锚元素。

之后,你可以使用 click() 方法点击它。

login_button.click().await?;

rust 2.png

然后可以使用driver.find()方法定位表单元素,并使用send_keys()方法填充它们。

let username_field = driver.find(By::Css("#username")).await?;
    username_field.send_keys("username").await?;

    let password_field = driver.find(By::Css("#password")).await?;
    password_field.send_keys("password").await?;

最后,你可以找到并单击提交按钮。

let submit_button = driver.find(By::Css("input[type=submit]")).await?;
    submit_button.click().await?;

下面是这部分的完整代码:

use thirtyfour::prelude::*;

#[tokio::main]
async fn main() -> WebDriverResult<()> {
    let caps = DesiredCapabilities::chrome();
    let driver = WebDriver::new("http://localhost:9515", caps).await?;

    driver.goto("http://quotes.toscrape.com/scroll").await?;

    let login_button = driver
        .find(By::XPath("//a[contains(text(), 'Login')]"))
        .await?;

    login_button.click().await?;

    let username_field = driver.find(By::Css("#username")).await?;
    username_field.send_keys("username").await?;

    let password_field = driver.find(By::Css("#password")).await?;
    password_field.send_keys("password").await?;

    let submit_button = driver.find(By::Css("input[type=submit]")).await?;
    submit_button.click().await?;

    driver.quit().await?;

    Ok(())
}

在 Thirtyfour 中使用代理

虽然网络抓取是完全合法的,但一些网站管理员并不鼓励这种行为。如果他们发现你以自动化的方式连接到他们的网站,可能会决定阻止你的 IP 地址访问网站。

因此,在进行网络抓取时使用代理是一个好主意。代理在客户端和服务器之间充当中介,改变请求,使其看起来像是来自代理服务器而不是客户端。

如果你使用代理,服务器无法获取你的真实 IP 地址,你就不会因为抓取网站而被禁止访问,例如在你的计算机上观看 YouTube 视频。相反,代理的 IP 会被禁止。

代理有两种类型:免费和付费。虽然免费代理无需任何费用,但它们通常速度慢,提供的 IP 数量有限,甚至可能会收集你的私人数据并出售给其他方。而付费代理提供全球范围内的代理池,并在每次请求时默认轮换代理 IP。

要在 Thirtyfour 中添加代理,你需要使用 set_proxy() 方法将其添加到 WebDriver 的功能中。

use thirtyfour::{prelude::*, CapabilitiesHelper};

然后,在初始化功能后对其进行调整。

let mut caps = DesiredCapabilities::chrome();
    caps.set_proxy(
        thirtyfour::common::capabilities::desiredcapabilities::Proxy::Manual {
            ftp_proxy: None,
            http_proxy: Some("http://link-to-proxy.com".to_string()),
            ssl_proxy: Some("http://link-to-proxy.com".to_string()),
            socks_proxy: None,
            socks_version: None,
            socks_username: None,
            socks_password: None,
            no_proxy: None,
        },
    )?;

这对于免费代理有效。不幸的是,Selenium 与需要填写身份验证窗口的认证代理配合得不好,因为 Selenium 无法访问该窗口。

要使用认证代理,最好的(也是最简单的)方法是将你的 IP 列入提供商的白名单。

下面我们将以IPRoyal 住宅代理为例,详细介绍如何操作。

首先,通过 WhatIsMyIP.com 之类的工具找到你的 IP 地址。

然后,进入 IPRoyal 住宅代理的控制面板,选择白名单部分。

rust 3.png

单击Add按钮

rust 4.png

将IP地址粘贴到白名单中

rust 5.png

现在,当你将代理添加到功能中时,请在URL中不带身份验证详细信息地使用它。

例如: http://geo.iproyal.com:12321

而不是这样: http://username:[email protected]:12321

由于你的 IP 已被添加到白名单,代理将不会要求授权。

以下是添加代理并测试它的完整代码示例:

use thirtyfour::{prelude::*, CapabilitiesHelper};
use tokio::time::Duration;

#[tokio::main]
async fn main() -> WebDriverResult<()> {
    let mut caps = DesiredCapabilities::chrome();
    caps.set_proxy(
        thirtyfour::common::capabilities::desiredcapabilities::Proxy::Manual {
            ftp_proxy: None,
            http_proxy: Some("link-to-proxy".to_string()),
            ssl_proxy: Some("link-to-proxy".to_string()),
            socks_proxy: None,
            socks_version: None,
            socks_username: None,
            socks_password: None,
            no_proxy: None,
        },
    )?;

    let driver = WebDriver::new("http://localhost:9515", caps).await?;

    driver.goto("https://ipv4.icanhazip.com").await?;

    let ip = driver.find(By::Css("body")).await?.text().await?;
    println!("{}", ip);

    driver.quit().await?;

    Ok(())
}

常见问题

错误:NewSessionError

出现此错误是因为 ChromeDriver 未运行。运行你在设置阶段下载的可执行文件,然后再次尝试运行 Rust 代码。

错误:NoSuchElement

当库无法定位元素时会出现此错误。检查你为元素使用的选择器是否正确且没有拼写错误。

错误:the trait std::fmt::Display is not implemented for impl Future<Output = Result<String, WebDriverError>>

出现此错误(及类似错误)的原因是你在某个语句末尾没有添加 .await。因此,future 没有解析为实际值。


结    论

虽然 Rust 不是进行网络抓取任务的最常用选择,但它绝对是一种可以用于这个目的的语言。尽管你不会有庞大的生态系统支持(例如,Python 中通过 selenium-wire 解决了代理问题),但尝试使用 Rust 进行网络抓取是一个有趣的练习。

要更多地练习这门语言,你可以尝试抓取像 Reddit 这样动态的网站,因为自动生成的类名,找到元素并不那么简单。

Written by 河小马

河小马是一位杰出的数字营销行业领袖,广告中国论坛的重要成员,其专业技能涵盖了PPC广告、域名停放、网站开发、联盟营销以及跨境电商咨询等多个领域。作为一位资深程序开发者,他不仅具备强大的技术能力,而且在出海网络营销方面拥有超过13年的经验。