Хотел бы поделится парсером магазина продуктов на Rust. Изначально хотел создать парсер на PHP Symfony, но столкнулся с рядом проблем связанных с производительностью. Скорее всего без Symfony на нативном PHP парсер заработал бы без проблем, но зачем брать для этой цели PHP если есть Rust ;)
Итак, для начала требуется установить Rust. Можно забрать инструкцию с главной страницы официального сайта Rust для своей системы, мне же было достаточно одной команды для установки
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup | sh
После установки можно проверить версию командой
$ rustc --version rustc 1.84.0 (9fc6b4312 2025-01-07)
Теперь создаем рабочую директорию для нового проекта в моем случае это ~/parse
Внутри директории создаем Cargo.toml с содержимым
[package] name = "parser" version = "0.1.0" edition = "2021" [dependencies] reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] } regex = "1.7"
далее создаем директорию ~/parse/src в которой будет находится основной код парсера
Создаем внутри main.rs и директорию parsers ( в ней я планирую создавать модули для парсенга разных магазинов )
// Регестрируем модули mod parsers; // Подключаем необходимые библиотеки use std::collections::HashMap; use std::env; fn main() { // Получаем аргументы командной строки let args: Vec<String> = env::args().collect(); if args.len() < 2 { eprintln!("Usage: {} <command>", args[0]); return; } let command = &args[1]; // Создаём HashMap с командами и функциями let mut command_map: HashMap<&str, fn()> = HashMap::new(); // Регистрируем функции из разных модулей command_map.insert("avoska", parsers::avoska::run); // command_map.insert("magnit", parsers::magnit::run); // command_map.insert("dixy", parsers::dixy::run); // Ищем команду в HashMap и вызываем соответствующую функцию match command_map.get(command.as_str()) { Some(func) => func(), None => eprintln!("Unknown command: {}", command), } }
Концепция main.rs заключается в том чтобы можно было при помощи передаваемого параметра ей запускать парсер который находится в директории parsers/<название_магазина>
Вызов такое команды будет таким:
$ cargo run <grocery_store>
Так же нужно создать файл с перечислением модулей в директории src/parsers/mod.rs
pub mod avoska // pub mod magnit // pub mod dixy // ...
Это требуется чтобы наши парсеры были зарегистрированны в качестве модулей в проекте
Теперь основной функицонал парсера для продуктового магазина «Авоська». Код парсера будет находится тут src/parsers/avoska.rs
// Подключаем необходимые модули, браузер, регулярки, модули для работы со временем use reqwest::blocking::Client; use regex::Regex; use std::time::Duration; use std::time::Instant; pub fn run() { // Засекаем время let start = Instant::now(); // Создаем браузер для получения данных let client = Client::builder() .timeout(Duration::from_secs(10)) // Устанавливаем таймаут .build() .expect("Failed to build client"); // Обозначаем с какой страницы начинать парсинг let mut page = 1; // Это общий счетчик полученных продуктов с сайта let mut counter = 1; // Открываем бесконечный цикл. Rust не умеет в do {...} while() как PHP :( loop { // Формируем url для сбора товаров let url = format!("https://www.avoska.ru/products?o={}", page); // Сразу же устанавливаем следующую страницу page += 1; // Регулярное выражение, которое позволяет выдергивать из данных название и цену продукта let re = Regex::new(r#"<div class="promo-products-card-title-block">.*?<h4 class="promo-products-card-title">(.*?)</h4>.*?<p class="promo-products-card-text">(.*?)</p>.*?<sub></sub>(.*?)<sup>(.*?)</sup>"#) .expect("Invalid regex pattern"); // Пробуем сделать запрос match client.get(url).send() { Ok(response) => { if let Ok(body) = response.text() { // Из полученных данных лучше убрать все возможные переводы строк, чтобы не нагружать этим регуярку выше let re1 = Regex::new(r"\r?\n").unwrap(); let cleaned_body = re1.replace_all(&body, ""); // Если никакие данные не распасрились, то это может означать что мы дошли до конца всех страниц каталога и поэтому тормозим процесс парсинга if re.captures_iter(&cleaned_body).next().is_none() { break; } else { // Если получили данные, то проходим по всем товарам на странице и сохраняем их в переменных for captures in re.captures_iter(&cleaned_body) { let title = captures.get(1).map_or("1", |m| m.as_str()); let description = captures.get(2).map_or("", |m| m.as_str()); let price_integer = captures.get(3).map_or("", |m| m.as_str()); let price_fraction = captures.get(4).map_or("", |m| m.as_str()); // Данные получины, в данном примере я вывожу лог с информацией о товаре в стандартный поток вывода в консоль println!("{}. Title: {} {} \n Price: {}.{} \n ---", counter, title, description, price_integer, price_fraction); // Увеличиваем счетчик на 1 товар counter += 1; } } } else { // Если что-то пошло не так eprintln!("Failed to read response body"); } } Err(err) => { // Если что-то пошло не так при попытке запроса данных с сайта eprintln!("Request failed: {}", err); } } // Получаем продолжительность работы парсера в секундах let duration = start.elapsed(); // Выводим статистику по затраченному времени и полученным товарам println!("Counter: {}, Time: {:?}", counter, duration); } }
Запускаем все это командой
$ cargo run avoska
Вот и все. Таким простым способом можно распарсить продуктовый магазин и получить актуальные цены на продукты питания в любом магазине. На момент написания текста, парсер до конца не отработал и вывел около 200к товаров
![](https://killercoder.ru/wp-content/uploads/2025/01/image-1.png)
Каким образом можно использовать данную информацию, да каким угодно! Можно, например, организовать сравнение стоимости в разных магазинах, можно создать API и отдавать эти данные другим клиентам-приложениям, например построить на базе этого API Оракул и передавать актуальную стоимость хлеба в блокчейн смарт-контракта :) Ну или фиксировать ежедневные цены и формировать реальную инфляцию на продукты питания за определенный период. Вообще данные есть и тут все зависит только от фантазии :)