![](https://habrastorage.org/webt/fj/yi/qn/fjyiqnduv3l-rdgxnbu_t_qpgxk.jpeg)
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-канале ↩