Просто спарсь что угодно с помощью языка Rust (ну… или просто скачай файл)
Известно, что 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.