Почему "похищение" файла не является хорошей практикой?
Почему "прихлебывание" файла не является хорошей практикой для обычного ввода-вывода текстового файла и когда это полезно?
Например, почему я не должен использовать их?
File.read('/path/to/text.txt').lines.each do |line|
# do something with a line
end
или же
File.readlines('/path/to/text.txt').each do |line|
# do something with a line
end
3 ответа
Снова и снова мы видим вопросы, задаваемые о чтении текстового файла, чтобы построчно обрабатывать его, в котором используются различные варианты read
, или же readlines
, которые вытягивают весь файл в память одним действием.
Документация дляread
говорит:
Открывает файл, дополнительно ищет указанное смещение, затем возвращает длину в байтах (по умолчанию для остальной части файла). [...]
Документация дляreadlines
говорит:
Читает весь файл, указанный именем как отдельные строки, и возвращает эти строки в массиве. [...]
Извлечение небольшого файла не представляет особой проблемы, но наступает момент, когда память приходится перетасовывать по мере увеличения буфера входящих данных, что приводит к уменьшению времени процессора. Кроме того, если данные занимают слишком много места, операционная система должна вмешаться только для того, чтобы скрипт продолжал работать, и начинает буферизоваться на диск, что поставит программу на колени. На HTTPd (веб-хосте) или чем-то, требующем быстрого ответа, это приведет к повреждению всего приложения.
Slurping обычно основан на неправильном понимании скорости файлового ввода-вывода или на мысли, что лучше читать, а затем разбивать буфер, чем читать по одной строке за раз.
Вот некоторый тестовый код, чтобы продемонстрировать проблему, вызванную "slurping".
Сохраните это как "test.sh":
echo Building test files...
yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000 > kb.txt
yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000000 > mb.txt
yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000000000 > gb1.txt
cat gb1.txt gb1.txt > gb2.txt
cat gb1.txt gb2.txt > gb3.txt
echo Testing...
ruby -v
echo
for i in kb.txt mb.txt gb1.txt gb2.txt gb3.txt
do
echo
echo "Running: time ruby readlines.rb $i"
time ruby readlines.rb $i
echo '---------------------------------------'
echo "Running: time ruby foreach.rb $i"
time ruby foreach.rb $i
echo
done
rm [km]b.txt gb[123].txt
Создает пять файлов увеличивающихся размеров. Файлы 1К легко обрабатываются и встречаются очень часто. Раньше считалось, что файлы размером 1 МБ считались большими, но теперь они широко распространены. 1 ГБ является обычным явлением в моей среде, и файлы за пределами 10 ГБ встречаются периодически, поэтому очень важно знать, что происходит при 1 ГБ и более.
Сохраните это как "readlines.rb". Он ничего не делает, кроме внутреннего чтения всего файла построчно и добавления его в массив, который затем возвращается, и кажется, что это будет быстро, так как все написано на C:
lines = File.readlines(ARGV.shift).size
puts "#{ lines } lines read"
Сохраните это как "foreach.rb":
lines = 0
File.foreach(ARGV.shift) { |l| lines += 1 }
puts "#{ lines } lines read"
Бег sh ./test.sh
на моем ноутбуке я получаю:
Building test files...
Testing...
ruby 2.1.2p95 (2014-05-08 revision 45877) [x86_64-darwin13.0]
Чтение файла 1К:
Running: time ruby readlines.rb kb.txt
28 lines read
real 0m0.998s
user 0m0.386s
sys 0m0.594s
---------------------------------------
Running: time ruby foreach.rb kb.txt
28 lines read
real 0m1.019s
user 0m0.395s
sys 0m0.616s
Чтение 1 МБ файла:
Running: time ruby readlines.rb mb.txt
27028 lines read
real 0m1.021s
user 0m0.398s
sys 0m0.611s
---------------------------------------
Running: time ruby foreach.rb mb.txt
27028 lines read
real 0m0.990s
user 0m0.391s
sys 0m0.591s
Чтение 1 ГБ файла:
Running: time ruby readlines.rb gb1.txt
27027028 lines read
real 0m19.407s
user 0m17.134s
sys 0m2.262s
---------------------------------------
Running: time ruby foreach.rb gb1.txt
27027028 lines read
real 0m10.378s
user 0m9.472s
sys 0m0.898s
Чтение файла 2 ГБ:
Running: time ruby readlines.rb gb2.txt
54054055 lines read
real 0m58.904s
user 0m54.718s
sys 0m4.029s
---------------------------------------
Running: time ruby foreach.rb gb2.txt
54054055 lines read
real 0m19.992s
user 0m18.765s
sys 0m1.194s
Чтение файла 3GB:
Running: time ruby readlines.rb gb3.txt
81081082 lines read
real 2m7.260s
user 1m57.410s
sys 0m7.007s
---------------------------------------
Running: time ruby foreach.rb gb3.txt
81081082 lines read
real 0m33.116s
user 0m30.790s
sys 0m2.134s
Обратите внимание, как readlines
работает в два раза медленнее каждый раз, когда размер файла увеличивается, и используя foreach
замедляется линейно. На 1MB мы можем видеть, что что-то влияет на "блюкающий" ввод / вывод, что не влияет на построчное чтение. И, поскольку файлы размером 1 МБ очень распространены в наши дни, легко увидеть, что они замедлят обработку файлов в течение всего жизненного цикла программы, если мы не будем забегать вперед. Пара секунд здесь или немного, когда они случаются один раз, но если они происходят несколько раз в минуту, это к концу года приведет к серьезному снижению производительности.
Я столкнулся с этой проблемой много лет назад при обработке больших файлов данных. Код Perl, который я использовал, периодически останавливался, перераспределяя память при загрузке файла. Переписав код, чтобы он не обрабатывал файл данных, а вместо этого читал и обрабатывал его построчно, я получил огромное повышение скорости с пяти минут до менее одного и преподал мне большой урок.
иногда "полезно" прихлебывать файл, особенно если вам нужно что-то сделать за границами строки, однако, стоит потратить некоторое время на обдумывание альтернативных способов чтения файла, если вам нужно это сделать. Например, попробуйте сохранить небольшой буфер, созданный из последних "n" строк, и отсканируйте его. Это позволит избежать проблем с управлением памятью, вызванных попыткой чтения и хранения всего файла. Это обсуждается в связанном с Perl блоге " Perl Slurp-Eaze", который охватывает "когда" и "почему", чтобы оправдать использование полных операций чтения файлов, и хорошо относится к Ruby.
По другим уважительным причинам, чтобы не "прихлебывать" ваши файлы, прочитайте " Как найти в тексте файла шаблон и заменить его заданным значением".
Это немного по-старому, но я немного удивлен тем, что никто не упоминает, что прихлебывание входного файла делает программу практически бесполезной для конвейеров. В конвейере входной файл может быть небольшим, но медленным. Если ваша программа работает с ошибками, это означает, что она не работает с данными, когда они становятся доступными, и скорее заставляет вас ждать, сколько бы времени не потребовалось для завершения ввода. Сколько? Это может быть что угодно, например часы или дни, более или менее, если я делаю grep
или же find
в большой иерархии. Он также может быть разработан, чтобы не завершить, как бесконечный файл. Например, journalctl -f
будет продолжать выводить любые события, происходящие в системе, без остановки; tshark
будет выводить все, что видит в сети без остановки; ping
продолжит пинговать без остановки. /dev/zero
бесконечен, /dev/urandom
бесконечен.
Единственный раз, когда я мог бы видеть, что slurping приемлемо, может быть в файлах конфигурации, так как программа, вероятно, ничего не может сделать, пока не закончит чтение.
Почему "прихлебывание" файла не является хорошей практикой для обычного ввода-вывода из текстового файла
Оловянный человечек попал в цель. Я также хотел бы добавить:
Во многих случаях чтение всего файла в память не поддается отслеживанию (потому что либо файл слишком велик, либо манипуляции со строками имеют экспоненциальное пространство O())
Часто вы не можете предвидеть размер файла (особый случай выше)
Вы всегда должны быть осведомлены об использовании памяти, и чтение всего файла за раз (даже в тривиальных ситуациях) не является хорошей практикой, если существует альтернативный вариант (например, построчно). По своему опыту я знаю, что VBS ужасен в этом смысле, и каждый вынужден манипулировать файлами через командную строку.
Эта концепция применима не только для файлов, но и для любого другого процесса, в котором объем памяти быстро увеличивается, и вам приходится обрабатывать каждую итерацию (или строку) одновременно. Функции генератора помогают вам, обрабатывая процесс, или чтение строки, одну за другой, чтобы не работать со всеми данными в памяти.
Помимо всего прочего, Python очень умен для чтения файлов и open()
Метод предназначен для чтения построчно по умолчанию. См. " Улучшение вашего Python:" доходность "и объяснение генераторов", где объясняется хороший пример использования функций генератора.