Потоковая передача и распаковка большого CSV-файла с помощью ruby

У меня проблема, когда мне нужно скачать, разархивировать, а затем обработать построчно очень большой файл CSV. Я думаю, полезно дать вам представление о том, насколько велик файл:

  • big_file.zip ~ 700 МБ
  • big_file.csv ~ 23 ГБ

Вот некоторые вещи, которые я хотел бы сделать:

  • Не нужно загружать весь файл перед распаковкой
  • Не нужно распаковывать весь файл перед разбором строк csv
  • Не используйте слишком много памяти/диска при выполнении всего этого

Я не знаю, возможно это или нет. Вот о чем я думал:

require 'open-uri'
require 'rubyzip'
require 'csv'

open('http://foo.bar/big_file.zip') do |zipped|
  Zip::InputStream.open(zipped) do |unzipped|
    sleep 10 until entry = unzipped.get_next_entry && entry.name == 'big_file.csv'
    CSV.foreach(unzipped) do |row|
      # process the row, maybe write out to STDOUT or some file
    end
  end
end

Вот проблемы, о которых я знаю:

  • open-uri читает весь ответ и сохраняет его в Tempfile, что не годится для файла такого размера. Мне, вероятно, нужно было бы использовать Net::HTTP напрямую, но я не уверен, как это сделать и при этом получить IO.
  • Я не знаю, насколько быстрой будет загрузка и будет ли Zip::InputStream работать так, как я показал. Может ли он распаковать часть файла, когда он еще не весь?
  • Будет ли CSV.foreach работать с InputStream rubyzip? Достаточно ли он ведет себя как File, чтобы разобрать строки? Не взбесится ли он, если захочет прочитать, но буфер пуст?

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


person ZombieDev    schedule 29.04.2014    source источник
comment
Я не думаю, что потоковая передача zip будет работать из-за структуры zip-файлов. Возможно, он мог бы сделать что-то вроде funzip, если бы в zip был только один файл (или тот, который я хотел, был первым), но это не тот случай.   -  person ZombieDev    schedule 30.04.2014


Ответы (1)


Прошло некоторое время с тех пор, как я опубликовал этот вопрос, и, если кто-то еще столкнется с ним, я подумал, что, возможно, стоит поделиться тем, что я нашел.

  1. Для того количества строк, с которым я имел дело, стандартная библиотека Ruby CSV была слишком медленной. Мой CSV-файл был достаточно простым, поэтому мне не нужны были все эти вещи для обработки строк в кавычках или принуждения типов. Было намного проще просто использовать IO#gets, а затем разделить строку запятыми.
  2. Мне не удалось передать все это с http на Zip::Inputstream на какой-то IO, содержащий данные csv. Это связано с тем, что структура файла zip имеет конец центрального каталога (EOCD) в конце файла. Это необходимо для извлечения файла, чтобы его потоковая передача с http не работала.

Решение, к которому я пришел, заключалось в том, чтобы загрузить файл на диск, а затем использовать библиотеку Ruby open3 и пакет Linux unzip для потоковой передачи несжатого файла csv из zip.

require 'open3'

IO.popen('unzip -p /path/to/big_file.zip big_file.csv', 'rb') do |io|
  line = io.gets
  # do stuff to process the CSV line
end

Переключатель -p при распаковке отправляет извлеченный файл на стандартный вывод. IO.popen затем используйте конвейеры, чтобы сделать это IO объектом на рубине. Работает довольно хорошо. Вы можете использовать его и с CSV, если вам нужна дополнительная обработка, но для меня это было слишком медленно.

require 'open3'
require 'csv'

IO.popen('unzip -p /path/to/big_file.zip big_file.csv', 'rb') do |io|
  CSV.foreach(io) do |row|
    # process the row
  end
end
person ZombieDev    schedule 06.08.2015