Хотел бы поделится парсером магазина продуктов на 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к товаров

Каким образом можно использовать данную информацию, да каким угодно! Можно, например, организовать сравнение стоимости в разных магазинах, можно создать API и отдавать эти данные другим клиентам-приложениям, например построить на базе этого API Оракул и передавать актуальную стоимость хлеба в блокчейн смарт-контракта :) Ну или фиксировать ежедневные цены и формировать реальную инфляцию на продукты питания за определенный период. Вообще данные есть и тут все зависит только от фантазии :)
