Парсинг Stackoverflow с использованием Rust
Table Of Content
- Он будет извлекать заголовок, ссылку на вопрос, количество ответов, количество просмотров и голоса с Stackoverflow в зависимости от параметра тега и количества. Этот парсер вдохновлен [Kadekillary Scarper](https://github.com/kadekillary/scraping-with-rust) с обновленными библиотеками и некоторыми дополнительными функциями.
- Темы -> rust, reqwest, selectrs, парсинг
- Используемые библиотеки
- Особенности
- Шаг 1 -> Получение аргумента из командной строки с использованием библиотеки Clap
- Шаг 2 -> Отправка запроса с использованием библиотеки Reqwest
- Шаг 3 -> Парсинг с использованием библиотеки Selectrs
- Полный код
- Как запустить
- Пример вывода
- Rust Парсер Stackoverflow.mp4
- Редактировать описание
- Развертывание
Он будет извлекать заголовок, ссылку на вопрос, количество ответов, количество просмотров и голоса с Stackoverflow в зависимости от параметра тега и количества. Этот парсер вдохновлен Kadekillary Scarper с обновленными библиотеками и некоторыми дополнительными функциями.
Темы -> rust, reqwest, selectrs, парсинг
Используемые библиотеки
- Reqwest => Эргономичный HTTP-клиент для Rust с включенными батарейками.
- Select** **=> Библиотека на Rust для извлечения полезных данных из HTML-документов, подходящая для парсинга веб-страниц.
- Clap => Простая в использовании, эффективная и полнофункциональная библиотека для разбора аргументов командной строки и подкоманд.
Особенности
- Простой и быстрый
- Асинхронные GET-запросы
- Режим командной строки
Установка библиотек
Просто добавьте следующие библиотеки в файл Cargo.toml
[dependencies]
reqwest = { version = "0.10", features = ["json"] }
tokio = { version = "0.2", features = ["full"] }
select = "0.6.0-alpha.1"
clap = "2.33.3"
rand = "0.8.4"
Прежде чем продолжить, мы должны знать о CSS-селекторах
Что такое селекторы/локаторы?
CSS-селектор - это комбинация селектора элемента и значения, которое идентифицирует веб-элемент на веб-странице.
Выбор локатора зависит в значительной степени от вашего тестируемого приложения
Id
Id элемента в XPath определяется с помощью: "[@id='example']", а в CSS с помощью: "#" - ID должны быть уникальными в пределах DOM.
Примеры:
XPath: //div[@id='example']
CSS: #example
Тип элемента
Предыдущий пример показал //div в xpath. Это тип элемента, который может быть input для текстового поля или кнопки, img для изображения или "a" для ссылки.
Xpath: //input or
Css: =input
Прямой потомок
HTML-страницы структурированы как XML, с дочерними элементами, вложенными в родительские элементы. Если вы можете найти, например, первую ссылку внутри div, вы можете создать строку для ее поиска. Прямой потомок в XPath определяется с помощью "/", а в CSS - ">".
Примеры:
XPath: //div/a
CSS: div > a
Потомок или подпотомок
Написание вложенных div может быть утомительным - и приводить к хрупкому коду. Иногда вы ожидаете изменений в коде или хотите пропустить слои. Если элемент может находиться внутри другого или одного из его дочерних элементов, в XPath он определяется с помощью "//", а в CSS - просто пробелом.
Примеры:
XPath: //div//a
CSS: div a
Класс
Для классов в XPATH используется: "[@class='example']", а в CSS - просто "."
Примеры:
XPath: //div[@class='example']
CSS: .example
Шаг 1 -> Получение аргумента из командной строки с использованием библиотеки Clap
Мы используем библиотеку Clap для получения аргумента из командной строки.
Есть три случая.
Сначала мы инициализируем приложение командной строки с именем StackOverflow Scraper. Затем указываем все три случая с их коротким и длинным именем.
fn main() {
let matches = App::new("StackOverflow Scraper")
.version("1.0")
.author("Praveen Chaudhary <chaudharypraveen98@gmail.com>")
.about("Он будет парсить вопросы с StackOverflow в зависимости от тега.")
.arg(
Arg::with_name("tag")
.short("t")
.long("tag")
.takes_value(true)
.help("принимает тег и парсит вопросы в соответствии с ним"),
)
.arg(
Arg::with_name("count")
.short("c")
.long("count")
.takes_value(true)
.help("возвращает n количество постов"),
)
.get_matches();
....
....
После того, как мы указали все случаи, теперь нам нужно извлечь значение аргумента с помощью сопоставления, которое помогает нам найти определенный шаблон.
fn main() {
.....
.....
if matches.is_present("tag") && matches.is_present("count") {
let url = format!(
"https://stackoverflow.com/questions/tagged/{}?tab=Votes",
matches.value_of("tag").unwrap()
);
let count: i32 = matches.value_of("count").unwrap().parse::().unwrap();
stackoverflow_post(&url, count as usize);
} else if matches.is_present("tag") {
let url = format!(
"https://stackoverflow.com/questions/tagged/{}?tab=Votes",
matches.value_of("tag").unwrap()
);
stackoverflow_post(&url, 16);
} else if matches.is_present("count") {
let url = get_random_url();
let count: i32 = matches.value_of("count").unwrap().parse::().unwrap();
stackoverflow_post(&url, count as usize);
} else {
let url = get_random_url();
stackoverflow_post(&url, 16);
}
}
В приведенном выше коде мы использовали функцию stackoverflow_post. Мы узнаем об этом в Шаге 3
Шаг 2 -> Отправка запроса с использованием библиотеки Reqwest
Мы будем использовать библиотеку reqwest для отправки GET-запроса на веб-сайт StackOverflow с использованием заданного тега ввода.
#[tokio::main]
async fn hacker_news(url: &str, count: usize) -> Result<(), reqwest::Error> {
let resp = reqwest::get(url).await?;
....
Шаг 3 -> Парсинг с использованием библиотеки Selectrs
Мы будем использовать CSS-селекторы для получения вопросов с StackOverflow.
#[tokio::main]
async fn hacker_news(url: &str, count: usize) -> Result<(), reqwest::Error> {
.....
.....let document = Document::from(&*resp.text().await?);for node in document.select(Class("s-post-summary")).take(count) {
let question = node
.select(Class("s-post-summary--content-excerpt"))
.next()
.unwrap()
.text();
let title_element = node
.select(Class("s-post-summary--content-title").child(Name("a")))
.next()
.unwrap();
let title = title_element.text();
let question_link = title_element.attr("href").unwrap();
let stats = node
.select(Class("s-post-summary--stats-item-number"))
.map(|stat| stat.text())
.collect::<Vec<_>>();
let votes = &stats[0];
let answer = &stats[1];
let views = &stats[2];
let tags = node
.select(Class("post-tag"))
.map(|tag| tag.text())
.collect::<Vec<_>>();
println!("Вопрос => {}", question);
println!(
"Ссылка на вопрос => [https://stackoverflow.com{](https://stackoverflow.com{)}",
question_link
);
println!("Заголовок вопроса => {}", title);
println!("Голоса => {}", votes);
println!("Просмотры => {}", views);
println!("Теги => {}", tags.join(" ,"));
println!("Ответы => {}", answer);
println!("--------------------------------------------\n");
}
Ok(())
}
Полный код
extern crate clap;
extern crate reqwest;
extern crate select;
extern crate tokio;
use clap::{App, Arg};
use rand::seq::SliceRandom;
use select::document::Document;
use select::predicate::{Attr, Class, Name, Or, Predicate};
fn main() {
let matches = App::new("StackOverflow Парсер")
.version("1.0")
.author("Praveen Chaudhary <[chaudharypraveen98@gmail.com](mailto:chaudharypraveen98@gmail.com)>")
.about("Парсит вопросы с StackOverflow в зависимости от тега.")
.arg(
Arg::with_name("tag")
.short("t")
.long("tag")
.takes_value(true)
.help("принимает тег и парсит вопросы по этому тегу"),
)
.arg(
Arg::with_name("count")
.short("c")
.long("count")
.takes_value(true)
.help("возвращает n количество постов"),
)
.get_matches();
if matches.is_present("tag") && matches.is_present("count") {
let url = format!(
"[https://stackoverflow.com/questions/tagged/{}?tab=Votes](https://stackoverflow.com/questions/tagged/%7B%7D?tab=Votes)",
matches.value_of("tag").unwrap()
);
let count: i32 = matches.value_of("count").unwrap().parse::<i32>().unwrap();
hacker_news(&url, count as usize);
} else if matches.is_present("tag") {
let url = format!(
"[https://stackoverflow.com/questions/tagged/{}?tab=Votes](https://stackoverflow.com/questions/tagged/%7B%7D?tab=Votes)",
matches.value_of("tag").unwrap()
);
hacker_news(&url, 16);
} else if matches.is_present("count") {
let url = get_random_url();
let count: i32 = matches.value_of("count").unwrap().parse::<i32>().unwrap();
hacker_news(&url, count as usize);
} else {
let url = get_random_url();
hacker_news(&url, 16);
}
}
#[tokio::main]
async fn hacker_news(url: &str, count: usize) -> Result<(), reqwest::Error> {
let resp = reqwest::get(url).await?;
// println!("body = {:?}", resp.text().await?);
// assert!(resp.status().is_success());
let document = Document::from(&*resp.text().await?);
for node in document.select(Class("s-post-summary")).take(count) {
let question = node
.select(Class("s-post-summary--content-excerpt"))
.next()
.unwrap()
.text();
let title_element = node
.select(Class("s-post-summary--content-title").child(Name("a")))
.next()
.unwrap();
let title = title_element.text();
let question_link = title_element.attr("href").unwrap();
let stats = node
.select(Class("s-post-summary--stats-item-number"))
.map(|stat| stat.text())
.collect::<Vec<_>>();
let votes = &stats[0];
let answer = &stats[1];
let views = &stats[2];
let tags = node
.select(Class("post-tag"))
.map(|tag| tag.text())
.collect::<Vec<_>>();
println!("Вопрос => {}", question);
println!(
"Ссылка на вопрос => [https://stackoverflow.com{](https://stackoverflow.com{)}",
question_link
);
println!("Заголовок вопроса => {}", title);
println!("Голоса => {}", votes);
println!("Просмотры => {}", views);
println!("Теги => {}", tags.join(" ,"));
println!("Ответы => {}", answer);
println!("--------------------------------------------\n");
}
Ok(())
}
// Получение случайного тега
fn get_random_url() -> String {
let default_tags = vec!["python", "rust", "c#", "android", "html", "javascript"];
let random_tag = default_tags.choose(&mut rand::thread_rng()).unwrap();
let url = format!(
"[https://stackoverflow.com/questions/tagged/{}?tab=Votes](https://stackoverflow.com/questions/tagged/%7B%7D?tab=Votes)",
random_tag
);
url.to_string()
}
Как запустить
Пример вывода
Rust Парсер Stackoverflow.mp4
Редактировать описание
drive.google.com
Развертывание
Вы можете развернуть на Heroku с помощью Circle CI
Вы можете прочитать больше об этом на CircleCI Blog
Ссылка на предварительный просмотр -> stackoverflow-парсинг-с-помощью-rust
Ссылка на исходный код -> GitHub