Rust小項(xiàng)目: 寫一個(gè)簡(jiǎn)單的網(wǎng)頁(yè)爬蟲
本文主要是對(duì)之前reqwest庫(kù)的一個(gè)簡(jiǎn)單的擴(kuò)展,通過(guò)寫一個(gè)簡(jiǎn)單的爬蟲項(xiàng)目來(lái)練習(xí)練習(xí)Rust, 爬蟲有用也有趣,但是不要給目標(biāo)網(wǎng)站造成過(guò)大的壓力,否則可能會(huì)觸犯法律,切記切記。
本文的爬蟲任務(wù)是獲取GitHub的Trending相關(guān)數(shù)據(jù), 獲取當(dāng)日列表的倉(cāng)庫(kù)名,倉(cāng)庫(kù)Star數(shù),今日Star數(shù)。
本文的依賴如下
[dependencies]
reqwest = "0.11.23"
scraper = "0.18.1"
tokio = { version = "1.35.1", features = ["full"] }
快速入門
爬蟲的任務(wù)總結(jié)起來(lái)并不復(fù)雜,獲取數(shù)據(jù),解析數(shù)據(jù),示例代碼如下:
有反爬機(jī)制的網(wǎng)站獲取數(shù)據(jù)會(huì)很難的,需要耗費(fèi)很多精力破解相關(guān)機(jī)制的。。。
use reqwest::Result;
use scraper::{Html, Selector};
#[tokio::main]
async fn main() -> Result<()>{
let target_url = "https://github.com/trending/rust";
// 獲取數(shù)據(jù)
let body = reqwest::get(target_url)
.await
.expect("請(qǐng)求地址失敗")
.text()
.await
.expect("解析網(wǎng)頁(yè)內(nèi)容失敗");
// 解析數(shù)據(jù)
let document = Html::parse_document(body.as_str());
// 我為啥知道是Box-row, h2 a之類的路徑? 因?yàn)槭謩?dòng)獲取的呀^_^
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();
// 以每行作為后續(xù)的解析入口
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!("倉(cāng)庫(kù)鏈接: {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數(shù)失敗").to_string();
let text = text.replace(",", "");
print!("今日star數(shù): {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數(shù)失敗").to_string();
let text = text.replace(",", "");
print!("總star數(shù): {text}")
}
println!("")
}
Ok(())
}
輸出如下:
倉(cāng)庫(kù)鏈接: /llenotre/maestro 今日star數(shù): 232 總star數(shù): 2187
倉(cāng)庫(kù)鏈接: /rustls/rustls 今日star數(shù): 20 總star數(shù): 5077
倉(cāng)庫(kù)鏈接: /aptos-labs/aptos-core 今日star數(shù): 1 總star數(shù): 5599
倉(cāng)庫(kù)鏈接: /microsoft/windows-rs 今日star數(shù): 15 總star數(shù): 9291
....省略其他....
如果上面的代碼沒(méi)有輸出了,可能是Github換前端樣式了,爬蟲運(yùn)行一段時(shí)間不生效是很正常的事情。
獲取數(shù)據(jù)
如果只是獲取沒(méi)有太復(fù)雜的反爬機(jī)制的網(wǎng)頁(yè)還是很簡(jiǎn)單的,通過(guò)http客戶端構(gòu)造一個(gè)請(qǐng)求就能獲得網(wǎng)頁(yè)內(nèi)容了,反爬機(jī)制有很多,反反爬機(jī)制也有很多,這里簡(jiǎn)單說(shuō)一個(gè),代理。
最簡(jiǎn)單的辦法就是在命令行設(shè)置http_proxy或者h(yuǎn)ttps_proxy變量,這個(gè)變量對(duì)于大多數(shù)http庫(kù)有效,包括reqwest。
export http_proxy=http://127.0.0.1:18080
至于代理從何而來(lái),可以去爬提供代理的網(wǎng)頁(yè)呀^_^這里就不展開(kāi)了,爬代理池主要注意的是定時(shí)檢查代理是否還有效。
內(nèi)容解析
由程序生成和閱讀的數(shù)據(jù)總是是格式化的數(shù)據(jù),所以一定有固定的格式,網(wǎng)頁(yè)也不例外,網(wǎng)頁(yè)一般是HTML(也有直接返回txt格式的網(wǎng)頁(yè))。
定位HTML頁(yè)面的各個(gè)元素一般有兩種語(yǔ)法,XPath和CSS選擇器, 我比較喜歡后者,本文涉及的第三方庫(kù)scraper也支持這個(gè)語(yǔ)法,CSS選擇器的語(yǔ)法可參考這個(gè)鏈接: https://www.runoob.com/cssref/css-selectors.html
下面是官方的幾個(gè)示例。
獲取列表元素
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總是返回一個(gè)迭代器,所以需要調(diào)用相關(guān)迭代器的方法訪問(wèn)其中的元素或者使用for循環(huán)依次遍歷。
獲取元素屬性
常見(jiàn)的屬性有class和href, 總的來(lái)說(shuō),除了元素名, 包裹的文本或html內(nèi)容之外哪些鍵值對(duì)都是熟悉, 比如這里的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會(huì)遞歸的獲取指定元素的文本以及包含子節(jié)點(diǎn)的所有文本,所以返回一個(gè)列表這并不奇怪。
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);
總結(jié)
隨著反爬和反反爬技術(shù)的不斷碰撞,大多數(shù)爬蟲項(xiàng)目的難點(diǎn)在于破解數(shù)據(jù)的加密措施而非解析頁(yè)面然后獲取數(shù)據(jù),由于本人對(duì)反反爬的技術(shù)不是太精通,所以這里只是簡(jiǎn)單的介紹了一下相關(guān)第三方庫(kù), reqwest和scraper。
參考鏈接
-
https://github.com/trending/rust
-
https://docs.rs/scraper/latest/scraper/
-
https://www.runoob.com/cssref/css-selectors.html
