Комментарии 47
Кстати по поводу парсинга, машина Тьюринга помогает составить удобный алфавит с алгоритмом на бумажке для направленного прохода по большим данным, который очень легко запрограммировать.
Для такого парсинга существует стриминг…
Вам для JSON пригодится готовая библиотека github.com/salsify/jsonstreamingparser или github.com/kuma-giyomu/JSONParser
Необязательный, третий аругмент stream_copy_to_stream делает то же самое. stream_copy_to_stream читает первый поток по одной строке и пишет во второй.
Поправка: stream_copy_to_stream не оперирует строками, третий аргумент задаёт размер копируемого участка в байтах (можно самому в цикле итеративно копировать чанками конкретного размера, не создавая буферных PHP переменных), если не задан (-1), то копируется от текущей позиции (может со смещением) до конца источника чанками размером 8192 байт.
Но
Когда я вижу
require "memory.php";
относительные пути в ФС — мне становится плохо.
Пожалуйста, не делайте так, особенно в статьях, которыми потом будут руководствоваться начинающие!
Абсолютные указывать?! Мне вот от них становится плохо. Или вы имеете в виду, что будет по include path искать и вам от этого плохо?
Указывать относительные пути — это дорога в ад отладки. Задайте себе простой вопрос «А относительно КАКОЙ конкретно директории будет отсчитываться этот путь?» На моей памяти ни один программист PHP не сумел правильно с первого раза ответить на этот простой вопрос.
А относительно КАКОЙ конкретно директории будет отсчитываться этот путь?
Я попытаюсь ответить.
Относительно рабочей директории процесса.
(Ответ не претендует на абсолютное значение истины.)
Если путь не указан вообще, то, по порядку:
- По списку include_path, слева направо
- В рабочей директории процесса
- В директории текущего файла
Если указан относительный, то он вычислится относительно текущего файла. По сути та же подстановка 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 — процесса
Да, это текущая директория процесса.
Если указан относительный, то он вычислится относительно текущего файла. По сути та же подстановка DIR, но на уровне языка, а не пользовательского кода.
Ошибка. Относительно текущей директории. Которая заранее вам неизвестна и, скорее всего, равна рабочей директории процесса (и она тоже неизвестна и может изменяться)
В общем не пользуйтесь относительными путями в ФС. Так надежнее.
__DIR__ . "/memory.php"
в этом контексте мало чем отличается от "./memory.php"
с точки зрения семантики поиска. Только сообщения об ошибках разные и вычисление в PHP происходит в первом случае. Если совсем не указывать путь, только имя, то там есть нюансы, да.
А вот какова она — вы в общем случае заранее не знаете.
Хм… похоже на то, как-то очень давно не пытался использовать скрипты в неизвестном окружении.
Вы, как разработчик не знаете, как будет запускаться ваш скрипт. Apache (модуль) или php-fpm? Или встроенный сервер? Или cli-режим? ВМ? Контейнер?
Ваша задача сделать так, чтобы максимально абстрагироваться от окружения. А для этого есть отличное правило «явное лучше неявного».
Рискну не согласиться. В подавляющем большинстве случаев (если только это не очередной вордпресс), зафиксировать окружение не только можно, но и необходимо.
А в нынешний контейнерный век это еще и удобно.
Я не пишу "коробочные" продукты, которые будет устанавливать неизвестно кто. Мне окружение либо известно, либо я его задаю, раньше чаще путём постановки задачам админам, сейчас чаще путем всяких докер и докер-композ файлов.
Хотя мы разбили документ на 1,216 кусков, мы использовали лишь 459KB памяти. Всё это, благодаря особенности генераторов — объем памяти для их работы равен размеру самой большой итерируемой части. В данном случае, самая большая часть состоит из 101,985 символов.
Нет, не разбили, а всего лишь посчитали кол-во \n\n\n в файле, причем довольно странным подходом (даже с т.з. ограниченния памяти — а вдруг у вас виртуалка на 486sx и 500кбайт оперативки, а вы зачем-то храните временную строку и ищите по ней регуляркой — явный перерасход ресурсов).
И в чем тут особенность генераторов? В том, что вы, в отличие от первого примера, не прочитали весь файл целиком? это всего лишь отличие file_get_contents от fget(s).
С фильтрами есть ещё косяк, что обработка входящих данных в самом фильтра умножает расход памяти на 3 от величины чанка. Намного выгоднее обрабатывать данные без кастомных фильтров.
Если нужно ещё и писать, то делаем обертку для потока. Что дает обертка? Возможность преобразовывать данные и писать в исходящий поток без лишних операций копирования, что и позволяет контролировать расход памяти.
А вот использовать просто фильтры, в которых будут преобразовываться данные, крайне не рекомендуется. Сначала вы получите копию чанка для обработки в фильтре. А потом копию чанка для того, чтобы его из фильтра отдать обратно в поток. Итого 3x вместо x, если бы вместо фильтра была бы обертка.
Какое преимущество дает контроль памяти? Ускорение обработки данных. Если нашему скрипту доступен один гиг оперативки, то можно через обертку читать и отправлять чанками около 1 гига. А через фильтр доступный максимальный размер чанка будет в 3 раза меньше. Выигрыш в скорости будет очень заметен.
Для более сложных случаев существуют обертки над потоками, которые тоже существуют с незапамятных времен. Но почему-то иногда владение какой-то частью базового функционала языка рассматривается как продвинутый уровень.
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));
Расход памяти будет одинаковым.
Генератор нужен для того, чтобы за раз возвращать одно значение, а не все сразу. Какая разница, если все читается из потока одинаковыми кусками?
Дальше даже читать не стал…
Какая-то жесть это:
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 раз.
Пользуясь случаем спрошу у аудитории:
В случае использования apache+mod_php и nginx-php_fpm:
Как часто происходит «парсинг» php-файла — при каждом запросе или как-то «по-умному»? Сервер сам контролирует изменение файла и «перепарсивает» его?
Или парсинг занимает незначительное количество времени относительно всего цикла запроса и не стоит беспокоиться об этом?
Как прочитать большой файл средствами PHP (не грохнув при этом сервак)