Как прочитать большой файл в PHP?

Рассказываю, с какими сложностями я столкнулся при написании класса для чтение большого текстового файла на языке 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-битных целых чисел.

Второй подводный камень — это скорость чтения с диска. При чтении большого файла диск будет загружен большим количеством операций чтения (и возможно записи, если в процессе обработки кусков будет эта операция) и это может продолжаться довольно долго. Это может привести к проблемам в работе других процессов, поэтому такое чтение следует совершать на диске, где нет других процессов, например, на специально выделенном хранилище для обработки логов. В идеале, следует продумать всю связку софта и железа заранее. Если вам нужно делать записи в базу, вероятно, лучше разместить её на другом диске или даже сервере, стоит продумать над величиной считываемого куска, чтобы уменьшить количество операций, возможно даже стоит сделать трех-ступенчатую обработку файла: считать куски, совершить легкую подготовку и сохранить данные и уже асинхронно провести окончательную обработку информации. Вроде такая простая задача, а сложностей может возникнуть огромное количество.

Вот мой пакет, о котором я писал выше: https://github.com/DmitriyNyu/chunked-file-reader, так же можно поискать и другие решения, в том числе заточенные под конкретные фреймворки.

Зачем решать задачи для программистов?

Рассказываю, какую пользу приносит решение задач для программистов.

Мне очень нравится сайт codewars.com. На нём представлены задачи для программистов самой разной сложности. Большое количество языков и возможность посмотреть чужие варианты решения для меня — очень крутые возможности сайта. В этой заметке, я объясню, почему каждый программист должен решать хотя бы одну задачу в день.

Задачи будут держать ваш мозг в тонусе

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

Вы будете лучше знать язык

Многие задачи заставили меня читать документацию, потому что оказалось, что с ходу я не могу вспомнить многие названия функций/методов. Например, в начале попалась задача для JavaScript — написать функцию, которая переводит число из десятичной системы счисления, в двоичную. Я стал вспоминать школьные уроки информатики, как мы записывали руками в тетрадку единички и нолики и написал такой алгоритм. Когда я отправил решение и стал смотреть чужие — я ахнул! Оказалось, что самое простое решение просто использует второй параметр функции parseInt(string, radix). Я вообще не знал, что у этой функции есть второй параметр. Более того, если его не использовать, могут быть проблемы с тем, как будет интерпретироваться строчка.

Другой пример любопытного кода на Javascript. Простая задача по написанию функции, которая считает объём конуса. Результат нужно было округлить вниз. Я вполне разумно использовал Math.floor(num), но, как оказалось, округлить можно и другим способом — использовав два раза битовую операцию NOT, которая в языке совершается через двойную тильду ~~. При этом, согласно некоторым бенчмаркам, такая операция совершается быстрее, чем вызов Math.floor():

В качестве третьего примера приведу задачу, где нужно было создать код, который позволял был делать что-то вроде:

Понятно, что решением такой задачи является каррирование, но как его реализовать с нуля в JavaScript? Ответа я не знал, зато в гугле нашёл отличную статью на эту тему — https://medium.com/@kevincennis/currying-in-javascript-c66080543528#.o5a0hf9z0

Вы будете внимательнее читать условия задачи

В таких задачах часто используется много воды, и чтобы понять, что же конкретно нужно сделать, надо внимательно прочитать задание несколько раз. Вот текст задачи:

Ask a small girl — «How old are you?». She always says strange things… Lets help her! For correct answer program should return int from 0 to 9 😉 Assume test input string always valid and may look like «1 year old» or «5 years old», etc.. The first char is number only =)

Моё решение брало первый символ строчки, преобразовывало его в целое и возвращало. А самый топовый вариант решения выглядит вот так:

И это годится именно по текущим условиям задачи.

Это интересно, полезно для кругозора и для будущего

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

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

Наконец, это действительно интересно, очень приятно, когда твоё решение наконец-то проходит все проверки и вы можете сказать, что да, я решил эту задачу!

3 частые проблемы производительности веб-приложений, первая проблема

Описываю самый распространённый анти-паттерн, который может быть в вашем коде прямо сейчас.

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

Дорогие запросы к базе данных внутри цикла.

Самый банальный пример и очень часто встречающийся паттерн в коде. Мы получаем откуда-то айдишники строк в БД (это может быть что угодно, перечень товаров на главной, содержимое корзины, номера статей с лайками пользователя) и далее по этим номерам делаем запросы и собираем массив/коллекцию:

Во-первых, тут присутствует красный флаг, о которым я писал ранее (https://dmitriynyu.com/perfomance-testing/) — нет ограничения на количества шагов в цикле. Код написан так, что мы вынуждены надеется, что количество айдишников, которое вернёт getIdsOfItemsInCart() будет небольшим, но на самом деле, мы этого точно не знаем.

Во-вторых, этот паттерн очень ловко маскируется. У вас в коде он может существовать не в таком явном виде, как в примере выше, это может быть например прикрепление свойств товара в интернет магазине к номенклатурной позиции. Менеджер добавил о 100 свойств к каждой детской игрушке — цвет, вес, форма и т.д., и теперь при каждом простом запросе вроде:

самописная и не очень умная ОРМ в глубине модели Item совершает 100 отдельных запросов к этим свойствам (это реальный случай).

В-третьих, этот вредный паттерн очень легко недооценить и проигнорировать. Например, внутри метода Item::first() может происходит кеширование. Полагаясь на это, вы подвергаете себя риску низкого быстродействия в случае, когда вдруг кэшу не хватит памяти и он решит почистить эти записи. Вполне реальна ситуация, когда в начале метод в цикле просто возвращал захардкоденные значения, а потом этот метод порефакторили, и он стал обращаться к базе. А про то, что метод вызывается до 100 раз в цикле, все забыли или не проверили.

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

Интересный доклад «Развитие ветки PHP-7» Дмитрия Стогова

Интересный доклад «Развитие ветки PHP-7» Дмитрия Стогова.

Самое интересное в конце доклада.

Perfomance testing

Какие проблемы могут возникать от невнимательности и что с этим можно сделать.

Однажды я заметил, что godesigner.ru стал тормозить. Открываю страницу, а она не загружается. Правда, через несколько секунд страница отобразилась нормально. Потом через десять минут эта проблема снова повторилась. И ещё раз. Тут я понял, что есть проблема, посмотрел графики New Relic и ужаснулся — значение средней загрузки за двадцать минут возросло до 40 (должно быть не больше единицы)

Я увидел, что проблема началась ещё раньше, но это не сказывалось на быстродействии страниц. По повторяющимся спайкам графика я понял, что излишняя загрузка возникает при работе фоновых задач, которые запускаются по крону. Вскоре виновник был найден — это скрипт, который наполняет содержимым ленту новостей, а точнее строчка 3 в коде:

С каждым вызовом этого скрипта происходил вызов этого метода, внутри которого из базы данных собирались ВСЕ записи новостей без всяких ограничений. Этот список постоянно рост, скрипт работал медленнее и медленнее, вплоть до того, что один вызов скрипта не успевал отработать до следующего вызова по расписанию. Настоящая катастрофа из-за дурацкой ошибки.

Конечно, такого рода ошибка — когда происходит запрос сущностей из постоянно растущего пула возможных вариантов безо всяких ограничений может произойти где угодно. Вы можете написать неэффективный список на JavaScript, где при обновлении одной строчки браузер будет перерисовывать всё dom дерево целиком, а таких строчек может быть тысячи.

Проблема в том, что такой код пройдёт юнит-тесты на малых объемах данных. При этом на больших объемах тесты будут исполняться долго, что мешает процессу TDD. Получается, что нам нужны отдельные тесты на быстродействие, которые будут проверять наш код на взаимодействие с большим количеством данных. Кроме времени исполнения надо отслеживать и другие вещи, например потребление памяти. Однако результаты могут отличаться в зависимости от среды — на продакшн сервере среда может сильно отличаться от тестового сервера, а тесты в браузере могут вести себя не так, как у пользователя на компьютере.

Я до конца не уверен, как правильно создавать и производить тестирование кода на производительность. Вот парочка ссылок, которые я нашёл по теме, когда искал информацию в интернете:
https://testitquickly.com/2008/12/08/essential-about-performance-testing/
https://msdn.microsoft.com/en-us/library/bb924357.aspx

Ещё я нашёл пару книг, которые прочитаю:
https://www.amazon.com/Art-Application-Performance-Testing-Programmers/dp/0596520662
https://www.amazon.com/Performance-Testing-Guidance-Web-Applications/dp/0735625700

Думаю, что после прочтения у меня будет понятие о тестировании производительности.

Как провести unit-тестирование setcookie() в php?

Какие сложности ждут вас при попытке провести юнит-тестирование методов, вызывающих setcookie().

Однажды, я писал код для класса, который определял, какой рекламный попап показывать пользователю. Алгоритм следующий — проверяем, есть ли в браузере куки А, если такой куки нет — показываем попап, А и сохраняем куки А. Если куки, А уже установлена, проверяем сохранена ли кука Б, и либо показываем попап Б и сохраняем куку Б, либо идём проверять существование куки С и так далее. Не самое изящное, но простое решение.

Строчка за строчкой, я дошёл до места в тестах, где предстояло проверить тот факт, что куки с правильными параметрами устанавливаются у пользователя. Тогда я не смог быстро найти решение, и положился на то, что функция setcookie () сработает как надо, если предоставить ей покрытые тестами параметры.

Сегодня мне показалось, что это любопытный вопрос. Как лучше протестировать setcookie ()? Как определить юнит-тестом, что произошла установка куки? Это возможно?

Эта функция добавляет в заголовки, передаваемые браузеру специально сформированную строчку, которая обрабатывается браузером. Это значит, что можно проанализировать заголовки, подготовленные к отправке и проверить наличие такой строчки.

Очевидная проблема — в режиме командной строки, который обычно используют для запуска тестов функции, связанные с заголовками ничего не делают, провести такую проверку невозможно. Но если у вас применяется веб-интерфейс, который запускает юнит-тесты через браузер, то это становится реальным. В этом случае, проверка тривиальна — создаем новый метод assertCookieSet ($params, $headers) внутри которого проверяем наличие в заголовках строчки, сформированный по спецификации, которая должна появиться после вызова setcookie ().

Правда, в этом случаем мы проверяем не факт установки куки, а то, что мы подготовили всё необходимое для браузера. Мы полагаемся на то, что браузер правильно поймёт наш заголовок и создаст куку. Получается, что в этом случае задача тоже не решена полностью — мы по-прежнему не знаем, использования нашего кода приводит к появлению куки у пользователя на компьютере или нет.

На мой взгляд, юнит тестирование не может ответить нам на этот вопрос. Помимо технических ограничений, мы пытаемся проверить сайд эффект вызова функции, которые затрагивает не столько сам класс, сколько взаимодействие между нашим кодом и браузером пользователя. Гораздо лучше вместо юнит-тестирования в этом случае написать тестирование чёрного ящика. В конечном итоге, мне нужно, чтобы попапы появлялись в нужном порядке, а не в том, ставятся куки или нет. Поэтому в этом случае лучше написать тесты для Селениума или аналогов, где мы проверим нашу функциональность.

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

Стоит отметить ещё одну опасность setcookie (). Функция возвращает true даже если куки не могут быть установлены при вызовы через php-cli. False мы получаем либо в случае провала проверки на валидность некоторых параметров, либо в случае, если сделали вывод перед вызовом setcookie (). Вот определение функции на С:

Это означает, что если использовать возврат setcookie () без учёта контекста, у нас возникнет проблема и так делать нельзя:

Если этот метод коллега использует в скрипте для командной строки, никакого сохранения не произойдёт, что приведёт к ошибке. Это возможно, если код плохо прокомментирован, коллега не просмотрел исходный код или установка куки спрятана за слоями абстракций. В этом случае необходимо проверить серверное api или бросить исключение.

Функция setcookie — http://php.net/manual/ru/function.setcookie.php
Стандарт — https://tools.ietf.org/html/rfc6265