Rust小项目: 写一个简单的网页爬虫

文章目录

本文主要是对之前reqwest库的一个简单的扩展,通过写一个简单的爬虫项目来练习练习Rust, 爬虫有用也有趣,但是不要给目标网站造成过大的压力,否则可能会触犯法律,切记切记。

本文的爬虫任务是获取GitHub的Trending相关数据, 获取当日列表的仓库名,仓库Star数,今日Star数。

本文的依赖如下

[dependencies]
reqwest = "0.11.23"
scraper = "0.18.1"
tokio = { version = "1.35.1", features = ["full"] }

快速入门

爬虫的任务总结起来并不复杂,获取数据,解析数据,示例代码如下:

有反爬机制的网站获取数据会很难的,需要耗费很多精力破解相关机制的。。。

use reqwest::Result;
use scraper::{Html, Selector};


#[tokio::main]
async fn main() -> Result<()>{
    let target_url = "https://github.com/trending/rust";
	// 获取数据
    let body = reqwest::get(target_url)
    .await
    .expect("请求地址失败")
    .text()
    .await
    .expect("解析网页内容失败");
    
	// 解析数据
    let document = Html::parse_document(body.as_str());
    // 我为啥知道是Box-row, h2 a之类的路径? 因为手动获取的呀^_^
    let rows_selector = Selector::parse(".Box-row").unwrap();
    let repo_link_selector = Selector::parse("h2 a").unwrap();
    let repo_today_star_selector: Selector = Selector::parse("span.d-inline-block.float-sm-right").unwrap();
    let repo_total_star_selector = Selector::parse("a.Link.Link--muted.d-inline-block.mr-3").unwrap();

  	// 以每行作为后续的解析入口
    for row in document.select(&rows_selector) {
        if let Some(repo_link) = row.select(&repo_link_selector).nth(0) {
            if  let Some(href) = repo_link.value().attr("href") {
                print!("仓库链接: {href} ")
            }
        }

        if let Some(today_star) = row.select(&repo_today_star_selector).nth(0) {
            let texts: Vec<_> = today_star.text().collect();
            let text = texts.join("").split_whitespace().nth(0).expect("获取今日star数失败").to_string();
            let text = text.replace(",", "");
            print!("今日star数: {text} ")
        }

        if let Some(total_star) = row.select(&repo_total_star_selector).nth(0) {
            let texts: Vec<_> = total_star.text().collect();
            let text = texts.join("").split_whitespace().nth(0).expect("获取总star数失败").to_string();
            let text = text.replace(",", "");
            print!("总star数: {text}")
        }
        println!("")
    }

    Ok(())
}

输出如下:

仓库链接: /llenotre/maestro 今日star: 232 star: 2187
仓库链接: /rustls/rustls 今日star: 20 star: 5077
仓库链接: /aptos-labs/aptos-core 今日star: 1 star: 5599
仓库链接: /microsoft/windows-rs 今日star: 15 star: 9291
....省略其他....

如果上面的代码没有输出了,可能是Github换前端样式了,爬虫运行一段时间不生效是很正常的事情。

获取数据

如果只是获取没有太复杂的反爬机制的网页还是很简单的,通过http客户端构造一个请求就能获得网页内容了,反爬机制有很多,反反爬机制也有很多,这里简单说一个,代理。

最简单的办法就是在命令行设置http_proxy或者https_proxy变量,这个变量对于大多数http库有效,包括reqwest

export http_proxy=http://127.0.0.1:18080

至于代理从何而来,可以去爬提供代理的网页呀^_^这里就不展开了,爬代理池主要注意的是定时检查代理是否还有效。

内容解析

由程序生成和阅读的数据总是是格式化的数据,所以一定有固定的格式,网页也不例外,网页一般是HTML(也有直接返回txt格式的网页)。

定位HTML页面的各个元素一般有两种语法,XPath和CSS选择器, 我比较喜欢后者,本文涉及的第三方库scraper也支持这个语法,CSS选择器的语法可参考这个链接: https://www.runoob.com/cssref/css-selectors.html

下面是官方的几个示例。

获取列表元素

use scraper::{Html, Selector};

let html = r#"
    <ul>
        <li>Foo</li>
        <li>Bar</li>
        <li>Baz</li>
    </ul>
"#;

let fragment = Html::parse_fragment(html);
let selector = Selector::parse("li").unwrap();

for element in fragment.select(&selector) {
    assert_eq!("li", element.value().name());
}

select总是返回一个迭代器,所以需要调用相关迭代器的方法访问其中的元素或者使用for循环依次遍历。

获取元素属性

常见的属性有classhref, 总的来说,除了元素名, 包裹的文本或html内容之外哪些键值对都是熟悉, 比如这里的name="foo"value="bar"

use scraper::{Html, Selector};

let fragment = Html::parse_fragment(r#"<input name="foo" value="bar">"#);
let selector = Selector::parse(r#"input[name="foo"]"#).unwrap();

let input = fragment.select(&selector).next().unwrap();
assert_eq!(Some("bar"), input.value().attr("value"));

获取元素文本

scraper会递归的获取指定元素的文本以及包含子节点的所有文本,所以返回一个列表这并不奇怪。

use scraper::{Html, Selector};

let fragment = Html::parse_fragment("<h1>Hello, <i>world!</i></h1>");
let selector = Selector::parse("h1").unwrap();

let h1 = fragment.select(&selector).next().unwrap();
let text = h1.text().collect::<Vec<_>>();

assert_eq!(vec!["Hello, ", "world!"], text);

总结

随着反爬反反爬技术的不断碰撞,大多数爬虫项目的难点在于破解数据的加密措施而非解析页面然后获取数据,由于本人对反反爬的技术不是太精通,所以这里只是简单的介绍了一下相关第三方库, reqwestscraper

参考链接