Статьи

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

65vHPcY

Хорошая работа, сынок! – Дядя Боб (если вы не уверены, кто такой дядя Боб, возможно, вы захотите прочитать первую часть этого урока)

Дядя Боб доволен работой, которую мы проделали. Но осталась одна вещь … Помните этот файл?

Новые сотрудники растеряны, что означает каждый столбец? Что такое 34 или 2548? Что нам нужно сделать, это добавить заголовки в наш файл CSV. Давайте обновим наш файл (ниже приведен простой текстовый формат рисунка выше с включенными заголовками):

Name,Times arrived,Total $ spent,Food feedback 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!!!!! 

Теперь давайте пойдем и импортируем наш файл в Ruby (предположим, что он называется ‘guest.csv’):

 require 'csv' # since CSV is part of the standard library, we do need to 'require' it, remember? guests = CSV.read('guests.csv') # read the entire file into a 'guests' variable 

Библиотека CSV не обладает магическими способностями, поэтому она не знает, что первая строка здесь является заголовком. Поэтому, если мы запустим приведенный выше код, переменная «guest» будет представлять собой двойной массив из 5 элементов массива, первый из которых будет «header» и обрабатывается так же, как и остальные строки:

 ["Name", "Times arrived", "Total $ spent", "Food feedback"] 

Давайте скажем библиотеке CSV НЕ обрабатывать первый ряд, как все остальные. как нам это сделать? Помните, что каждому методу CSV, который делает что-то «похожее на csv» в Ruby, может быть предоставлен список опций (мы использовали конвертеры: числовые в первой части), которые в основном инструктируют библиотеку обрабатывать файл (или строку) по-разному. чем то, как он это обрабатывает по умолчанию. Среди множества опций есть один, чтобы библиотека CSV знала, что первая строка является заголовком, а именно : headers => true . Давайте изменим нашу вторую строку в нашем 2-строчном коде выше, добавив эту опцию:

 guests = CSV.read('guests.csv', headers:true) 

Итак, вы можете задаться вопросом, что теперь отличается? Давайте попробуем проверить, что в данный момент содержит переменная guest (подсказка: это больше не двойной массив!):

 #<CSV::Table mode:col_or_row row_count:5> 

Не беспокойтесь о том, что все это значит сейчас, мы объясним это шаг за шагом.

Файлы с заголовками являются специальными

Вместо (двойного) массива наша переменная guest теперь является объектом типа CSV :: Table . Как вы, наверное, знаете, у каждого объекта в Ruby есть свои собственные отдельные методы. То же самое верно для CSV :: Table.

К счастью, этот новый тип объекта ведет себя очень похоже на наш двойной массив. Точно так же, как вы можете перебирать двойной массив для получения каждой строки с each , вы можете использовать один и тот же метод для CSV::Table для достижения того же:

 guests.each do |guest_row| p guest_row #<CSV::Row "Name:"Dan"... end 

Вы должны увидеть 4 строки в качестве результата (да 4 вместо 5, теперь он распознает, что наша первая строка является фактическим заголовком). Вот первые 2 строки:

 #<CSV::Row "Name":"Dan" "Times arrived":"34" "Total $ spent":"2548" "Food feedback":"Lovin it!"> #<CSV::Row "Name":"Maria" "Times arrived":"55" "Total $ spent":"5054" "Food feedback":"Good, delicious food"> 

Как видите, вместо получения множества массивов вы теперь получаете новый CSV::Row . Каждый из этих объектов, как следует из его названия, также ссылается на одну строку.

В основном, если мы включаем headers:true мы получаем:

  • CSV::Table вместо двойного массива и
  • CSV::Row объекты, представляющие строки.

Если вы хотите вернуть двойной массив, просто вызовите метод CSV::Table#to_a . Давай посмотрим что происходит:

 guests = CSV.read('guests.csv', headers:true) #=> <CSV::Table mode:col_or_row row_count:5> p guests.to_a # will output... #[["Name", "Times arrived", "Total $ spent", "Food feedback"], ["Dan", "34", "2548", "Lovin it!"]........... 

Вы также можете использовать #to_s вместо to_a чтобы получить строковое представление файла:

 p guests.to_s #=> "Name,Times arrived,Total $ spent,Food feedback\nDan,34,2548......... 

Почему разные объекты?

Какой смысл иметь эти странные объекты типа CSV::Row для каждой строки CSV вместо хорошего старого массива? Давайте возьмем Dan, наш второй ряд из файла guest.csv , в качестве примера. Без каких-либо опций Дэн будет представлен в этом формате в Ruby:

 ["Dan", "34", "2548", "Lovin it!"] 

Все меняется, когда мы добавляем headers:true option:

 #<CSV::Row "Name":"Dan" "Times arrived":"34" "Total $ spent":"2548" "Food feedback":"Lovin it!"> 

Представьте, что вы хотите напечатать только первый столбец guest.csv , содержащий имена гостей. Если Dan представлен в виде обычного массива, вам нужно вызвать dan_array[0] чтобы получить эти данные. Наш код будет выглядеть примерно так (на этот раз я буду использовать метод foreach для чтения из файла CSV вместо read ):

 guests = CSV.foreach('guests.csv') do |row| puts row[0] end #=> outputs "Name", "Dan", "Maria", "Carlos", "Stephany" on separate lines 

Если бы кто-то еще читал ваш код, он, вероятно, задавался бы вопросом, что означает row[0] . Кроме того, у нас есть «Имя» в качестве первой строки в выводе, что определенно НЕ то, что мы хотим. Мы хотим напечатать имена гостей без заголовков! А еще лучше, используйте заголовки в качестве указателей на конкретную ячейку под этим столбцом в строке. Свидетель силы CSV::Row :

 guests = CSV.foreach('guests.csv', headers:true) do |row| puts row['Name'] # For each row, give me the cell that is under the 'Name' column end #=> outputs "Dan", "Maria", "Carlos", "Stephany" on separate lines. "Name" is not printed. 

Отлично! CSV::Row имеет свои удобные методы, как вы можете видеть. В этом случае вторая строка является просто синтаксическим сахаром для row.[]('Name') которая будет проходить по всем строкам и выводить только те ячейки, которые находятся под столбцом «Имя». Вы можете заменить «Имя» на «Время прибытия», «Всего потраченных $» или «Обратная связь по продуктам», чтобы получить соответствующие значения из других столбцов. Важно помнить, что имена заголовков чувствительны к регистру. Это не будет работать:

 guests = CSV.foreach('guests.csv', headers:true) do |row| puts row['Food Feedback'] #=> Will print nil for all rows, the correct column name is 'Food feedback' end 

Если бы мне пришлось описать CSV::Row , я бы описал его как дочерний элемент массива и хэша. В отличие от массива, вы можете ссылаться на его элементы по имени и, в отличие от хэша, вы можете иметь дубликаты «ключей», как мы увидим позже.

Если мы хотим передать другой элемент в объект CSV::Table , мы можем использовать тот же метод, который мы использовали бы с обычным массивом ( push или ) with an array as the argument. To get only the headers, we use the ) with an array as the argument. To get only the headers, we use the метод headers . Вот некоторые примеры:

 guests = CSV.read('guests.csv',headers:true) #<CSV::Table mode:col_or_row row_count:5> guests << ['Eve', 24, 54, 'Delicious'] #<CSV::Table mode:col_or_row row_count:6> print guests.headers #=> ["Name", "Times arrived", "Total $ spent", "Food feedback"] 

Метод CSV::Table#delete удаляет весь столбец из объекта CSV::Table и возвращает удаленные записи в виде массива.

 guests.delete('Name') #=> returns ["Dan", "Maria", "Carlos", "Stephany"] 

Что, если мы хотим удалить строку вместо массива? На этот раз мы можем использовать тот же метод .delete , предоставляя числовой индекс (на основе 0, поэтому 0 = первая строка, 1 = вторая строка). Возвращаемое значение, как и в предыдущем примере, будет удаленной строкой.

 guests.delete(0) # This method returns #<CSV::Row "Name":"Dan" " Times arrived":"34" "Total $ spent":"2548" "Food feedback":"Lovin it!"> 

Здесь все становится немного странным, если вы знакомы с массивами и хэшами. Если в переменной есть CSV::Table , для доступа к значениям в столбце вы используете имя столбца в качестве индекса, а для доступа к строке вы используете числовые значения, начиная с 0 (0 – первая строка, 1 – вторая строка и т. Д. ):

 guests = CSV.read('guests.csv', headers:true) guests['Times arrived'] #=> ["34", "55", "22", "34"] guests[0] #=> #<CSV::Row "Name":"Carlos" "Times arrived":"22" "Total $ spent":"4352" "Food feedback":"I am \"pleased\", but could be better"> 

Мы даже можем объединить эти 2 обозначения для доступа только к определенной ячейке:

 guests['Times arrived'][1] #=> Returns '55' guests[1]['Times arrived'] #=> Also returns 55 

Ruby с этим кодом говорит следующее: «Дайте мне значение ячейки, которая находится под столбцом« Время пришло »во второй строке». Ухоженная!

Практическое использование заголовков CSV

Давайте попробуем взять наш файл guest.csv и изменить столбец «Всего $ потрачено», чтобы он содержал десятичные числа вместо целых чисел:

 new_guests_csv = [] # We create an array to gold the new CSV data CSV.foreach('guests.csv',headers:true) do |guest| # Iterate over each row of our CSV file guest['Total $ spent'] = guest['Total $ spent'].to_f # new_guests_csv << guest # Add the new row into new_guests_csv end 

Я решил использовать метод .foreach для чтения из файла здесь. Я мог бы также использовать CSV#read :

 new_guests_csv = [] # We create an array to gold the new CSV data old_guests_csv = CSV.read('guests.csv', headers:true) # Reads the entire content of the CSV into the variable old_guests_csv.each do |guest| # old_guests_csv is CSV::Table object which has #each method to iterate over its rows guest['Total $ spent'] = guest['Total $ spent'].to_f # Same thing as with our previous code new_guests_csv << guest # Add the new row into new_guests_csv end 

Мы могли бы сделать это, если мы хотим сохранить наши данные в новом файле:

 CSV.open('updated_guests.csv', 'w') do |csv| # Create a new file updated_guests.csv csv << ['Name', 'Times arrived', 'Total $ spent (decimals)', 'Food feedback'] # Add new headers new_guests_csv.each do |row| # Since we now have the entire updated CSV file in this variable as a double array, # we iterate over each (array) element csv.puts row end end 

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

Подсказка: что если у вас есть файл с двумя одинаковыми именами столбцов? Предположим, что наш файл guest.csv содержит еще один столбец «Имя», как мы будем к нему обращаться? Как вы уже знаете, доступ к первому столбцу можно получить с помощью row_object['Name'] . Чтобы получить доступ ко второму, напишите row_object['Name', 2]

Как читать БОЛЬШОЙ файл CSV, не теряя здравомыслие

Язык Ruby в первую очередь оптимизирован для людей, а не для компьютеров. Таким образом, производительность является вторичной, а удобство – первым в списке приоритетов. К сожалению, это может быть проблемой, когда вы пытаетесь что-то вроде чтения большого файла в память. Если размер вашего файла составляет 300 мегабайт, а на компьютере, на котором выполняется ваш код, скажем, 512 МБ ОЗУ, возникнет проблема.

Вернемся к дяде Бобу. Перенесемся в будущее, у дяди Боба есть франшиза и многомиллионный бизнес. Он все еще хранит свой CSV-файл, хотя теперь в нем более 70 000 строк! Если мы попытаемся прочитать и манипулировать этим файлом с помощью CSV.read (который будет считывать ВЕСЬ файл в память), размер нашей программы в ОЗУ увеличится в несколько раз. Это происходит потому, что в памяти есть объект (массив CSV::Row если у нас есть заголовки) для каждой строки нашего CSV-файла. Это 70 000 объектов! Допустим, мы хотим вернуть только те строки с людьми, которые приходили в ресторан более 10 раз:

 #...doing some regular stuff, the program memory size is 12MB guests = CSV.read('guests.csv',headers:true) # a big CSV file is read into memory, the size is now over 100MB! guests.select do |row| row['Times arrived'].to_i > 10 # if we put the return value of the select block in another variable, we'll have even bigger size end 

Конечно, мы можем избежать этого с помощью CSV.foreach, который вместо чтения всего файла в память будет повторять строку за строкой и не будет влиять на общий размер памяти программы после завершения итерации. Но здесь есть небольшая проблема, нам придется реструктурировать нашу программу:

 guests_who_visited_more_than_ten_times = Array.new CSV.foreach('guests.csv', headers:true) do |guest| guests_who_visited_more_than_ten_times << guest if guest['Times arrived'] > 10 end 

Здесь мы обмениваем выразительность на эффективность, первый пример, когда мы читали из памяти, был лучше, потому что наши данные были реальным объектом, на котором мы могли использовать Array#select . К счастью, вы можете получить лучшее из обоих миров, сохраняя память и выразительность, используя перечислитель ( опора для Avdi из RubyTapas, где я выучил этот удивительный трюк):

 CSV.open('guests.csv', headers:true) do |guest| guests = guest.each guests.select do |row| row['Times arrived'].to_i > 10 end end 

Если вы не знакомы со второй строкой, мы в основном создаем объект перечислителя и сохраняем его в переменной guest. Перечислители позволяют нам выполнять итерацию «по требованию», скажем, если бы у меня был код, который насчитал от 1 до 100:

 1.upto(10) { |x| px } 

Превращение этого объекта в объект перечислителя позволит мне, например, считать от 1 до 5 в одной части моей программы, а затем закончить отсчет до 10:

 enum = 1.upto(10) p enum.next #=> 1 p 'bunch of other code here' p enum.next #=> 2 p 'and here' p enum.next #=> 3 

Характер «по требованию» счетчиков делает их дружественными к памяти. По сравнению с первым примером с использованием CSV.read у нас не будет тысяч объектов в оперативной памяти, и в отличие от CSV.foreach , нам не нужно повторно CSV.foreach код путем изменения логики.

Ну, это конец серии Ruby CSV. Я надеюсь, что вы многому научились!