Библиотека preg для регулярных выражений как в PHP

  !   Данная информация предназначена только только для IT-специалистов по системной интеграции модулей БИОСОФТ-М. (см. Руководства пользователя к программным продуктам)

Что такое Regular Expression все отлично знают (иначе срочно бегут изучать!). Ликбеза не пишу, Только по поводу реализации сабжа в классах preg и pregfind, инкапсулирующих доступ к низкоуровневой библиотеке PCRE.

PCRE эмулирует регулярные выражения стиля Perl (самый распространенный синтаксис) и подробно описана в pcre.txt

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

_________

см. также альтернативу сабжу -    charmap - подмножество ASCII символов

Компиляция паттернов

Класс preg инкапсулирует паттерн, который в скомпилированном виде хранится в объекте и не может быть изменен (однако preg объекты могут быть присвоены или переданы как параметры):

     preg pregIntNumber("^[\-\+]?[0-9]+$");
Кеширование паттернов

Дополнительно Cx обеспечивает фичу (    magicstr - кеширование по константным строкам ) позволяющую предкомпилировать константные паттерны только одни раз за сеанс работы программы. Она по умолчанию применяется ко всем паттернам состоящим только из констант, позволяя для всех практических приложений считать время компиляции паттерна практически нулевым.

В тех же случаях когда сам паттрен является переменным, (или Cx отсутсвует в проекте) надо указывать это явно

preg pregXxx(preg::E_VariablePattern, sPattern)

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

Простой поиск

Для выполнения одной итерации поиска паттерна к preg применяется метод FindIn(sSubject) который возвращает объект pregfind.

pregfind, это тоже read-only объект, олицетворяющий один результат поиска, из него можно узнать все о найденом фрагменте, (а так же о тексте sSubject, который был пропущен между итерациями поиска), и затем из него можно получить следующий pregfind через pregfind::FindNextMatch(), что продолжит поиск следующих фрагментов, удовлетворяющих паттерну далее в том же sSubject.

(хотя для последовательного перебора всех результатов поиска следует использовать итератор как будет описано ниже).

Из pregfind получаем:

- bool IsFound() - найдено чтото или нет;

- str GetMatchSubstring() - найденая подстрока, соответсвующая паттерну;

- int GetMatchPos() - начало найденой подстроки в sSubject

- str GetSkipSubstring() - пропущенный фрагмент sSubject

- int CountMatches() - последовательно искать и подсчитать все оставшиеся случаи соответсвия паттерну, включая текущий;

- и проч., см. хидер (включая GetCaptureByName() см. ниже);

Можно естествено писать и кратко:

     if (preg("....").FindIn(sSubject).IsFound())
         Xxxx();

Самая простая форма проверки наличия подстроки пишется компактно примерно так:

     ASSERTM(
         !preg("\\\\[a-z]+\-").FindIn(sRtf).IsFound(),
         "this sRtf cannot contain negative command args like \\cmd-123");

Простейшая форма проверки строки на полное соответсвие паттерну:

     if (preg(
             "^"  // anchor - checking the whole string, we are NOT searching here
                 "\\\\" // single slash: RTF command prefix
                 "[a-z]+" // command is lowercase one or more letters
                 "\-?" // hypen may precede negative argument
                 "[0-9]*"  // optional argument is a number
                 "$"). // (close anchor)
             FindIn(sRtf).IsFound())
     {
         HandleValidRtfCommand();
Named Capture

В тех случаях когда нам надо извлечь подстроку, соответствующую определенной ЧАСТИ паттерна, этот фрагмент патерна нужно именовать идентификатором и он закаптурится при удачной попытке поиска. Обычно для идентификации каптуров используют последовательные номера, которые назначаются автоматически выражениям в круглых скобках (в фигурных в Visual Studio). У наc нумерование переменных запрещено, и все каптуры должны иметь имя, которое к тому же не должно дублироваться в коде - оно декларируется #define C_szCaptureXxxx.

Синтаксис именования субпаттерна:

    "(?P<" C_szCaptureXxxx ">субпаттерн ")"

получить фрагмент найденный для субпаттерна можно только после успешного поиска (IsFound() == true) методом GetCaptureByName(C_szCaptureXxxx).

Например при извлечении даты в формате год-месяц-день из кривого текста можно написать:

     str sSubject =
         "win 98 OSR 2.1 driver released 97- 6 - 11 provide support for USB 1.0";
 
     // Pattern names
     #define C_szCaptureYear "Year"
     #define C_szCaptureMonth "Month"
     #define C_szCaptureDay "Day"
 
     // Template for date extraction in form of year - month - day
     preg pregDate =
         preg(
             ""
                 "(?P<" C_szCaptureYear ">"
                     "[0-9]{2,4}"
                 ")"
 
                 Preg_space "*"
                 "\\-"
                 Preg_space "*"
 
                 "(?P<" C_szCaptureMonth ">"
                     "[0-9]{1,2}"
                 ")"
 
                 Preg_space "*"
                 "\\-"
                 Preg_space "*"
 
                 "(?P<" C_szCaptureDay ">"
                     "[0-9]{1,2}"
                 ")"
 
                 "");
 
     pregfind pregfindDate = pregDate.FindIn(sSubject);
 
     if (pregfindDate.IsFound())
     {
         str sYear = pregfindDate.GetCaptureByName(C_szCaptureYear);
         str sMonth = pregfindDate.GetCaptureByName(C_szCaptureMonth);
         str sDay = pregfindDate.GetCaptureByName(C_szCaptureDay);
 
         ASSERT(sYear == "97");
         ASSERT(sMonth == "6");
         ASSERT(sDay == "11");
     }
Замена
Простая замена

В простых случаях замену найденых в тексте мачей на фиксированную строчку можно выполнить методом

preg::ReplaceInSubjectWithText(sSubject, sReplaceWith, nLimitMaxCount = off)

как в тесте:

     str sSubject = "abc 123 def 555 1 xxx";
     str sReplaceNumbers = "#";
 
     sResult = preg("[0-9]+").ReplaceInSubjectWithText(sSubject, sReplaceNumbers);
 
     ASSERT(sResult == "abc # def # # xxx");

но таким образом невозможно делать сложные замены, где подставляемая строка переменная и/или зависит от содержания найденных фрагментов или их позиций в sSubject. Обычно для таких случаев используют back references на захваченные каптуры указываемые в подставляемой строке с помощью некоего синтаксиса.

PCRE замены не поддерживает (там только поиск).

Замена с подстановкой найденных фрагментов (back ref)

В типовых ситуациях где заменяемая строка линейно зависит от найденых фрагментов текста можно использовать функцию ReplaceInSubjectWithCaptures().

Вместо того чтобы изобретать хитрый синтаксис для ссылок на named captures эта функция вместо строки для замены принимает массив text. В нем через одну чередуются строки которые нужно подставить буквально и символические имена найденых фрагментов. Таким образом литеральные строчки не требуют искейпов.

//VL: 2010-07-23
    // Replace with substitutions.
    //   In textReplaceWith array every other item is eiter a capture name
    //   or a special reference:
    #define C_szReplaceWithMatchSubstring "*"
    //   So the literal strings and capture name references are
    //   INTERLEAVED in the text array. This way we don't need any special
    //   syntax ($1, \1, ...) to reference the captures.
    //   Literal text can be an empty string.
    //   A capture name must be present in the pattern.
    //   C_szReplaceWithMatchSubstring references the whole found text fragment.
    //
    //   example: replace:  [COLOR=red]Text[/COLOR]
    //               with:  <font color'red'>Text</font>
    //     preg(
    //        Preg_MultilineSubject
    //        Preg_IgnoreCase
    //        "\\[COLOR\\=(?P<Color>[^\\]]+)\\]"
    //            "(?P<Content>.+" Preg_Ungreedy ")\\[\\/COLOR\\]").
    //        ReplaceInSubjectWithCaptures(
    //            sResult,
    //            text(
    //                array<str>()
    //                    << "<font color='"    // literal
    //                    << "Color"            // capture name
    //                    << "'>"               // another literal
    //                    << "Content"          // another capture name
    //                    << "</font>"));       // literal ending
    //
    str ReplaceInSubjectWithCaptures(
            str sSubject,
            text textReplaceWith,
            int nLimitMaxCount = off)
            const;
//VL.
Сложные замены и разбивка текста

Для сложной подстановки или для разбивки текста на фрагменты по заданным синтаксическим границам удобно и надежно использовать итератор пар пропущенных и найденных фрагментов на основе preg::IterateSkipMatchPairs(*& i, *& pregfindIter, sSubject).

Для того, чтобы цикл поиска/замены или разбиения был максимально простым и во избежание дублирования кода, данный итератор повторяется на один раз больше чем будет найдено соответствий паттерну. Сначала итератор возвращает все пары пропущенных фрагметов + найденых фрагментов из sSubject, а затем на последней итерации - остаток sSubject после последнего мача. Это избавляет от необходимости обрабатывать этот остаток отдельно от цикла при формировании результата замены.

Таким образом если мы хотим заменить в примере выше "abc 123 def 555 1 xxx" все цифры на что-то, то нам надо обработать 4 пары фрагметов { skip match }:

 { "abc " & "123" }, { " def ", "555" }, { " ", "1" }, { " xxx", <not-found> }

при замене мы для каждой пары добавляем порцию skip к результату без изменений а match заменяем и тоже добавляем к результату (за исключением последней итерации).

Предположим нам надо сделать заглавными все первые буквы слов в тексте,

это решается так:

     str sSubject = "some Example 9text to caPITALIZE with Preg_word-check";
     str sExpect =  "Some Example 9text To CaPITALIZE With Preg_word-Check";
     str sResult;
 
     preg pregPattern(Preg_word "+");
 
     pregfind pregfindIter;
     for (iter i; pregPattern.IterateSkipMatchPairs(*& i, *& pregfindIter, sSubject);)
     {
         // Always append mismatched fragment verbatim
         str sSkip = pregfindIter.GetSkipSubstring();
         sResult += sSkip;
 
         if (pregfindIter.IsFound())
         {
             // Modify and append matching fragment
             sResult += pregfindIter.GetMatchSubstring().CapFirst();
         }
     }
 
     ASSERT(sResult == sExpect);

Аналогично просто записывается и разбиение текста на фрагменты - просто получаем последовательно pregfindIter.GetSkipSubstring() и pregfindIter.GetMatchSubstring().

В итоге имеем возможнось выполнять замены по гибкости значительно превосходящие логику на back reference.

Сложный пример

И для закрепления понимания приведу продвинутый тест на котором отлаживал всю эту байду.

Предположим требуется в хидере С++

    int GetN();
    str GetText();
    void NonGetFn();
 
  // a comment line
    ref<CXxxx> GetXxxx();
 
        friend long double GetAstronomical(P* pThis);

заменить все Get-функции на эквивалентные Set-функции, при этом корректно создав параметры функций из типов возвращаемых в Get значений, сохранить запись в одну строку и учесть что "friend long double" это отдельно модификатор и тип данных.

Имеем следующее решение:

     str sSubject =
         ""
             "   int GetN();\n"
             "   str GetText();\n"
             "   void NonGetFn();\n"
             "                            \n"
             " // a comment line          \n"
             "   ref<CXxxx> GetXxxx(); \n"
             "\n"
             "       friend long double GetAstronomical(P* pThis);\n"
       ;
 
     str sExpect =
         ""
             "   void SetN(int value);\n"
             "   void SetText(str value);\n"
             "   void NonGetFn();\n"
             "                            \n"
             " // a comment line          \n"
             "   void SetXxxx(ref<CXxxx> value); \n"
             "\n"
             "       friend void SetAstronomical(P* pThis, long double value);\n"
       ;
 
     // Declare all captured subpatterns as named
     #define C_szCaptureModifier "Modifier"
     #define C_szCaptureType "Type"
     #define C_szCaptureFunctionStart "FunctionStart"
     #define C_szCaptureFirstArgs "FirstArgs"
 
     // Patter (is constant here, despite symbolic names present)
     preg pregPattern =
         preg(
             ""
                 // 'friend' keyword may be present, this is not a part of type
                 "(?P<" C_szCaptureModifier ">"
                     "friend"
                     Preg_space "+" // include the following space
                 ")"
                 "?" // 'friend' modifier is optional
 
                 // type spec may be anything not startng as Get function name
                 "(?P<" C_szCaptureType ">"
                     "("
                         // type is restricted to a space separated word starting with
                         //   a lowercase letter but containing any
                         //   non-space/non-newline chars
                         "[a-z]" "[^" Preg_space "\\n]*" "[ \\t]+"
                     ")+" // multiple words allowed (see 'long double')
                 ")"
 
                 // function name and optional parameters
                 "Get" // we only interested in Get functions,
                       //   and dont wanna capture the 'Get' word itself
                 "(?P<" C_szCaptureFunctionStart ">"
                     Preg_word "+" "\\(" // GetXxxx( ...
                     "(?P<" C_szCaptureFirstArgs ">"
                         "[^\\)]*" // optional 1st arg
                     ")"
                 ")"
 
                 // declaration ending
                 "\\)");
 
     str sResult;
 
     // Stylized Loop
     pregfind pregfindIter;
     int nFoundCount = 0;
     for (iter i; pregPattern.IterateSkipMatchPairs(*& i, *& pregfindIter, sSubject);)
     {
         // Always append mismatched fragment verbatim
         str sSkip = pregfindIter.GetSkipSubstring();
         sResult += sSkip;
 
         if (pregfindIter.IsFound())
         {
             nFoundCount++;
 
             // Lookup captures (some maybe empty)
             str sModifier = pregfindIter.GetCaptureByName(C_szCaptureModifier);
             str sType = pregfindIter.GetCaptureByName(C_szCaptureType);
             str sFunctionStart = pregfindIter.GetCaptureByName(C_szCaptureFunctionStart);
             str sFirstArgs = pregfindIter.GetCaptureByName(C_szCaptureFirstArgs);
 
             // (dont need pregfindIter.GetMatchSubstring(); here)
 
             // Compose the result
 
             // note slow: sResult += sModifier + "void Set" + sFunctionStart;
             sResult += sModifier;
             sResult += "void Set";
             sResult += sFunctionStart;
 
             if (sFirstArgs.GetLength() > 0)
             {
                 ASSERT(sFunctionStart.FindPos(sFirstArgs) > 0);
                 sResult += ", ";
             }
 
             sResult += sType;
             sResult += "value)";
         }
     }
 
     ASSERT(nFoundCount == 4);
     ASSERT(sResult == sExpect);
Кратко о поддерживаемом синтаксисе

см preg_readme.txt

META CHARACTERS

 There are two different sets of meta-characters: those that are recognized

 anywhere in the pattern except within square brackets, and those

 that are recognized in square brackets. Outside square brackets, the

 meta-characters are as follows:

     \ general escape character with several uses

     ^ assert start of string (or line, in multiline mode)

     $ assert end of string (or line, in multiline mode)

     . match any character except including newline

     | start of alternative branch

     ( start subpattern

     ) end subpattern

     ? extends the meaning of (

         also 0 or 1 quantifier

         also quantifier minimizer

     * 0 or more quantifier

     + 1 or more quantifier

         also "possessive quantifier"

     { start min/max quantifier

     [ start character class definition

 Part of a pattern that is in square brackets is called a character class.

 In a character class the only meta-characters are:

     \ general escape character

     ^ negate the class, but only if the first character

     - indicates character range

     [ POSIX character class (only if followed by POSIX syntax)

     ] terminates the character class

PREDEFINED CLASSES (enabled in []class):

     Islib Macro PCRE Description

     

     Preg_digit \d any decimal digit

     Preg_notDigit \D any character that is not a decimal digit

     Preg_space \s any whitespace character

     Preg_notSpace \S any character that is not a whitespace character

     Preg_word \w any "word" character (letter or digit or the underscore)

     Preg_notWord \W any "non-word" character

BOUNDARY ASSERTIONS (disabled in []class):

     Islib Macro PCRE Description

     Preg_boundary \b matches at a word boundary (prev xor next char isn't word)

     Preg_notBoundary \B matches when not at a word boundary

     Preg_AtStart \A (^-anchor) matches at start of subject

     Preg_GlobalNext \G (^-anchor) matches at first matching position in subject

                             (matches both original subject start pos and subsequent

                             initial restart pos for the next calls)

     Preg_ZeeEndOrEol \Z ($-anchor) matches at end of subject or before \n at end

     Preg_zeeEnd \z ($-anchor) matches at end of subject

 Lookahead assertions start with

     (?= positive assertions and

     (?! negative assertions.

 Lookbehind assertions start with

     (?<= positive assertions and

     (?<! negative assertions

 Note: If you want to force a matching failure at some point in a pattern, the

       most convenient way: "(?!)"

Capturing

 The Python syntax (?P<SubstringName>...) is used.

     Names consist of alphanumeric characters and underscores,

     and must be unique within a pattern.

 Back references to named subpatterns use the Python syntax (?P=name).

         (?<p1>(?i)rah)\s+(?P=p1)

     matches "rah rah" and "RAH RAH", but not "RAH rah", even though the

     original capturing subpattern is matched caselessly.

Repetitions

 {3} - strict count

 {2,4} - strict range

 {2,} - to infinity

 {,4} - error!

     * = {0,}

     + = {1,}

     ? = {0,1}

 (a?)* - infinite loop, stops if does not match anything

 ? after quantifier removes greediness

CONDITIONAL SUBPATTERNS

 (?(condition)yes-pattern)

 (?(condition)yes-pattern|no-pattern)

 1) If the text between the parentheses

     consists of a sequence of digits, the condition is satisfied if the

     capturing subpattern of that number has previously matched.

     example: ( \( )? [^()]+ (?(1) \) ) -- checks for closing paren if needed

 2) If the condition is the string ®, it is satisfied if a recursive call

     to the pattern or subpattern has been made.

 3) Otherwise it must be an assertion.

     This may be a positive or negative lookahead or lookbehind assertion.

 see pcre.txt

RECURSIVE PATTERNS

 For example, this PCRE pattern solves the nested parentheses problem

     (assume the PCRE_EXTENDED option is set so that white space is ignored):

     \( ( (?>[^()]+) | (?R) )* \)

 PCRE uses (?P>name), which is an extension to the Python syntax that PCRE

     uses for named parentheses.

     We could rewrite the above example as follows:

     (?P<Open> \( ( (?>[^()]+) | (?P>Open) )* \) )

 see pcre.txt

ATOMIC GROUPING AND POSSESSIVE QUANTIFIERS

 (?>\d+)foo

 see pcre.txt

(рекурсивные и все сложные паттерны естественно запрещены, пока не найдем им обоснованного применения)

Обратить внимание

Все знаки препинания в паттерне надо префиксировать "\\" даже не используемые в синтаксисе preg, т.е. "\\," для запятой или "\\%" для процента и т.п.

- Осторожно со слешами!!!! Один раз они удваиваются для С++ и еще раз для preg. Помним, чтоб указать сам символ слеша в паттерне его надо повторить 4 раза!

      if (preg(Preg_boundary "[A-Za-z]\:" "\\\\").FindIn(sInput).IsFound())

           HandleRootDrivePathDetectedSomewhere()

- Везде где определены именованные константы #define Preg_xxx (см. lib__preg.h) можно испльзовать только их, применение скрываемых ими слеш-кодов типа "\\w\\S" ЗАПРЕЩЕНО!У нас тут не Perl!

- По умолчанию выражения non-anchored и

    if (preg("[0-9A-Fa-f]+").FindIn(sInput).IsFound())

НЕ проверяет sInput на то, что это корректное шеcнадцатеричное число, а только проверяет нет ли гдето в нем корректных символов, оно успешно выполнится для например "ab123z@$%!" для точного сличения с паттерном всего текста целиком надо не забывать писать

    if (preg("^[0-9A-Fa-f]+$").FindIn(sInput).IsFound())

- Режимы устанавливаемые в начале паттерна Preg_MultilineSubject, Preg_IgnoreCase, Preg_FriendlySyntax редко нужны (их эффект часто неочевиден), справивайте заранее. Использовать коды (?x) для этой цели запрещено.

Подробнее

см.

lib__preg.h, lib__pregfind.h - детали доступного интерфейса и главное макросы которые необходимо использовать вместо закорючек!

preg_readme.txt - сводка синтаксиса

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

Ограничения в PRD-проектах

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

Любое прикладное использование каждого нового выражения или в каждом новом контексте должно обязательно предварительно обсуждаться в форуме (см.    Прецеденты применения preg ), до тех пор пока

- мы не выработаем правила и контексты где это безвредно,

- пока не убедимся в отсутсвии буг в моем инкапсуляторе,

- и пока я не выявлю разработчиков которые достаточно прониклись идей и не нуждаются в постоянном надзоре.

Болееменее свободно можно применять сабж в ASSERTах и    UnitTests

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

(тесты надо интегрировать в файл lib__preg_test.cpp по аналогии с тем что там есть, но все равно постите и сами тесты сюда тоже, а то забудем интегрировать)