Автор Redline


Урок 3. Специальные приемы


Все рассмотренные ниже приемы используются довольно редко и, возможно, вам они никогда не пригодятся, но если вы хотите изучить RexExp поглубже - то это для вас :)

Лень, жадность и ревность квантификаторов (lazy, greedy and possessive quantifier)


Не путать с грехами ;)
Вернемся к квантификаторам (символам повтора) "*" и "+", по-умолчанию они являются жадными, т.е. они будут "съедать" текст максимально возможной длины, но такое поведение иногда может мешать.
Пример показывает извлечение текста, находящегося между тегами <td> и </td>:

#include <Array.au3>
$sText = 'собака<td>слон</td><td>жираф</td><td>кот</td>рыба'
$sPattern = '<td>(.+)</td>'
$aResult = StringRegExp($sText, $sPattern, 3)
_ArrayDisplay($aResult)


В результате мы получили не совсем то, что хотели :(
Дело в том, что шаблон "<td>(.+)</td>" "съел" весь текст между первым тегом <td> и последним </td>.
Для получения нужного результата нужно чтобы квантификатор "+" стал ленивым, дописываем после него знак "?" и тогда новый шаблон "<td>(.+?)</td>" будет "съедать" наименьший фрагмент текста:

#include <Array.au3>
$sText = 'собака<td>слон</td><td>жираф</td><td>кот</td>рыба'
$sPattern = '<td>(.+?)</td>'
$aResult = StringRegExp($sText, $sPattern, 3)
_ArrayDisplay($aResult)


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

#include <Array.au3>
$sText = 'жираф!'
$sPattern = '(.+)!'
If StringRegExp($sText, $sPattern) Then
    ConsoleWrite(StringRegExpReplace($sText, $sPattern, '$1') & @CRLF)
Else
    ConsoleWrite('none' & @CRLF)
EndIf


Шаблон "(.+)!" сработает примерно так: "(.+)" захватит весь текст, но поскольку после этого стоит "!", то он будет вынужден вернуть восклицательный знак, и на выходе мы будем иметь строку без ! в конце.
Пример с ревнивым квантификатором:

#include <Array.au3>
$sText = 'жираф!'
$sPattern = '(.++)!'
If StringRegExp($sText, $sPattern) Then
    ConsoleWrite(StringRegExpReplace($sText, $sPattern, '$1') & @CRLF)
Else
    ConsoleWrite('none' & @CRLF)
EndIf


"(.++)" "съест" весь текст и не отдаст последний символ для "!", поэтому выражение не даст совпадений шаблону.
Для чего же нужен такой "бестолковый" квантификатор? В первую очередь для ускорения работы регулярных выражений, ведь при проверке шаблонов много времени тратится именно на возвраты, непосредственно в алгоритмах поиска текста ревнивые квантификаторы используются крайне редко.
Прирост скорости при использовании ревнивых квантификаторов мизерный, так что использовать их или нет решайте сами, но медленнее ваши выражения не станут точно. Использовать их надо так, чтобы они захватывали только нужный текст.
Пример:

$sText = 'abc123abc123'
$sPattern = '(\d+)'
$hTimer = TimerInit()
For $i = 1 To 100000
    $aResult = StringRegExp($sText, $sPattern, 3)
Next
ConsoleWrite(TimerDiff($hTimer) & @CRLF)
$sPattern = '(\d++)'
$hTimer = TimerInit()
For $i = 1 To 100000
    $aResult = StringRegExp($sText, $sPattern, 3)
Next
ConsoleWrite(TimerDiff($hTimer) & @CRLF)


В примере шаблон "(\d++)", не может захватить "лишний" текст, поэтому работает верно.
И несколько ссылок на темы, где в решениях применялись ревнивые квантификаторы:
[RegExp] Деление строки по разделителю с условием что разделитель не найден в кавычках
[RegExp] Замена N-ое кол-ство символов пробела на N-ое кол-ство "&nbsp;"
StringRegExp , Замена фрагментов строки по шаблону

Условия просмотра вперед и назад (lookahead & lookbehind assertions)


Данные условия позволяют находить в тексте определенные позиции.
"(?=pattern)" - позитивный просмотр вперед, т.е. это позиция в тексте перед "pattern".
Для ясности пример - нужно из текста "<td>Mark 25</td><td>Twen 15</td><td>Anna 30</td><td>Bob 40</td><td>Antony 60</td>" получить значения ячеек для имен, начинающихся на "A":

#include <Array.au3>
$sText = '<td>Mark 25</td><td>Twen 15</td><td>Anna 30</td><td>Bob 40</td><td>Antony 60</td>'
$sPattern = '<td>(?=A)(\S+\s+\d+)'
$aResult = StringRegExp($sText, $sPattern, 3)
_ArrayDisplay($aResult)


Расшифровка шаблона:
"<td>" - просто строка из литеральных символов
"(?=A)" - указывает на позицию в тексте перед буквой "A"
"(.+?\d+)" - группа с захватом: ".+?" - любое количество любых символов (но не менее одного), ленивый квантификатор "?" не даст съесть весь текст, а только до цифр, "\d+" - любое количество цифровых символов (но не менее одного).
"(?!pattern)" - негативный просмотр вперед, т.е. это позиция в тексте не перед "pattern".
Пример выводит из текста значения ячеек, которые начинаются не на "A":

#include <Array.au3>
$sText = '<td>Mark 25</td><td>Twen 15</td><td>Anna 30</td><td>Bob 40</td><td>Antony 60</td>'
$sPattern = '<td>(?!A)(\S+\s+\d+)'
$aResult = StringRegExp($sText, $sPattern, 3)
_ArrayDisplay($aResult)


"(?<=pattern)" - позитивный просмотр назад, т.е. это позиция в тексте после "pattern".
Пример показывает извлечение имен, которые идут после "USA":

#include <Array.au3>
$sText = '<td>USA Alex</td><td>Ireland Mark</td><td>USA Anna</td><td>India Louis</td>'
$sPattern = '(?<=USA)\s([^<]+)'
$aResult = StringRegExp($sText, $sPattern, 3)
_ArrayDisplay($aResult)


Расшифровка шаблона:
"(?<=USA)" - позиция после USA
"\s" - пробел одна штука
"([^<]+)" - группа с захватом - любое количество любых символов кроме "<", т.е. весь текст после пробела и до <.
"(?<!pattern)" - негативный просмотр назад, т.е. это позиция в тексте не после "pattern".
Пример показывает извлечение имен, которые идут не после "USA":

#include <Array.au3>
$sText = '<td>USA Alex</td><td>Ireland Mark</td><td>USA Anna</td><td>India Louis</td>'
$sPattern = '(?<!USA)\s([^<]+)'
$aResult = StringRegExp($sText, $sPattern, 3)
_ArrayDisplay($aResult)


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

Условные подмаски (conditional subpatterns)


Использование таких подмасок дает возможность направить RegEx в нужном направлении, при выполнении (или невыполнении) условия.
В качестве условий выступают рассмотренные выше Lookahead и Lookbehind условия.
Возможный формат условных подмасок:
"(?(condition)yes-pattern)"
"(?(condition)yes-pattern|no-pattern)"
"condition" - само условие
"yes-pattern" - подмаска, выполняемая при выполнении условия
"no-pattern" - подмаска, используемая при невыполнении условия
Пример (шаблон типа "(?(condition)yes-pattern|no-pattern)") из многострочного текста для стран, начинающихся на букву "Р" выводит всю строку, а для других только название страны:

#include <Array.au3>
$sText = 'Страна: Китай; Столица: Пекин; Население: 1347млн.' & @CRLF
$sText &= 'Страна: Россия; Столица: Москва; Население: 144млн.' & @CRLF
$sText &= 'Страна: США; Столица: Вашингтон; Население: 313млн.' & @CRLF
$sText &= 'Страна: Румыния; Столица: Бухарест; Население: 21млн.'
$sPattern = 'Страна:\s(?(?=Р).+|[^;]+)'
$aResult = StringRegExp($sText, $sPattern, 3)
_ArrayDisplay($aResult)


Расшифровка шаблона:
"'Страна:\s" - слово "Страна" с пробелом на конце
"(?=Р)" - условие позитивного просмотра вперед - позиция перед буквой "Р", т.е. сначала идет "Страна, пробел", а потом буква "Р", перед которой и встанет курсор (если конечно найдет такую позицию)
".+" - захватит всю строку до символа начала новой строки
"[^;]+" - захватит всё, пока не встретит символ ";"
Второй пример (шаблон типа "(?(condition)yes-pattern)") из текста выводит слова, которые начинаются на с буквы "т":

#include <Array.au3>
$sText = 'текст окно тело горн овес порт отвертка кино тоник'
$sPattern = '(?:\s|^)(?(?=т)\S+)'
$aResult = StringRegExp($sText, $sPattern, 3)
For $i = 0 To UBound($aResult) - 1
    $aResult[$i] = '[' & $aResult[$i] & ']'
Next
_ArrayDisplay($aResult)


Расшифровка шаблона:
"(?:\s|^)" - группа без захвата - начало строки или пробельный символ
"(?=т)" - позиция перед буквой "т" (сразу за пробелом или началом строки)
"\S+" - любое количество непробельных символов, но не менее одного (этим и производится захват слова от буквы "т" до конца слова)

Важное замечание

Есть один неприятный момент - при использовании условных масок (даже при использовании групп без захвата) в вывод попадает все что лежит за условной подмаской и записано в шаблоне, во всяком случае у меня не получилось иначе.
Т.е. в вывод второго примера попадают пробелы. Если в первом примере часть шаблон изменить на такой: "(?:рана:\s)(?(?=Р).+|[^;]+)" или "рана:\s(?(?=Р).+|[^;]+)", то результат будет одинаков (кусок слова "рана" из "Страна" попадет в вывод).

Атомарная группировка (atomic grouping)


Это группировка без захвата и без возврата найденных значений.
"(?>pattern)" - такая группировка обладает свойствами группы без захвата - "(?:pattern)" и ревнивого квантификатора одновременно, т.е. всё что будет соответствовать такому шаблону останется при нем и не попадет в вывод RegExp.
Пример выводит из текста имена, чей возраст лежит в пределах от 20 до 39 лет:

#include <Array.au3>
$sText = 'Anna 25 Joe 30 Carl 20 Piter 13 Lora 21 Nicole 33 Ted 22 Bob 42'
$sPattern = '\S+\s(?>2|3)\d'
$aResult = StringRegExp($sText, $sPattern, 3)
_ArrayDisplay($aResult)


Расшифровка шаблона:
"\S+\s" - любое количество непробельных символов, но не менее одного и один пробел
"(?>2|3)" - атомарная группировка - выбор между цифрами 2 и 3
"\d" - один цифровой символ
Поскольку атомарная группировка не захватывает символы, то в вывод не попадает то что находится внутри скобок, а то что соответствует шаблону в целом.
Второй пример демонстрирует ревность атомарной группировки:

#include <Array.au3>
$sText = '1234'
$sPattern = '(?>\d+)4'
$aResult = StringRegExp($sText, $sPattern, 3)
_ArrayDisplay($aResult)


Атомарная группировка является сверхжадной и данный пример не выдаст результата, т.к. часть шаблона "(?>\d+)" "съест" все цифры до конца строки и не отдаст последний символ для "4" (чтобы убедиться в этом исправьте ">" на ":" - тогда данная группа перестанет быть сверхжадной и RegExp даст совпадение)
Кроме вышеуказанных свойств атомарная группировка как и сверхжадная квантификация не делает откатов, а значит экономит время на проверку выражения.