Php strpos() function

Поиск определенного текста с помощью регулярных выраженийFinding specific text using regular expressions

Класс System.Text.RegularExpressions.Regex можно использовать для поиска строк.The System.Text.RegularExpressions.Regex class can be used to search strings. Такой поиск может отличаться по сложности от самых простых до очень сложных текстовых шаблонов.These searches can range in complexity from simple to complicated text patterns.


В следующем примере кода выполняется поиск слов «the» и «their» в предложении без учета регистра.The following code example searches for the word «the» or «their» in a sentence, ignoring case. Статический метод Regex.IsMatch выполняет поиск.The static method Regex.IsMatch performs the search. В метод передается строка и шаблон поиска.You give it the string to search and a search pattern. В нашем примере третий аргумент задает поиск без учета регистра.In this case, a third argument specifies case-insensitive search. Для получения дополнительной информации см. System.Text.RegularExpressions.RegexOptions.For more information, see System.Text.RegularExpressions.RegexOptions.

Шаблон поиска описывает текст для поиска.The search pattern describes the text you search for. Следующая таблица описывает каждый элемент шаблона поиска.The following table describes each element of the search pattern. (В таблице ниже используется один , который в строке C# необходимо экранировать как ).(The table below uses the single , which must be escaped as in a C# string).

ШаблонPattern ЗначениеMeaning
соответствует тексту «the»match the text «the»
Соответствует 0 или 1 вхождению «eir»match 0 or 1 occurrence of «eir»
Соответствует пробелу.match a white-space character

Совет

Методы обычно удобнее при поиске точного совпадения со строкой.The methods are usually better choices when you are searching for an exact string. Регулярные выражения больше подходят при поиске определенных шаблонов в исходной строке.Regular expressions are better when you are searching for some pattern in a source string.

Классификация алгоритмов поиска подстроки в строке[править]

Сравнение — «чёрный ящик»править

Во всех алгоритмах этого типа сравнение является «чёрным ящиком» для программиста.

Преимущества:

позволяет использовать стандартные функции сравнения участков памяти (man *cmp(3)), которые, зачастую, оптимизированы под конкретное железо.

Недостатки:

не выдается точка, в которой произошло несовпадение.

По порядку сравнения паттерна в текстеправить

Прямойправить

Преимущества:

отсутствие регрессии на «плохих» данных.

Недостатки:

не самая хорошая средняя асимптотическая сложность.

Обратныйправить

Паттерн движется по тексту слева направо, но сравнение подстрок происходит справа налево.

Преимущества:

при несовпадении позволяет перемещать паттерн по строке сразу на несколько символов.

Недостатки:

производительность сильно зависит от данных.

Сравнение в необычном порядкеправить

Специфические алгоритмы, основанные, как правило, на некоторых эмпирических наблюдениях над словарём.

По количеству поисковых шаблоновправить

Сколько поисковых шаблонов может обработать алгоритм за один раз.

  • один шаблон (англ. single pattern algorithms)
  • конечное количество шаблонов (англ. finite set of patterns)
  • бесконечное количество шаблонов (англ. infinite number of patterns) (см. Теория формальных языков)

Виды препроцессинга:

  • Префикс-функция
  • Z-функция
  • Бор
  • Суффиксный массив

Алгоритмы, использующие препроцессинг — одни из самых быстрых в этом классе.

Наивный алгоритм поиска[править]

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

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

Таким образом время работы алгоритмы , где — текст, — образец.

Псевдокодправить

Поиск диапазона

— функция, сравнивающая строки по -тому символу.

_, _ — функции бинарного поиска.

Элементы строк нумеруются с единицы

function elementary_search(p: String, s: String): 
    left = 0                                         
    right = n                                        
    for i = 1 to n 
        left = lower_bound(left, right, p, cmp (i) )
        right = upper_bound(left, right, p, cmp (i) )
    if (right - left > 0)   
        print left                   
        print right                 
    else
        print "No matches"

Метод хеширования[править]

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

Определение:
Пусть дана строка . Тогда полиномиальным хешем (англ. polynomial hash) строки называется число , где — некоторое простое число, а код -ого символа строки .

Проблему переполнения при вычислении хешей довольно больших строк можно решить так считать хеши по модулю (или ), чтобы модуль брался автоматически при переполнении типов.

Для работы алгоритма потребуется считать хеш подстроки . Делать это можно следующим образом:

Рассмотрим хеш :

Разобьем это выражение на две части:

Вынесем из последней скобки множитель :

Выражение в первой скобке есть не что иное, как хеш подстроки , а во второй — хеш нужной нам подстроки . Итак, мы получили, что:

Отсюда получается следующая формула для :

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

.

.

Получается : .

Поиск по подстрокам


Скорее всего, вы знаете много разных алгоритмов поиска подстроки в строке. Мы расскажем о тех, что используются в ClickHouse. Сначала введём пару определений:

  1. haystack — строка, в которой мы ищем; типично длина обозначается n.
  2. needle — строка или регулярное выражение, по которому мы ищем; длина будет обозначаться m.

После изучения большого количества алгоритмов могу сказать, что есть 2 (максимум 3) вида алгоритмов поиска подстрок. Первый — создание в том или ином виде суффиксных структур. Второй вид — алгоритмы, основанные на сравнении памяти. Ещё есть алгоритм Рабина — Карпа, который использует хэши, но он достаточно уникален в своём роде. Самого быстрого алгоритма не существует, всё зависит от размера алфавита, длины needle, haystack и частоты вхождения.

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

  1. Кнута — Морриса — Пратта,
  2. Бойера — Мура,
  3. Бойера — Мура — Хорспула,
  4. Рабина — Карпа,
  5. Двусторонний (используется в glibc под названием «memmem»),
  6. BNDM.

Список можно продолжать. Мы в ClickHouse честно всё попробовали, но в итоге остановились на более экстраординарном варианте.

Алгоритм Волницкого

Алгоритм был опубликован в блоге программиста Леонида Волницкого в конце 2010 года. Он чем-то напоминает алгоритм Бойера — Мура — Хорспула, только улучшенную версию.

Если m < 4, то применяется стандартный алгоритм поиска. Сохраним все биграммы (2 идущих подряд байта) needle с конца в хэш-таблицу с открытой адресацией размера |Sigma|2 элементов (на практике это 216 элементов), где оффсеты данной биграммы будут значениями, а сама биграмма — хэшом и индексом одновременно. Изначальная позиция будет на позиции m — 2 от начала haystack. Походим по haystack с шагом m — 1, посмотрим на очередную биграмму с этой позиции в haystack и рассмотрим все значения по биграмме в хэш-таблице. Затем будем сравнивать два куска памяти обычным алгоритмом сравнения. Хвост, который останется, обработаем этим же алгоритмом.

Шаг m — 1 выбран таким образом, что если есть вхождение needle в haystack, то мы обязательно рассматриваем биграмму этого вхождения — тем самым гарантируя, что вернём позицию вхождения в haystack. Первое вхождение гарантируется тем, что в хэш-таблицу по биграмме мы добавим индексы с конца. Это значит, что когда пойдём слева направо, то сначала будем рассматривать биграммы с конца строки (возможно, изначально рассматривая совершенно ненужные биграммы), потом — ближе к началу.

Рассмотрим пример. Пусть строка haystack будет и needle равна . Хэш-таблица будет .

Видим биграмму . В needle она есть, подставляем в равенство:

Не совпало. После в хэш-таблице нет никаких записей, шагаем с шагом 3:

Биграммы в хэш-таблице нет, идём дальше:

Биграмма в needle есть, смотрим на оффсет и находим вхождение:


У алгоритма много плюсов. Во-первых, можно не выделять память на куче, а 64 КБ на стеке не являются сейчас чем-то заоблачным. Во-вторых, 216 — отличное число для взятия по модулю для процессора; это просто инструкции movzwl (или как мы шутим, «мовзвл») и семейство.

В среднем этот алгоритм проявил себя лучше всех. Данные мы взяли из Яндекс.Метрики, запросы почти реальные. Скорость на один поток, больше лучше, KMP: алгоритм Кнута — Морриса — Пратта, BM: Бойера — Мура, BMH: Бойера — Мура — Хорспула.

Чтобы не быть голословным, алгоритм может работать квадратичное время:

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

Поиск по многим подстрокам

Задача заключается в том, что есть много needle, и хочется понять, входит ли хотя бы одна из них в haystack. Существуют достаточно классические методы такого поиска, например алгоритм Ахо — Корасик. Но он оказался не слишком быстрым для нашей задачи. Об этом чуть позже поговорим.

Лёша ClickHouse любит нестандартные решения, поэтому мы решили попробовать что-то иное и, возможно, самим сделать новый алгоритм поиска. И сделали.

Мы посмотрели на алгоритм Волницкого и модифицировали его так, чтобы он начал искать сразу много подстрок. Для этого надо всего лишь добавлять биграммы всех строк и хранить в хэш-таблице дополнительно индекс строки. Шаг будет выбран как минимум из всех длин needle минус 1, чтобы снова гарантировать свойство, что при наличии вхождения мы посмотрим его биграмму. Хэш-таблица вырастет до 128 КБ (строки длиннее 255 обрабатываются стандартным алгоритмом, будем рассматривать не более 256 needle). Я очень ленивый, поэтому вот пример из презентации (читать слева направо сверху вниз):

Начали смотреть, как такой алгоритм ведёт себя по сравнению с другими (строки взяты из реальных данных). И для маленького количества строк он уделывает всех (указана скорость вместе с разжатием — примерно 2,5 ГБ/с).

Дальше стало интересно. Например, при большом количестве похожих биграмм мы проигрываем некоторым конкурентам. Оно и понятно — начинаем сравнивать много кусков памяти и деградировать.

Нельзя сильно ускоряться, если минимальная длина needle достаточно большая. Очевидно, что у нас больше возможностей пропустить целые куски haystack, ничего за это не заплатив.

Переломный момент начинается где-то на 13-15 строках. Примерно 97% запросов, которые я видел на кластере, были меньше 15 строк:

Ну и совсем страшная картинка — 41 строка, много повторяющихся биграмм:


В итоге в ClickHouse (19.5) мы реализовали через этот алгоритм следующие функции:

— — 1, если хоть кто-то из needle входит в haystack. — — самая левая позиция вхождения в haystack (с единицы) или 0, если не нашлось. — — самый левый индекс needle, который нашёлся в haystack; 0, если не нашлось. — — все первые позиции всех needle, возвращает array.

Суффиксы -UTF8 (не нормализируем), -CaseInsensitive (добавляем 4 биграммы с разным регистром), -CaseInsensitiveUTF8 (есть условие, что большие и маленькие буквы должны быть одинакового количества байтов). Реализацию можно посмотреть здесь.

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

Список литературы

  1. Стивенс Род. Алгоритмы теория и практическое применение, 2016. –163с.   2. Кен Браунси. Структура данных и реализация в С++, 2016. –122с.   3. Алгоритм Бойера-Мура , 2020. URL: https://neerc.ifmo.ru/wiki/index.php?title=Алгоритм_Бойера-Мура (дата обращения 10.03.2020).   4. Алгоритмы поиска данных. Бинарный поиск. , 2020. URL: https://www.intuit.ru/studies/courses/648/504/lecture/11466м (дата обращения 08.03.2020).   5. Никлаус Вирт. Алгоритмы и структуры данных, 2017. –114с   6. Алгоритмы поиска данных. Интерполяционный поиск. , 2020. URL: http://kvodo.ru/interpoliruyushhiy-poisk.html (дата обращения 07.03.2020).

Применения[править]

Основные положенияправить

Построим суффиксный массив строки и посчитаем на нем LCP. Для суффикса символом будем обозначать индекс этого суффикса в суффиксном массиве.

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

Для произвольной строки и двух суффиксов, соответствующих ей, введем два условия:

Утверждение:
Строка входит в дважды и не пересекаясь тогда и только тогда, когда она удовлетворяет условию 1.

Необходимое условие:

Если строка входит в дважды и не пересекаясь, то один из суффиксов и хотя бы на длиннее другого. Т.е. условие 1 выполнено.

Достаточное условие:

Из того, что выполняется условие 1 следует, что один из суффиксов хотя бы на длиннее другого. При этом они оба начинаются со строки . Поэтому строка входит в дважды и не пересекаясь.
Утверждение:
Если строка является максимальной входящей в дважды, то она удовлетворяет условию 2.
Пусть это не так и (больше она быть не может). Тогда получим, что меньше, чем длина наибольшего общего префикса суффиксов и , чего быть не может по построению и .

Наивный алгоритмправить

  1. Построим суффиксный массив, посчитаем на нём LCP.
  2. Переберем все пары и такие, что они удовлетворяют условиям 1 и 2 и возьмем среди них максимум по длине строки.

Этот алгоритм можно реализовать за или за , где — время построения суффиксного массива.

Идеяправить

Будем перебирать всевозможные подстроки строки такие, что они входят в дважды и удовлетворяют условию 2 при любых и , где и — суффиксы, соответствующие двум любым вхождениям в (т.е. не обязательно непересекающимся). Для каждой такой строки попробуем найти и , удовлетворяющие условию 1. Таким образом, мы рассмотрим все строки, соответствующие условиям 1 и 2, и, следовательно, найдем ответ. Алгоритм корректный.

Заметим теперь, что искомые строки — это префиксы суффиксов длины . Для того, чтобы найти для каждой такой строки суффиксы и , удовлетворяющие условию 1, воспользуемся стеком.

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

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

Поиск по регулярным выражениям

Расскажем, как в ClickHouse оптимизирован поиск по регулярным выражениям. Много регулярных выражений содержат внутри подстроку, которая обязательно должна быть внутри haystack. Чтобы не строить конечный автомат и проверять по нему, будем вычленять такие подстроки.

Сделать это достаточно просто: любые открывающие скобки увеличивают уровень вложенности, любые закрывающие — уменьшают; также есть символы, специфичные для регулярных выражений (например, ‘.’, ‘*’, ‘?’, ‘\w’ и т. д.). Нам надо достать все подстроки на уровне 0. Рассмотрим пример:

Разбиваем на те подстроки, которые обязаны быть в haystack из регулярного выражения, после чего выбираем максимальную длину, ищем по ней кандидатов и дальше уже проверяем обычным движком регулярных выражений RE2. На картинке выше есть регулярное выражение, оно обрабатывается обычным движком RE2 за 736 МБ/с, Hyperscan (о нём чуть позже) справляется за 1,6 ГБ/с, а мы справляемся за 1,69 ГБ/c на одно ядро вместе с разжатием LZ4. В целом, такая оптимизация лежит на поверхности и сильно ускоряет поиск по регулярным выражениям, но часто её не имплементируют в тулзах, что меня сильно удивляет.

Ключевое слово LIKE тоже оптимизировано с помощью данного алгоритма, только после LIKE может идти очень упрощённое регулярное выражение через %%%%% (произвольная подстрока) и (произвольный символ).

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

Что дальше?

Ещё в рамках подготовки диплома я сделал кучу другой похожей работы (ссылки ведут на пул-реквесты):

— Соптимизировал функцию concat в 2 раза; — Сделал простейший python format для запросов; — Ускорил LZ4 ещё на 4%; — Проделал огромную работу по SIMD для ARM и PPC64LE; — И проконсультировал пару студентов ФКН с дипломами по ClickHouse.

В итоге оказалось, что, по моим впечатлениям, каждый месяц Лёша пытался схантить меня ClickHouse максимально приятная система для написания высокопроизводительного кода, где есть документация, комментарии, прекрасная developer- и devops-поддержка. ClickHouse офигенен, серьёзно. Устали от перекладывания форматов JSON? Придите к Лёше и попросите задачу любого уровня — он вам её предоставит, и за выходные вы получите огромное удовольствие от написания кода.

Но при всех достижениях ClickHouse и его дизайна, дело, наверное, не в них. Даже в первую очередь не в них.

Я прошёл 4 года бакалавриата ФКН, в июне закончил Вышку с красным дипломом, проработал полтора года в офигенной команде в Яндексе, хорошо прокачавшись. Без суммарного опыта за всё это время и железа ничего из написанного в посте не получилось бы. ФКН очень крут, если брать от него максимум. Спасибо Ване Пузыревскому ivan_puzyrevskiy, Игнату Колесниченко, Глебу Евстропову, Максу Бабенко maxim_babenko за то, что встретились в моём забавном приключении на ФКН. А также спасибо всем преподавателям, которые чему-то меня научили.


С этим читают