1ЧТО ЭТО 2======= 3 4Pire (Perl Incompatible Regular Expressions) — это библиотека, 5заточенная на быструю проверку текста большим количеством регулярных 6выражений. Она умеет проверять, матчится ли текст паттерну, но умеет 7делать это действительно быстро (400 Мб/с вне зависимости от сложности 8регулярки). Если же паттернов более одного, их можно объединять, 9выполняя за один проход проверку примерно десятью паттернами (с той же скоростью). 10 11Pire может давать гарантии на скорость работы (на нижнем уровне она 12просматривает входную строку один раз, подряд, без откатов, и тратит 13(на x86 и x86_64) по 5 инструкций на символ), так что в принципе 14может использоваться в realtime-задачах. 15 16Расплачиваться за такую скорость приходится ограниченной функциональностью. 17В pire нету сложных перловых конструкций типа условных регулярок, 18backtracking-а, lookahead-а. Кроме того, pire вообще не занимается 19захватом фрагментов сматчившегося текста (хотя в ограниченном варианте 20такой захват можно реализовать, см.ниже). 21 22Вся теория, которая делает такую скорость возможной, описана в the Dragon Book, 23настоятельно рекомендуемой для изучения. 24 25Pire была разработана в компании Яндекс (изначально как часть робота, но 26в итоге развилась в самостоятельный проект). 27 28 29БЫСТРЫЙ СТАРТ 30============= 31 32#include <stdio.h> 33#include <vector> 34#include <pire/pire.h> 35 36Pire::Scanner CompileRegexp(const char* pattern) 37{ 38 // Переводим шаблон в UCS4 39 std::vector<Pire::wchar32> ucs4; 40 Pire::Encodings::Utf8().FromLocal(pattern, pattern + strlen(pattern), std::back_inserter(ucs4)); 41 // или другая кодировка 42 43 return Pire::Lexer(ucs4.begin(), ucs4.end()) 44 .AddFeature(Pire::Features::CaseInsensitive()) // если хочется нечувствительность к регистру 45 .SetEncoding(Pire::Encodings::Utf8()) // Устанавливаем кодировку, в которой будет приходить проверяемый текст 46 .Parse() // Разбираем шаблон 47 .Surround() // если мы не хотим логику PCRE_ANCHORED 48 .Compile<Pire::Scanner>(); // Компилируем регулярку 49} 50 51bool Matches(const Pire::Scanner& scanner, const char* ptr, size_t len) 52{ 53 return Pire::Runner(scanner) 54 .Begin() // Начало строки 55 .Run(ptr, len) // Строка 56 .End(); // Конец строки 57 // Оно неявно кастится к bool 58} 59 60int main() 61{ 62 char re[] = "hello\\s+w.+d$"; 63 char str[] = "Hello world"; 64 65 Pire::Scanner sc = CompileRegexp(re); 66 67 bool res = Matches(sc, str, strlen(str)); 68 69 printf("String \"%s\" %s \"%s\"\n", str, (res ? "matches" : "doesn't match"), re); 70 71 return 0; 72} 73 74 75КАК ВЫЗЫВАТЬ MATCH 76================== 77 78Скомпилированная регулярка хранится в сканере (т.е. конечном автомате, представленом 79в виде, оптимизированном для прохода по строке). В Pire есть несколько сканеров 80(как минимум Scanner, SlowScanner и SimpleScanner), работа с ними во многом совпадает. 81 82У каждого сканера есть тип State, хранящий состояние автомата, и методы 83Initialize(State&), отдающий начальное состояние автомата, Next(State&, char), 84переводящий автомат в следующее состояние и Final(const State&), сообщающий, 85является ли данное состояние допускающим. Соответственно, чтобы проверить 86строчку на соответствие паттерну, необходимо вызвать Initialize(), Next() 87на каждую букву, и Final(), чтобы проверить, соответствует ли строчка паттерну. 88 89Существует метод Pire::Run(const Scanner&, Scanner::State&, const char* begin, 90const char* end), осуществляющий соптимизированный вызов Scanner::Next() на 91каждый символ диапазона. 92 93В паттерне могут попадаться символы начала и конца строки (^ и $). При компиляции 94они заменяются специальными символьными символами (sentinels) — BeginMark и EndMark. 95 96Если одну и ту же строчку требуется прогнать через два сканера, то можно воспользоваться 97специальной функцией Pire::Run(const Scanner1&, const Scanner2&, Scanner1::State&, 98Scanner2::State&, const char* begin, const char* end), которая работает несколько 99быстрее, чем два последовательных вызова обычной Run(). 100 101Наконец, есть специальный класс RunHelper, облегчающий работу с циклом 102Initialize-Step-Run-Step-Final и позволяющий записать его в виде одного выражения (см.пример). 103 104Существуют ещё четыре функции, полезные при организации лексического разбора: 105LongestPrefix(), LongestSuffix(), ShortestPrefix() и ShortestSuffix(). Они возвращают 106наибольший/наименьший префикс/суффикс строки, допускаемый паттерном. Для использования 107в LongestSuffix() и ShortestSuffix() автомат надо предварительно «вывернуть наизнанку» 108(при помощи Fsm::Reverse()). 109 110 111СКЛЕЙКА СКАНЕРОВ 112================ 113 114Как уже говорилось, время проверки строки на соответствие паттерну не зависит от размера 115паттерна, таким образом с точки зрения скорости работы лучше иметь несколько сложных 116регулярок, чем много простых. В то же время иногда возникает необходимость проверить 117текст достаточно большим количеством регулярок, многие из которых могут быть очень 118простыми. В таком случае существует возможность объединения нескольких сканеров в один. 119 120Сканеры объединяются функцией Pire::Scanner::Glue(). В результате получается сканер, 121который за один проход проверяет строку на соответствие всем содержащимся в нём 122паттернам. Общее количество паттернов возвращает Scanner::RegexpsCount(). Вместо 123Final() имеет смысл вызвать AcceptedRegexps(const State&), возвращающая пару 124итераторов (с семантикой [begin,end)), указывающую на диапазон номеров регулярок, 125которым соответствует прочитанная строка. 126 127Номера начинаются с нуля, при склейке двух сканеров все номера правого сдвигаются на 128количество регулярок в левом. 129 130Склеенные сканеры в свою очередь можно склеить между собой. В то же время процесс склейки 131нельзя повторять до бесконечности, потому как количество состояний в полученном автомате 132растёт экспоненциально с каждым добавленным паттерном. Иногда имеет смысл ограничить 133размер полученного автомата, указав ненулевой параметр maxSize в Glue(). В таком случае 134при превышении указанного размера возвращается пустой автомат (Size() == 0, Empty() == true). 135 136 137РАЗБОР РЕГУЛЯРНОГО ВЫРАЖЕНИЯ 138============================ 139 140До сих пор речь велась о том, как гонять по тексту уже готовый сканер, 141но не говорилось о том, откуда этот сканер взять. Самый простой способ получить 142сканер — это сконструировать его из строки, содержащей регулярное выражение, 143воспользовавшись классом Pire::Lexer. 144 145Строка, задающая регулярное выражение, имеет стандартный синтаксис, похожий 146на POSIX regexps. Поддерживаются стандартные возможности (a|b, a*, a+, ., [a-z], 147a{3}, a{3,5}, a{3,}) и классы символов (\w (буква), \W (не буква), \d (цифра), 148\D (не цифра), \s (whitespace), \S (не whitespace)). Кроме того, могут быть добавлены 149операции a&b (пересечение, см. Fsm::operator &) и ~a (инверсия, см. Fsm::Complement). 150Помимо этого, для Fsm::SlowCapturingScanner есть возможность использовать нежадные аналоги 151повторений - операторы *?, +?, ??. В этом сканере данные операторы будут принимать 152как можно более короткие выражения, в отличие от жадных аналогов (которые, наоборот, принимают 153как можно более длинные. 154 155Lexer принимает регулярку в кодировке UCS-4, как диапазон из wchar32 (или любых других 156типов, неявно преобразовывающихся к wchar32). Таким образом, если регулярное выражение 157задано как const char* regexp и все символы в ней есть в latin-1, можно просто вызвать 158lexer.Assign(regexp, regexp + strlen(regexp)), но если оно задано в UTF-8 или KOI8-R —- 159то его для начала нужно явно перевести в UCS-4. 160 161lexer.SetEncoding() указывает кодировку, в которой будут поступать строки для 162сопоставления с выражением (ещё раз: она не указывает кодировку самого регулярного 163выражения!). Это необходимо, поскольку сканер работает уже не на уровне символов, 164а на уровне байтов, а, скажем, точка (один любой символ) в UTF-8 —- это весьма 165нетривиальная конструкция. 166 167Тонкая настройка лексера возможна посредством добавления туда «фич» (Pire::Feature), 168которые можно добавлять к лексеру вызовами lexer.AddFeature(). Скажем, 169нечувствительность к регистру реализована фичей Pire::CaseInsensitive. 170 171После настройки лексера следует вызвать lexer.Parse(), который вернёт разобранный 172конечный автомат (Pire::Fsm). Его можно скомпилировать в сканер, вызвав 173Fsm::Compile<Pire::Scanner>(). 174 175Иногда шаблон регулярки оказывается слишком сложным, чтобы его можно было скомпилировать 176в детерминированный автомат разумного размера. В этом случае Fsm::Compile() раскрутит 177исключение. Если хочется обрабатывать такую ситуацию, может иметь смысл явно 178вызвать Fsm::Determine(size_t maxSize) и, если он возвратил false, не пытаться 179компилировать автомат в Pire::Scanner, а ограничиться Pire::SlowScanner-ом (см. далее). 180 181 182РУЧНОЕ ПОСТРОЕНИЕ АВТОМАТА 183========================== 184 185Кроме разбора шаблона регулярного выражения, автомат можно строить непосредственно, 186для чего служит класс Pire::Fsm. Наиболее полезные его методы: 187 188 * Конструктор по умолчанию — создаёт автомат, допускающий пустую строку. 189 * MakeFalse() — создаёт автомат, не допускающий ни одной строки. 190 * Size() — размер автомата (количество состояний). 191 * Append(unsigned char c) — добавляет в автомат переход для матча символа c. 192 * AppendStrings(const vector<string>&) — добавляет в автомат переходы для 193 матча хотя бы одной из переданных строк. 194 * operator + (const Fsm&) — возвращает конкатенацию автоматов (допускающую 195 строки, делящиеся на две части так, что первая допускается первым автоматом, 196 а вторая — вторым). 197 * operator | (const Fsm&) — возвращает объединение (union) авмоматов (допускающее 198 строки, допускаемые хотя бы одним автоматом). 199 * operator * () — возвращает итерацию автоматов (допускающую строки, 200 представляющие собой повторение строки, допускаемой исходным автоматом, 201 0 или более раз (т.н. звезда Клини)). 202 * operator ~ () — возвращает инверсию автомата (допускающего строки, которые 203 не допускаются исходным автоматом). 204 * operator & (const Fsm&) — возвращает пересечение автоматов (допускающее строки, 205 которые допускаются обоими автоматами). 206 * operator * (size_t n) — возвращает автомат, допускающий строки, представляющие 207 собой повторение строки, допускаемой исходным автоматом, ровно n раз. 208 * Surrounded() — возвращает автомат, допускающий любые строки, содержащие в себе 209 строку, допускаемую исходным автоматом (т.е. добавляет в начало и в конец по /.*/). 210 * operator +=, operator |=, Iterate, Complement, operator &=, operator *=, и 211 Surround, двойственные к семи выше описанным. 212 * Reverse() — возвращает автомат, допускающий строки, являющиеся зеркальным 213 обращением строк, допускаемых исходным автоматом. 214 215Возможно комбинированное построение автомата (например, 216(lexer1.Parse() | lexer2.Parse()).Compile<Scanner>()). Таким образом возможно, например, 217прочитать из файла список паттернов, скомпилировать их в Fsm'ы и объединить их все 218в один большой Fsm, который скомпилировать в сканер. 219 220 221СКАНЕРЫ 222======= 223 224В Pire существует три сканера: Scanner, SimpleScanner и SlowScanner. 225 226 * Scanner — это основная рабочая лошадка всего Pire. До сих пор речь велась именно о нём. 227 * SimpleScanner — это более простая версия сканера, пригодная для проверки 228 текста одной несложной регуляркой. Она не поддерживает склейки автоматов, 229 и её таблица переходов несколько больше, чем в Scanner-е, зато SimpleScanner 230 работает приблизительно на треть быстрее. 231 * SlowScanner — работает крайне медленно, но не требует детерминизации автомата и, 232 таким образом, может использоваться при матче сложных конструкций типа /x.{50}$/, 233 которые не могут быть скомпилированы в Scanner. 234 235Нужный тип сканера может быть получен вызовом нужной специализации метода Fsm::Compile() 236(или явным конструированием нужного сканера из Fsm-а). 237 238 239КОДИРОВКИ 240========= 241 242«Из коробки» Pire поддерживает кодировки Latin-1 и UTF-8. Нужные экземпляры классов 243Pire::Encoding могут быть получены вызовами соотвествующих функций из namespace Pire::Encodings. 244 245При желании можно добавить поддержку других кодировок, понаследовавшись от Pire::Encoding. 246В наследованном классе следует переопределить методы: 247 248 * wchar32 FromLocal(const char*& begin, const char* end) — читает и возвращает очередной 249 символ входного потока, продвигает begin. В случае невалидной последовательности 250 на входе кидает исключение. 251 * std::string ToLocal(wchar32) — возвращает байтовое представление символа в данной 252 кодировке. Если символ в данной кодировке непредставим, возвращает пустую строку. 253 * AppendDot(Fsm&) — добавляет к конечному автомату фрагмент, допускающий один любой 254 символ в данной кодировке. 255 256После этого экземпляр такого класса можно передать в lexer.SetEncoding() наравне со встроенными. 257 258 259СЕРИАЛИЗАЦИЯ, MMAP() И INLINING 260=============================== 261 262Построенные сканеры могут быть сохранены в std::ostream или загружены из std::istream 263путем вызова Scanner::Save() и Scanner::Load() (аналогично для других сканеров). 264 265Если сканер был сохранен в файл, а файл потом при-mmap()-лен в память, то вместо вызова 266Scanner::Load() можно воспользоваться Scanner::Mmap(), который не будет выполняеть 267копирования таблицы переходов (которая может быть очень большой). Mmap() возвращает 268указатель на первый байт после сериализованного представления регулярки. 269 270Следует, однако, учесть, что начало сканера должно находиться в памяти по адресу, 271выровненному по границе машинного слова. Если в файл пишется ещё что-то кроме 272сериализованных сканеров (сами представления сканеров всегда занимают целое количество 273машинных слов), следует позаботиться о выравнивании самостоятельно или воспользоваться 274классами AlignedInput и AlignedOutput, предоставляющими нужную функциональность. 275 276Сериализованное представление сканера непереносимо между архитектурами (даже между x86 и x86_64). 277При попытке прочитать/приммапить регулярку, сериализованную на другой архитектуре, будет exception. 278 279Если же в коде есть необходимость использовать одно и то же заранее определённое регулярное 280выражение, то можно, конечно, написать static const Scanner = Pire::Lexer(«...»).Parse().Compile(), 281но в таком варианте компиляция выражения будет происходить при каждом запуске программы, 282что может быть достаточно неприятным, особенно если шаблон достаточно сложный. Чтобы 283избежать таких задержек, выражение можно скомпилировать в сканер, сериализовать его, 284подставить сериализованное представление в код в виде строкового литерала, а затем вызвать 285на нём Scanner::Mmap(). Всё это делает программа pire_inline, которая принимает на вход файл 286с кодом на C++, ищет там все вхождения PIRE_REGEXP("pattern", "flags") вне комментариев и строк 287и заменяет их на выражение, возвращающее автомат с готовой предвычисленной таблицей переходов. 288 289В строчке с флагами могут находиться следующие символы: 290 291 * i — нечувствительность к регистру; 292 * u — скомпилировать регулярку в UTF-8; 293 * s — обрамить регулярку .* с каждой стороны; 294 * a — включить поддержку операторов & и ~ в регулярках (см.выше); 295 * g — выполнить преобразование fsm = ~fsm.Surrounded() + fsm (для нужд Scan()). 296 297 298РАСШИРЕНИЯ PIRE 299=============== 300 301Если требуется какая-нибудь функциональность, в Pire отсутствующая, есть вероятность, 302что её можно реализовать, воспользовавшись существующими в Pire точками расширения 303или спустившись на уровень ниже (возможно, до этого стоит прочитать Dragon Book). 304 305 306** ЕЩЕ РАЗ ПРО FSM 307 308Fsm — это конечный автомат с выходом. Соответственно, в нём есть множество состояний 309(пронумерованных целыми числами от 0 до Fsm::Size() – 1), для каждого состояния и 310каждой буквы задано множество состояний, в которые по этой букве из этого состояния 311можно перейти (возможно, это множество пусто), одно состояние объявлено начальным, 312а часть состояний отмечены допускающими. 313 314Для манипуляции этими данными у Fsm есть функции: 315 316 * Size(), Resize(); 317 * Destinations(), Connected(), Connect(); 318 * Initial(), SetInitial(); 319 * Finals(), IsFinal(), SetFinal(), ClearFinal(). 320 321Кроме того, с каждым состоянием и каждым переходом автомата может быть ассоциировано 322битовое поле флагов. Флаги переходов называются выходами (outputs, поскольку реализуют 323классический КА с выходом), для работы с ними предназначены функции Output(), 324SetOutput() и ClearOutputs(). Флаги состояний называются тегами и поддерживаются 325функциями Tag() и SetTag(). 326 327При детерминизации или ином объединении состояний все флаги объединяются побитовым ИЛИ. 328 329Ещё ряд функций не принадлежит никакой группе: 330 331 * Divert(size_t from, size_t to, size_t dest) — разрывает все переходы из `from' в `to' 332 и перенаправляет их в состояние dest. Все флаги, которыми были помечены переходы, 333 переносятся на новые переходы. 334 * Import(const Fsm&) — копирует внешний автомат внутрь данного. Все его состояния 335 сдвигаются на количество состояний в данном. Сами состояния никак не подсоединяются. 336 * DeadStates() — возвращает набор состояний, из которых недостижимо ни одно из допускающих 337 состояний. 338 * RemoveEpsilons() — ликивдирует в автомате все epsilon-переходы (переходы из одного 339 состояния в другое по пустой строке). 340 341 342** ЕЩЕ РАЗ ПРО LEXER И FEATURE 343 344Основная функция Lexer-а — это разбирать входную строку на последовательность 345термов (Pire::Term). 346 347Feature — это возможность изменить поведение лексера. У фичи есть три основные 348возможности: 349 350 * Обработать фрагмент паттерна каким-либо своим способом, вернув терм 351 (функции Accepts() и Lex()); 352 * Изменить уже выделенный терм (Alter()); 353 * Изменить выделенный фрагмент конечного автомата, заключенный 354 в круглые скобки (Parenthesized()). 355 356Если фича разбирает фрагмент паттерна, то она должна реализовывать функцию 357Accepts(wchar32), которая возвращает true, если передаваемый символ является 358началом последовательности символов, воспринимаемой фичей, и функцию Lex(), 359которая собственно и осуществляет лексический разбор (если Accepts() вернула 360false, Lex() вызвана не будет). Внутри фичи доступны стандартные функции 361работы с входным потоком: GetChar(), PeekChar() и UngetChar(). Если 362Accepts() вернула true, а уже внутри Lex()-а стало понятно, что последовательность 363не подходит, можно положить её всю обратно посредством UngetChar(), а затем 364вернуть пустой Term(), показав тем самым лексеру, что фича отказалась 365обрабатывать поток. 366 367В лексере может быть несколько фич, они отсортированы по приоритету и объединены 368в цепочку. Lex() и Accepts() вызываются от большего приоритета к меньшему 369до тех пор, пока какая-нибудь фича не обработает входной поток с получением 370терма. Этот терм пропускается в обратном порядке через Feature::Alter(). 371 372При чтении закрывающей круглой скобки фрагмент автомата, соовтетствующий 373находящемуся в скобках фрагменту регулярки, прогоняется через 374Feature::Parenthesized(), опять-таки в порядке возрастания приоритетов. 375 376Простой пример использования: нечувствительность к регистру сделана именно фичей, 377у которой метод Alter() изменяет каждый возвращаемый диапазон символов, добавляя 378к каждому символу из диапазона его в верхнем и нижнем регистре. 379 380 381** ПРИМЕР: CapturingScanner 382 383Скажем, нам нужен ограниченный захват одного фрагмента в тексте (например, матчить текст 384шаблону /id\s*=\s*['"]([a-z0–9]+)['"]/ и извлекать собственно ID). 385 386 * Заводим два флага для переходов: BeginCapture и EndCapture. 387 * Пишем фичу, в которой отслеживаем все скобки. Lex() возвращает собственно 388 скобку и при этом отсчитывает нужную скобку по порядку. 389 * После того, как нужная закрывающая скобка найдена, в Parenthesized() фича получает 390 автомат, который матчит то, что в скобках. Расширяем этот автомат на два состояния, 391 переносим делаем новым начальным состоянием предпоследнее, единственным допускающим —- 392 последнее, и соединяем их со старым начальным и допускающими состояниями 393 epsilon-переходами. Эти переходы отмечаем соответственно флагами 394 BeginCapture и EndCapture. 395 * Пишем собственный сканер, который при переходе, отмеченном BeginCapure или EndCapture, 396 отмечает соответствующую позицию во входном потоке. 397 398Пример, где всё это реализовано, находится в extra/capture.h и extra/capture.cpp. 399 400