Pull to refresh

Просто спарсь что угодно с помощью языка Rust (ну… или просто скачай файл)

Level of difficultyMedium
Reading time4 min
Views11K

Известно, что Rust — типобезопасный язык программирования, код которого проверяется компилятором ещё до сборки. С одной стороны это хорошо: меньше вероятности внезапного сбоя в самый неподходящий момент. С другой стороны — код, выполняющий одну функцию, в итоге может стать очень сложным и иногда нечитаемым.

В пример привожу функцию, которая парсит названия валют с сайта floatrates и выводит их (пожалуйста, напишите в комментариях, за сколько минут вы поняли эту строку):

// main.rs
use anyhow::anyhow;
use reqwest::blocking::get;
use scraper::{Html, Selector};

fn main() -> anyhow::Result<()> {
    let body = get("http://www.floatrates.com/json-feeds.html")?.text()?;
    let document = Html::parse_document(&body);
    let selector = Selector::parse("div.bk-json-feeds>div.body>ul>li>a")
        .map_err(|err| anyhow!("{err}"))?;

    for element in document.select(&selector) {
        println!("{}", element.inner_html());
    }
    Ok(())
}

В этой статье я хочу рассказать о своей новой библиотеке, значительно упрощающей парсинг на Rust. Приятного чтения!

Дисклеймер

Пожалуйста, не «бейте» меня за некоторые ошибки в терминах, в комментариях или в синтаксисе кода: я программирую на Rust всего 2 месяца.

Однако опыт в программировании у меня есть (год в python), поэтому прекрасно понимаю, о чём здесь пишу.

Буду очень признателен, если вы напишите в комментариях, что нужно исправить в крэйте)

Благодарю reloginn и Black Soul за помощь в разработке версии 1.0

Инициализация структуры Scraper

// src/error.rs
#[derive(Debug)]
pub enum ScrapingErrorKind {
    NotFound,
    InvalidSelector
}

#[derive(Debug)]
pub enum ScrapingHttpErrorKind {
    GettingDataError
}

// src/scraping.rs
use reqwest::blocking;
use scraper::{ElementRef, Html, Selector, error::SelectorErrorKind};
use crate::error::{ScrapingErrorKind, ScrapingHttpErrorKind};

/// Создание нового экземпляра
fn instance(url: &str) -> Result<Scraper, ScrapingHttpErrorKind> {
    let document = blocking::get(url)
        .map_err(|_| ScrapingHttpErrorKind::GettingDataError)?
        .text()
        .map_err(|_| ScrapingHttpErrorKind::GettingDataError)?;

    Ok(Scraper { document: Html::parse_document(&document) })
}

/// Простой парсер
/// // ...
pub struct Scraper {
    document: Html,
}

Вместо того, чтобы отдельно отправлять запрос, из его результата получать код сайта и инициализировать объект scraper::Html, можно просто вызвать команду let scraper = scr::Scraper::new("scrapeme.live/shop/").unwrap() (да, «https://» и «http://» вводить не надо). Структура сохраняет в себе scraper::Html для дальнейшего парсинга. Вот, что происходит «под капотом» инициализатора:

// scr::scraping
// ...
impl Scraper {
    /// создание нового экземпляра парсера,
    /// <i>используя код сайта **(без https://)**</i>
    pub fn new(url: &str) -> Result<Scraper, ScrapingHttpErrorKind> {
        instance(format!("https://{url}").as_str())
    }

    /// создание нового экземпляра парсера,
    /// <i>используя код сайта **(без http://)**</i>
    pub fn from_http(url: &str) -> Result<Scraper, ScrapingHttpErrorKind> {
        instance(format!("http://{url}").as_str())
    }
    // ...
}

Также есть возможность инициализировать структру, используя фрагмент HTML страницы:

// scr::scraping
// ...
impl Scraper {
    // ...
    /// создание нового экземпляра парсера,
    /// <i>используя **фрагмент** кода сайта</i>
    pub fn from_fragment(fragment: &str) -> Result<Scraper, SelectorErrorKind> {
        Ok(Scraper { document: Html::parse_fragment(fragment) })
    }
    // ...
}

Получение элементов

Под командой scraper.get_els("путь#к>элементам").unwrap(); скрываются выбор элементов по специальному пути c применением структуры scraper::Selector и преобразование полученного результата в Vec<scraper::ElementRef>.

// scr::scraping
// ...
impl Scraper {
    // ...
    /// получение элементов
    pub fn get_els<'a>(&'a self, sel: &'a str) -> Result<Vec<ElementRef>, ScrapingErrorKind> {
        let parsed_selector = Selector::parse(sel).map_err(|_| ScrapingErrorKind::InvalidSelector)?;

        let elements = self.document
            .select(&parsed_selector)
            .collect::<Vec<ElementRef>>();

        Ok(elements)
    }

    /// получение элемента
    pub fn get_el<'a>(&'a self, sel: &'a str) -> Result<ElementRef, ScrapingErrorKind> {
        let element = *self.get_els(sel)?
            .get(0)
            .ok_or(ScrapingErrorKind::NotFound)?;

        Ok(element)
    }
    // ...
}

Получение текста (inner_html) и атрибута элемента (-ов)

Можно получить текст или атрибут как одного, так и нескольких элементов, полученных с помощью scraper.get_els("путь#к>элементам").unwrap();

// scr::scraping
// ...
impl Scraper {
    // ...
    /// получение текста из элемента
    /// ...
    pub fn get_text_once(&self, sel: &str) -> Result<String, ScrapingErrorKind> {
        let text = self.get_el(sel)?
            .inner_html();

        Ok(text)
    }

    /// получение текста из всех элементов
    /// ...
    pub fn get_all_text(self, sel: &str) -> Result<Vec<String>, ScrapingErrorKind> {
        let text = self.get_els(sel)?
            .iter()
            .map(|element| element.inner_html())
            .collect();

        Ok(text)
    }

    /// получение атрибута элемента
    /// ...
    pub fn get_attr_once<'a>(&'a self, sel: &'a str, attr: &'a str) -> Result<&str, ScrapingErrorKind> {
        let attr = self.get_el(sel)?
            .value()
            .attr(attr)
            .ok_or(ScrapingErrorKind::NotFound)?;

        Ok(attr)
    }

    /// получение атрибута всех элементов
    /// ...
    pub fn get_all_attr<'a>(&'a self, sel: &'a str, attr: &str) -> Result<Vec<&str>, SelectorErrorKind> {
        let attrs = self.get_els(sel)
            .unwrap()
            .iter()
            .map(|element| element.value().attr(attr).expect("Some elements do not contain the desired attribute"))
            .collect();

        Ok(attrs)
    }
}

Загрузка файлов (структура FileLoader)

Это — простой загрузчик файлов. Достаточно просто ввести одну команду. Исходный код структуры: *тык*

Известная проблема

К сожалению, пока нельзя вернуть экземпляр структуры scr::FileLoader (ошибка [E0515]).

Планы на версию 2.0.0

Пока что scr работает только синхронно, отчего крэйт моментами достаточно медленный. Поэтому через некоторое время я внедрю async (скорее всего отдельной фичей или модулем).

На этом пока всё. Очень надеюсь, что я заинтересовал вас. По желанию вы можете внести изменения в библиотеку, создав форк репозитория, совершив некоторые изменения и отправив их мне через pull request.

Tags:
Hubs:
Total votes 7: ↑5 and ↓2+3
Comments19

Articles