Файловият формат CSV е много често срещан метод за съхраняване на таблични данни. Тази статия обхваща основите на четенето и писането на CSV файлове, (де)сериализиране на CSV данни с помощта на библиотеката Serde и завършва с примерен пример за агрегиране на данни.
Въведение
Идеята за тази статия се появи, защото имах нужда от бърз и удобен начин за работа с CSV форматирани данни. Единият вариант беше да напиша собствена библиотека, но не смятах, че това е удобно решение. И така, след малко проучване, реших да използвам кутията csv, създадена от Andrew Gallant. Избрах тази щайга, защото API е доста ясен и, най-важното за мен, документацията е много изчерпателна.
Настройвам
За да използвате този сандък, просто създайте нов проект на Rust, като използвате cargo new
и добавете това под [dependencies]
във файла Cargo.toml
:
csv = "1.1"
Освен това част от тази статия ще използва функционалността за персонализирано извличане на Serde, така че продължете и добавете и това:
serde = { version = "1", features = ["derive"] }
Източници на данни
Всички данни в тази статия са генерирани от Mockaroo, която е безплатна услуга за фиктивни данни, която е много гъвкава и лесна за използване и споделяне. Схемите, които ще използвам, могат да бъдат намерени на следните връзки:
Една бърза бележка относно схемата
Order
. Той има връзка с външен ключ към схематаCustomer
. За да настроите това на сайта Mockaroo, първо създайте схематаCustomer
, изтеглете я като csv файл, качете файла csv като набор от данни, след което можете да създадете схематаOrder
.
Четене на CSV данни
Касата csv
осигурява Reader
структура, която се използва за трансформиране на необработени CSV данни в стандартни типове Rust. Reader
има функционалността да чете от входен поток или файл и да десериализира данни в персонализирана структура с помощта на Serde.
Четене от stdin
Четенето от stdin
може да не е обичаен случай на употреба, освен ако не предавате CSV данни в приложение за команден ред. За четене от stdin
кутията предоставя функция, наречена from_reader
. Този метод работи за всеки тип, който прилага характеристиката, което означава, че можете да четете от масив от байтове в Reader
. Следният пример показва как да четете CSV данни от stdin
:
След като този код бъде компилиран, той може да бъде извикан чрез предаване на изтегления CSV файл. Например, компилирах горното с помощта на cargo build --release
и успях да го стартирам, като напиша това в терминала:
./target/release/read_csv_stdin < data/Customers.csv
Резултатът ще изглежда нещо подобно:
StringRecord(["26fa5a49-5194-40eb-8faf-219c9fff831e", "Arline", "Jardine", "[email protected]", "61635 Dawn Hill"]) StringRecord(["708fcc04-e387-47e3-b85d-99ccd4e18320", "Baillie", "Pierri", "[email protected]", "70540 Gerald Way"]) StringRecord(["9a8ffa76-0280-4754-b75c-4de7508cdd67", "Jenda", "Print", "[email protected]", "928 Bluestem Parkway"]) StringRecord(["c5b4b822-2b59-435f-a697-6f1d4bbb0684", "Lucia", "Rhucroft", "[email protected]", "212 Schiller Pass"]) StringRecord(["f266b61e-534f-483b-9f70-0029b7424aa8", "Sybila", "Valdes", "[email protected]", "2 Gateway Avenue"])
Едно нещо, което може би сте забелязали е, че .records()
не включва заглавния ред. Този метод пропуска първия ред, тъй като той се интерпретира като заглавен запис по подразбиране. Структурата Reader
има отделен метод, наречен .headers()
за извличане на заглавния запис.
Четене от файл
Четенето от файл е може би най-честият случай на използване на структурата Reader
. Щайгата предоставя метод, наречен from_path
, който създава Reader
от пътя на CSV файла с данни. Кодът за четене от файл изглежда много подобен на кода за четене от stdin
. Следният пример показва как да четете CSV от файл:
Четене със Serde
Удобна функция за тази кутия е поддръжката за Serde, която предоставя функционалност за четене на CSV данни и десериализиране в персонализирани типове данни Rust. Например необработените Customer
данни, които изглеждат така -
customer_guid,first_name,last_name,email,address 26fa5a49-5194-40eb-8faf-219c9fff831e,Arline,Jardine,[email protected],61635 Dawn Hill
- може да се десериализира в структура на Rust, която изглежда така:
struct Customer {
customer_guid: String,
first_name: String,
last_name: String,
email: String,
address: String
}
Вместо да използвате метода .records
, както в предишния пример. Структурата Reader
предоставя друг метод, наречен .deserialize
за създаване на итератор върху всеки десериализиран запис. Следващият пример показва как да четете CSV данни и да ги десериализирате в тип Rust:
Едно важно нещо, което трябва да се отбележи, е, че десериализирането на данни идва с някои присъщи допълнителни разходи. Ако сравните първите два примера с този, ще откриете, че десериализиране на данни ще бъде по-бавно.
Обработка на невалидни данни със Serde
Всички предишни примери са предполагали доста чисти данни. Файловият формат CSV обаче не налага такива предположения. Така че, ако се опитаме да десериализираме данни, които не могат да бъдат преобразувани в декларирания тип, тогава ще има грешка.
Например за схемата на поръчките структурата на Rust може да изглежда по следния начин:
Тогава нека приемем, че има запис, който изглежда така:
order_guid,customer_guid,order_date,total 02a771c1-9885-40d6-9acc-75cd108e9218,022d6000-b4da-412b-ba7c-cd0dbf868af9,4/16/2020,Null
Когато Serde се опита да десериализира стойността Null
, ще възникне грешка, която изглежда така:
CSV deserialize error: record 1 (line: 2, byte: 42): field 3: invalid float literal
За да коригира този проблем, кутията предоставя помощна функция, която може да се приложи към Option
полета, която казва на Serde да преобразува стойността в None
, ако възникне грешка. Добавянето на тази функционалност изглежда така:
#[derive(Debug, Deserialize)]
struct Order {
order_guid: String,
customer_guid: String,
order_date: String,
#[serde(deserialize_with = "csv::invalid_option")]
total: Option<f64>
}
Освен помощния метод invalid_option
, останалата част от кода е абсолютно същата като в предишния пример.
Писане на CSV данни
Писането на CSV данни е малко по-лесно от четенето им, тъй като имате повече контрол върху изхода. Щайгата предоставя структура Writer
, която има много подобен интерфейс на структурата Reader
. Структурата Writer
предоставя функционалност за запис в stdout
, запис във файл и сериализиране на типове Rust в CSV данни.
Пиша до stdout
Един прост случай на използване може да бъде запис на CSV данни в stdout
, така че да могат да бъдат прехвърлени в друго приложение. За да пишете в stdout
, кутията предоставя from_writer
метода за създаване на Writer
структура, конфигурирана да записва в stdout
. След като структурата Writer
бъде създадена, записите се добавят чрез извикване на метода write_record
на екземпляра Writer
. Следното показва как да запишете CSV данни в stdout
:
Тъй като това просто пише до stdout
, програмата може да се извика така:
./target/release/csv_write_stdout
Или можете да прехвърлите във файл като този:
/target/release/csv_write_stdout > test.csv
Писане във файл
Друг случай на употреба за писане на CSV данни е записът във файл. Касата csv
предоставя метода from_path
за създаване на Writer
структура за запис в дадения файл. Освен метода from_path
, този пример е същият като писането в stdout
.
Писане със Serde
Подобно на структурата Reader
, структурите Writer
поддържат сериализиране на данни от типове Rust в CSV записи с помощта на Serde. Този пример използва повторно схемата Customer
от преди и преобразува екземпляри на Customer
в CSV данни.
Ключовата разлика в този пример в сравнение с другите примери за запис е използването на метода serialize
вместо метода write_record
за запис на данни.
CSV агрегиране
Когато работите с CSV данни, обичайният случай на употреба е да прочетете CSV файл, да обработите данните и след това да запишете резултатите обратно във файл. Този пример чете два CSV файла, извършва вътрешно свързване и след това записва резултата във файл. Тъй като този пример е по-дълъг от останалите, искам само да подчертая няколко раздела, но изходният код може да бъде намерен тук.
Първото нещо, което реших да направя, беше да дефинирам структура за съхранение на заглавките и записите, за да направя агрегирането малко по-лесно. Тази структура изглежда така:
#[derive(Debug)]
struct DataSet {
/// Header row of CSV file
headers: StringRecord,
/// Records from CSV file
records: Vec<StringRecord>,
}
Типът StringRecord
е структурата от кутията csv
, която дефинира запис. Използването на този тип в DataSet
позволява лесна интеграция с вътрешните елементи на Reader
и Writer
при четене и писане на CSV данни.
За да обединя тези два набора от данни, ще използвам алгоритъм за свързване на сортиране и сливане. По принцип той сортира и двата набора от данни в дадена колона и след това сканира и двата набора от данни, търсейки съвпадения. Когато се намери съвпадение, той обединява записите от двата набора от данни в един запис и го добавя към нов набор от данни.
Тъй като имам персонализирана структура, която съдържа записите, мога да добавя блок impl
с функция за сортиране на записите в набор от данни.
/// Sort data records by the given index.
///
/// # Errors
///
/// An error occurs if the index is out of bounds
fn sort_by_index(&mut self, index: usize) -> Result<(), Box<dyn Error>> {
if index >= self.headers.len() {
Err(Box::new(IndexError(format!(
"Index '{}' out of bounds",
index
))))
} else {
self.records.sort_by(|a, b| a[index].cmp(&b[index]));
Ok(())
}
}
След като и двата набора от данни бъдат сортирани, мога да обединя наборите от данни заедно. Основната логика на метода за вътрешно присъединяване изглежда така:
let mut left_cursor = 0;
let mut right_cursor = 0;
while left_cursor < self.records.len() && right_cursor < right.records.len() {
// If two fields match, merge fields into a single record
// and add to records vector
// If they don't match and the left value is less then right value advance the left cursor
// else advance the right cursor
if self.records[left_cursor][left_index] == right.records[right_cursor][right_index] {
let record = StringRecord::from(
self.records[left_cursor]
.iter()
.chain(right.records[right_cursor].iter())
.collect::<Vec<&str>>(),
);
records.push(record);
// Since data sets are sorted
// Advance cursor through right data set to
// see if there are matches
let mut k = right_cursor + 1;
while k < right.records.len()
&& self.records[left_cursor][left_index] == right.records[k][right_index]
{
let record = StringRecord::from(
self.records[left_cursor]
.iter()
.chain(right.records[k].iter())
.collect::<Vec<&str>>(),
);
records.push(record);
k += 1;
}
left_cursor += 1;
} else if self.records[left_cursor][left_index]
< right.records[right_cursor][right_index]
{
left_cursor += 1;
} else {
right_cursor += 1;
}
}
Алгоритъмът за сортиране и сливане е един от многото за ефективно свързване на набори от данни. Ако искате да прочетете повече за тези типове алгоритми, открих, че тази статия е добра отправна точка.
Преглед
Като цяло мисля, че щайгата csv
е много лесна за работа и е първият ми избор, когато искам бърза библиотека за анализ на CSV. Щайгата се възползва от добре документиран изходен код и подробен учебен документ, който навлиза в повече подробности. Общността на Rust изглежда се чувства по същия начин, тъй като този сандък е изтеглен над 3 милиона пъти.
Благодаря
Благодаря за четенето! Ако искате да се свържете или искате да предоставите обратна връзка, не се колебайте да се свържете с мен в LinkedIn.
Първоначално публикувано на адрес https://andrewleverette.github.io на 30 юни 2020 г.