CoderCastrov logo
CoderCastrov
Руст

Парсинг Stackoverflow с использованием Rust

Парсинг Stackoverflow с использованием Rust
просмотров
5 мин чтение
#Руст

Он будет извлекать заголовок, ссылку на вопрос, количество ответов, количество просмотров и голоса с 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 &lt;[chaudharypraveen98@gmail.com](mailto:chaudharypraveen98@gmail.com)&gt;")
        .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