Последнее обновление:
Язык РНР: каким способом будет быстрее получить требуемые сообщения комментариев из файла html?
В этой статье рассмотрим, насколько эффективными по быстродействию являются регулярные выражения и функции класса domDocument
. Допустим, у нас есть файл с сообщениями пользователей на сайте. И мы хотим сделать из него выборку только тех сообщений, которые оставил какой-нибудь один пользователь, например, имеющий ник USER_1
.
Отметим сразу, что в последние годы (а то и десятков лет) многие сайты выполняются с использованием баз данных (БД), а не на файлах. Считается, что файловый способ является уже устаревшим. Пока не будем с этим спорить, лишь приведем ниже определенные доводы.
Например, у нас есть файл с именем comments.html
, содержащий примерно следующее:
<div>
<div id="comm_0" class="qwer" data-time="1740776850">
<span id="user_0" class="tyuio">USER_0</span><div id="mess_0">укенгшщзхъфывапролд</div>
</div>
<div id="comm_1" class="qwer" data-time="1740776850">
<span id="user_1" class="tyuio">USER_1</span><div id="mess_1">йцукенгшщзхъ</div>
</div>
<div id="comm_2" class="qwer" data-time="1740776850">
<span id="user_2" class="tyuio">USER_2</span><div id="mess_2">зхъфывапролджэяч</div>
</div>
<div id="comm_3" class="qwer" data-time="1740776850">
<span id="user_3" class="tyuio">USER_3</span><div id="mess_3">укенгшщзхъфы</div>
</div>
<div id="comm_4" class="qwer" data-time="1740776850">
<span id="user_4" class="tyuio">USER_4</span><div id="mess_4">кенгшщзхъфывапро</div>
</div>
<div id="comm_5" class="qwer" data-time="1740776850">
<span id="user_5" class="tyuio">USER_5</span><div id="mess_5">зхъфывапролджэя</div>
</div>
…
</div>
Т.е. этот файл представляет собой совокупность сообщений пользователей, содержащий их ники и тексты сообщений. Также в одном из блоков каждого сообщения есть атрибут data-time
, который задает метку времени UNIX. Если необходимо, ее можно прочитать средствами javascript и разместить в том или ином месте сообщения в требуемом формате (например, в виде Дата – Время).
Создание тестового файла с сообщениями
Чтобы создать подобный файл, можно использовать следующий код на языке PHP (назовем его create_file.php
):
<?php // Создаем файл с "сообщениями"
mb_internal_encoding('utf-8');
header('Content-type: text/html; charset=utf-8');
$file_name = 'comments.html';
$num_str = 40000; // Варьируется по ходу экспериментов
$word_str = 'йцукенгшщзхъфывапролджэячсмитьбю1234567890qwertyuiopasdfghjkl;zxcvbnm,.';
$len = strlen($word_str);
$str_Arr = preg_split('//', $word_str);
$fd = fclose(fopen($file_name, 'w'));
$fd = fopen($file_name, 'r+');
$comm = '<div>';
fwrite($fd, $comm. "\n");
for($i=0; $i < $num_str; $i++){
$word = substr($word_str, rand(0, 10)*2, rand(11, 20)*2);
$comm = '<div id="comm_'. $i. '" class="qwer" data-time="'. time(). '">
<span id="user_'.$i.'" class="tyuio">USER_'. ($i%10) .'</span><div id="mess_'. $i .'">'.$word. '</div>
</div>';
fwrite($fd, $comm. "\n");
}
$comm = '</div>';
fwrite($fd, $comm. "\n");
fclose($fd);
echo 'Файл создан.';
Число сообщений задается при помощи переменной $num_str
(на данный момент она равна 40000). Меняя ее, затем обновляя страницу (т.е. перезапуская файл create_file.php
), в том же каталоге сервера будет заново создаваться текстовый файл comments.html
. Число блоков с сообщениями в нем будет равно значению переменной $num_str
.
Тестируем функцию preg_match_all
Не будем останавливаться на ее описании, так как это достаточно хорошо изложено в мануале, т.е. на сайте php.net
.
Создадим файл с именем preg_match_all.php
и поместим туда следующий код:
<?php
header('Content-type: text/html; charset=utf-8');
$t0 = microtime (true);
$USER = 'USER_1';
$content = file_get_contents('comments.html');
$str_to_find = '<span id="user_';
$str_to_find1 = '<div id="mess_';
$mess_num = preg_match_all('!'. preg_quote($str_to_find). '[^>]*?>([^<]+)'. '!', $content, $mathes, PREG_PATTERN_ORDER);
$mess_num1 = preg_match_all('!'. preg_quote($str_to_find1). '[^>]*?>([^<]+)'. '!', $content, $mathes1, PREG_PATTERN_ORDER);
$mess_Arr = array_filter($mathes[1], function ($var) use ($USER){
return (strval($var) === $USER);
}) ;
$mess_Arr1 = array();
for($i=0; $i< ($mess_num); $i++){
if(isset($mess_Arr[$i])){
$mess_Arr1[$i] = $mathes1[1][$i];
}
}
echo 'Всего '. $mess_num. ' сообщений, из них '. sizeof($mess_Arr). ' принадлежат пользователю '. $USER. ', всего '. sizeof($mess_Arr1) .'<br/>';
echo '<table border="1"><tr><td>n: '. $i .'</td><td>Общее время:</td><td>'. (1*microtime(true) - 1*$t0). '</td>'.'<td>Максимально используемая память: </td><td>' .memory_get_peak_usage(true) .'</td></tr></table>';
file_put_contents('rezults.txt', implode("\n", $mess_Arr1));
Тестируем класс domDocument
Создаем файл domDocument.php
и там пишем:
<?php
header('Content-type: text/html; charset=utf-8');
$t0 = microtime(true);
$USER = 'USER_1';
$content = file_get_contents('comments.html');
$Encoding = 'utf-8';
define('XMLHead', "<?xml version='1.0' encoding='".$Encoding."'?>");
$domdocument = new domDocument('1.0', $Encoding);
$domdocument->loadXML(XMLHead . "<html><body>" . $content . "</body></html>");
// Получаем все теги с именем div и span
$div = $domdocument->getElementsByTagname("div");
$span = $domdocument->getElementsByTagname("span");
$mess_Arr1 = array();
for($i=0; $i< $span->length; $i++){
if($span->item($i)->textContent === $USER){
$mess_Arr1[] = $span->item($i)->nextSibling->textContent;
}
}
echo '<table border="1"><tr><td>n: '. $i .'</td><td>Общее время:</td><td>'. (1*microtime(true) - 1*$t0). '</td>'.'<td>Максимально используемая память: </td><td>' .memory_get_peak_usage(true) .'</td></tr></table>';
file_put_contents('rezults1.txt', implode("\n", $mess_Arr1));
Если все сделано правильно, оба этих файла будут создавать свои текстовые файлы, содержащие сообщения пользователя с ником USER_1
. Имена этих файлов rezults.txt
и rezults1.txt
соответственно. Их размеры, равно как и содержимое, должны тождественно соответственно совпадать, т.е. быть полностью идентичными. Если такого нет, значит, в одной из приведенных программ РНР была допущена ошибка, следует ее исправить.
Обсуждение результатов (PHP5.3)
Теперь посмотрим, что получается в итоге. Вначале тестирование проводилось с использованием языка РНР5.3 в среде Denwer, операционная система Windows 7. И вот что получилось для разных размеров файлов с сообщениями:
preg_match_all:
n: 10 | Общее время: | 0.00059294700622559 | Максимально используемая память: | 524288 |
n: 100 | Общее время: | 0.0013430118560791 | Максимально используемая память: | 524288 |
n: 500 | Общее время: | 0.0077590942382812 | Максимально используемая память: | 786432 |
n: 1000 | Общее время: | 0.014673948287964 | Максимально используемая память: | 1048576 |
n: 2000 | Общее время: | 0.029839992523193 | Максимально используемая память: | 1835008 |
n: 3000 | Общее время: | 0.037868976593018 | Максимально используемая память: | 2621440 |
n: 4000 | Общее время: | 0.023346900939941 | Максимально используемая память: | 3145728 |
n: 5000 | Общее время: | 0.028859853744507 | Максимально используемая память: | 3932160 |
n: 20000 | Общее время: | 0.11564588546753 | Максимально используемая память: | 14680064 |
n: 40000 | Общее время: | 0.22864890098572 | Максимально используемая память: | 29884416 |
Примечание: при =40000 объем файла с сообщениями составил почти 7 МБ.
domDocument:
n: 10 | Общее время: | 0.00034093856811523 | Максимально используемая память: | 524288 |
n: 100 | Общее время: | 0.0029091835021973 | Максимально используемая память: | 524288 |
n: 500 | Общее время: | 0.037052869796753 | Максимально используемая память: | 786432 |
n: 1000 | Общее время: | 0.092877864837646 | Максимально используемая память: | 1048576 |
n: 2000 | Общее время: | 0.32314991950989 | Максимально используемая память: | 2097152 |
n: 3000 | Общее время: | 0.72289109230042 | Максимально используемая память: | 2097152 |
n: 4000 | Общее время: | 1.5535569190979 | Максимально используемая память: | 2883584 |
n: 5000 | Общее время: | 2.9380710124969 | Максимально используемая память: | 3670016 |

Для наглядности, посмотрим график.
Вывод – однозначен. Функция, основанная на регулярных выражениях, работает гораздо быстрее, чем класс domDocument. И если при числе сообщений до 500 различие в скорости еще не столь существенно, то когда число сообщений достигает 2000 и более, использование класса domDocument
является уже попросту нецелесообразным. Ибо время работы этого класса просто катастрофически возрастает по мере роста объема файла (числа сообщений).
При этом максимальный расход оперативной памяти у этого класса, конечно, немного меньше, чем у функции preg_match_all
(использующие регулярные выражения). Однако, разница – несущественна.
Интересно, что скорость поиска сообщений и в том, и в другом варианте подчиняется полиномиальным кубическим трендам примерно с одинаковой степенью достоверности.
Таким образом, при применении PHP5.3 использование класса domDocument целесообразно, разве что, на небольших файлах. А для мало-мальски серьезной работы об этом классе лучше… забыть. Впрочем, возможно, в следующих версиях языка PHP его работа будет как-то оптимизирована.
Обсуждение результатов (PHP8.0)
Интерпретатор РНР запускался из командной строки в виртуальной машине Windows 7
. И вот что получилось:
preg_match_all:
n: 100 | Общее время: | 0 | Максимально используемая память: | 2097152 |
n: 500 | Общее время: | 0.0010430812835693 | Максимально используемая память: | 2097152 |
n: 1000 | Общее время: | 0.0010120868682861 | Максимально используемая память: | 2097152 |
n: 1000 | Общее время: | 0.0009770393371582 | Максимально используемая память: | 2097152 |
n: 3000 | Общее время: | 0.0016880035400391 | Максимально используемая память: | 4194304 |
n: 4000 | Общее время: | 0.0036709308624268 | Максимально используемая память: | 4194304 |
n: 5000 | Общее время: | 0.0041470527648926 | Максимально используемая память: | 4194304 |
n: 6000 | Общее время: | 0.0054330825805664 | Максимально используемая память: | 6291456 |
n: 7000 | Общее время: | 0.0044009685516357 | Максимально используемая память: | 6291456 |
n: 8000 | Общее время: | 0.0064430236816406 | Максимально используемая память: | 4194304 |
n: 9000 | Общее время: | 0.0084199905395508 | Максимально используемая память: | 4194304 |
n: 10000 | Общее время: | 0.0061380863189697 | Максимально используемая память: | 4194304 |
n: 20000 | Общее время: | 0.018905878067017 | Максимально используемая память: | 8388608 |
n: 40000 | Общее время: | 0.042385101318359 | Максимально используемая память: | 31457280 |
n: 60000 | Общее время: | 0.050030946731567 | Максимально используемая память: | 35651584 |
n: 80000 | Общее время: | 0.058995008468628 | Максимально используемая память: | 52428800 |
n: 100000 | Общее время: | 0.1001980304718 | Максимально используемая память: | 50331648 |
n: 150000 | Общее время: | 0.14366793632507 | Максимально используемая память: | 85983232 |
n: 200000 | Общее время: | 0.16236116409302 | Максимально используемая память: | 100663296 |
n: 250000 | Общее время: | 0.21462988853455 | Максимально используемая память: | 130023424 |
Примечание: при n = 250000 объем файла с сообщениями составил почти 42,1 МБ.
domDocument:
n: 100 | Общее время: | 0.001953125 | Максимально используемая память: | 2097152 |
n: 500 | Общее время: | 0.029081106185913 | Максимально используемая память: | 2097152 |
n: 1000 | Общее время: | 0.13582992553711 | Максимально используемая память: | 2097152 |
n: 1000 | Общее время: | 0.11102414131165 | Максимально используемая память: | 2097152 |
n: 3000 | Общее время: | 1.9373848438263 | Максимально используемая память: | 2097152 |
n: 4000 | Общее время: | 4.1183609962463 | Максимально используемая память: | 2097152 |
n: 5000 | Общее время: | 11.798079013824 | Максимально используемая память: | 4194304 |

Для сопоставления, отложим эти и ранее полученные данные на одном графике.
Как видно, результат более, чем неожиданный. Класс domDocument
, по сути, работает крайне медленно даже при небольшом числе сообщений в файле. И для РНР8.0 он работает еще медленнее (примерно в 2 раза) по сравнению с РНР5.3.
А вот код, основанный на функции preg_match_all
, что интересно, напротив, в PHP8.0 работает существенно быстрее (аж на порядок), чем в РНР5.3.
Чем это вызвано – сложно сказать, не разбирая реализацию данных функций (на языке С++).
8
языка РНР, то она, на одном и том же компьютере, зачастую работает на порядок быстрее, чем старая версия 5
.Ясно одно: прежде, чем использовать ту или иную альтернативу в языке РНР, следует протестировать имеющиеся варианты, почитать отзывы о них. Ибо разница в быстродействии получается весьма высокая.
Окончательный вывод
Как бы ни было грустно, но класс domDocument
языка PHP, по всей видимости, не обладает достаточным быстродействием для мало-мальски больших объемов данных. Вся его область применения – это небольшие файлы (до нескольких мегабайт) XML/HTML.
ЕДИНСТВЕННОЕ, ради чего его возможно применять, это… некое удобство по сравнению с функциями из серии preg_match_all
, не говоря уже о str_replace
. А также - вызванная этим удобством более высокая скорость разработки. И, пожалуй, не более того.
Хотя, вроде как, решения, основанные на использовании готовых функций/классов, должны бы быть как-то более быстродействующими – в интерпретируемых языках. Так обстоит дело практически во всех языках и PHP, вроде как, не должен бы быть исключением. Но, увы…
Видимо, именно поэтому класс domDocument
языка PHP является каким-то... недоработанным. Например, есть поиск тегов по идентификатору (id
), по имени тега. И, вроде бы, все. Даже поиск по классу, по другим атрибутам, как это реализовано в языке javascript, отсутствует. И это понятно, почему: потому что нет смысла делать такой поиск, ведь он будет тормозить при мало-мальски серьезном использовании. Поэтому создатели языка PHP, вероятно, и выполнили лишь самую основную функциональность для класса domDocument
, а потом, по сути, забыли про него. При разработке новых версий РНР уделив внимание наиболее актуальным функциям. И это, как видится, совершенно правильная стратегия.
Xpath
. Автор статьи не тестировал ее, но, судя по отзывам, она - еще более медленная. И годится тоже, в основном, лишь для демонстрации работы.Впрочем, так обстоит дело, как выяснилось на практике, лишь для операций поиска и выборки сообщений. А вот что будет наблюдаться, например, при замене или редактировании сообщений – пока неизвестно. Это надо тоже проводить соответствующие эксперименты. Понятно, однако, что прежде, чем делать замену/редактирование, соответствующее сообщение нужно, по крайней мере, найти. Это наводит на мысль, что и по таким операциям класс domDocument
будет проигрывать по скорости функциям типа preg_match_all
.
Единственное полезное применение класса domDocument
Впрочем, есть у этого класса одно очень полезное применение. Это - быстрая проверка корректности строки на соответствие правилам кода XML или HTML. Это может быть полезным, например, перед сохранением данных в соответствующем формате. Ибо писать соответствующую проверочную функцию самостоятельно - это, мягко говоря, такое себе... Хотя, вполне можно.
Правда, и там есть проблема. Дело в том, что интерпретатор функций типа xmlload
или htmlload
работает лишь для... html 4. Да, до сих пор. Поэтому новые теги, например, <video>
, <aside>
и т.д. вызывают ошибку. Создатели языка обещались, что, вроде бы, в 8-й версии они добавят (или уже добавили?) поддержку новых тегов html 5.
Кстати…
Попутно, зададимся вопросом: а где же могут применяться столь большие файлы, как выше в примере – до 42 МБ? Содержащие сотни тысяч сообщений. Ведь это уже, по сути, объем не столько уже файла, сколько (очень небольшой) базы данных. И очень редко бывает так, чтобы для одной html-страницы имелся такой большой объем комментариев. В самом деле, если принять средний размер одного комментария, вместе с его html-разметкой, равным 4 кБ, то число сообщений в таком файле составит 42.000/4 = 10500 сообщений. Такое, по-видимому, бывает крайне редко. Автору данной статьи за многие-многие годы встречался только один сайт, где на одной странице число сообщений превысило 10 тысяч. Ну, можно вспомнить еще социальные сети, конечно. Когда человек пишет посты в своей ленте. Например, нынче у крупных Telegram-каналов число таких постов как раз составляет сотни тысяч.
Дело в том, что если хранить сообщения пользователей отдельно от контента самой страницы, причем, на каждую страницу делать отдельный файл, то нетрудно догадаться, подобная архитектура сайта будет не намного хуже, чем те сайты, которые выполняются на основе баз данных. А по быстродействию, скорее всего, она будет даже выше, чем на базах данных. Потому что при работе с какой-либо одной страницей код PHP будет обращаться лишь к одному файлу с сообщениями, остальные будут себе спокойно лежать на жестком диске. А при использовании баз данных запрос будет направляться ко всей базе данных. В этом разница. Впрочем, для полноценного вывода необходимо делать, конечно, сравнительные эксперименты. С учетом возможной индексации баз данных, в том числе.
Замечание
Приведенные выше результаты обнадеживают и прямо-таки оптимистично настраивают на использование функции preg_match_all
и аналогичных ей, столь же быстродействующих. Однако, в общем случае их применение может показаться… сложным, особенно начинающим; и для тех, кто привык решать «все проблемы» одним-двумя запросами к базе данных.
Для удобства разработки, а также для обеспечения максимальной скорости работы РНР-кодов, целесообразнее делать соответствующую html-верстку.
Правда, в последние годы среди вебразработчиков бытует утверждение, что, мол, «следует отделять верстку от функциональности». В целом, это правильно, но… они забывают о том, что сам язык РНР и был придуман как раз для того, чтобы СМЕШИВАТЬ html-верстку и код на PHP. Точнее, чтобы вставлять в html-страницы активное содержимое, обрабатываемое сервером. Там, где это требуется. Поэтому видится, что иногда, в целях обеспечения более высокой скорости открытия html-страниц, можно поступиться этим принципом (который, по идее, выглядит несколько спорным). И планировать верстку сообщений таким образом, чтобы она была, по возможности, более приспособлена к использованию простых, но быстродействующих функций языка PHP. Пусть и требующих более тщательной работы.
Другое дело, что использование технологий баз данных существенно облегчает выборку информации. Например, чтобы получить все сообщения одного пользователя, нам пришлось писать программные коды (для демонстрации овзможности). А в случае с базами данных – там будет лишь один запрос (например, SQL). Т.е., по сути, весь код уместится в одну строчку.
Другое дело, если потребуется получить все сообщения пользователя не для какой-либо одной страницы, а для нескольких (в самом сложном случае – для всех). Там также будет, скорее всего, лишь один запрос. А вот при работе с файлами придется просматривать каждый их них и применять один из приведенных выше кодов. Это может, в ряде случаев, существенно увеличить время работы скрипта.
Правда, как и в случае с базами данных, можно реализовать авторскую (кастомную) индексацию. Для этого, конечно, придется, опять же, писать программный код. Тогда как с применением технологий баз данных задача решается просто – путем одной команды.
А какой же тогда смысл от применения файлов вместо баз данных? - может возникнуть очевидный вопрос. Смысла, пожалуй, два.
1. Все дело в безопасности. Не зря ведь, в конце концов, в языке РНР все старые функции для работы с SQL-запросами были признаны устаревшими (т.е. уязвимыми) и вместо них были разработаны полностью новые, аналогичные функции. В сухом остатке это означает, что, в свое время, сайты, которые использовали те (ныне устаревшие) функции, были уязвимыми. И такие уязвимости носили, очевидно, массовый характер. И это правда: достаточно посмотреть статьи в интернете на предмет уязвимостей. Поэтому риск от использования удобных, но готовых решений все же есть. До сих пор.
Однако, не стоит обольщаться. Если вебразработчиком будет принято решение – выполнить сайт на файлах, без использования готовых баз данных, то придется, по сути, создать… свою, кастомную базу данных. Если, конечно, не будет идти речь о простом статическом сайте.
Насколько она будет эффективнее готовых решений – вопрос дискуссионный. И это при том, что каждую мелочь, даже такую, как выборку сообщений по тому или иному критерию, придется реализовывать самостоятельно. И в ряде случаев это будет не слишком простой задачей. Если использовать-то "низкоуровневые" функции типа preg_match_all
. Тут придется, повторимся, тщательно продумывать и HTML-верстку, и структуру каждого соответствующего файла и т.д. Придется думать о том, как ускорить доступ к соответствующей части файла; как производить индексацию (в виде заранее запасенных копий соответствующих выборок). Подобные задачи по силам лишь опытным специалистам в области PHP.
2. Легче восстановить сайт, выполненный на файлах, без использования баз данных, в случае повреждения или взлома сайта. Потому как в данном случае, всего-навсего, потребуется восстановить некоторые поврежденные (или удаленные) файлы. Из бэк-апа, сохраненного на сервере или из их локальной, облачной копии. А вот с базой данных - все сложнее. Ибо она, в отличие от файлов, должна восстанавливаться целиком, как правило. Согласитесь, что восстановить несколько файлов общим размером не более нескольких мегабайт - гораздо проще, чем восстанавливать многогигабайтную базу данных.