Статьи

Руководство по библиотеке Ruby CSV, часть I

Несколько недель назад мне нужно было что-то сделать в Ruby, включая обработку большого количества CSV-файлов. Я был счастлив узнать, что есть хорошая, всеобъемлющая библиотека CSV, которая облегчит мою работу.

Однако одна вещь поразила меня, как только я начал искать учебники: ни один из них не освещал эту библиотеку подробно. Большинство статей, которые я видел, едва касались того, что может делать библиотека CSV. Я был полон решимости получить полное представление об этой части Ruby, поэтому я начал читать все возможные уроки и главы книг по CSV. Эта серия из двух частей — результат моих усилий.

Я делаю некоторые основные предположения в этой статье:

  • Вы знаете, как выглядит CSV (файл, разделенный запятыми).
  • У вас есть базовые знания Ruby.
  • У вас есть базовые знания по работе с файлами в Ruby. Это поможет в последнем разделе.

Как избежать распространенных ошибок в файлах CSV

Представьте, что у вас есть дядя (назовем его Боб), у которого есть ресторан.

Сотрудники дяди Боба хранят электронные таблицы всех клиентов. Каждая строка содержит (в отдельной ячейке):

  1. Имя заказчика.
  2. Общее количество раз, когда они приходили и ели в ресторане.
  3. Всего потрачено денег.
  4. Короткую фразу они использовали, когда их попросили описать еду ресторана.

Для начала дядя Боб дал вам небольшой CSV-файл, который содержал 4 строки с 4 самыми частыми клиентами. Вот визуальное представление файла:

CSV-файл

CSV дяди Боба

Давайте пройдем небольшой тест. Как мы собираемся представлять эти данные в виде простого текста? Заполнить бланки:

Dan,34,2548,Lovin it!
Maria,55,5054,_____________
Carlos,22,4352,_________________
Stephany,34,6542,I want bigger steaks!!!!!

Ответ: 2 незавершенных строки будут выглядеть так:

 Maria,55,5054,"Good, delicious food"
Carlos,22,4352,"I am ""pleased"", but could be better"

Если CSV разделен запятой и у вас есть запятая внутри ячейки, вам нужно заключить текст этой ячейки в двойные кавычки (как в 3-й строке). В противном случае вы рискуете запутать парсер.

Теперь, если все становится еще сложнее и, помимо запятой, у вас также есть двойные кавычки внутри ячейки, вы просто избегаете каждой двойной кавычки с другой двойной кавычкой (как в 4-й строке).

Как Ruby видит файлы CSV

С нашим улучшенным пониманием файлов CSV, давайте узнаем, как получить их в Ruby. Прежде чем мы начнем что-либо делать, нам нужно require 'csv'

Продолжая задачу нашего дяди Боба, теперь у нас есть файл (назовем его customers.csv Как мы импортируем его в Ruby? В Ruby вы можете импортировать ваш CSV-файл либо сразу (сохраняя все содержимое файла в памяти), либо считывать его построчно . У каждого подхода есть свои плюсы и минусы. Например, вы не хотите импортировать файл с 300 000 строк одновременно на компьютер с 512 МБ ОЗУ и вывести из строя вашу программу, как мы узнаем позже.

В любом случае, Ruby будет хранить каждую строку таблицы в виде массива , причем каждая ячейка является строковым элементом массива.

Руби CSV-файл

Как Ruby видит файл CSV

Чтобы проиллюстрировать это, давайте попробуем импортировать наш маленький файл CSV из 4 строк. Мы делаем это с CSV.readcustomers . Этот метод собирается прочитать весь файл и сохранить его в переменной require 'csv'
customers = CSV.read('customers.csv')

 customers

Наша переменная CSV.foreach Поскольку мы знаем, что Ruby представляет каждую строку таблицы в виде массива, наша переменная customer — это в основном массив, содержащий другие массивы. Не смущайтесь, если вы не сталкивались с массивом, хранящим другие массивы в Ruby. Попробуйте запустить код с вышеприведенными данными, если это все еще неясно для вас (это было для меня, пока я сам не попробовал бегать и возиться с вещами).

Вот иллюстрация, чтобы прояснить ситуацию:

массив массивов

Массив массивов

Давайте попробуем читать наш файл построчно. Мы можем сделать это с CSV.foreach('customers.csv') do |row|
puts row.inspect
end
метода ["Dan", "34", "2548", "Lovin it!"]
["Maria", "55", "5054", "Good, delicious food"]
["Carlos", "22", "4352", "I am \"pleased\", but could be better"]
["Stephany", "34", "6542", "I want bigger steaks!!!!!"]
, передав имя файла в качестве аргумента и предоставив ему блочную переменную, которая будет содержать уже обработанную строку в виде массива:

 String

Выход этого кода будет:

 CSV.parse

Все, что обрабатывается из файла CSV, представляет собой строку, даже цифры (хотя есть несколько советов по изменению поведения по умолчанию… подробнее об этом позже :)).

Библиотека CSV также может обрабатывать строки, а не только файлы

Если у вас есть данные, разделенные запятыми, в качестве объекта a_string = "Dan,34\nMaria,55"
CSV.parse(a_string) #=> [["Dan", "34"], ["Maria", "55"]]
CSV.parseCSV.parse(a_string) { |row| puts row.inspect }
#=> produces ["Dan", "34"] and ["Maria", "55"] on separate lines

 CSV.parse

Вы также можете предоставить блок CSV.read

 CSV.parse

CSV.readCSV.parse(File.read('customers.csv'))
# File.read returns a (big) string of the data in 'customers.csv', comma-separated
# and CSV.parse just re-structures that data into an array data structure.
CSV.read('customers.csv')CSV.parseCSV.foreach Делая это:

 CSV.read

будет выводить точно так же, как это:

 James;1;43;Not bad
Robin;1;56;Fish is tasty
Anna;1;79;"Good; better; the best!"

Использование :col_sep => ':'new_customers = CSV.read('newcomers.csv', { :col_sep => ';' })
CSV.foreach('newcomers.csv', { :col_sep => ';' }) { |row| p row }
Оба позволяют вам работать с одной строкой за раз (не так, как symbol_key: value

Что делать, когда CSV — это SSV (значения, разделенные точкой с запятой)

Ох, ох Дядя Боб передал в качестве другого файла, newcomers.csv . Этот содержит список самых новых клиентов за день, и, поскольку было утро, список не был таким большим:

 CSV.foreach('newcomers.csv', col_sep: ';') { |row| p row }
a_string = "Dan;34\nMaria;55"
CSV.parse(a_string, col_sep: ';') #=> [["Dan", "34"], ["Maria", "55"]]

Хьюстон, у нас проблема. Эти файлы НЕ разделены запятыми. Это не CSV, это SSV! (хорошо, я не совсем уверен, есть ли стандартизированный термин для файлов, разделенных точкой с запятой, но давайте пока воспользуемся SSV.) Что мы будем делать? Все ли методы, которые мы только что изучили, бесполезны для файлов SSV? Не бойся!

Среди всех 4 методов, которые мы изучили до сих пор, есть один общий шаблон: все они принимают только 1 аргумент, то есть имя файла (или, скорее, путь к имени файла), который мы хотим обработать в Ruby. В действительности все они также принимают второй (необязательный) аргумент, который представляет собой хэш (пара ключ-значение), содержащий различные параметры, которые указывают Ruby, как обрабатывать файл. Наиболее используемая опция col_sep Все описанные выше методы будут работать, если мы добавим этот хэш-аргумент:

 average_money_spent = Array.new
  CSV.foreach('customers.csv') do |row|
    average_money_spent << row[2] / row[1]
    # row is just an ordinary array and you access its elements with []
  end #=> Undefined method '/' for "2548":String

Чтобы прояснить ситуацию, мы можем использовать новый синтаксис хэша converters: :numeric

 CSV.foreach('customers.csv')

Есть много других опций, похожих на CSV.foreach('customers.csv', converters: :numeric) Мы рассмотрим наиболее распространенные из этой серии, но если вам интересно, вы можете увидеть их все здесь .

Давайте сделаем некоторые манипуляции

Дядя Боб хочет, чтобы мы брали CSV с наиболее частыми клиентами (customer.csv) и подсчитывали средние деньги, потраченные на каждое прибытие . Достаточно просто, правда? Мы просто делим общее количество денег, потраченных клиентом, на общее время, которое они пришли и поели в ресторане. У нас уже есть эти данные в столбце 3 и столбце 2:

 :converters

В чем дело? Помните, что хотя наш CSV-файл содержит числа, в Ruby они не рассматриваются как числа . По умолчанию все из файла CSV рассматривается как строка . К счастью, вы можете сказать библиотеке CSV отклониться от этого поведения по умолчанию с помощью еще одного аргумента параметра ключ-значение ( :numeric Давайте изменим нашу вторую строку с CSV.read('customers_separated_with_semicolons.csv', col_sep: ';', converters: :numeric)Dan,34,2548,Lovin it!
Maria,55,5054,"Good, delicious food"
Carlos,22,4352,"I am ""pleased"", but could be better"
Stephany,34,6542,I want bigger steaks!!!!!

В этом случае ключ является символом ( average_money_spent = Array.new
CSV.foreach('customers.csv', converters: :numeric) do |row|
average_money_spent << row[2] / row[1]
end
File
Поскольку мы сказали, что все параметры являются частью хэша, мы можем указать более одного одновременно, например:

 CSV.read

Теперь все числа будут преобразованы в их приблизительные форматы. Целые числа станут Fixnum, десятичные дроби станут Float, а числа с сотнями десятичных знаков Bignums. Сладкий!

Вывод наших результатов в файл

Давайте попробуем добавить новый столбец на наш лист. Помните, что исходный файл CSV-файла (Customers.csv):

 CSV.foreach

и у нас есть этот код:

 File

что дает нам средние деньги, потраченные на каждого из 4 клиентов в массиве. Мы хотим добавить 5-й столбец в наш CSV-файл, содержащий эти значения.

Если вы работали с файлами CSV в Excel, обычным рабочим процессом является внесение изменений и их сохранение. Ну, в Ruby все работает не так (если вы знакомы с классом CSV.open(file_name, 'mode-like-r(+)-w(+)-or-a(+)', { options like converters: :numeric in key-value pairs})

  1. Загрузите строки в память программы с помощью таких методов, как .eachCSV.foreach Если вы знакомы с классом CSV.readCSV.foreachcustomers_array Это просто более длинный способ сделать average-money-spent

  2. Вы можете делать с каждой строкой все, что хотите, используя различные выражения Ruby, получая доступ к содержимому строки так же, как к массиву. С customers_array = CSV.read('test.txt')
    customers_array.each do |customer|
    customer << average_money_spent.shift
    end
    or

  3. После того как вы сделали свою работу, вы можете сохранить свои данные обратно в новый файл (например, «Сохранить как» в программе для работы с электронными таблицами). Это та часть, на которой мы собираемся сосредоточиться в этом разделе.

Возвращаясь к нашему примеру, скажем, что мы получили содержимое всего нашего файла customer.csv в памяти в виде массива (назовем его putsFile

 File.open

Здорово! Теперь каждая строка (т.е. массив) содержит новую ячейку (т.е. элемент) в конце. Давайте обновим наш CSV-файл. Но подождите … нет волшебной кнопки «CTRL + S», которую вы можете нажать или выполнить команду, чтобы сделать это. Вы не можете просто волшебным образом изменить файл CSV с обновленными значениями. Работа с библиотекой CSV в этом отношении почти на 100% аналогична работе с классом File, и применяется та же логика:

Вы открываете файл (CSV) для режима чтения, записи или добавления и используете либо, CSV.openFile.open Если вы не знаете, как работает класс Ruby CSV.openотличных видео для этого).

Единственное отличие между customer_arrayCSV.open('our-new-customers-file.csv', 'w') do |csv_object|
customers.array.each do |row_array|
csv_object << row_array
end
end
с our-new-customers-file.csv
С end . Давайте сделаем пример с нашим обновленным do … end

 {}

Это оно! Теперь у нас есть новый обновленный файл с именем bundle install Дядя Боб очень доволен нами.

Во второй части мы углубимся в работу с CSV-файлами с фактическими заголовками (обратите внимание, что в нашем примере CSV-файлов не было никаких заголовков) и исследуем проблемы с памятью при работе с большими файлами и ограниченным объемом оперативной памяти. Мы также узнаем о некоторых изящных вещах, таких как использование перечислителей с методами итераторов CSV, как добавление заголовков дает вам множество новых методов, а также некоторые другие полезные приемы. Будьте на связи!