Последнее обновление:
Регистронезависимый поиск по строкам в языке РНР
В языке PHP существует ряд функций для осуществления поиска по строкам, в том числе, регистронезависимого. Это такие функции, как stristr
, stripos
, substr_count
и их аналоги mb_stristr
, mb_stripos
, mb_substr_count
для поиска в строках, содержащих многобайтовые символы (например, кириллицу или символ №
на русскоязычной клавиатуре).
Также можно использовать, например, функцию preg_match
, использующую регулярные выражения.
Задача
Поставим задачу: обнаружить, входит ли в состав текстового файла размером, к примеру, 3,3 МБ, закодированного в кодировке windows-1251 (cp1251)
подстрока абвгДе
без учета влияния регистра. Т.е. если в тексте есть абвгде
или АбвГдЕ
, или т.п., то поиск должен считаться успешным.
Отметим, что в самом конце файла есть две подстроки: абвгдЕ
и абВгде
. Т.е. чтобы осуществить поиск и обнаружить эти строки, скрипт должен просмотреть практически весь файл до конца.
Как насчет производительности?
Ведь поиск должен не просто выполняться, а выполняться, по возможности, быстро. Рассмотрим сразу программный код на языке PHP, использующий 7 различный способов поиска. И посмотрим, какой из способов будет работать быстрее:
<?php
$text = file_get_contents('1.txt'); // В кодировке cp-1251
$str = 'абвгДе';
$utf = 'utf-8';
$cp1251 = 'cp-1251';
mb_internal_encoding($utf);
mb_regex_encoding($utf);
header('Content-type: text/html; charset=utf-8');
$t0 = microtime(true);
$variant = 7; // Выбор варианта функции поиска (задается вручную)
$mes = '';
$text = mb_convert_encoding($text, $utf, $cp1251);
if($variant === 1){
if(mb_stristr($text, $str) != false){
echo 'YES. ';
}else{
echo 'NO. ';
die();
}
echo 'Вариант '. $variant. '. Time = '. (microtime(true)-$t0);
$mes = 'mb_stristr($text, $str)';
}
if($variant === 2){
if(mb_stripos($text, $str) != false){
echo 'YES. ';
}else{
echo 'NO. ';
die();
}
echo 'Вариант '. $variant. '. Time = '. (microtime(true)-$t0);
$mes = 'mb_stripos($text, $str)';
}
if($variant === 3){
$text = mb_strtolower($text);
$str = mb_strtolower($str);
if(mb_substr_count($text, $str) != false){
echo 'YES. ';
}else{
echo 'NO. ';
die();
}
echo 'Вариант '. $variant. '. Time = '. (microtime(true)-$t0);
$mes = 'mb_substr_count(mb_strtolower($text), mb_strtolower($str))';
}
if($variant === 4){
$text = mb_strtolower($text);
$str = mb_strtolower($str);
if(mb_strpos($text, $str) != false){
echo 'YES. ';
}else{
echo 'NO. ';
die();
}
echo 'Вариант '. $variant. '. Time = '. (microtime(true)-$t0);
$mes = 'mb_strpos(mb_strtolower($text), mb_strtolower($str))';
}
if($variant === 5){
$text = strtolower_OUR($text, $str);
$str = mb_strtolower($str);
if(strpos($text, $str) != false){
echo 'YES. ';
}else{
echo 'NO. ';
die();
}
echo 'Вариант '. $variant. '. Time = '. (microtime(true)-$t0);
$mes = 'strpos(strtolower_OUR($text), mb_strtolower($str))';
}
if($variant === 6){
$str = '/'. $str. '/iu';
if(preg_match($str, $text) != false){
echo 'YES. ';
}else{
echo 'NO. ';
die();
}
echo 'Вариант '. $variant. '. Time = '. (microtime(true)-$t0);
$mes = 'preg_match(\'/\'. $str. \'/iu\', $text)';
}
if($variant === 7){
$text = strtolower_OUR($text, $str);
$str = mb_strtolower($str);
if(strstr($text, $str) != false){
echo 'YES. ';
}else{
echo 'NO. ';
die();
}
echo 'Вариант '. $variant. '. Time = '. (microtime(true)-$t0);
$mes = 'strstr(strtolower_OUR($text), mb_strtolower($str))';
}
// ******************************************************************************************
$filename = '1_times.csv';
if(!is_file($filename)){
fclose(fopen($filename, 'w'));
}
$str = $variant. ';'. (microtime(true)-$t0). ';'. $mes; // В таком формате сохраняем строки в файле результатов
file_put_contents($filename, $str. PHP_EOL, FILE_APPEND);
$arr = str_getcsv_Arr($filename, ';'); // Читаем файл результатов и преобразуем его строки в двумерный массив
echo '<pre>';
$arr_MIN = 0;
$arr_MAX = 0;
$data_Arr = array(); // Массив мин. и макс. значений по каждому варианту (1...7)
$variant_max = 10; // Можно было бы установить не менее, чем равным числу вариантов - 7.
for($j=0; $j < $variant_max; $j++){
$arr1 = array_filter($arr, function ($k) use ($j){
return $k[0] == $j;
});
$arr2 = array_map(function($k) use ($arr1, &$k_saved){
$k_saved = $k;
return ($arr1[$k][1]) ;
}, array_keys($arr1), array_values($arr1));
if(sizeof($arr1) > 0){
$arr_MIN = min($arr2);
$arr_MAX = max($arr2);
$data_Arr[$j] = $arr_MIN. ';'. $arr_MAX. ';'. sizeof($arr1). ';'. $arr1[$k_saved][2];
}
}
echo '<table border="1"><tbody>';
echo '<tr><td>'. 'Variant'. '</td><td>'. 'Min'.'</td><td>'. 'Max'. '</td><td>'. 'Число <br/>экспериментов:' . </td><td>'. 'Функция:' .'</td></tr>';
for($i=0; $i < $variant_max; $i++){ // Перебор по разным вариантам
if(isset($data_Arr[$i])){
$data_Arr_Arr = explode(';', $data_Arr[$i]);
echo '<tr><td>'. $i. '</td><td>'. $data_Arr_Arr[0]. '</td><td>'. $data_Arr_Arr[1]. </td><td>'. $data_Arr_Arr[2]. '</td><td>'. $data_Arr_Arr[3] .'</td></tr>';
}
}
echo '</tbody></table>';
function strtolower_OUR($text, $str){
// $up = array('А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ё', 'Ж', 'З', 'И', 'Й', 'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ъ', 'Ы', 'Ь', 'Э', 'Ю', 'Я');
// $low = array('а', 'б', 'в', 'г', 'д', 'е', 'ё', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'ю', 'я');
$low = preg_split('//u', mb_strtolower($str), null, PREG_SPLIT_NO_EMPTY); // Преобразуем искомую строку в нижний регистр, затем записываем в массив посимвольно
$up = preg_split('//u', mb_strtoupper($str), null, PREG_SPLIT_NO_EMPTY); // И - в верхний
return str_replace($up, $low, $text);
}
function str_getcsv_Arr($file, $delim){
$csv = array_map(function ($elem) use ($delim){
return str_getcsv($elem, $delim);
}, file($file));
return $csv;
}
После очередного обновления страницы данный программный код запускает поиск в текстовом файле с именем 1.txt
, находящемся в том же самом каталоге, что и файл с приведенным программным кодом. После этого он создает файл результатов с именем 1_times.csv
, если его еще не существовало.
Затем в этот файл записывается результат расчета по тому или иному варианту, в зависимости от значения переменной $variant
(равной 1, …, 7). После чего этот файл прочитывается полностью, из него выбираются результаты, соответствующие тем или иным вариантам, среди них выбирается минимальное и максимальное время, потребовавшееся для расчетов и число расчетов по этому варианту. Наконец, результаты выводятся в таблицу, которая отображается на экране.
Результаты на PHP 5.3:
Вот что получилось при запуске данного скрипта в PHP 5.3 (среда Denwer):
Variant | Время выполнения, сек. | Число экспериментов: |
Функция: | |
Min | Max | |||
1 | 0.6788 | 0.7120 | 11 | mb_stristr($text, $str) |
2 | 0.6513 | 0.6889 | 11 | mb_stripos($text, $str) |
3 | 0.4689 | 0.5111 | 12 | mb_substr_count(mb_strtolower($text), mb_strtolower($str)) |
4 | 0.4565 | 0.4865 | 11 | mb_strpos(mb_strtolower($text), mb_strtolower($str)) |
5 | 0.1495 | 0.1786 | 11 | strpos(strtolower_OUR($text), mb_strtolower($str)) |
6 | 0.1770 | 0.2093 | 11 | preg_match('/'. $str. '/iu', $text) |
7 | 0.1482 | 0.1809 | 12 | strstr(strtolower_OUR($text), mb_strtolower($str)) |
Как видим, рекордсменом по производительности стали функции strpos и strstr
. Функция preg_match
, использующая регулярные выражения, работает чуть медленнее, но тоже вполне приемлемо.
А вот остальные функции имеют в разы худшее быстродействие.
Результаты на PHP 8.0 (запуск из командной строки):
При использовании РНР 8.0 результаты получились достаточно близкими:
Variant | Время выполнения, сек. | Число экспериментов: |
Функция: | |
Min | Max | |||
1 | 0.3235 | 0.3836 | 10 | mb_stristr($text, $str) |
2 | 0.2265 | 0.2807 | 10 | mb_stripos($text, $str) |
3 | 0.2946 | 0.3486 | 10 | mb_substr_count(mb_strtolower($text), mb_strtolower($str)) |
4 | 0.3258 | 0.4116 | 10 | mb_strpos(mb_strtolower($text), mb_strtolower($str)) |
5 | 0.1996 | 0.2390 | 10 | strpos(strtolower_OUR($text), mb_strtolower($str)) |
6 | 0.1249 | 0.1483 | 10 | preg_match('/'. $str. '/iu', $text) |
7 | 0.1290 | 0.1557 | 10 | strstr(strtolower_OUR($text), mb_strtolower($str)) |
Это странно, так как обычно 8-я версия PHP работает примерно на порядок быстрее, чем 5-я (более старая). Однако, здесь каких-то кардинальных различий в быстродействии не обнаружено.
В PHP 8.0 наиболее быстро работает функция preg_match
.
Обсуждение
Рассмотрим подробнее особенности реализации каждого варианта. Так как текстовый файл с текстом для поиска закодирован не в utf-8
, а в cp1251
, то вначале, после прочтения его содержимого, необходимо преобразовать это содержимое в кодировку utf-8
. С тем, чтобы кодировка содержимого и кодировка строки абвгДе
совпадали. Ибо файл скрипта с кодом РНР (содержащий, в том числе, строку абвгДе
) закодирован в utf-8
.
Так как поиск осуществляется по кириллическому (русскоязычному) тексту, то необходимо либо применять специальные функции для поиска мультибайтовых строк.
Напомним, что в кодировке utf-8
каждый русскоязычный (кириллический) символ состоит из двух байтов. И символ № (он такой – единственный на типичной русскоязычной клавиатуре) – из трех байтов.
Поэтому, в общем случае, для поиска придется использовать функции с приставкой mb_
. Либо использовать специальные подходы.
1вариант
Используется функция mb_stristr
. Отметим, что функцию stristr
использовать нельзя в силу только что сказанного.
Обратим внимание на наличие буквы i
. Существуют еще функции без i
, т.е. mb_strstr
и strstr
. Они предназначены для регистрозависимого поиска. Который выполняется, как правило, на прядок быстрее, чем регистронезависимый.
2 вариант
Используется функция mb_stripos
.
3 вариант
Используется функция mb_substr_count
. Однако, у этой функции нет регистронезависимого варианта, поэтому перед применением применяем следующий прием: и искомую строку абвгДе
, и большой текст, в котором она ищется, преобразуем в нижний регистр и уже потом осуществляем поиск.
Для преобразования и поисковой строки, и большого текста (в котором осуществляется поиск) используется функция mb_strtolower
. Которая преобразуется в нижний регистр ВСЕ возможные символы – как в искомой строке, так и в большом тексте, где осуществляется поиск.
4 вариант
Используем функцию mb_strpos
. Она – регистрозависимая, поэтому, как и в предыдущем варианте, и искомую поисковую строку, и большой текст следует преобразовать в нижний регистр – с учетом замечаний из предыдущего пункта.
5 вариант
Используем функцию strpos
. Это – мало того, что регистрозависимая функция, так еще и предназначенная для однобайтовых символов.
mb_strpos
и чем stripos
и mb_stripos
. Поэтому для того, чтобы эта функция работала корректно, следует сделать преобразования, как и выше, в нижний регистр.
В данном варианте, в отличие от варианта 4, большой текст будем преобразовывать лишь частично.
Дело в том, что если в большом тексте преобразовывать в нижний регистр ВСЕ символы, это сильно замедлит работу скрипта. Поэтому пребразуются те и только те символы большого текста, которые содержатся в строке искомой абвгДе
, преобразованной в верхний регистр. Т.е. это - символы А, Б, В, Г, Д, Е
.
Для такого преобразования используется функция strtolower_OUR
.
При этом сама строка абвгДе
тоже преобразуется в нижний регистр, получается абвгде
.
6 вариант
Используется функция preg_match
, основанная на регулярных выражениях. Так как поиск - регистронезависимый и строки - мультибайтовые, то приходится указать флаги регулярного выражения ui
. И, да, строки (как искомая, так и большой текст) должны быть обязательно закодированы в utf-8
.
7 вариант
Используется функция strstr
, по аналогии с вариантом 5.
Выводы
Какие можно сделать выводы. Если тезисно:
- Как в РНР 5.3, так и в PHP 8.0, варианты 1-4 работают заметно дольше, чем варианты 5-7. Поэтому именно варианты 5-7 являются предпочтительными для использования на практике при регистронезависимом поиске мультибайтовых символов в больших текстах.
- С учетом п.1, наиболее предпочтительным, с учетом простоты написании программного кода, является поиск с использованием регулярных выражений, т.е. функция
preg_match
.
Исходя из этого, по всей видимости, можно сделать вывод, что функции mb_strtolower, mb_substr_count, mb_stristr, mb_strpos
могут быть полезными, разве что, для работы с текстовыми строками небольших размеров. Целесообразны они, разве что, для написания такого программного кода, который легче понять. И, пожалуй, не более того. Впрочем, не стоит считать их устаревшими хотя бы потому, что они достаточно широко используются в старых программных кодах.
Судя по результатов экспериментов, разработчики PHP 8.0 усовершенствовали их работу, но пока они (эти функции) все равно проигрывают в быстродействии функции preg_match
.
А вообще, было бы разумным не удалять из языка PHP устаревшие (deprecated) функции полностью, а обособить их в отдельном (динамически) подключаемом модуле. Т.е. сохранить их в виде, своего рода, полифила. Удалив из языка только уязвимые, опасные функции. Кстати, этот подход мог бы касаться не только языка РНР, но и многих других языков.
Это могло бы обеспечить, с одной стороны, ускорение работы новых программных кодов, не использующих устаревшие функции. Так как, в целом, разумное снижение числа функций, используемых в том или ином языке, тем он быстрее работает. А, с другой стороны, - позволило бы все-таки поддерживать программы, написанные ранее, с использованием устаревших функций. Последнее несколько замедлило бы их выполнение, но оно все-таки осуществлялось бы.