php построчное чтение большого файла
Как прочитать большой файл средствами PHP (не грохнув при этом сервак)
PHP разработчикам не так уж часто приходится следить за расходом памяти в своих приложениях. Сам движок PHP неплохо подчищает мусор за нами, да и модель веб-сервера с контекстом исполнения, «умирающим» после выполнения каждого запроса, позволяет даже самому плохому коду не создавать больших долгих проблем.
Однако, в некоторых ситуациях, мы можем столкнуться с проблемами нехватки оперативной памяти — например, пытаясь запустить композер на маленьком VPS, или при открытии большого файла на сервере не богатом ресурсами.
Последняя проблема и будет рассмотрена в этом уроке.
Мерила Успеха
При проведении любых оптимизаций кода, мы всегда должны замерять результаты его выполнения до и после, для того чтобы оценивать эффективность(или пагубность) наших оптимизаций.
Обычно измеряют загрузку CPU и использование оперативной памяти. Часто бывает, что экономия одного, ведёт к увеличенным затратам другого и наоборот.
В асинхронной модели приложения(мультипроцессорные и многопоточные) всегда очень важно следить как за процессором, так и за памятью. В классических приложениях контроль ресурсов становится проблемой лишь при приближении к лимитам сервера.
Измерять использование CPU внутри PHP плохая идея. Лучше использовать какую-либо утилиту, как top из Ubuntu или macOS. Если вы у вас Windows, то можно использовать Linux Subsystem, чтобы иметь доступ к top.
В этом уроке мы будем измерять использование памяти. Мы посмотрим, как память расходуется в традиционных скриптах, а затем применим парочку фишек для оптимизации и сравним результаты. Надеюсь, к концу статьи, читатель получит базовое понимание основных принципов оптимизации расхода памяти при чтении больших объемов данных.
Будем замерять память так:
Эту функцию мы будем использовать в конце каждого скрипта, и сравнивать полученные значения.
Какие есть варианты?
Существует много разных подходов для эффективного чтения данных, но всех их условно можно разделить на две группы: мы либо считываем и сразу же обрабатываем считанную порцию данных(без предварительной загрузки всех данных в память), либо вовсе преобразуем данные в поток, не заморачиваясь над его содержимым.
Давайте представим, что для первого варианта мы хотим читать файл и отдельно обрабатывать каждые 10000 строк. Нужно будет держать по крайней мере 10000 строк в памяти и передавать их в очередь(в какой бы форме она не была реализована).
Для второго сценария, предположим, мы хотим сжать содержимое очень большого ответа API. Нам не важно, что за данные там содержатся, важно вернуть их в сжатой форме.
В обоих случаях нужно считать большие объемы информации. В первом, нам известен формат данных, во втором, формат значения не имеет. Рассмотрим оба варианта.
Чтение Файла Строка За Строкой
Есть много функций для работы с файлами. Давайте напишем с их помощью свой ридер:
Тут мы считываем файл с работами Шекспира. Размер файла около 5.5MB и пиковое использование памяти 12.8MB.
А теперь, давайте воспользуемся генератором:
Файл тот же, а пиковое использование памяти упало до 393KB! Но пока мы не выполняем со считываемыми данными никаких операций, это не имеет практической пользы. Для примера, мы можем разбивать документ на части, если встретим две пустые строки:
Хотя мы разбили документ на 1,216 кусков, мы использовали лишь 459KB памяти. Всё это, благодаря особенности генераторов — объем памяти для их работы равен размеру самой большой итерируемой части. В данном случае, самая большая часть состоит из 101,985 символов.
Генераторы могут применяться и в других ситуациях, но данный пример хорошо демонстрирует производительность при чтении больших файлов. Возможно, генераторы один из лучших вариантов для обработки данных.
Пайпинг между файлами
В ситуациях, когда обработка данных не требуется, мы можем пробрасывать данные из одного файла в другой. Это называется пайпингом( pipe — труба, возможно потому что мы не видим что происходит внутри трубы, но видим что входит и выходит и неё). Это можно сделать с помощью потоковых методов. Но сперва, давайте напишем классический скрипт, который тупо передает данные из одного файла в другой:
Неудивительно, что этот скрипт использует намного больше памяти, чем занимает копируемый файл. Это связано с тем, что он должен читать и хранить содержимое файла в памяти до тех пор пока файл не будет скопирован полностью. Для маленьких файлов в этом нет ничего страшного, но не для больших.
Давайте попробуем стримить(или пайпить) файлы, один в другой:
Код довольно странный. Мы открываем оба файла, первый на чтение, второй на запись. Затем мы копируем первый во второй, после чего закрываем оба файла. Возможно будет сюрпризом, но мы потратили всего 393KB.
Для того чтобы осуществить задуманное этим способом потребовалось 581KB. Теперь попробуем сделать то же самое с помощью потоков.
Потратили немного меньше памяти(400KB) при одинаковом результате. А если б нам не нужно было сохранять картинку в памяти, мы могли бы сразу застримить её в stdout :
Другие потоки
Существуют и другие потоки, в/из которых можно стримить:
Фильтры
Есть еще одна фишка, которую мы можем использовать — это фильтры. Промежуточный вариант, который дает нам немного контроля над потоком, без необходимости детально погружаться в его содержимое. Допустим, мы хотим сжать файл. Можно применить zip extension:
Хороший код, но он потребляет почти 11MB. С фильтрами, получится лучше:
Здесь мы используем php://filter/zlib.deflate который считывает и сжимает входящие данные. Мы можем пайпить сжатые данные в файл, или куда-нибудь еще. Этот код использовал лишь 896KB.
Я знаю что это не совсем тот же формат, что и zip архив. Но задумайтесь, если у нас есть возможность выбрать иной формат сжатия, затратив в 12 раз меньше памяти, стоит ли это делать?
Чтобы распаковать данные, применим другой zip фильтр.
Вот парочка статей, для тех кому хотелось бы поглубже погрузиться в тему потоков: “Understanding Streams in PHP” и“Using PHP Streams Effectively”.
Кастомизация потоков
fopen и file_get_contents имеют ряд предустановленных опций, но мы можем менять их как душе угодно. Чтобы сделать это, нужно создать новый контекст потока:
В этом примере мы пытаемся сделать POST запрос к API. Прописываем несколько заголовков, и обращаемся к API по файловому дескриптору. Существует много других опций для кастомизации, так что не будет лишним ознакомиться с документацией по этому вопросу.
Создание своих протоколов и фильтров
Перед тем как закончить, давайте поговорим о создании кастомных протоколов. Если посмотреть в документацию, то можно увидеть пример:
Написание своей реализации такого тянет на отдельную статью. Но если все же озадачиться и сделать это, то можно будет легко зарегистрировать свою обертку для стримов:
Аналогичным образом, можно создать и кастомные фильтры потока. Пример класса фильтра из доков:
И его также легко зарегистрировать:
Хотя это не самая частая проблема, с которой мы мучаемся, очень легко накосячить при работе с большими файлами. В асинхронных приложениях, вообще очень просто положить весь сервер, если не контролировать использование памяти в своих скриптах
Надеюсь, что этот урок подарил вам несколько новых идей(или освежил их в памяти) и теперь вы сможете работать с большими файлами гораздо эффективнее. Познакомившись с генераторами и потоками( и перестав использовать функции по типу file_get_contents ) можно избавить наши приложения от целого класса ошибок. That seems like a good thing to aim for!
Как прочитать большой файл в PHP?
Задача по прочтению больших текстовых файлов редко встаёт перед PHP-разработчиком, но к ней нужно быть готовым, потому что есть некоторые подводные камни, которые всплывают непосредственно во время работы скриптов.
Давайте определимся — что такое большой файл? На мой взгляд, большой файл, это файл такого размера, который не может целиком уместиться в рабочую оперативную память php процесса. Мы не можем просто взять и разместить всё содержимое в строковую переменную, потому что поймаем ошибку «Fatal error: Allowed memory size of XXX bytes exhausted».
Раз нельзя прочесть файл целиком, то надо его прочитать по частям. Есть функция fgets() или более гибкий вариант stream_get_line. Но если мы не знаем формата файла и не уверены, что там есть какие-либо обозначения новой строчки или форматирование, нам придется читать кусками фиксированный длины с помощью функции fread().
Принцип простой — нам нужно два механизма. Первый должен считывать текст по кусочкам из файла. Второй должен принимать эти кусочки и обрабатывать их. В этом посте речь идёт о первом механизме. Для удобства я создал класс, который реализует интерфейс SeekableIterator, что позволяет прочитать файл таким образом:
Теперь о первом подводном камне — класс для реализации интерфейса использует функцию fseek(). Функция устанавливает курсор (указатель) на нужную позицию, чтобы начать считывать байты с нужной позиции. Но она перестает работать, когда позиция превышает внутреннюю константу PHP_INT_MAX, на 32-битной установке PHP (и на 64-битных версиях для Windows, которые внутри используют 32-битные целые числа), эта константа практически равна количеству байтов в двух гигабайтах. Поэтому чтобы нормально работать с большими файлами, PHP должен быть скомпилирован с поддержкой 64-битных целых чисел.
Второй подводный камень — это скорость чтения с диска. При чтении большого файла диск будет загружен большим количеством операций чтения (и возможно записи, если в процессе обработки кусков будет эта операция) и это может продолжаться довольно долго. Это может привести к проблемам в работе других процессов, поэтому такое чтение следует совершать на диске, где нет других процессов, например, на специально выделенном хранилище для обработки логов. В идеале, следует продумать всю связку софта и железа заранее. Если вам нужно делать записи в базу, вероятно, лучше разместить её на другом диске или даже сервере, стоит продумать над величиной считываемого куска, чтобы уменьшить количество операций, возможно даже стоит сделать трех-ступенчатую обработку файла: считать куски, совершить легкую подготовку и сохранить данные и уже асинхронно провести окончательную обработку информации. Вроде такая простая задача, а сложностей может возникнуть огромное количество.
Парсинг от А до Я
Блог о программировании парсеров и web-автоматизации
Поиск по блогу
понедельник, 31 мая 2010 г.
PHP: построчное чтение и обработка больших CSV-файлов
С проблемой обработки больших CSV-файлов на PHP в первый раз я столкнулась недавно. На PHP я вообще мало программирую, только если возникают задачи написать что-либо конкретно на этом языке.
В предыдущей статье были рассмотрены разные варианты импорта CSV-файла в базу данных MySQL. Там же я отметила, что работа с большими файлами требует особого подхода. Основным ограничением для импорта большого объема данных является время выполнения скрипта, которое задается хостером (как правило 30 секунд).
Мне необходимо было именно автоматизировать процесс полного импорта. Перед вставкой в таблицу значения полей, полученные из scv-файла, требовали анализа и дополнительной обработки.
Когда я прочитала в описании утилиты BigDump (в предыдущей статье я на нее ссылалась) о принципе работы:
The script executes only a small part of the huge dump and restarts itself. The next session starts where the last was stopped. ( Перевод: Скрипт выполняет лишь небольшую часть SQL-команд из файла и перезапускает сам себя. В следующий раз импорт начинается с того места, в котором скрипт прервал свою работу.)
я поняла, что мне обязательно нужно попробовать такое решение. Поиски в инете чего-то похожего окончились успешно.
Я протестировала этот скрипт на файле размером 60 Mb. Отработал он правильно, все проимпортировал. Но время работы, все-таки, хотелось бы уменьшить.
В той же ветке форума, откуда я стырила это решение, обсуждалось, что ускорить работу скрипта при импорте данных в базу можно, заменив одиночные инсерты групповыми.
Команда INSERT, использующая VALUES, может быть использована для вставки сразу нескольких рядов. Чтобы сделать это, перечислите наборы значений, которые вам надо вставить. Пример:
INSERT INTO tbl_name (a,b,c) VALUES(1,2,3),(4,5,6),(7,8,9);
Апгрейдив скрипт на групповую вставку, получила и вправду более подходящий по быстродействию результат. Но думаю, что на этом пока рано останавливаться, буду искать дальше.
Несправедливо было бы обойти вниманием комментарий maxnag-а к предыдущему посту и не упомянуть о возможности импорта данных из CSV средствами MySQL. Почитала документацию по LOAD DATA INFILE, осталось потестировать на больших файлах 🙂 Сначала я отмела для своего случая такой вариант, но потом решила, что, если он будет достаточно производительным, можно будет проимпортировать данные во временную таблицу, а затем произвести обработку и записать, куда надо. Но о результатах теста как-нибудь в следующий раз.
Всем удачных решений! 🙂
Чтобы быть в курсе обновлений блога, можно подписаться на RSS.
Как произвести чтение больших файлов в PHP (и не угробить сервер)
Дата публикации: 2017-12-06
От автора: PHP разработчики редко заботятся об управлении памятью. PHP движок превосходно выполняет свою работу и подчищает за нами. Серверная модель кратковыполняемого контекста значит, что даже самый плохой код имеет долгосрочный эффект.
Нам мало когда необходимо выходить за эти комфортные рамки. Например, когда мы пытаемся запустить Composer в большом проекта на минимальном VPS, или когда необходимо произвести в PHP чтение большого файла на все таком же маленьком сервере.
В этом уроке мы обсудим последнюю проблему. Код к уроку можно найти на GitHub.
Измеряем успех
Единственный способ понять, что мы что-то улучшили в коде, это измерить плохой участок, после чего сравнить эти измерения после фикса. Другими словами, если мы не знаем, насколько «решение» помогло нам (если вообще помогло), мы не можем утверждать, что это вообще решение.
Нас заботят два фактора. Первый – потребление CPU. С какой скоростью работает процесс, над которым мы будем работать? Второй фактор – потребление памяти. Сколько памяти выделяется на выполнение скрипта? Зачастую эти два фактора обратно пропорциональны – т.е. мы можем разгрузить память за счет CPU и наоборот.
Бесплатный курс по PHP программированию
Освойте курс и узнайте, как создать динамичный сайт на PHP и MySQL с полного нуля, используя модель MVC
В курсе 39 уроков | 15 часов видео | исходники для каждого урока
В асинхронной модели выполнения (многопроцессовые или многопоточные приложения PHP) потребление CPU и памяти важные факторы. В стандартной архитектуре PHP они становятся проблемой, когда один из факторов достигает ограничений сервера.
Внутри PHP измерять потребление CPU непрактично. Если вы хотите сосредоточиться на этой области, попробуйте использовать что-то типа top, Ubuntu или macOS. В Windows попробуйте использовать Linux Subsystem, чтобы использовать top в Ubuntu.
В рамках урока мы будем измерять потребление памяти. Мы посмотрим, сколько памяти используется в «обычных» скриптах. Проведем пару стратегий оптимизации и измерим их. В конце я хочу, чтобы вы могли делать образованный выбор.
Методы, которые мы будем использовать для измерения потребления памяти:
fgets
(PHP 4, PHP 5, PHP 7, PHP 8)
fgets — Читает строку из файла
Описание
Читает строку из файлового указателя.
Список параметров
Указатель на файл должен быть корректным и указывать на файл, успешно открытый функциями fopen() или fsockopen() (и всё ещё не закрытый функцией fclose() ).
Возвращаемые значения
Примеры
Пример #1 Построчное чтение файла
Примечания
Замечание: Если у вас возникают проблемы с распознаванием PHP концов строк при чтении или создании файлов на Macintosh-совместимом компьютере, включение опции auto_detect_line_endings может помочь решить проблему.
Смотрите также
User Contributed Notes 38 notes
A better example, to illustrate the differences in speed for large files, between fgets and stream_get_line.
This example simulates situations where you are reading potentially very long lines, of an uncertain length (but with a maximum buffer size), from an input source.
As Dade pointed out, the previous example I provided was much to easy to pick apart, and did not adequately highlight the issue I was trying to address.
Note that specifying a definitive end-character for fgets (ie: newline), generally decreases the speed difference reasonably significantly.
Note that, in a vast majority of situations in which php is employed, tiny differences in speed between system calls are of negligible importance.
There’s an error in the documentation:
The file pointer must be valid, and must point to a file successfully opened by fopen() or fsockopen() (and not yet closed by fclose()).
You should also add «popen» and «pclose» to the documentation. I’m a new PHP developer and went to verify that I could use «fgets» on commands that I used with «popen».
if you for some reason need to get lines from a string instead of a file pointer, try
tag without a hitch, but when I moved the code to a LAMP production server, every \r\n created two fgets and I got free empty lines.
Here is the example code:
One thing I discovered with fgets, at least with PHP 5.1.6, is that you may have to use an IF statement to avoid your code running rampant (and possibly hanging the server). This can cause problems if you do not have root access on the server on which you are working.
This is the code I have implemented ($F1 is an array):
I’m using this function to modify the header of a large postscript document on copy. Works extremely quickly so far.
function write($filename) <
$fh = fopen($this->sourceps,’r’);
$fw = fopen($filename,’w’);
fscanf($file, «%s\n») isn’t really a good substitution for fgets(), since it will stop parsing at the first whitespace and not at the end of line!
(See the fscanf page for details on this)
I think that the quickest way of read a (long) file with the rows in reverse order is
I would have expected the same behaviour from these bits of code:-
It’s strange no one mentions «0» in this context.
Since «0» is considered to be false, a line with a single «0» can be treated as EOF if using the while assign idiom.
while ($line = fgets(STDIN, 2)) <
>
This may surprisingly break if a line starts with «)»
It appears that fgets() will return FALSE on EOF (before feof has a chance to read it), so this code will throw an exception:
while (!feof($fh)) <
$line = fgets($fh);
if ($line === false) <
throw new Exception(«File read error»);
>
>
Some people try to call feof before fgets, and then ignoring the return value of fgets. This method leads to processing value FALSE when reaching the end of file.
Regarding Leigh Purdie’s comment (from 4 years ago) about stream_get_line being better for large files, I decided to test this in case it was optimized since then and I found out that Leigh’s comment is just completely incorrect
fgets actually has a small amount of better performance, but the test Leigh did was not set up to produce good results
The suggested test was:
The reason this is invalid is because the buffer size of 65535 is completely unnecessary
piping the output of «yes ‘this is a test line'» in to PHP makes each line 19 characters plus the delimiter
Here are the results on one of my servers:
Buffer size 65535
stream_get_line: 0.340s
fgets: 2.392s
Buffer size of 1024
stream_get_line: 0m0.348s
fgets: 0.404s
Buffer size of 8192 (the default for both)
stream_get_line: 0.348s
fgets: 0.552s
Buffer size of 100:
stream_get_line: 0.332s
fgets: 0.368s
Saku’s example may also be used like this:
Error in the example number 1 of this page.
change this line:
$buffer = fgets($fd, 4096);
into:
$buffer = fgets($handle, 4096);
= ‘imapserver’ ;
$user = ‘user’ ;
$pass = ‘pass’ ;
It was not possible to open the File!
real 0m1.482s
user 0m1.616s
sys 0m0.152s
real 0m7.281s
user 0m7.392s
sys 0m0.136s
When working with VERY large files, php tends to fall over sideways and die.
Here is a neat way to pull chunks out of a file very fast and won’t stop in mid line, but rater at end of last known line. It pulled a 30+ million line 900meg file through in
//File to be opened
$file = «huge.file»;
//Open file (DON’T USE a+ pointer will be wrong!)
$fp = fopen($file, ‘r’);
//Read 16meg chunks
$read = 16777216;
//\n Marker
$part = 0;
WARNING! fgets() and I presume any read() call to a file handle, e.g.
while(!feof(STDIN)) <
$line = fgets(STDIN);
. will result in a timeout after a default time of 60 seconds on my install. This behavior is non standard (not POSIX like) and seems to me to be a bug, or if not a major caveat which should be documented more clearly.
After the timeout fgets() will return FALSE (=== FALSE), however, you can check to see if the stream (file handle) has really closed by checking feof($stream), e.g.
while(!feof(STDIN)) <
$line = fgets(STDIN);
For anyone who wants a proper non-blocking fgets for sockets, there is a tiny snippet that does just that (performance should be horrible compared to fgets though):
This goes out to Leigh Purdie (5 years ago) and also Dade Brandon (4 months ago)
So i say Leigh posting and though omg i need to change all my fgets to stream_get_line. Then i ran the tests as shown in Leigh Purdie comment His results:
real 0m1.482s
user 0m1.616s
sys 0m0.152s
real 0m7.281s
user 0m7.392s
sys 0m0.136s
real 0m0.341s
user 0m0.352s
sys 0m0.148s
real 0m4.283s
user 0m4.128s
sys 0m0.448s
My results do show the same issue his results show. But first off PHP has at least gotten about 2-5 times faster then when the tests were first run (or better hardware).
Now to relate to Dade Brandon who states if you use a correct buffer size the perfomance is neck and neck.
real 0m0.336s
user 0m0.412s
sys 0m0.076s
real 0m0.312s
user 0m0.364s
sys 0m0.192s
As you can see very close and fgets just coming just a little bit ahead. I suspect that fgets is reading backwards on the buffer or loads everything into its self then trys to figure it out where as a correct set buffer does the trick. Dade Brandon states that fgets lets you know how the line was delimited. stream_get_line lets you choose what you wanna call the delimiter using its 3rd option.
fgets has one more option that is important, you dont have to set the length of the line. So in a case where you may not know the length of the line maybe in handling Http protocol or something else like log lines you can simply leave it off and still get great performance.
real 0m0.261s
user 0m0.260s
sys 0m0.232s