Известно, что 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.
