
Hello world!
Книга рецептов — это коллекция простых примеров, демонстрирующих хорошие практики решения распространенных задач программирования с помощью крейтов экосистемы Rust.
Содержание
- 10. Кодирование
- 11. Обработка ошибок
- 12. Файловая система
- 13. Разное
- 14. Операционная система
- 15. Обработка текста
- 16. Веб-разработка
Обратите внимание:: для запуска примеров Книги вам потребуется такой файл Cargo.toml:
[package] name = "rust_cookbook" version = "0.1.0" edition = "2021" [dependencies] chrono = "0.4.31" crossbeam = "0.8.3" crossbeam-channel = "0.5.10" csv = "1.3.0" env_logger = "0.11.3" error-chain = "0.12.4" glob = "0.3.1" image = "0.25.0" lazy_static = "1.4.0" log = "0.4.20" mime = "0.3.17" num = "0.4.1" num_cpus = "1.16.0" postgres = "0.19.7" rand = "0.8.5" rayon = "1.8.0" regex = "1.10.2" reqwest = {version = "0.11.23", features = ["blocking", "json"]} same-file = "1.0.6" select = "0.6.0" serde = {version = "1.0.193", features = ["derive"]} serde_json = "1.0.110" threadpool = "1.8.1" tokio = { version = "1.35.1", features = ["full"] } unicode-segmentation = "1.10.1" url = "2.5.0" walkdir = "2.4.0" dotenv = "0.15.0" tempfile = "3.9.0" data-encoding = "2.5.0" ring = "0.17.7" clap = "4.5.2" ansi_term = "0.12.1" flate2 = "1.0.28" tar = "0.4.40" semver = "1.0.22" percent-encoding = "2.3.1" base64 = "0.22.0" toml = "0.8.12" memmap = "0.7.0" [dependencies.rusqlite] version = "0.31.0" features = ["bundled"]
Также обратите внимание, что некоторые примеры работают только на Linux.
10. Кодирование
10.1. Наборы символов
Процентное кодирование строки
Пример процентного кодирования строки с помощью функции uft8_percent_encode из крейта percent-encoding. Декодирование строки выполняется в помощью функции percent_decode.
use percent_encoding::{utf8_percent_encode, percent_decode, AsciiSet, CONTROLS}; use std::str::Utf8Error; /// https://url.spec.whatwg.org/#fragment-percent-encode-set const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); fn main() -> Result<(), Utf8Error> { let input = "confident, productive systems programming"; // Кодируем и собираем строку let iter = utf8_percent_encode(input, FRAGMENT); let encoded: String = iter.collect(); assert_eq!(encoded, "confident,%20productive%20systems%20programming"); // Декодируем строку let iter = percent_decode(encoded.as_bytes()); let decoded = iter.decode_utf8()?; assert_eq!(decoded, "confident, productive systems programming"); Ok(()) }
Набор кодировок (FRAGMENT) определяет, какие байты (помимо байтов, отличных от ASCII, и элементов управления (controls)) должны кодироваться. Состав этого набора зависит от контекста. Например, url кодирует ? в пути (path) URL, но не в строке запроса (query string).
utf8_percent_encode возвращает итератор срезов &str, которые собираются (collect) в String.
Кодирование строки в application/x-www-form-urlencoded
Пример кодирования строки в application/x-www-form-urlencoded с помощью метода form_urlencoded::byte_serialize. Декодирование выполняется с помощью метода form_urlencoded::parse. Обе функции возвращают итераторы, которые собираются (collect) в String.
use url::form_urlencoded::{byte_serialize, parse}; fn main() { // Кодируем строку let urlencoded: String = byte_serialize("What is ❤?".as_bytes()).collect(); assert_eq!(urlencoded, "What+is+%E2%9D%A4%3F"); // Декодируем строку let decoded: String = parse(urlencoded.as_bytes()) .map(|(key, val)| [key, val].concat()) .collect(); assert_eq!(decoded, "What is ❤?"); }
Шестнадцатеричное кодирование и декодирование
Крейт data_encoding предоставляет метод HEXUPPER::encode, который принимает &[u8] и возвращает String, содержащую шестнадцатеричное представление данных.
Этот крейт также предоставляет метод HEXUPPER::decode, который принимает &[u8] и возвращает Vec<u8> при успешном декодировании данных.
use data_encoding::{DecodeError, HEXUPPER}; fn main() -> Result<(), DecodeError> { let original = b"The quick brown fox jumps over the lazy dog."; let expected = "54686520717569636B2062726F776E20666F78206A756D7073206F76\ 657220746865206C617A7920646F672E"; // Кодируем данные let encoded = HEXUPPER.encode(original); assert_eq!(encoded, expected); // Декодируем данные let decoded = HEXUPPER.decode(&encoded.into_bytes())?; assert_eq!(decoded, original); Ok(()) }
base64 кодирование и декодирование
Крейт base64 предоставляет методы encode и decode для кодирования и декодирования байтовых срезов в base64:
use error_chain::error_chain; use base64::{engine::general_purpose::STANDARD, Engine as _}; use std::str; error_chain! { foreign_links { Base64(base64::DecodeError); Utf8Error(str::Utf8Error); } } fn main() -> Result<()> { let hello = b"hello rustaceans"; let encoded = STANDARD.encode(hello); let decoded = STANDARD.decode(&encoded)?; println!("origin: {}", str::from_utf8(hello)?); println!("base64 encoded: {}", encoded); println!("back to origin: {}", str::from_utf8(&decoded)?); Ok(()) }
10.2. Обработка CSV
Чтение записей CSV
Пример чтения стандартных записей CSV в структуру csv::StringRecord — слаботипизированное представление данных, которое ожидает валидные строки UTF-8. В качестве альтернативы можно использовать структуру ByteRecord, которая не проверяет строки.
use csv::Error; fn main() -> Result<(), Error> { let csv = "year,make,model,description 1948,Porsche,356,Luxury sports car 1967,Ford,Mustang fastback 1967,American car"; let mut reader = csv::Reader::from_reader(csv.as_bytes()); for record in reader.records() { let record = record?; println!( "In {}, {} built the {} model. It is a {}.", &record[0], &record[1], &record[2], &record[3] ); } Ok(()) }
Метод csv::Reader::deserialize десериализует данные в строготипизированные структуры. Обратите внимание на явную типизацию десериализуемой записи.
use serde::Deserialize; #[derive(Deserialize)] struct Record { year: u16, make: String, model: String, description: String, } fn main() -> Result<(), csv::Error> { let csv = "year,make,model,description 1948,Porsche,356,Luxury sports car 1967,Ford,Mustang fastback 1967,American car"; let mut reader = csv::Reader::from_reader(csv.as_bytes()); for record in reader.deserialize() { // Типизация записи let record: Record = record?; println!( "In {}, {} built the {} model. It is a {}.", record.year, record.make, record.model, record.description ); } Ok(()) }
Чтение записей CSV с другим разделителем
Пример чтения записей CSV, разделителем которых является таб:
use csv::Error; use serde::Deserialize; #[derive(Debug, Deserialize)] struct Record { name: String, place: String, #[serde(deserialize_with = "csv::invalid_option")] id: Option<u64>, } use csv::ReaderBuilder; fn main() -> Result<(), Error> { let data = "name\tplace\tid Mark\tMelbourne\t46 Ashley\tZurich\t92"; let mut reader = ReaderBuilder::new() // указываем разделитель .delimiter(b'\t') .from_reader(data.as_bytes()); // Другой способ типизации записи for result in reader.deserialize::<Record>() { println!("{:?}", result?); } Ok(()) }
Фильтрация записей CSV, совпадающих с предикатом
В следующем примере возвращаются только те строки data, которые совпадают с query:
use error_chain::error_chain; use std::io; error_chain! { foreign_links { Io(std::io::Error); CsvError(csv::Error); } } fn main() -> Result<()> { let query = "CA"; let data = "\ City,State,Population,Latitude,Longitude Kenai,AK,7610,60.5544444,-151.2583333 Oakman,AL,,33.7133333,-87.3886111 Sandfort,AL,,32.3380556,-85.2233333 West Hollywood,CA,37031,34.0900000,-118.3608333"; // Средство чтения CSV let mut rdr = csv::ReaderBuilder::new().from_reader(data.as_bytes()); // Средство записи данных в stdout (терминал) let mut wtr = csv::Writer::from_writer(io::stdout()); // Пишем в терминал заголовки CSV wtr.write_record(rdr.headers()?)?; for result in rdr.records() { let record = result?; // Пишем в терминал запись, содержащую поле, совпадающее с `query` (`CA`) if record.iter().any(|field| field == query) { wtr.write_record(&record)?; } } // `writer` использует внутренний буфер, см. ниже wtr.flush()?; Ok(()) }
Обработка невалидных данных с помощью serde
Файлы CSV часто содержат невалидные данные. Для таких случаяв крейт csv предоставляет кастомный десериализатор, csv::invalid_option, который автоматически преобразует невалидные данные в значения None:
use csv::Error; use serde::Deserialize; #[derive(Debug, Deserialize)] struct Record { name: String, place: String, #[serde(deserialize_with = "csv::invalid_option")] id: Option<u64>, } fn main() -> Result<(), Error> { // Последняя запись содержит невалидный ID let data = "name,place,id mark,sydney,46.5 ashley,zurich,92 akshat,delhi,37 alisha,colombo,xyz"; let mut rdr = csv::Reader::from_reader(data.as_bytes()); for result in rdr.deserialize() { let record: Record = result?; // Результат десериализации последней записи выглядит как // `Record { name: "alisha", place: "colombo", id: None }` println!("{:?}", record); } Ok(()) }
Сериализация записей в CSV
Пример сериализации кортежей Rust. Структура csv::writer поддерживает автоматическую сериализацию типов Rust в записи CSV. Метод write_record предназначен для работы с простыми записями, содержащими только строковые данные. Для работы с данными, содержащими более сложные значения, такие как целые числа, числа с плавающей точкой и опциональные значения, используются метод serialize. Поскольку в средстве записи (writer) используется внутренний буфер, необходимо явно вызывать метод flush для его очистки.
use error_chain::error_chain; use std::io; error_chain! { foreign_links { CSVError(csv::Error); IOError(std::io::Error); } } fn main() -> Result<()> { let mut wtr = csv::Writer::from_writer(io::stdout()); wtr.write_record(&["Name", "Place", "ID"])?; wtr.serialize(("Mark", "Sydney", 87))?; wtr.serialize(("Ashley", "Dublin", 32))?; wtr.serialize(("Akshat", "Delhi", 11))?; wtr.flush()?; Ok(()) }
Сериализация записей в CSV с помощью serde
Пример сериализации кастомной структуры в запись CSV с помощью крейта serde:
use error_chain::error_chain; use serde::Serialize; use std::io; error_chain! { foreign_links { IOError(std::io::Error); CSVError(csv::Error); } } #[derive(Serialize)] struct Record<'a> { name: &'a str, place: &'a str, id: u64, } fn main() -> Result<()> { let mut wtr = csv::Writer::from_writer(io::stdout()); let rec1 = Record { name: "Mark", place: "Melbourne", id: 56, }; let rec2 = Record { name: "Ashley", place: "Sydney", id: 64, }; let rec3 = Record { name: "Akshat", place: "Delhi", id: 98, }; wtr.serialize(rec1)?; wtr.serialize(rec2)?; wtr.serialize(rec3)?; wtr.flush()?; Ok(()) }
10.3. Структурированные данные
Сериализация и десериализация неструктурированного JSON
Крейт serde_json предоставляет функцию from_str для разбора &str в формате JSON.
Неструктурированный JSON разбирается в универсальный тип serde_json::Value, который может представлять любой валидный JSON.
Следующим пример демонстрирует разбор &str JSON. Макрос json! используется для определения ожидаемого значения.
use serde_json::json; use serde_json::{Value, Error}; fn main() -> Result<(), Error> { let j = r#"{ "userid": 103609, "verified": true, "access_privileges": [ "user", "admin" ] }"#; let parsed: Value = serde_json::from_str(j)?; let expected = json!({ "userid": 103609, "verified": true, "access_privileges": [ "user", "admin" ] }); assert_eq!(parsed, expected); Ok(()) }
Десериализация TOML
Пример разбора TOML в универсальное toml::Value, которое может представлять любые валидные данные в формате TOML:
use toml::{Value, de::Error}; fn main() -> Result<(), Error> { let toml_content = r#" [package] name = "your_package" version = "0.1.0" authors = ["You! <you@example.org>"] [dependencies] serde = "1.0" "#; let package_info: Value = toml::from_str(toml_content)?; assert_eq!(package_info["dependencies"]["serde"].as_str(), Some("1.0")); assert_eq!(package_info["package"]["name"].as_str(), Some("your_package")); Ok(()) }
Крейт serde позволяет разбирать TOML в кастомные структуры:
use serde::Deserialize; use std::collections::HashMap; use toml::de::Error; #[derive(Deserialize)] struct Config { package: Package, dependencies: HashMap<String, String>, } #[derive(Deserialize)] struct Package { name: String, version: String, authors: Vec<String>, } fn main() -> Result<(), Error> { let toml_content = r#" [package] name = "your_package" version = "0.1.0" authors = ["You! <you@example.org>"] [dependencies] serde = "1.0" "#; let package_info: Config = toml::from_str(toml_content)?; assert_eq!(package_info.package.name, "your_package"); assert_eq!(package_info.package.version, "0.1.0"); assert_eq!(package_info.package.authors, vec!["You! <you@example.org>"]); assert_eq!(package_info.dependencies["serde"], "1.0"); Ok(()) }
11. Обработка ошибок
Правильная обработка ошибок в main
Пример обработки ошибки, возникающей при попытке открыть несуществующий файл. Для этого используется error-chain, библиотека, которая инкапсулирует большое количество шаблонного кода, необходимого для обработки ошибок в Rust.
Io(std::io::Error) внутри foreign_links автоматически преобразует структуру std::io::Error в тип, определенный макросом error_chain! и реализующий трейт Error.
В следующем примере мы пытаемся выяснить, сколько времени работает система путем открытия файла Unix /proc/uptime и разбора его содержимого для извлечения первого числа. Функция read_uptime возвращает время безотказной работы или ошибку.
use error_chain::error_chain; use std::fs::File; use std::io::Read; error_chain!{ foreign_links { Io(std::io::Error); ParseInt(std::num::ParseIntError); } } fn read_uptime() -> Result<u64> { let mut uptime = String::new(); File::open("/proc/uptime")?.read_to_string(&mut uptime)?; Ok(uptime .split('.') .next() .ok_or("Невозможно разобрать данные")? .parse()?) } fn main() { match read_uptime() { Ok(uptime) => println!("Время безотказной работы: {} секунд", uptime), Err(err) => eprintln!("Ошибка: {}", err), }; }
Обработка всех возможных ошибок
Крейт error-chain делает возможным и относительно компактным сопоставление разных типов ошибок, возвращаемых функцией. Тип ошибки определяется перечислением ErrorKind.
Используем reqwest::blocking для получения произвольного целого числа из веб-сервиса. Преобразуем строку из ответа в целое число. Стандартная библиотека Rust, reqwest и веб-сервис могут генерировать ошибки. Мы определяем ошибки с помощью foreign_links. Дополнительный вариант ErrorKind для веб-сервиса использует блок errors макроса error_chain!.
use error_chain::error_chain; error_chain! { foreign_links { Io(std::io::Error); Reqwest(reqwest::Error); ParseIntError(std::num::ParseIntError); } errors { RandomResponseError(t: String) } } fn parse_response(response: reqwest::blocking::Response) -> Result<u32> { let mut body = response.text()?; body.pop(); body.parse::<u32>() .chain_err(|| ErrorKind::RandomResponseError(body)) } fn run() -> Result<()> { let url = format!("https://www.random.org/integers/?num=1&min=0&max=10&col=1&base=10&format=plain"); let response = reqwest::blocking::get(&url)?; let random_value: u32 = parse_response(response)?; println!("Произвольное целое число между 0 и 10: {}", random_value); Ok(()) } fn main() { if let Err(error) = run() { match *error.kind() { ErrorKind::Io(_) => println!("Стандартная ошибка ввода/вывода: {:?}", error), ErrorKind::Reqwest(_) => println!("Ошибка Reqwest: {:?}", error), ErrorKind::ParseIntError(_) => { println!("Стандартная ошибка разбора целого числа: {:?}", error) } ErrorKind::RandomResponseError(_) => println!("Кастомная ошибка: {:?}", error), _ => println!("Другая ошибка: {:?}", error), } } }
Получение трассировки сложной ошибки
Следующий пример демонстрирует обработку сложной ошибки и вывод ее трассировки. chain_err используется для расширения списка возможных ошибок путем добавления новых ошибок. Стек ошибки может быть распутан (unwound), что предоставляет лучший контекст для понимания того, почему возникла ошибка.
В примере мы пытаемся десериализовать значение 256 в u8. Ошибка всплывает (bubble up) из serde через csv в пользовательский код.
use error_chain::error_chain; use serde::Deserialize; use std::fmt; error_chain! { foreign_links { Reader(csv::Error); } } #[derive(Debug, Deserialize)] struct Rgb { red: u8, blue: u8, green: u8, } impl Rgb { fn from_reader(csv_data: &[u8]) -> Result<Rgb> { let color: Rgb = csv::Reader::from_reader(csv_data) .deserialize() .nth(0) .ok_or("Невозможно разобрать первую запись CSV")? .chain_err(|| "Невозможно разобрать цвет RGB")?; Ok(color) } } impl fmt::UpperHex for Rgb { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let hexa = u32::from(self.red) << 16 | u32::from(self.blue) << 8 | u32::from(self.green); write!(f, "{:X}", hexa) } } fn run() -> Result<()> { let csv = "red,blue,green 102,256,204"; let rgb = Rgb::from_reader(csv.as_bytes()).chain_err(|| "Невозможно прочитать данные CSV")?; println!("{:?} в шестнадцатеричном формате: #{:X}", rgb, rgb); Ok(()) } fn main() { if let Err(ref errors) = run() { eprintln!("Уровень ошибки - описание"); errors .iter() .enumerate() .for_each(|(index, error)| eprintln!("└> {} - {}", index, error)); if let Some(backtrace) = errors.backtrace() { eprintln!("{:?}", backtrace); } // В реальном приложении ошибки должны обрабатываться. Например, так: // std::process::exit(1); } }
Обратная трассировка ошибки:
Уровень ошибки - описание └> 0 - Невозможно прочитать данные CSV └> 1 - Невозможно разобрать цвет RGB └> 2 - CSV deserialize error: record 1 (line: 2, byte: 15): field 1: number too large to fit in target type
Запустите пример с RUST_BACKTRACE=1 для отображения подробной обратной трассировки этой ошибки.
12. Файловая система
12.1. Чтение и запись
Чтение линий строк из файла
Записываем сообщение, состоящее из трех строк, в файл, затем читаем его построчно с помощью итератора Lines, созданного с помощью метода BufRead::lines. Структура File реализует трейт Read, который предоставляет трейт BufReader. Метод File::create открывает файл для записи, а метод File::open — для чтения.
use std::fs::File; use std::io::{Write, BufReader, BufRead, Error}; fn main() -> Result<(), Error> { let path = "lines.txt"; let mut output = File::create(path)?; write!(output, "Rust\n💖\nFun")?; let input = File::open(path)?; let buffered = BufReader::new(input); for line in buffered.lines() { println!("{}", line?); } Ok(()) }
Блокировка одновременного чтения и записи файла
Структура same_file::Handle используется для сравнения обработчика файла с другими обработчиками. В следующем примере сравниваются обработчики чтения и записи файла:
use same_file::Handle; use std::fs::File; use std::io::{BufRead, BufReader, Error, ErrorKind}; use std::path::Path; fn main() -> Result<(), Error> { let path_to_read = Path::new("message.txt"); let stdout_handle = Handle::stdout()?; let handle = Handle::from_path(path_to_read)?; if stdout_handle == handle { return Err(Error::new( ErrorKind::Other, "Вы читаете и пишете в один и тот же файл", )); } else { let file = File::open(&path_to_read)?; let file = BufReader::new(file); for (num, line) in file.lines().enumerate() { println!("{} : {}", num, line?.to_uppercase()); } } Ok(()) }
cargo run отображает содержимое файла message.txt, а cargo run >> ./message.txt завершается ошибкой, поскольку операции чтения и записи выполняются над одним файлом.
Произвольный доступ к файлу с помощью карты памяти
Создаем карту памяти (memory map) с помощью memmap и имитируем произвольные чтения файла. Использование карты памяти означает, что мы индексируем фрагмент, а не пытаемся перемещаться по файлу.
Функция Mmap::map предполагает, что файл из карты памяти не модифицируется в то же время другим процессом. Иначе возникнет гонка за данными.
use memmap::Mmap; use std::fs::File; use std::io::{Write, Error}; fn main() -> Result<(), Error> { write!(File::create("content.txt")?, "My hovercraft is full of eels!")?; let file = File::open("content.txt")?; let map = unsafe { Mmap::map(&file)? }; let random_indexes = [0, 1, 2, 19, 22, 10, 11, 29]; assert_eq!(&map[3..13], b"hovercraft"); let random_bytes: Vec<u8> = random_indexes.iter() .map(|&idx| map[idx]) .collect(); assert_eq!(&random_bytes[..], b"My loaf!"); Ok(()) }
12.2. Обход директории
Получение названий файлов, модифицированных в течение последних 24 часов
Получаем текущую рабочую директорию путем вызова env::current_dir, затем для каждой сущности в fs::read_dir, извлекаем DirEntry::path и получаем метаданные через fs::Metadata. Metadata::modified возвращает SystemTime::elapsed — время, прошедшее с момента последней модификации. Duration::as_secs преобразует время в секунды и сравнивает его с 24 часами (24 60 60). Metadata::is_file отфильтровывает директории.
use error_chain::error_chain; use std::{env, fs}; error_chain! { foreign_links { Io(std::io::Error); SystemTimeError(std::time::SystemTimeError); } } fn main() -> Result<()> { // Получаем текущую директорию let current_dir = env::current_dir()?; println!( "Файлы, модифицированные в течение последних 24 часов в {:?}:", current_dir ); // Перебираем сущности, находящиеся в текущей директории for entry in fs::read_dir(current_dir)? { let entry = entry?; // Получаем путь сущности let path = entry.path(); // Извлекаем метаданные сущности let metadata = fs::metadata(&path)?; // Получаем время последней модификации и преобразуем его в секунды let last_modified = metadata.modified()?.elapsed()?.as_secs(); // Если с момента последней модификации прошло меньше 24 часов и // сущность является файлом if last_modified < 24 * 3600 && metadata.is_file() { println!( "Файл: {:?}, с момента последней модификации прошло {:?} секунд, файл доступен только для чтения: {:?}, размер: {:?} байтов.", path.file_name().ok_or("Название файла отсутствует")?, last_modified, metadata.permissions().readonly(), metadata.len(), ); } } Ok(()) }
Рекурсивный поиск дубликатов
Пример рекурсивного поиска повторяющихся файлов, находящихся в текущей директории:
use std::collections::HashMap; use walkdir::WalkDir; fn main() { let mut filenames = HashMap::new(); // Перебираем сущности, находящиеся в текущей директории for entry in WalkDir::new(".") .into_iter() .filter_map(Result::ok) // игнорируем директории .filter(|e| !e.file_type().is_dir()) { // Получаем название файла let f_name = String::from(entry.file_name().to_string_lossy()); // Счетчик количества названий файлов let counter = filenames.entry(f_name.clone()).or_insert(0); *counter += 1; // Если название файла дублируется if *counter == 2 { println!("{}", f_name); } } }
Рекурсивный поиск файлов с заданным предикатом
Пример поиска всех файлов JSON, находящихся в текущей директории и модифицированных в течение последних 24 часов. Метод follow_links считает символические ссылки обычными директориями и файлами.
use error_chain::error_chain; use walkdir::WalkDir; error_chain! { foreign_links { WalkDir(walkdir::Error); Io(std::io::Error); SystemTime(std::time::SystemTimeError); } } fn main() -> Result<()> { for entry in WalkDir::new(".") // учитываем символические ссылки .follow_links(true) .into_iter() .filter_map(|e| e.ok()) { let f_name = entry.file_name().to_string_lossy(); let sec = entry.metadata()?.modified()?; // Если мы имеем дело с файлом JSON и с момента его // последнего изменения прошло меньше 24 часов if f_name.ends_with(".json") && sec.elapsed()?.as_secs() < 86400 { println!("{}", f_name); } } Ok(()) }
Обход директорий с пропуском файлов, название которых начинается с точки
Используем метод filter_entry для рекурсивного перебора сущностей. Функция is_not_hidden возвращает индикатор того, является ли файл или директория скрытыми (если название сущности начинается с точки, значит сущность является скрытой). Iterator::filter применяется к каждой WalkDir::DirEntry, даже если предком сущности является скрытая директория.
Корневая директория не считается скрытой благодаря использованию WalkDir::depth в is_not_hidden.
use walkdir::{DirEntry, WalkDir}; fn is_not_hidden(entry: &DirEntry) -> bool { entry .file_name() .to_str() // сущность является корневой директорией или ее название начинается с точки .map(|s| entry.depth() == 0 || !s.starts_with(".")) .unwrap_or(false) } fn main() { WalkDir::new(".") .into_iter() // отфильтровываем скрытые сущности .filter_entry(|e| is_not_hidden(e)) .filter_map(|v| v.ok()) .for_each(|x| println!("{}", x.path().display())); }
Рекурсивное вычисление размера файлов до заданной глубины
Глубина рекурсии может быть гибко установлена с помощью методов WalkDir::min_depth и WalkDir::max_depth. Вычисляем размер файлов на глубине трех поддиректорий, игнорируя файлы в корневой директории:
use walkdir::WalkDir; fn main() { let total_size = WalkDir::new(".") .min_depth(1) .max_depth(3) .into_iter() .filter_map(|entry| entry.ok()) .filter_map(|entry| entry.metadata().ok()) .filter(|metadata| metadata.is_file()) .fold(0, |acc, m| acc + m.len()); println!("Общий размер: {} байтов.", total_size); }
Рекурсивный поиск всех файлов PNG
Пример рекурсивного поиска всех файлов PNG в текущей директории. В данном случае паттерн ** совпадает с текущей директорией и всеми ее поддиректориями.
Паттерн ** может использоваться в любом месте пути. Например, /media/**/*.png совпадает со всеми файлами PNG в директории media и всех вложенных директориях.
use error_chain::error_chain; use glob::glob; error_chain! { foreign_links { Glob(glob::GlobError); Pattern(glob::PatternError); } } fn main() -> Result<()> { for entry in glob("**/*.png")? { println!("{}", entry?.display()); } Ok(()) }
Поиск всех файлов PNG, совпадающий с заданным паттерном, независимо от регистра
Пример поиска всех изображений в директории media, совпадающих с паттерном img_[0-9]*.png.
В функцию glob_with передается структура MatchOptions с настройкой case_sensitive: false, что делает поиск нечувствительным к регистру. Остальные настройки остаются дефолтными.
use error_chain::error_chain; use glob::{glob_with, MatchOptions}; error_chain! { foreign_links { Glob(glob::GlobError); Pattern(glob::PatternError); } } fn main() -> Result<()> { let options = MatchOptions { case_sensitive: false, ..Default::default() }; for entry in glob_with("/media/img_[0-9]*.png", options)? { println!("{}", entry?.display()); } Ok(()) }
13. Разное
Проверка количества логических ядер центрального процессора
fn main() { println!("Количество логических ядер ЦП: {}", num_cpus::get()); }
Определение лениво оцениваемой константы
Пример определения лениво оцениваемой (lazy evaluated) константной HashMap. HashMap оценивается один раз и хранится за глобальной статической ссылкой.
use lazy_static::lazy_static; use std::collections::HashMap; lazy_static! { static ref PRIVILEGES: HashMap<&'static str, Vec<&'static str>> = { let mut map = HashMap::new(); map.insert("Игорь", vec!["user", "admin"]); map.insert("Алекс", vec!["user"]); map }; } fn show_access(name: &str) { let access = PRIVILEGES.get(name); println!("{}: {:?}", name, access); } fn main() { let access = PRIVILEGES.get("Игорь"); println!("Игорь: {:?}", access); show_access("Алекс"); }
Обработка запросов на неиспользуемом порту
В следующем примере порт отображается в терминале и программа принимает подключения до получения запроса. При установке порта в значение 0, SocketAddrV4 присваивает произвольный порт.
use std::net::{SocketAddrV4, Ipv4Addr, TcpListener}; use std::io::{Read, Error}; fn main() -> Result<(), Error> { let loopback = Ipv4Addr::new(127, 0, 0, 1); let socket = SocketAddrV4::new(loopback, 0); let listener = TcpListener::bind(socket)?; let port = listener.local_addr()?; println!("Listening on {}, access this port to end the program", port); let (mut tcp_stream, addr) = listener.accept()?; // блокировка до получения запроса println!("Connection received! {:?} is sending data.", addr); let mut input = String::new(); let _ = tcp_stream.read_to_string(&mut input)?; println!("{:?} says {}", addr, input); Ok(()) }
14. Операционная система
14.1. Внешняя команда
Запуск внешней команды и обработка stdout
Запускаем git log --oneline как внешнюю Command и исследуем ее Output с помощью Regex для получения хеша и сообщений последних 5 коммитов:
use error_chain::error_chain; use std::process::Command; use regex::Regex; error_chain!{ foreign_links { Io(std::io::Error); Regex(regex::Error); Utf8(std::string::FromUtf8Error); } } #[derive(PartialEq, Default, Clone, Debug)] struct Commit { hash: String, message: String, } fn main() -> Result<()> { let output = Command::new("git").arg("log").arg("--oneline").output()?; if !output.status.success() { error_chain::bail!("Выполнение команды завершилось кодом ошибки"); } let pattern = Regex::new(r"(?x) ([0-9a-fA-F]+) # хеш коммита (.*) # сообщение коммита")?; String::from_utf8(output.stdout)? .lines() .filter_map(|line| pattern.captures(line)) .map(|cap| { Commit { hash: cap[1].to_string(), message: cap[2].trim().to_string(), } }) .take(5) .for_each(|x| println!("{:?}", x)); Ok(()) }
Обратите внимание: эта программа должна выполняться в директории с инициализированным GIT (fatal: not a git repository (or any of the parent directories): .git), содержащим хотя бы один коммит (fatal: your current branch 'master' does not have any commits yet).
Запуск внешней команды, передача ей stdin и проверка кода ошибки
Запускаем интерпретатор python с помощью внешней Command и передаем ему инструкцию для выполнения. Затем разбираем Output.
use error_chain::error_chain; use std::collections::HashSet; use std::io::Write; use std::process::{Command, Stdio}; error_chain!{ errors { CmdError } foreign_links { Io(std::io::Error); Utf8(std::string::FromUtf8Error); } } fn main() -> Result<()> { let mut child = Command::new("python").stdin(Stdio::piped()) .stderr(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; child.stdin .as_mut() .ok_or("stdin дочернего процесса не был перехвачен")? .write_all(b"import this; copyright(); credits(); exit()")?; let output = child.wait_with_output()?; if output.status.success() { let raw_output = String::from_utf8(output.stdout)?; let words = raw_output.split_whitespace() .map(|s| s.to_lowercase()) .collect::<HashSet<_>>(); println!("Найдено {} уникальных слов:", words.len()); println!("{:#?}", words); Ok(()) } else { let err = String::from_utf8(output.stderr)?; error_chain::bail!("Выполнение внешней команды провалилось:\n {}", err) } }
Обратите внимание: для успешного выполнения этой программы на вашей машине должен быть установлен Python.
Запуск внешних команд в конвейере
Получаем список из 10 самых больших файлов и директорий, находящихся в текущей рабочей директории с помощью команды du -ah . | sort -hr | head -n 10, выполняемой программно.
Command представляет процесс. Вывод (output) дочернего процесса перехватывается с помощью Stdio::piped между предком и ребенком.
use error_chain::error_chain; use std::process::{Command, Stdio}; error_chain! { foreign_links { Io(std::io::Error); Utf8(std::string::FromUtf8Error); } } fn main() -> Result<()> { // Путь к текущей директории let directory = std::env::current_dir()?; // du -ah . let mut du_output_child = Command::new("du") .arg("-ah") .arg(&directory) .stdout(Stdio::piped()) .spawn()?; if let Some(du_output) = du_output_child.stdout.take() { // sort -hr let mut sort_output_child = Command::new("sort") .arg("-hr") .stdin(du_output) .stdout(Stdio::piped()) .spawn()?; du_output_child.wait()?; if let Some(sort_output) = sort_output_child.stdout.take() { // head -n 10 let head_output_child = Command::new("head") .args(&["-n", "10"]) .stdin(sort_output) .stdout(Stdio::piped()) .spawn()?; let head_stdout = head_output_child.wait_with_output()?; sort_output_child.wait()?; println!( "10 самых больших файлов и директорий в '{}':\n{}", directory.display(), String::from_utf8(head_stdout.stdout).unwrap() ); } } Ok(()) }
Обратите внимание: эта программа предназначена для выполнения в системах Unix. В Windows аналогичную команду можно выполнить с помощью bash -c "du -ah . | sort -hr | head -n 10".
Перенаправление stdout и stderr дочернего процесса в один файл
Создаем (spawn) дочерний процесс и перенаправляем stdout и stderr в один и тот же файл. Этот пример похож на предыдущий, за исключением того, что process::Stdio пишет в указанный файл. File::try_clone ссылается на один обработчик для stdout и stderr. Это гарантирует, что оба дескриптора пишут с одной и той же позиции курсора. Выполнение этой программы аналогично выполнению команды ls . oops >out.txt 2>&1.
use std::fs::File; use std::io::Error; use std::process::{Command, Stdio}; fn main() -> Result<(), Error> { let outputs = File::create("out.txt")?; let errors = outputs.try_clone()?; Command::new("ls") // вызываем ошибку .args(&[".", "oops"]) .stdout(Stdio::from(outputs)) .stderr(Stdio::from(errors)) .spawn()? .wait_with_output()?; Ok(()) }
Обратите внимание: эта программа предназначена для выполнения в системах Unix. В Windows аналогичную команду можно выполнить с помощью bash -c "ls . oops >out.txt 2>&1".
Непрерывная обработка входных данных дочернего процесса
В примере "Запуск внешней команды и обработка stdout" обработка начиналась только после завершения выполнения внешней Command. В следующем примере мы создаем конвейер с помощью Stdio::piped и непрерывно (continuously) читаем stdout при обновлении BufReader. Выполнение этой программы эквивалентно выполнению команды journalctl | grep usb.
use std::process::{Command, Stdio}; use std::io::{BufRead, BufReader, Error, ErrorKind}; fn main() -> Result<(), Error> { let stdout = Command::new("journalctl") .stdout(Stdio::piped()) .spawn()? .stdout .ok_or_else(|| Error::new(ErrorKind::Other, "Невозможно перехватить stdout"))?; let reader = BufReader::new(stdout); reader .lines() .filter_map(|line| line.ok()) .filter(|line| line.find("usb").is_some()) .for_each(|line| println!("{}", line)); Ok(()) }
Обратите внимание: эта программа предназначена для выполнения в системах Unix. В Windows аналогичную команду можно выполнить с помощью bash -c "journalctl | grep usb".
Чтение переменных среды окружения
Пример чтения переменной среды окружения с помощью std::env::var:
use std::env; use std::fs; use std::io::Error; fn main() -> Result<(), Error> { // Читаем `config_path` из переменной среды окружения `CONFIG`. // Если переменная `CONFIG` не установлена, используется дефолтный путь let config_path = env::var("CONFIG") .unwrap_or("/etc/myapp/config".to_string()); let config: String = fs::read_to_string(config_path)?; println!("Настройки: {}", config); Ok(()) }
Для чтения переменных из файлов .env* используется крейт dotenv.
15. Обработка текста
15.1. Регулярные выражения
Проверка и извлечение логина из адреса email
Пример валидации email и извлечения всего, что предшествует @:
use lazy_static::lazy_static; use regex::Regex; // Функция извлечения логина fn extract_login(input: &str) -> Option<&str> { // Лениво оцениваемая статическая ссылка - регулярное выражение lazy_static! { static ref RE: Regex = Regex::new(r"(?x) ^(?P<login>[^@\s]+)@ ([[:word:]]+\.)* [[:word:]]+$ ").unwrap(); } RE.captures(input).and_then(|cap| { // login - захваченная группа (capture group) cap.name("login").map(|login| login.as_str()) }) } fn main() { assert_eq!(extract_login(r"I❤email@example.com"), Some(r"I❤email")); assert_eq!( extract_login(r"sdf+sdsfsd.as.sdsd@jhkk.d.rl"), Some(r"sdf+sdsfsd.as.sdsd") ); assert_eq!(extract_login(r"More@Than@One@at.com"), None); assert_eq!(extract_login(r"Not an email@email"), None); }
Извлечение списка уникальных хештегов из текста
Пример извлечения, сортировки и удаления дублирующихся хештегов из текста.
Регулярное выражение для проверки хештега учитывает только латинские хештеги, которые начинаются с буквы. Полная регулярка проверки хештегов Twitter гораздо сложнее.
use lazy_static::lazy_static; use regex::Regex; use std::collections::HashSet; // Функция извлечения хештегов fn extract_hashtags(text: &str) -> HashSet<&str> { // Лениво оцениваемая статическая ссылка - регулярное выражение lazy_static! { static ref RE: Regex = Regex::new( r"\#[a-zA-Z][0-9a-zA-Z_]*" ).unwrap(); } RE.find_iter(text).map(|mat| mat.as_str()).collect() } fn main() { let tweet = "Hey #world, I just got my new #dog, say hello to Till. #dog #forever #2 #_ "; let tags = extract_hashtags(tweet); assert!(tags.contains("#dog") && tags.contains("#forever") && tags.contains("#world")); assert_eq!(tags.len(), 3); }
Извлечение из текста номеров телефона
Пример обработки текста с помощью Regex::captures_iter для захвата нескольких номеров телефона. Регулярное выражение учитывает только американские номера.
use error_chain::error_chain; use regex::Regex; use std::fmt; error_chain!{ foreign_links { Regex(regex::Error); Io(std::io::Error); } } struct PhoneNumber<'a> { area: &'a str, exchange: &'a str, subscriber: &'a str, } impl<'a> fmt::Display for PhoneNumber<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "1 ({}) {}-{}", self.area, self.exchange, self.subscriber) } } fn main() -> Result<()> { let phone_text = " +1 505 881 9292 (v) +1 505 778 2212 (c) +1 505 881 9297 (f) (202) 991 9534 Alex 5553920011 1 (800) 233-2010 1.299.339.1020"; let re = Regex::new( r#"(?x) (?:\+?1)? # опциональный код страны [\s\.]? (([2-9]\d{2})|\(([2-9]\d{2})\)) # код региона [\s\.\-]? ([2-9]\d{2}) # код обмена [\s\.\-]? (\d{4}) # код подписчика"#, )?; let phone_numbers = re.captures_iter(phone_text).filter_map(|cap| { let groups = (cap.get(2).or(cap.get(3)), cap.get(4), cap.get(5)); match groups { (Some(area), Some(ext), Some(sub)) => Some(PhoneNumber { area: area.as_str(), exchange: ext.as_str(), subscriber: sub.as_str(), }), _ => None, } }); assert_eq!( phone_numbers.map(|m| m.to_string()).collect::<Vec<_>>(), vec![ "1 (505) 881-9292", "1 (505) 778-2212", "1 (505) 881-9297", "1 (202) 991-9534", "1 (555) 392-0011", "1 (800) 233-2010", "1 (299) 339-1020", ] ); Ok(()) }
Замена всех подстрок в строке
Пример замены всех стандартных дат ISO 8601 YYYY-MM-DD эквивалентными датами в привычном нам формате. Например 2013-01-15 становится 15.01.2013.
Метод Regex::replace_all заменяет все вхождения всего регулярного выражения. &str реализует трейт Replacer, который позволяет переменным вроде $abcde ссылаться на соответствующие захваченные группы (?P<abcde>REGEX) из результатов поиска регулярки. См. синтаксис замены в строке для примеров и деталей экранирования.
use lazy_static::lazy_static; use std::borrow::Cow; use regex::Regex; // Функция форматирования даты fn reformat_dates(before: &str) -> Cow<str> { // Лениво оцениваемая статическая ссылка - регулярное выражение lazy_static! { static ref RE : Regex = Regex::new( r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})" ).unwrap(); } RE.replace_all(before, "$d.$m.$y") } fn main() { let before = "2012-03-14, 2013-01-15 и 2014-07-05"; let after = reformat_dates(before); assert_eq!(after, "14.03.2012, 15.01.2013 и 05.07.2014"); }
15.3. Разбор строки
Сбор графем Юникода
Собираем индивидуальные графемы Юникода из UTF-8 строки с помощью метода UnicodeSegmentation::graphemes из крейта unicode-segmentation:
use unicode_segmentation::UnicodeSegmentation; fn main() { let name = "Йогурт захватил мир\r\n"; let graphemes = UnicodeSegmentation::graphemes(name, true) .collect::<Vec<&str>>(); assert_eq!(graphemes[0], "Й"); }
Реализация трейта FromStr для кастомной структуры
Создаем кастомную структуру RGB и реализуем на ней трейт FromStr для преобразования цвета HEX в цвет RGB:
use std::str::FromStr; #[derive(Debug, PartialEq)] struct RGB { r: u8, g: u8, b: u8, } impl FromStr for RGB { type Err = std::num::ParseIntError; // Преобразует цвет HEX `#rRgGbB` в экземпляр `RGB` fn from_str(hex_code: &str) -> Result<Self, Self::Err> { // `u8::from_str_radix(src: &str, radix: u32)` преобразует строковый срез // в u8 в указанной системе счисления let r: u8 = u8::from_str_radix(&hex_code[1..3], 16)?; let g: u8 = u8::from_str_radix(&hex_code[3..5], 16)?; let b: u8 = u8::from_str_radix(&hex_code[5..7], 16)?; Ok(RGB { r, g, b }) } } fn main() { let code = "#fa7268"; match RGB::from_str(code) { Ok(rgb) => { println!( "The RGB color code is: R: {} G: {} B: {}", rgb.r, rgb.g, rgb.b ); } Err(_) => { println!("{} is not a valid color hex code!", code); } } assert_eq!( RGB::from_str(&r"#fa7268").unwrap(), RGB { r: 250, g: 114, b: 104 } ); }
16. Веб-разработка
16.1. Извлечение ссылок
Извлечение всех ссылок из HTML-страницы
Выполняем GET-запрос HTTP с помощью reqwest::get и разбираем ответ в документ HTML с помощью Document::from_read. find с a в Name извлекает все ссылки. Вызов filter_map на Selection извлекает URL из ссылок, имеющих attr (атрибут) href.
use error_chain::error_chain; use select::document::Document; use select::predicate::Name; error_chain! { foreign_links { ReqError(reqwest::Error); IoError(std::io::Error); } } #[tokio::main] async fn main() -> Result<()> { // Выполняем GET-запрос let res = reqwest::get("https://www.rust-lang.org/en-US/") .await? // преобразуем ответ в текст .text() .await?; // Разбираем текст ответа Document::from(res.as_str()) // находим ссылки .find(Name("a")) // отфильтровываем ссылки без атрибута `href` .filter_map(|n| n.attr("href")) .for_each(|x| println!("{}", x)); Ok(()) }
Поиск сломанных ссылок на веб-странице
Вызываем get_base_url для извлечения базового URL. Если документ имеет базовый тег, получаем из него значение attr (атрибута) href. По умолчанию используется Position::BeforePath оригинального URL.
Перебираем ссылки документа и создаем задачу с помощью tokio::spawn для разбора индивидуальной ссылки с помощью url::ParseOptions и Url::parse. Задача выполняет запрос с помощью reqwest и проверяет StatusCode ответа. Программа await (ожидает) выполнения задач перед завершением.
use error_chain::error_chain; use reqwest::StatusCode; use select::document::Document; use select::predicate::Name; use std::collections::HashSet; use url::{Position, Url}; error_chain! { foreign_links { ReqError(reqwest::Error); IoError(std::io::Error); UrlParseError(url::ParseError); JoinError(tokio::task::JoinError); } } // Функция получения базового URL async fn get_base_url(url: &Url, doc: &Document) -> Result<Url> { let base_tag_href = doc.find(Name("base")).filter_map(|n| n.attr("href")).nth(0); let base_url = base_tag_href.map_or_else(|| Url::parse(&url[..Position::BeforePath]), Url::parse)?; Ok(base_url) } // Функция проверки ссылки async fn check_link(url: &Url) -> Result<bool> { let res = reqwest::get(url.as_ref()).await?; // Проверяем только отсутствующие страницы (ошибка 404) Ok(res.status() != StatusCode::NOT_FOUND) } #[tokio::main] async fn main() -> Result<()> { let url = Url::parse("https://www.rust-lang.org/en-US/")?; let res = reqwest::get(url.as_ref()).await?.text().await?; let document = Document::from(res.as_str()); let base_url = get_base_url(&url, &document).await?; let base_parser = Url::options().base_url(Some(&base_url)); let links: HashSet<Url> = document .find(Name("a")) .filter_map(|n| n.attr("href")) .filter_map(|link| base_parser.parse(link).ok()) .collect(); let mut tasks = vec![]; for link in links { tasks.push(tokio::spawn(async move { if check_link(&link).await.unwrap() { println!("Ссылка `{}` в порядке", link); } else { println!("Ссылка `{}` сломана", link); } })); } for task in tasks { task.await? } Ok(()) }
Извлечение уникальных ссылок из разметки MediaWiki
Получаем страницу MediaWiki с помощью reqwest::get и ищем все внутренние и внешние ссылки с помощью Regex::captures_iter. Использование Cow позволяет избежать чрезмерного выделения (allocation) String.
Описание синтаксиса MediaWiki можно найти здесь.
use lazy_static::lazy_static; use regex::Regex; use std::borrow::Cow; use std::collections::HashSet; use std::error::Error; // Функция извлечения ссылок с помощью регулярного выражения fn extract_links(content: &str) -> HashSet<Cow<str>> { lazy_static! { static ref WIKI_REGEX: Regex = Regex::new( r"(?x) \[\[(?P<internal>[^\[\]|]*)[^\[\]]*\]\] # внутренние ссылки | (url=|URL\||\[)(?P<external>http.*?)[ \|}] # внешние ссылки " ) .unwrap(); } let links: HashSet<_> = WIKI_REGEX // ищем ссылки на странице .captures_iter(content) // ищем совпадение с образцом-кортежем .map(|c| match (c.name("internal"), c.name("external")) { (Some(val), None) => Cow::from(val.as_str().to_lowercase()), (None, Some(val)) => Cow::from(val.as_str()), _ => unreachable!(), }) .collect(); links } #[tokio::main] async fn main() -> Result<(), Box<dyn Error>> { let content = reqwest::get( "https://en.wikipedia.org/w/index.php?title=Rust_(programming_language)&action=raw", ) .await? .text() .await?; println!("{:#?}", extract_links(content.as_str())); Ok(()) }
16.2. URL
Разбор URL из строки в тип Url
Метод parse крейта url валидирует и разбирает &str в структуру Url. Строка может быть повреждена, поэтому parse возвращает Result<Url, ParseError>.
use url::{Url, ParseError}; fn main() -> Result<(), ParseError> { let s = "https://github.com/rust-lang/rust/issues?labels=E-easy&state=open"; let parsed = Url::parse(s)?; println!("Путь URL: {}", parsed.path()); // Путь URL: /rust-lang/rust/issues Ok(()) }
Создание базового URL путем удаления сегментов пути
Базовый URL включает протокол и домен. Такие URL не содержат директорий, файлов и строк запроса (query string). Метод PathSegmentsMut::clear удаляет пути, а метод Url::set_query удаляет строку запроса.
use error_chain::error_chain; use url::Url; error_chain! { foreign_links { UrlParse(url::ParseError); } errors { CannotBeABase } } fn main() -> Result<()> { let full = "https://github.com/rust-lang/cargo?asdf"; let url = Url::parse(full)?; let base = base_url(url)?; println!("Базовый URL: {}", base); // Базовый URL: https://github.com/ Ok(()) } fn base_url(mut url: Url) -> Result<Url> { match url.path_segments_mut() { Ok(mut path) => { path.clear(); } Err(_) => { return Err(Error::from_kind(ErrorKind::CannotBeABase)); } } url.set_query(None); Ok(url) }
Создание новых URL из базового
Метод join позволяет создавать новые URL из базового и относительного путей:
use url::{Url, ParseError}; fn main() -> Result<(), ParseError> { let path = "/rust-lang/cargo"; let gh = build_github_url(path)?; println!("Объединенный URL: {}", gh); // Объединенный URL: https://github.com/rust-lang/cargo Ok(()) } fn build_github_url(path: &str) -> Result<Url, ParseError> { const GITHUB: &'static str = "https://github.com"; let base = Url::parse(GITHUB)?; let joined = base.join(path)?; Ok(joined) }
Извлечение источника (схема / хост / порт)
Структура Url предоставляет разные методы для извлечения информации об URL, который она представляет:
use url::{Url, Host, ParseError}; fn main() -> Result<(), ParseError> { let s = "ftp://rust-lang.org/examples"; let url = Url::parse(s)?; assert_eq!(url.scheme(), "ftp"); assert_eq!(url.host(), Some(Host::Domain("rust-lang.org"))); assert_eq!(url.port_or_known_default(), Some(21)); Ok(()) }
Аналогичный результат можно получить с помощью метода origin:
use error_chain::error_chain; use url::{Url, Origin, Host}; error_chain! { foreign_links { UrlParse(url::ParseError); } } fn main() -> Result<()> { let s = "ftp://rust-lang.org/examples"; let url = Url::parse(s)?; let expected_scheme = "ftp".to_owned(); let expected_host = Host::Domain("rust-lang.org".to_owned()); let expected_port = 21; let expected = Origin::Tuple(expected_scheme, expected_host, expected_port); let origin = url.origin(); assert_eq!(origin, expected); Ok(()) }
Удаление идентификаторов фрагментов и пар запросов из URL
Разбираем строку в структуру Url и обрезаем URL с помощью url::Position для удаления лишних частей:
use url::{Url, Position, ParseError}; fn main() -> Result<(), ParseError> { let parsed = Url::parse("https://github.com/rust-lang/rust/issues?labels=E-easy&state=open")?; let cleaned: &str = &parsed[..Position::AfterPath]; println!("Очищенный URL: {}", cleaned); // Очищенный URL: https://github.com/rust-lang/rust/issues Ok(()) }
16.3. Типы медиа
Извлечение MIME-типа из строки
Следующий пример демонстрирует разбор строки в тип MIME с помощью крейта mime. Структура FromStrError генерирует дефолтный MIME-тип в методе unwrap_or.
use mime::{Mime, APPLICATION_OCTET_STREAM}; fn main() { let invalid_mime_type = "i n v a l i d"; // Дефолтный [MIME-тип] let default_mime = invalid_mime_type .parse::<Mime>() .unwrap_or(APPLICATION_OCTET_STREAM); println!( "MIME для {:?} - дефолтный {:?}", invalid_mime_type, default_mime ); // MIME для "i n v a l i d" - дефолтный "application/octet-stream" let valid_mime_type = "TEXT/PLAIN"; let parsed_mime = valid_mime_type .parse::<Mime>() .unwrap_or(APPLICATION_OCTET_STREAM); println!( "MIME для {:?} был разобран как {:?}", valid_mime_type, parsed_mime ); // MIME для "TEXT/PLAIN" был разобран как "text/plain" }
Извлечение MIME-типа из названия файла
Следующий пример демонстрирует извлечение корректного MIME-типа из названия файла с помощью крейта mime. Программа проверяет расширение файла и ищет совпадение с известным списком. Возвращаемым значением является mime::Mime.
use mime::Mime; fn find_mimetype (filename : &String) -> Mime { // Разбиваем название файла на части по точке let parts : Vec<&str> = filename.split('.').collect(); // Ищем совпадение с последней частью названия файла - его расширением let res = match parts.last() { Some(v) => match *v { "png" => mime::IMAGE_PNG, "jpg" => mime::IMAGE_JPEG, "json" => mime::APPLICATION_JSON, _ => mime::TEXT_PLAIN, }, None => mime::TEXT_PLAIN, }; return res; } fn main() { let filenames = vec!("foobar.jpg", "foo.bar", "foobar.png"); for file in filenames { let mime = find_mimetype(&file.to_owned()); println!("MIME для {}: {}", file, mime); } }
Извлечение MIME-типа из ответа HTTP
При получении ответа HTTP с помощью reqwest MIME-тип можно найти в заголовке Content-Type. Метод reqwest::header::HeaderMap::get извлекает заголовок как reqwest::header::HeaderValue, которое может быть преобразовано в строку. Крейт mime затем может разобрать эту строку в значение mime::Mime.
Крейт mime определяет некоторые распространенные MIME-типы.
Обратите внимание, что модуль reqwest::header экспортируется из крейта http.
use error_chain::error_chain; use mime::Mime; use std::str::FromStr; use reqwest::header::CONTENT_TYPE; error_chain! { foreign_links { Reqwest(reqwest::Error); Header(reqwest::header::ToStrError); Mime(mime::FromStrError); } } #[tokio::main] async fn main() -> Result<()> { let response = reqwest::get("https://www.rust-lang.org/logos/rust-logo-32x32.png").await?; // Извлекает заголовки из ответа let headers = response.headers(); match headers.get(CONTENT_TYPE) { None => { println!("Ответ не содердит заголовка `Content-Type`"); } Some(content_type) => { let content_type = Mime::from_str(content_type.to_str()?)?; let media_type = match (content_type.type_(), content_type.subtype()) { (mime::TEXT, mime::HTML) => "документ HTML", (mime::TEXT, _) => "текст", (mime::IMAGE, mime::PNG) => "изображение PNG", (mime::IMAGE, _) => "изображение", _ => "не текст и не изображение", }; println!("Ответ содержит {}", media_type); // Ответ содержит изображение PNG } }; Ok(()) }
16.3. Клиенты
Отправка запроса HTTP
Отправляем синхронный GET-запрос HTTP с помощью метода reqwest::blocking::get, получаем структуру reqwest::blocking::Response, читаем тело ответа в String с помощью метода read_to_string, печатаем статус, заголовки и тело ответа:
use error_chain::error_chain; use std::io::Read; error_chain! { foreign_links { Io(std::io::Error); HttpRequest(reqwest::Error); } } fn main() -> Result<()> { // Отправляем запрос/получаем ответ let mut res = reqwest::blocking::get("http://httpbin.org/get")?; let mut body = String::new(); // Читаем тело ответа в строку res.read_to_string(&mut body)?; println!("Статус: {}", res.status()); println!("Заголовки:\n{:#?}", res.headers()); println!("Тело ответа:\n{}", body); Ok(()) }
Async
Асинхронный вариант предыдущего примера с использованием крейта tokio:
use error_chain::error_chain; error_chain! { foreign_links { Io(std::io::Error); HttpRequest(reqwest::Error); } } #[tokio::main] async fn main() -> Result<()> { let res = reqwest::get("http://httpbin.org/get").await?; println!("Статус: {}", res.status()); println!("Заголовки:\n{:#?}", res.headers()); // Читаем тело запроса как текст let body = res.text().await?; println!("Тело ответа:\n{}", body); Ok(()) }
Обращение к GitHub API
Отправляем запрос к stargazers API v3 с помощью reqwest::get для получения списка пользователей, поставивших звезду проекту GitHub. Структура reqwest::Response десериализуется в структуру User, реализующую трейт serde::Deserialize.
tokio::main используется для установки асинхронного исполнителя (executor). Процесс ждет (await) завершения запроса перед обработкой ответа.
use reqwest::{header::USER_AGENT, Error}; use serde::Deserialize; #[derive(Deserialize, Debug)] struct User { login: String, id: u32, } #[tokio::main] async fn main() -> Result<(), Error> { // Формируем URL let request_url = format!( "https://api.github.com/repos/{owner}/{repo}/stargazers", owner = "harryheman", repo = "my-js" ); println!("{}", request_url); let client = reqwest::Client::new(); // Отправляем запрос let response = client .get(request_url) // Обязательный заголовок .header(USER_AGENT, "") .send() .await?; // Преобразуем тело ответа в формате JSON в объекты `User` let users: Vec<User> = response.json().await?; println!("{:?}", users); Ok(()) }
Проверка существования ресурса API
Отправляем HEAD-запрос HTTP (Client::head) в конечную точку пользователей GitHub и определяем успех по статусу ответа. Так можно быстро проверить существование ресурса без получения тела ответа. Настройка reqwest::Client с помощью метода ClientBuilder::timeout отменяет запрос, если он выполняется дольше 5 секунд.
Поскольку методы ClientBuilder::build и ReqwestBuilder::send возвращают типы reqwest::Error, в качестве типа значения, возвращаемого функцией main, используется reqwest::Result.
use reqwest::header::USER_AGENT; use reqwest::ClientBuilder; use reqwest::Result; use std::time::Duration; #[tokio::main] async fn main() -> Result<()> { // Имя пользователя let user = "harryheman"; // Конечная точка let request_url = format!("https://api.github.com/users/{}", user); println!("{}", request_url); // Таймаут, по истечении которого запрос отменяется let timeout = Duration::new(5, 0); // Создаем и настраиваем экземпляр клиента let client = ClientBuilder::new().timeout(timeout).build()?; // Отправляем HEAD-запрос let response = client .head(&request_url) // Обязательный заголовок .header(USER_AGENT, "") .send() .await?; // Определяем успех запроса по статусу ответа (200 ОК) if response.status().is_success() { println!("{} является пользователем", user); } else { println!("{} не является пользователем", user); } Ok(()) }
Создание и удаление Gist с помощью GitHub API
Создаем gist с помощью POST-запроса HTTP (Client::post) к gists API v3 и удаляем его с помощью DELETE-запроса (Client::delete).
Структура reqwest::Client отвечает за формирование запроса, включая URL, тело и аутентификацию. Тело запроса в формате JSON формируется с помощью макроса serde_json::json!. Оно устанавливается с помощью метода RequestBuilder::json. Заголовок авторизации устанавливается с помощью метода RequestBuilder::header. Метод RequestBuilder::send отправляет запрос.
Для авторизации в GitHub API необходимо создать токен доступа (не забудьте поставить галочку gist), и добавить его в файл .env в корне проекта (GH_TOKEN=ghp_...). Для доступа к переменным среды окружения из этого файла используется крейт dotenv.
use dotenv::dotenv; use error_chain::error_chain; use reqwest::{ header::{AUTHORIZATION, USER_AGENT}, Client, }; use serde::Deserialize; use serde_json::json; use std::env; error_chain! { foreign_links { EnvVar(env::VarError); HttpRequest(reqwest::Error); } } #[derive(Deserialize, Debug)] struct Gist { id: String, html_url: String, } #[tokio::main] async fn main() -> Result<()> { // Получаем переменные среды окружения из файла `.env`, // находящего в корневой директории dotenv().ok(); // Тело запроса let gist_body = json!({ "description": "описание gist", "public": true, "files": { "main.rs": { "content": r#"fn main() { println!("всем привет!");}"# } }}); // Конечная точка let request_url = "https://api.github.com/gists"; // Отправляем POST-запрос на создание gist let response = Client::new() .post(request_url) // Обязательный заголовок .header(USER_AGENT, "") // Заголовок авторизации .header(AUTHORIZATION, format!("Bearer {}", env::var("GH_TOKEN")?)) // Добавляем тело .json(&gist_body) .send() .await?; if response.status().is_success() { let gist: Gist = response.json().await?; println!("Создан {:?}", gist); // Конечная точка let request_url = format!("{}/{}", request_url, gist.id); // Отправляем DELETE-запрос на удаление gist let response = Client::new() .delete(&request_url) // Обязательный заголовок .header(USER_AGENT, "") // Заголовок авторизации .header(AUTHORIZATION, format!("Bearer {}", env::var("GH_TOKEN")?)) .send() .await?; if response.status().is_success() { println!( "Gist {} удален. Статус-код: {}", gist.id, response.status() ); } else { println!("Запрос провалился. Статус-код: {}", response.status()); } } else { println!("Запрос провалился. Статус-код: {}", response.status()); } Ok(()) }
Скачивание файла во временную директорию
Создаем временную директорию с помощью структуры tempfile::Builder и асинхронно скачиваем в нее файл через HTTP с помощью метода reqwest::get.
Создаем целевой File с названием, извлеченным из Response::url, внутри метода tempdir, и копируем в нее скачанные данные с помощью метода io::copy. Временная директория автоматически удаляется после завершения программы.
use error_chain::error_chain; use std::fs::File; use std::io::copy; use tempfile::Builder; error_chain! { foreign_links { Io(std::io::Error); HttpRequest(reqwest::Error); } } #[tokio::main] async fn main() -> Result<()> { // Временная директория let tmp_dir = Builder::new().prefix("example").tempdir()?; // Целевой файл let target = "https://www.rust-lang.org/logos/rust-logo-512x512.png"; // Отправляем запрос let response = reqwest::get(target).await?; let mut dest = { let fname = response .url() .path_segments() .and_then(|segments| segments.last()) .and_then(|name| if name.is_empty() { None } else { Some(name) }) .unwrap_or("tmp.bin"); // Названием файла является последняя часть пути или `tmp.bin` println!("файл для скачивания: '{}'", fname); let fname = tmp_dir.path().join(fname); // Путь к временной директории + название файла println!("будет находиться в: '{:?}'", fname); // Создаем и возвращаем дескриптор файла File::create(fname)? }; // Читаем тело ответа как текст let content = response.text().await?; // Копируем содержимое в файл copy(&mut content.as_bytes(), &mut dest)?; // Файл и временная директория будут существовать на протяжении 5 секунд, // после чего автоматически удалятся std::thread::sleep(std::time::Duration::from_secs(5)); Ok(()) }
Отправка файла в paste-rs
reqwest::Client устанавливает соединение с https://paste.rs с помощью паттерна reqwest::RequestBuilder. Client::post определяет назначение POST-запроса HTTP, RequestBuilder::body устанавливает тело запроса, а RequestBuilder::send отправляет запрос, блокирует поток до загрузки файла и получения ответа.
use error_chain::error_chain; use std::fs::File; use std::io::Read; error_chain! { foreign_links { HttpRequest(reqwest::Error); IoError(::std::io::Error); } } #[tokio::main] async fn main() -> Result<()> { // Конечная точка let paste_api = "https://paste.rs"; // Дескриптор файла let mut file = File::open("message.txt")?; let mut contents = String::new(); // Читаем содержимое файла в строку file.read_to_string(&mut contents)?; // Создаем клиента let client = reqwest::Client::new(); // Отправляем запрос let res = client.post(paste_api).body(contents).send().await?; // Читаем ответ как текст let response_text = res.text().await?; println!("{}", response_text); Ok(()) }
Обратите внимание: для корректной работы программы нужно создать непустой файл message.txt в корне проекта.
Частичная загрузка файла по HTTP с помощью заголовка диапазона
Используем reqwest::blocking::Client::head для получения Content-Length (размера содержимого) ответа.
Используем reqwest::blocking::Client::get для загрузки содержимого по частям размером 10240 байт с отслеживанием прогресса. Часть и позиция определяются с помощью заголовка Range, который определяется в RFC7233.
use error_chain::error_chain; use reqwest::header::{HeaderValue, CONTENT_LENGTH, RANGE}; use reqwest::StatusCode; use std::fs::File; use std::str::FromStr; error_chain! { foreign_links { Io(std::io::Error); Reqwest(reqwest::Error); Header(reqwest::header::ToStrError); } } struct PartialRangeIter { start: u64, end: u64, buffer_size: u32, } impl PartialRangeIter { pub fn new(start: u64, end: u64, buffer_size: u32) -> Result<Self> { if buffer_size == 0 { Err("невалидный `buffer_size`, размер буфера должен превышать 0")?; } Ok(PartialRangeIter { start, end, buffer_size, }) } } impl Iterator for PartialRangeIter { type Item = HeaderValue; fn next(&mut self) -> Option<Self::Item> { if self.start > self.end { None } else { let prev_start = self.start; self.start += std::cmp::min(self.buffer_size as u64, self.end - self.start + 1); Some( HeaderValue::from_str(&format!("bytes={}-{}", prev_start, self.start - 1)).unwrap(), ) } } } fn main() -> Result<()> { // Конечная точка let url = "https://httpbin.org/range/102400?duration=2"; // Размер части const CHUNK_SIZE: u32 = 10240; let client = reqwest::blocking::Client::new(); // Отправляем HEAD-запрос let response = client.head(url).send()?; // Получаем заголовок `Content-Length` let length = response .headers() .get(CONTENT_LENGTH) .ok_or("Ответ не содержит размера содержимого")?; // Получаем размер содержимого let length = u64::from_str(length.to_str()?).map_err(|_| "Невалидный заголовок `Content-Length`")?; // Дескриптор файла let mut output_file = File::create("download.bin")?; println!("Начинаем загрузку..."); for range in PartialRangeIter::new(0, length - 1, CHUNK_SIZE)? { println!("Диапазон {:?}", range); // Отправляем GET-запрос с заголовком `Range` let mut response = client.get(url).header(RANGE, range).send()?; let status = response.status(); if !(status == StatusCode::OK || status == StatusCode::PARTIAL_CONTENT) { error_chain::bail!("Неожиданный ответ сервера: {}", status) } // Копируем содержимое ответа в файл std::io::copy(&mut response, &mut output_file)?; } // Читаем ответ как текст let content = response.text()?; // Копируем байты ответа в файл std::io::copy(&mut content.as_bytes(), &mut output_file)?; println!("Загрузка успешно завершена"); Ok(()) }
Обратите внимание: в результате выполнения программы в корне проекта должен появиться файл download.bin.
Базовая аутентификация
Для выполнения базовой аутентификации HTTP используется метод reqwest::RequestBuilder::basic_auth:
use reqwest::blocking::Client; use reqwest::Error; fn main() -> Result<(), Error> { let client = Client::new(); let user_name = "testuser".to_string(); let password: Option<String> = None; // Отправляем GET-запрос с базовой аутентификацией let response = client .get("https://httpbin.org/") .basic_auth(user_name, password) .send()?; println!("{:?}", response); Ok(()) }
Это конец второй части и Книги в целом.
Happy coding!
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

