Наткнулся на чрезвычайно простую но интересную задачку, потребовавшую немного выйти за рамки рабоче-крестьянского курса регулярных выражений — и надеюсь краткий рассказ о ней будет полезен тем, кто еще не стал регулярным джедаем.
Безусловно, читая документацию регулярных выражений по диагонали вы, как и я — наверняка не раз наталкивались на опережающие и ретроспективные проверки, но без осознания для какой задачи они могут быть нужны —
Задача банальная — заменить переводы строк на <br/>, за исключением случая, если перед этим шел html-тэг (для простоты только символ >). Отходя от темы — такой алгоритм замены нужен чтобы иметь и автоматическое добавление переводов строки внутри блоков текста в стиле хабра, и при этом не ломать обычную HTML верстку.
Решение в лоб простое как топор — предыдущий символ — часть заменяемого паттерна, который мы повторно вставляем в результат:
preg_replace("/([^>])n/","\1<br />",$text);
И оно в принципе работало целый год пока внезапно не были «канонизированы» переводы строк т.е. чтобы код одинаково работал независимо от операционной системы, любые варианты перевода строк(n, r, rn) были заменены на n. Внезапно 2 перевода строки подряд перестали заменятся на 2 <br/>
Такое поведение вполне разумно (особенно после отладки) — preg_replace не пытяется еще раз проверять то, что он только что заменил во избежание зацикливания — а нам ведь нужно проверять предыдущий символ. Когда переводы строк были не канонизированы — у нас там на самом деле было rnrn (0xd 0xa 0xd 0xa) — и мы заменяли 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 парсерах ограничения слабее, проверяйте документацию).
Соответственно, регулярное выражение получается следующее:
preg_replace("/(?<!>)n/","<br />",$text);
И теперь оно работает и с канонизированными переводами строк и не требует костылей вроде вставления части сматченого регулярного выражения в результат без изменений.
PS. На хабре тема уже затрагивалась в статье Имитируем пересечение, исключение и вычитание, с помощью опережающих проверок, в регулярных выражениях в ECMAScript но читать её нужно усидчиво :-)
Автор: BarsMonster