Hello world!


Книга рецептов — это коллекция простых примеров, демонстрирующих хорошие практики решения распространенных задач программирования с помощью крейтов экосистемы Rust.



Содержание



Обратите внимание:: для запуска примеров Книги вам потребуется такой файл 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-канале

Комментарии (0)