Несколько недель назад мне нужно было что-то сделать в Ruby, включая обработку большого количества CSV-файлов. Я был счастлив узнать, что есть хорошая, всеобъемлющая библиотека CSV, которая облегчит мою работу.
Однако одна вещь поразила меня, как только я начал искать учебники: ни один из них не освещал эту библиотеку подробно. Большинство статей, которые я видел, едва касались того, что может делать библиотека CSV. Я был полон решимости получить полное представление об этой части Ruby, поэтому я начал читать все возможные уроки и главы книг по CSV. Эта серия из двух частей — результат моих усилий.
Я делаю некоторые основные предположения в этой статье:
- Вы знаете, как выглядит CSV (файл, разделенный запятыми).
- У вас есть базовые знания Ruby.
- У вас есть базовые знания по работе с файлами в Ruby. Это поможет в последнем разделе.
Как избежать распространенных ошибок в файлах CSV
Представьте, что у вас есть дядя (назовем его Боб), у которого есть ресторан.
Сотрудники дяди Боба хранят электронные таблицы всех клиентов. Каждая строка содержит (в отдельной ячейке):
- Имя заказчика.
- Общее количество раз, когда они приходили и ели в ресторане.
- Всего потрачено денег.
- Короткую фразу они использовали, когда их попросили описать еду ресторана.
Для начала дядя Боб дал вам небольшой CSV-файл, который содержал 4 строки с 4 самыми частыми клиентами. Вот визуальное представление файла:
Давайте пройдем небольшой тест. Как мы собираемся представлять эти данные в виде простого текста? Заполнить бланки:
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 из 4 строк. Мы делаем это с CSV.read
customers
. Этот метод собирается прочитать весь файл и сохранить его в переменной 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.parse
CSV.parse(a_string) { |row| puts row.inspect }
#=> produces ["Dan", "34"] and ["Maria", "55"] on separate lines
CSV.parse
Вы также можете предоставить блок CSV.read
CSV.parse
CSV.read
CSV.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.parse
CSV.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]
endFile
Поскольку мы сказали, что все параметры являются частью хэша, мы можем указать более одного одновременно, например:
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})
-
Загрузите строки в память программы с помощью таких методов, как
.each
CSV.foreach
Если вы знакомы с классомCSV.read
CSV.foreach
customers_array
Это просто более длинный способ сделатьaverage-money-spent
-
Вы можете делать с каждой строкой все, что хотите, используя различные выражения Ruby, получая доступ к содержимому строки так же, как к массиву. С
customers_array = CSV.read('test.txt')
customers_array.each do |customer|
customer << average_money_spent.shift
endor
-
После того как вы сделали свою работу, вы можете сохранить свои данные обратно в новый файл (например, «Сохранить как» в программе для работы с электронными таблицами). Это та часть, на которой мы собираемся сосредоточиться в этом разделе.
Возвращаясь к нашему примеру, скажем, что мы получили содержимое всего нашего файла customer.csv в памяти в виде массива (назовем его puts
File
File.open
Здорово! Теперь каждая строка (т.е. массив) содержит новую ячейку (т.е. элемент) в конце. Давайте обновим наш CSV-файл. Но подождите … нет волшебной кнопки «CTRL + S», которую вы можете нажать или выполнить команду, чтобы сделать это. Вы не можете просто волшебным образом изменить файл CSV с обновленными значениями. Работа с библиотекой CSV в этом отношении почти на 100% аналогична работе с классом File, и применяется та же логика:
Вы открываете файл (CSV) для режима чтения, записи или добавления и используете либо, CSV.open
File.open
Если вы не знаете, как работает класс Ruby CSV.open
отличных видео для этого).
Единственное отличие между customer_array
CSV.open('our-new-customers-file.csv', 'w') do |csv_object|
с
customers.array.each do |row_array|
csv_object << row_array
end
endour-new-customers-file.csv
С end
. Давайте сделаем пример с нашим обновленным do … end
{}
Это оно! Теперь у нас есть новый обновленный файл с именем bundle install
Дядя Боб очень доволен нами.
Во второй части мы углубимся в работу с CSV-файлами с фактическими заголовками (обратите внимание, что в нашем примере CSV-файлов не было никаких заголовков) и исследуем проблемы с памятью при работе с большими файлами и ограниченным объемом оперативной памяти. Мы также узнаем о некоторых изящных вещах, таких как использование перечислителей с методами итераторов CSV, как добавление заголовков дает вам множество новых методов, а также некоторые другие полезные приемы. Будьте на связи!