Добро пожаловать во вторую часть моей серии статей Инженерия данных с помощью Rust и Apache Arrow DataFusion. Доступ к первой части здесь.

Во второй статье я описываю инициализацию проекта Rust с его зависимостями и настройку первого интерфейса командной строки с использованием библиотеки Clap.

Сначала я инициализирую проект с помощью Cargo и описываю его зависимости: библиотеку Clap, крейт Apache Arrow DataFusion и некоторые утилиты для логирования и управления ошибками.

С помощью Clap, Синтаксического анализатора аргументов командной строки для Rust, вы можете легко создавать приложения командной строки с пользовательскими правилами синтаксического анализа и проверки, используя Rust.

В этой статье я использую Clap для создания простого, но эффективного приложения командной строки.

Шаг 1 — Инициализация проекта и зависимости

Начнем с создания нового проекта с cargo new.

$ cargo new mdata_app

Эта команда создает минимальный файл Cargo.toml и исходный файл main.rs.

$ tree mdata_app
 
mdata_app
├── Cargo.toml
└── src
    └── main.rs
 
1 directory, 2 files

Наш первоначальный файл Cargo.toml прост и содержит только имя пакета и начальный номер версии.

→ toml «mdata_app/Cargo.toml» =
 
[package]
name = "mdata_app"
version = "0.1.0"
edition = "2021"
 
(1)
<<custom-options>>
 
(2)
<<dependencies>>
 
(3)
<<dev-dependencies>>

Заполняем три блока кода <<custom-options>>, <<dependencies>> и <<dev-dependencies>>.

<<custom-options>> блок (1)

→ toml «custom-options» =
 
publish = false
 
[profile.release]
strip = true

Указываем два варианта:

  • publish = false — это упоминание о том, что этот пакет нельзя публиковать как крейт Rust. Полезно, чтобы убедиться, что этот код не опубликован по ошибке.
  • strip = true — это параметр компиляции для оптимизации размера нашего окончательного двоичного файла. Эта опция активна только в режиме компиляции release.

<<dependencies>> блок (2)

→ toml «dependencies» =
 
[dependencies]
clap = { version = "3",  features = ["derive"] }
thiserror = "1"
datafusion = "8"
tokio = "1"
log = "0.4"
env_logger = "0.9.0"
  • Библиотека Clap для разбора и обработки аргументов командной строки. Обратите внимание, что мы указываем версию ящика «3»; в этом случае cargo будет использовать последнюю версию основной ветви 3.x. Мы используем производную версию Clap API. Эта версия определяет параметры как структуру, интерпретируемую процедурными макросами для генерации нашего синтаксического анализатора аргументов.
  • Пакет «thiserror» помогает создавать собственные ошибки. Ошибки, полученные из этого крейта, реализуют трейт std::error::Error.
  • Ящики datafusion и tokio обрабатывают наши данные с помощью возможностей параллелизма.
  • log и env_logger помогают интегрировать простое ведение журнала в приложение.

<<dev-dependencies>> блок (3)

→ toml «dev-dependencies» =
 
[dev-dependencies]
fake = { version = "2.4", features=['derive']}
tempfile = "3.3.0"
rand = "0.8"
assert_cmd = "2"

Мы добавляем ящики, используемые для тестирования, но не интегрированные в окончательный артефакт.

  • Ящик поддельный генерирует настраиваемые случайные наборы данных.
  • Пакет tempfile для создания и удаления временных файлов и каталогов.
  • Пакет assert_cmd для тестирования двоичных файлов, запускаемых из командной строки.

Шаг 2 — Структура программы

Структура программы проста. Точкой входа пользователя в программу является оболочка ОС с интерфейсом командной строки. Пользователь может указать файл Parquet или CSV для загрузки, применить простое преобразование и записать его в указанном формате.

Ниже приведен наш основной программный файл, объединяющий структуру, утилиты и логику нашего приложения.

→ Rust «mdata_app/src/main.rs» =
 
use std::fs;
use std::num::ParseFloatError;
use std::path::{Path, PathBuf};
use std::str::ParseBoolError;
 
use clap::{ArgEnum, Parser, Subcommand};
use datafusion::arrow::datatypes::DataType;
use datafusion::error::DataFusionError;
use datafusion::prelude::*;
use env_logger::{Builder, Env};
use log::{debug, info, LevelFilter, trace};
use thiserror::Error;
 
(1)
<<cli-args>>
 
(2)
<<structures>>
 
(3)
<<utilities>>
 
(4)
<<data-processing>>
 
(5)
<<program-main>>
 
(6)
<<unit-testing>>

В нашем программном файле main.rs мы структурируем несколько блоков кода:

  • (1): определения аргументов командной строки
  • (2): Утилиты и обработка ошибок структур Rust
  • (3): служебные функции анализа файлов и каталогов.
  • (4): функции загрузки, обработки и записи данных.
  • (5): точка входа в программу
  • (6): код модульного тестирования

Основная программа анализирует аргументы командной строки с помощью Clap, настраивает уровень детализации журнала и вызывает основную функцию обработки данных.

→ Rust «program-main» =
 
#[tokio::main] (1)
async fn main() -> Result<(), MDataAppError> {
    let cli = MDataAppArgs::parse();
 
    (2)
    let log_level = match cli.verbose {
        1 => LevelFilter::Debug,
        2 => LevelFilter::Trace,
        _ => LevelFilter::Info,
    };
 
    (3)
    let env = Env::new().filter("MLOG");
    let _builder = Builder::new().filter(Some("mdata_app"), log_level).parse_env(env).init();
 
    debug!("Arguments {:#?}", cli);
 
    (4)
    mdata_app(cli).await?;
    Ok(())
}

Макрос #[tokio::main] (1) инициализирует среду выполнения tokio и управляет примитивами синхронизации.

В (2) и (3) мы инициализируем детализацию библиотеки журналов на основе крейтов env_logger и log. Например, в следующей строке отображаются записи CLI конечного пользователя только на уровне Подробный уровень отладки.

debug!("Arguments {:#?}", cli);

Наконец, (4) функция обработки данных mdata_app вызывается с аргументами CLI.

Шаг 3 — Разбор аргументов командной строки

Для разбора и обработки аргументов командной строки мы используем clap crate. Эта зависимость добавляется в файлCargo.toml.

Вы можете использовать ящик для хлопков в двух режимах:

  • С помощью Builder API вы программно создаете имена параметров и ограничения.
  • С помощью Derive API вы определяете структуру Rust, которая включает все ваши параметры и украшена макросами. Наконец, строится объект clap, определяющий правила синтаксического анализа и проверки. Это API, который мы используем в нашем примере.

Первым шагом с помощью Clap Derive API является создание структуры, определяющей все наши параметры.

Каждая опция описывается своим именем, ограничением типа, документацией и значением по умолчанию.

→ Rust «cli-args» =
 
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)] (1)
#[clap(propagate_version = true)]
pub struct MDataAppArgs {
    (2)
    #[clap(short, long, parse(from_os_str), help = "Input path")]
    input: PathBuf,
    #[clap(short, long, parse(from_os_str), help = "Output path")]
    output: PathBuf,
    #[clap(short, long, parse(from_occurrences),
    help = "Verbose level")]
    verbose: usize,
    #[clap(short, long, arg_enum, default_value_t = WriteFormat::Undefined,
    help = "Output format")]
    format: WriteFormat,
    #[clap(short, long, default_value_t = 0,
    help = "Limit the result to the first <limit> rows")]
    limit: usize,
    #[clap(short, long, parse(from_flag),
    help = "Display the inferred schema")]
    schema: bool,
 
    (3)
    <<subcommand-def>>
}
 
(4)
<<subcommand-struct>>
  • (1): с помощью этого первого макроса Clap использует определенную информацию в файле Cargo.toml для отображения информации автора, версии, о для конечный пользователь.
  • (2): мы добавляем запись структуры со спецификациями для каждого входного аргумента, используя макрос clap.
  • (3) + (4): мы определяем подкоманду хлопка для обработки наших операций по обработке данных. Я опишу этот шаг в следующем разделе.

Использование командной строки

В этой первой версии наши возможности минимальны.

Мы предлагаем только загрузку файла CSV или Parquet с --input, экспорт его в указанный выходной файл --output с определенным форматом --format.

Пользователи могут указать максимальное количество строк, возвращаемых нашей программой, с помощью --limit.

Наконец, в целях отладки пользователь может отобразить предполагаемую схему данных с помощью --schema.

Теперь мы можем вызвать команду --help, чтобы увидеть наши определенные аргументы.

$ cargo run -- --help
 
mdata_app 0.1.0
 
USAGE:
    mdata_app [OPTIONS] --input <INPUT> --output <OUTPUT> [SUBCOMMAND]
 
OPTIONS:
    -f, --format <FORMAT>    Output format [default: undefined] [possible values: undefined, csv,
                             parquet]
    -h, --help               Print help information
    -i, --input <INPUT>      Input path
    -l, --limit <LIMIT>      Limit the result to the first <limit> rows [default: 0]
    -o, --output <OUTPUT>    Output path
    -s, --schema             Display the schema
    -v, --verbose            Verbose level
    -V, --version            Print version information
 
SUBCOMMANDS:
    eq      Add equality test in the where clause
    help    Print this message or the help of the given subcommand(s)

Определение подкоманды

Наше основное приложение может загружать файл данных, применять некоторые преобразования и записывать результаты. Опции ввода и вывода управляются в определении первичной структуры CLI.

Для управления операцией преобразования данных мы используем функцию Clap: подкоманды. Он обеспечивает четкое разделение параметров между операциями с данными и атрибутами CLI.

Мы добавляем подкоманду фильтра, которая принимает необязательное определение Filters. Упоминание Option<> делает этот аргумент необязательным в нашем окончательном CLI.

→ Rust «subcommand-def» =
 
#[clap(subcommand)]
filter: Option<Filters>,

Мы определяем структуру Filter с ее компонентами. В нашем случае наша первая реализация — это фильтр равенства Eq. Мы применяем этот фильтр к определенному столбцу и определенному значению.

→ Rust «subcommand-struct» =
 
#[derive(Subcommand, Debug)]
enum Filters {
    /// Add equality test filter
    Eq { column: String, value: String },
}

С помощью подкоманды команда Clap позволяет создать специальное справочное сообщение для этой операции. Обратите внимание, что справочное сообщение определено в комментарии файла enum Filters.

$ cargo run -- eq --help
 
mdata_app-eq 0.1.0
Add equality test filter
 
USAGE:
    mdata_app --input <INPUT> --output <OUTPUT> eq <COLUMN> <VALUE>
 
ARGS:
    <COLUMN>
    <VALUE>
 
OPTIONS:
    -h, --help       Print help information
    -V, --version    Print version information

Заворачивать

Вот и все!

Теперь наша программа может анализировать, проверять и обрабатывать команды конечного пользователя, используя библиотеку Clap. Кроме того, операции с данными можно компоновать с помощью подкоманды Clap.

В следующей статье я описываю пример функции обработки данных с использованием Apache Arrow DataFusion.



Спасибо за прочтение!