Pull to refresh

Comments 47

Спасибо за статью! Очень полезная! В закладки однозначно!
Если б про то, как распарсить огромный json, не грохнув приложение с memory exceed.
была похожая задача, но так как структура json была заведомо известна, json дробился на части, по сотни объектов в каждой используя спец. разделитель типа `},{` для первого уровня, потом обрабатывались полученные данные, вылезла другая проблема — время выполнения, но это другая история :)

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

странно что не упомянута функция fread

для длинных в одну строчку можно указывать длину, до скольки байт читать в fgets
fgets ($handle, $length)
отличие, как я понимаю, в том, что в fread читать пока буффер прочитанного <= $length, а в fgets читать пока буффер < $length
Необязательный, третий аругмент stream_copy_to_stream делает то же самое. stream_copy_to_stream читает первый поток по одной строке и пишет во второй.

Поправка: stream_copy_to_stream не оперирует строками, третий аргумент задаёт размер копируемого участка в байтах (можно самому в цикле итеративно копировать чанками конкретного размера, не создавая буферных PHP переменных), если не задан (-1), то копируется от текущей позиции (может со смещением) до конца источника чанками размером 8192 байт.

Хорошее замечание, спасибо. В оригинале значит не совсем корректно было:
The third argument to stream_copy_to_stream is exactly the same sort of parameter (with exactly the same default). stream_copy_to_stream is reading from one stream, one line at a time, and writing it to the other stream.
Всё хорошо, с удовольствием прочел и поставил плюс
Но
Когда я вижу

require "memory.php";

относительные пути в ФС — мне становится плохо.
Пожалуйста, не делайте так, особенно в статьях, которыми потом будут руководствоваться начинающие!

Абсолютные указывать?! Мне вот от них становится плохо. Или вы имеете в виду, что будет по include path искать и вам от этого плохо?

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

Указывать относительные пути — это дорога в ад отладки. Задайте себе простой вопрос «А относительно КАКОЙ конкретно директории будет отсчитываться этот путь?» На моей памяти ни один программист PHP не сумел правильно с первого раза ответить на этот простой вопрос.
А относительно КАКОЙ конкретно директории будет отсчитываться этот путь?

Я попытаюсь ответить.
Относительно рабочей директории процесса.
(Ответ не претендует на абсолютное значение истины.)

Если путь не указан вообще, то, по порядку:


  1. По списку include_path, слева направо
  2. В рабочей директории процесса
  3. В директории текущего файла

Если указан относительный, то он вычислится относительно текущего файла. По сути та же подстановка DIR, но на уровне языка, а не пользовательского кода.

Если путь не указан вообще, то, по порядку:

Я не уверен что строго по порядку, но прислушаюсь вашего мнения. (опишу случаи дополнительно. Хотя и они не претендуют на абсолютную истину.)


По списку include_path, слева направо

В случае если он указан.


В рабочей директории процесса

В случае если запускается иным процессом с измененной средой.


В директории текущего файла

В случае запроса файла клиентом.


Если указан относительный, то он вычислится относительно текущего файла.

И то не всегда.


По сути та же подстановка __DIR__, но на уровне языка, а не пользовательского кода.

Не только __DIR__, но и $PWD (это в sh).

Насколько я знаю, тут точно без претензий на правоту:


В случае если он указан.

Он указан всегда, но может быть пустым, тогда поиск не производится


В случае если запускается иным процессом с измененной средой.

Используется то же значение, что возвращает \getcwd()


В случае запроса файла клиентом.

Просто не понял о чём речь.


И то не всегда.

Знаете исключения? Я не встречал.


Не только DIR, но и $PWD (это в sh).

DIR берёт директорию теущего файла, а $PWD — процесса, нет?

Знаете исключения?

Не клиентский запрос. (Документ А отдельно Документ Б. Для Б нужно содержимое А. В моем понимании это клиентский запрос. (


#document <!-- for example only "Документ Б" -->
<html>
    <head>
        <link rel="stylesheet" href="example.css" /> <!-- Документ А -->
    </head>
    <body>
        <div class="example">
            Example Text:
            <p class="bold upcase">
                must
           </p>
           is bold or upcased.
        </div>
    </body>
</html>

в качестве примера клиентского запроса.))


$PWD — процесса

Да, это текущая директория процесса.

Я знаю об ошибке в исходнике. Но время прошло для её исправления.


P. S. is bold or upcased. -> be bold or up-cased.

Если указан относительный, то он вычислится относительно текущего файла. По сути та же подстановка DIR, но на уровне языка, а не пользовательского кода.


Ошибка. Относительно текущей директории. Которая заранее вам неизвестна и, скорее всего, равна рабочей директории процесса (и она тоже неизвестна и может изменяться)

В общем не пользуйтесь относительными путями в ФС. Так надежнее.

__DIR__ . "/memory.php" в этом контексте мало чем отличается от "./memory.php" с точки зрения семантики поиска. Только сообщения об ошибках разные и вычисление в PHP происходит в первом случае. Если совсем не указывать путь, только имя, то там есть нюансы, да.

Вы ошибаетесь. Указание текущей директории в виде точки или двух точек как раз и приводит к поиску относительно… правильно! текущей директории.

А вот какова она — вы в общем случае заранее не знаете.

Хм… похоже на то, как-то очень давно не пытался использовать скрипты в неизвестном окружении.

Исходите из того, что для PHP любое окружение — неизвестное.

Вы, как разработчик не знаете, как будет запускаться ваш скрипт. Apache (модуль) или php-fpm? Или встроенный сервер? Или cli-режим? ВМ? Контейнер?

Ваша задача сделать так, чтобы максимально абстрагироваться от окружения. А для этого есть отличное правило «явное лучше неявного».

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


А в нынешний контейнерный век это еще и удобно.

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

почти 2018 год на дворе, есть autoloading в composer, есть spl_autoload_register, какой require/include?

Автолоадинги работаю только с классоподобными сущностями языка. Composer можно принудить загружать некоторые файлы при каждом require './vendor/autoload.php', но порядком загрузки управлять нельзя и, если не ошибаюсь, он различается в разных версиях.

Хотя мы разбили документ на 1,216 кусков, мы использовали лишь 459KB памяти. Всё это, благодаря особенности генераторов — объем памяти для их работы равен размеру самой большой итерируемой части. В данном случае, самая большая часть состоит из 101,985 символов.

Нет, не разбили, а всего лишь посчитали кол-во \n\n\n в файле, причем довольно странным подходом (даже с т.з. ограниченния памяти — а вдруг у вас виртуалка на 486sx и 500кбайт оперативки, а вы зачем-то храните временную строку и ищите по ней регуляркой — явный перерасход ресурсов).


И в чем тут особенность генераторов? В том, что вы, в отличие от первого примера, не прочитали весь файл целиком? это всего лишь отличие file_get_contents от fget(s).

С фильтрами есть ещё косяк, что обработка входящих данных в самом фильтра умножает расход памяти на 3 от величины чанка. Намного выгоднее обрабатывать данные без кастомных фильтров.

А почему в reading-files-line-by-line-1.php хранится само содержимое файла в переменной? Почему в других примерах не хранится? Потому что иначе «все не так однозначно»?
По большей части соглашусь.По началу также подумал, что это просто «читерство» для красивых цифр. Но по сути, автор создавал именно функцию для чтения. Т.е если бы мы в цикле не складывали содержимое в $lines а выполняли бы с ней операции, то памяти затратили бы в разы меньше, но потеряли бы в универсальности. Иными словами это уже была бы не функция чтения, а функция выполнения определенной операции над данными. Ну а генератор даёт нам именно такую возможность — многоразовое использование для любых целей. Хотя, откровенно говоря, это можно было бы повторить с помощью цикла и анонимных функций.
Ну так пусть генератор также складывает данные в массив, а то нечестно получается. Ну, или в первый вариант в качестве параметра передается функция обработки одного куска.
Реальный контроль памяти, на который хоть как-то можно полагаться, дает только fread. Если нужно только читать с контролем памяти, то этой функции вполне достаточно.
Если нужно ещё и писать, то делаем обертку для потока. Что дает обертка? Возможность преобразовывать данные и писать в исходящий поток без лишних операций копирования, что и позволяет контролировать расход памяти.

А вот использовать просто фильтры, в которых будут преобразовываться данные, крайне не рекомендуется. Сначала вы получите копию чанка для обработки в фильтре. А потом копию чанка для того, чтобы его из фильтра отдать обратно в поток. Итого 3x вместо x, если бы вместо фильтра была бы обертка.

Какое преимущество дает контроль памяти? Ускорение обработки данных. Если нашему скрипту доступен один гиг оперативки, то можно через обертку читать и отправлять чанками около 1 гига. А через фильтр доступный максимальный размер чанка будет в 3 раза меньше. Выигрыш в скорости будет очень заметен.
А какой-нибудь обертки на mmap() в php нет? Потому что это единственный простой способ читать большие файлы при ограниченной памяти.
Самый простой способ — это fread. Поддерживается из коробки с незапамятных времен.
Для более сложных случаев существуют обертки над потоками, которые тоже существуют с незапамятных времен. Но почему-то иногда владение какой-то частью базового функционала языка рассматривается как продвинутый уровень.
Я так понимаю, что потоки все равно построены вокруг read()? Т.е. все равно надо выделять память для чтения файла?
mmap() хорош тем, что он практически «бесплатен» по памяти для процесса. ОС в любом случае читает данные с диска в свой файловый кеш. Поэтому, почему бы просто не отобразить этот кеш в память процесса? И для программы это тоже плюс: она может обращаться к любой части файла, не тратя при этом ни байта памяти.

Например, обертка для mmap() в питоне просто оборачивает замапленный файл в string-like object. И все. Пиши-читай сколько хочешь.
Я так понимаю, что потоки все равно построены вокруг read()?

Наоборот. Потоки (php_stream) относятся к ядру. Функции работы с файловой системой являются примером реализации на их основе. Обертки являются интерфейсом для создания собственных реализаций.
Весь функционал работы с данными реализован через потоки (соответствующие функции, структуры данных). Там обычное выделение памяти и копирование происходит (malloc и memcpy) через внутрение функции-обертки.

Какая-то поддержка mmap на уровне ядра есть, но не более того. Есть полноценное внешнее расширение для работы с использованием mmap.

Он под капотом задействуется в некоторых ситуациях, в частности при работе с stream

> Пайпинг между файлами

зачем так сложно, когда есть copy?
Прошу прощения, но что стало с хабром? Почему я начинаю все чаще видеть этот бред?
Почему теперь стало так мало дельных статей?
И почему такой шлак пропускают?
С самых первых строк начинается бред:
чтение файла строками:
— первый пример (якобы плохой):
function readTheFile($path) {
    $handle = fopen($path, "r");

    while(!feof($handle)) {
        yield trim(fgets($handle));
    }

    fclose($handle);
}

readTheFile("shakespeare.txt");

require "memory.php";

— второй:
function readTheFile($path) {
    $handle = fopen($path, "r");

    while(!feof($handle)) {
        yield trim(fgets($handle));
    }

    fclose($handle);
}

readTheFile("shakespeare.txt");

require "memory.php";

$iterator = readTheFile("shakespeare.txt");

$buffer = "";

foreach ($iterator as $iteration) {
    preg_match("/\n{3}/", $buffer, $matches);

    if (count($matches)) {
        print ".";
        $buffer = "";
    } else {
        $buffer .= $iteration . PHP_EOL;
    }
}

require "memory.php";


Ниже текст:
Хотя мы разбили документ на 1,216 кусков, мы использовали лишь 459KB памяти. Всё это, благодаря особенности генераторов — объем памяти для их работы равен размеру самой большой итерируемой части. В данном случае, самая большая часть состоит из 101,985 символов.

Какой особенности итераторов? Это тут совсем не причем?
В первом примере тупо складывают в массив:
 $lines[] = trim(fgets($handle));

Если в нем эту строку заменить на:
preg_match("/\n{3}/", trim(fgets($handle)), $matches);
if (count($matches)) {
   print ".";
}

Или во втором поставить:
$lines[] = trim(fgets($iteration));

Расход памяти будет одинаковым.
Генератор нужен для того, чтобы за раз возвращать одно значение, а не все сразу. Какая разница, если все читается из потока одинаковыми кусками?
Дальше даже читать не стал…
В первом пример вставил не тот код, вот этот код:
$lines = [];
$handle = fopen($path, "r");

while(!feof($handle)) {
    $lines[] = trim(fgets($handle));
}

fclose($handle);

Уже минусанули? Автор это вы? Разве я не прав?
Сделайте тесты, чтобы убедиться…

Какая-то жесть это:


preg_match("/\n{3}/", $buffer, $matches);

    if (count($matches)) {

Зачем тут регулярка? Почему бы это не заменить на if (strpos($buffer, "\n\n\n") !== false)?


Да и вообще, заявленного код не делает — вставляется PHP_EOL, а проверяется на три \n, в винде проблемы будут. Кроме того, у нас же построчно всё приходит, можно просто считать количество пустых строк, пришедших подряд, как только их нужно количество, делать что требуется.

Файл тот же, а пиковое использование памяти упало до 393KB!

Да, потому что мы его не прочитали.


Всё это, благодаря особенности генераторов

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


но данный пример хорошо демонстрирует производительность при чтении больших файлов

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


Не похоже ли это на генератор, читающий каждую строчку?

Нет. Я что-то сомневаюсь, что в stream_copy_to_stream() генераторы используются.


Теперь попробуем сделать то же самое с помощью потоков. Потратили немного меньше памяти(400KB) при одинаковом результате

Ага, только не из-за потоков, а из-за того, что она теперь в памяти не хранится.


Но задумайтесь, если у нас есть возможность выбрать иной формат сжатия, затратив в 12 раз меньше памяти

Да не формат сжатия, а потому что file_get_contents() теперь не вызывается. Можно addFile() вместо addFromString() использовать, тоже будет меньше в 12 раз.

Хорошая, годная статья. Возможно ли указать ссылку на неё в php.net/manual/ru/ (в соответствующем разделе).

Пользуясь случаем спрошу у аудитории:
В случае использования apache+mod_php и nginx-php_fpm:
Как часто происходит «парсинг» php-файла — при каждом запросе или как-то «по-умному»? Сервер сам контролирует изменение файла и «перепарсивает» его?

Или парсинг занимает незначительное количество времени относительно всего цикла запроса и не стоит беспокоиться об этом?

В общем случае по умному, есть так называемый OPcache.

Sign up to leave a comment.

Articles