Опережающие и ретроспективные проверки в регулярных выражениях

  • Tutorial
Наткнулся на чрезвычайно простую но интересную задачку, потребовавшую немного выйти за рамки рабоче-крестьянского курса регулярных выражений — и надеюсь краткий рассказ о ней будет полезен тем, кто еще не стал регулярным джедаем.

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

Задача банальная — заменить переводы строк на <br/>, за исключением случая, если перед этим шел html-тэг (для простоты только символ >). Отходя от темы — такой алгоритм замены нужен чтобы иметь и автоматическое добавление переводов строки внутри блоков текста в стиле хабра, и при этом не ломать обычную HTML верстку.

Решение в лоб простое как топор — предыдущий символ — часть заменяемого паттерна, который мы повторно вставляем в результат:
preg_replace("/([^>])\n/","\\1<br  />",$text);

И оно в принципе работало целый год пока внезапно не были «канонизированы» переводы строк т.е. чтобы код одинаково работал независимо от операционной системы, любые варианты перевода строк(\n, \r, \r\n) были заменены на \n. Внезапно 2 перевода строки подряд перестали заменятся на 2 <br/>

Такое поведение вполне разумно (особенно после отладки) — preg_replace не пытяется еще раз проверять то, что он только что заменил во избежание зацикливания — а нам ведь нужно проверять предыдущий символ. Когда переводы строк были не канонизированы — у нас там на самом деле было \r\n\r\n (0xd 0xa 0xd 0xa, кстати, запоминать последовательность спец.символов можно как ReturN) — и мы заменяли \n, а \r — оставался, и именно он проверялся регулярным выражением на соответствие '>'. После канонизации, у нас пропадал этот «резерв» в 1 символ, и preg_replace начинал проверять строку на соответствие регулярному выражению непосредственно с символа \n — и естественно замены не происходило.

Именно для решения таких проблем и существуют Look-ahead и Look-behind выражения (с которыми я лично раньше не сталкивался).

Look-ahead & Look-behind Zero-Width Assertions (опережающие и ретроспективные проверки) — это возможность создать свои аналоги $ и ^: они задают условие, которое должно выполнятся или не выполнятся в начале или конце строки, и не являются частью «сматченого» выражения, т.е. не будут заменены в preg_replace. Это именно то, что нам нужно для этой задачи.

Look-behind — «смотрит» назад, соответственно ставится в начале регулярного выражения.
Look-ahead — в конце, и «смотрит» вперед.

Синтаксис у них такой:
(?<=pattern) положительное look-behind условие
(?<!pattern) отрицательное look-behind условие
(?=pattern) положительное look-ahead условие
(?!pattern) отрицательное look-ahead условие

На Look-behind assertions движками регулярных выражений накладываются различные ограничения — в большинстве случаев он должен проверять выражение фиксированной, известной заранее длины (В Java и .NET парсерах ограничения слабее, в JavaScript — не поддерживается вообще, проверяйте документацию).

Благодаря senia мы можем ознакомиться с исчерпывающей таблицей совместимости различных парсеров регулярных выражений, вот что касается нашей темы:
Feature .NET Java Perl PCRE ECMA Python Ruby Tcl ARE POSIX BRE POSIX ERE GNU BRE GNU ERE XML XPath
(?=regex) (positive lookahead) YES YES YES YES YES YES YES YES no no no no no no
(?!regex) (negative lookahead) YES YES YES YES YES YES YES YES no no no no no no
(?<=text) (positive lookbehind) full regex finite length fixed length fixed + altern ation no fixed length no no no no no no no no
(?<!text) (negative lookbehind) full regex full regex finite length fixed length fixed + altern ation no fixed length no no no no no no no
Соответственно, регулярное выражение с использованием отрицательной ретроспективной проверки получается следующее:
preg_replace("/(?<!>)\n/","<br  />",$text);

А если переписать для демонстрации с положительной ретроспективной проверкой:
(«До» должен быть любой символ кроме ">")
preg_replace("/(?<=[^>])\n/","<br  />",$text);

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

PS. На хабре тема уже затрагивалась в статье Имитируем пересечение, исключение и вычитание, с помощью опережающих проверок, в регулярных выражениях в ECMAScript но название у неё страшное и читать её нужно усидчиво :-)

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 48

    +7
    Сколько этим пользовался, а только сегодня узнал, что они называются опережающие и ретроспективные )
      +15
      Отдельное вам спасибо за уважение к русскому языку и попытку переода терминов. Хотя бы в заголовке.
        +4
        Раз такое дело: мало кто знает ещё о так называемых «possessive quantifiers». При этом они предоставляют довольно интересное поведение.

        Почитать: www.regular-expressions.info/possessive.html
          +2
          В регулярках вообще много такого, что мало кто знает. В PCRE есть «глаголы» (VERBS), например. Про рекурсивные регулярки мало кто слышал почему-то и так далее.
            +3
            Смею предположить потому, что всегда проще написать две регулярки последовательно, вместо того, чтобы ломать мозг потенциальному поддерживателю кода.
              0
              Это как сказать. Например, глагол «UCP» (так кажется, могу путать) нужен для корректной работы с UTF-8.
                0
                Чтобы использовать магическое слово «UCP» — не обязательно знать про «VERBS в PCRE», правда?
                  +1
                  Собеседовал недавно человека, решаем задачку… Он в конце регулярного выражения ставит модификаторы isU

                  Спрашиваю «А зачем они нужны и что значат»?
                  Ответ — «Не знаю, они нужны чтобы все правильно работало, всегда их ставлю»
                    0
                    Смешно, конечно.

                    Но я себя заставлял разбираться в PCRE года через три, после того, как начал пользоваться всякими регулярками. Просто болк — он педант, он шагу не ступит, пока не проверит поле вокруг на триста миль вперед на отсутствие мин. Это очень ценное качество, но оно не всем дано.

                      +5
                      Зато человек не жадный.
                        0
                        Я не про модификаторы, я про «глаголы».
                        0
                        Неправильно. UCP — это и есть VERBS. Применяется он, например, так: /(*UCP)\w+/.
                          0
                          Я знаю, как он применяется, спасибо.

                          Это никак не отменяет тезис «увидел на стаковерфло, и повторил», правда?

                    0
                    Рекурсивное регулярное выражение — это уже не регулярное выражение, а стековая машина. Это не делает их менее полезными, но важно понимать, что это уже не конечный автомат.
                      +3
                      Рекурсивное регулярное выражение — это всё ещё регулярное выражение, пусть и не автомат. Где есть требование делать их конечным автоматом?
                        0
                        Классическое использование термина «регулярное выражение» означает «выражение, описывающее некоторый регулярный язык». Это, как мы видим, не всегда так, но мою чуткую душу это коробит, так что вот и напоминаю.
                      0
                      Я много раз читал про рекурсивные регулярки, но мне так ни разу не попалась в жизни возможность ими воспользоваться. Зачем они? :)
                        +2
                        Например, мне как-то нужно было матчиться на функцию, в которую могла быть вложена другая функция. Что-то вроде sin(cos(0.3))
                          0
                          Ну так с ходу если максимальный уровень вложенности известен и он не больше двух то можно и без рекурсивок обойтись.
                            +1
                            Неизвестен он.
                    +8
                    Простите за банальность, но традиционно, про html и регекспы
                    Вас спасает надежда на то, что пользователь не будет злоупотреблять, но вообще когда-нибудь оно обязательно сломается ;)
                      0
                      Да, именно на этот вопрос я натыкался когда столкнулся с проблемой.
                      Но в данном случае пользователь — только я, и я не склонен ломать свой сайт ;-)
                      +1
                      www.books.ru/books/regulyarnye-vyrazheniya-3-e-izdanie-fail-pdf-626982/?show=1 там еще много интересного есть :)
                        0
                        Хорошая книга, тоже рекомендую, даже не жалко бумаги на бумажный вариант.
                        +1
                        Может лучше было использовать Markdown или другой движок? Пусть заморочка с абзацами (два пробела в конце строки или одна пустая), но зато готовый набор множества других «плюшек»
                          0
                          Ага. Здравствуй, бухгалтер, теперь ты пишешь на маркдауне.
                            +1
                            Неа. Здравствуй, бухгалтер, теперь ты пишешь на HTML. Бухгалтеру необязательно говорить, что две решетки перед заголовком это «MARKDOWN».
                            Но вообще я согласен. Даже заморочка с абзацами и переносами для некоторых целевых групп неприемлемы.
                            0
                            Изначально(лет 10 назад) на том сайте так и было, но с течением времени оказалось, что обычный HTML проще и удобнее.
                            Markdown у всех разный, а HTML — везде один :-)
                              0
                              HTML тоже разнится (я о кроссбраузерности). Хотя в данном случае набор поддерживаемых тегов подразумевается ограниченный весьма?
                                0
                                Да, как на хабре. Никаких css, и прочих радостей жизни.
                            0
                            Мне кажется, что стоит упомянуть, что данные проверки относятся к так называемым «позиционным» проверкам, т.е. они совпадают не с текстом, а с позицией в тексте. При этом текст не «поглощается», что позволяет матчить его другой частью регулярного выражения.
                              +3
                              Можно еще вспомнить, что опережающие проверки можно использовать перед искомым выражением (там, где обычно пишут условия для ретроспективной проверки) для имитации логического оператора «И» в регулярных выражениях. Например,
                              (?=.*\d)(?=^[A-Z]).{8,}
                              будет искать строки длиной не менее 8 символов, в которых есть хотя бы одна цифра И которые начинаются с прописной латинской буквы.
                                +4
                                Есть еще редко используемый модификатор — x. Он позволяет писать комментарии и игнорирует переносы строк:

                                $str = 'test-pcre-comments76755';
                                preg_match('
                                	~
                                	^      #начало строки
                                	(\w+)  #слово
                                	.+?    #любая последовательность (не жадная)
                                	(\d+)  #число
                                	$      #конец строки
                                	~x
                                ', $str, $matches);
                                var_dump($matches);
                                

                                array
                                  0 => string 'test-pcre-comments76755' (length=23)
                                  1 => string 'test' (length=4)
                                  2 => string '76755' (length=5)
                                
                                  0
                                  Он ещё и пробелы игнорирует.
                                  +1
                                  Наверное стоило так же упомянуть, что есть так же разные диалекты (в одном из них эти проверки не работаю), которые используют разные языки. К примеру в JS ретроспективная проверка не работает :). Я сейчас уже не помню, всех тонкостей, но их много
                                    0
                                    Там проблемы в основном с look-behind — это упомянуто. Look-ahead работает обычно везде.
                                    Добавил про JS.
                                      0
                                      C Look-ahead в JS не сталкивался, чаще требуется look-behind. Был разочарован, когда пол-часа долбился с написанием правила для такой проверки, и удивлялся, почему не работает. Оказалось что просто JS не поддерживает ее.
                                      +1
                                      Упоминать это можно в виде такой таблички.
                                        +1
                                        Неплохо бы добавить табличку к посту )
                                    –1
                                    Назовите хотя бы одну причину, почему не надо использовать нормальный html парсер, а пользоваться регексами, которыми невозможно нормально парсить context-free grammar?
                                      +4
                                      Причина проста: гвозди забивают молотком, а не микроскопом.

                                      Именно парсить HTML (выкусывать ссылки, картинки и прочее) — конечно стоит парсером.
                                      Заменять переводы строк — можно тем, что работает проще, быстрее и не требует поддержки.
                                        0
                                        Бред. Имеет смысл только для написанных на коленке приложениях для себя, или для очень небольших объемов html. Регексы, особенно чужие — write-only код, со всеми вытекающими.
                                          +1
                                          Еще раз — я не предлагаю выдирать урлы картинок регэкспорм — хотя это и можно сделать.
                                          Регулярные выражения в 20 символов для простых задач — проще и понятнее, чем парсер.
                                      0
                                      С некоторых пор пользуюсь Jevix в своих проектах — очень доволен!

                                      Практически забыл про оформление текстов. Вы, кстати, тоже им пользуетесь, прямо сейчас, на Хабре.
                                        0
                                        Вместо регэкспов можно спокойно использовать стандартную функцию nl2br
                                          +3
                                          Такое выражение поломается
                                          <ul>
                                          <li></li>
                                          <li></li>
                                          </ul>
                                          
                                          0
                                          В Perl еще есть нечасто используемый ключик e, который позволяет запускать код Perl и использовать возвращаемое значение в подстановке:

                                          # Преобразовываем urlencoded строки в читаемые utf8
                                          s/ ( (?: %[0-9A-F]{2} )+ )/ { my $a1 = uri_unescape($1); utf8::decode($a1); $a1; } /gex;

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

                                          Самое читаемое