И снова регулярные выражения. В третьей, завершающей статье я расскажу об использовании конструкции выбора (альтернативы), а также будут рассмотрены различные варианты позиционных проверок.
Альтернатива
Символ | (вертикальная черта) в регулярных выражениях означает «или» и позволяет выбрать один из нескольких вариантов. К примеру, конструкция a|b|c означает ″или a или b или c″, где a b и c являются альтернативами. Если взять выражение, рассмотренное в первой части:
Get-Service | where {$_.Name -match ″v[ds]s″}
то с помощью альтернатив его можно записать так:
Get-Service | where {$_.Name -match ″v(d|s)s″}
или так:
Get-Service | where {$_.Name -match ″vds|vss″}
Примечание. Не стоит сравнивать символьные классы и альтернативы. Хотя в предыдущем примере конструкции [ds] и (d|s) выдают одинаковый результат, в общем случае это совершенно разные вещи. Символьный класс описывает только один символ, тогда как в качестве альтернативы может выступать регулярное выражение неограниченной длины и сложности.
Хотя данная статья посвящена регулярным выражениям, не стоит слишком на них зацикливаться. К примеру конструкцию выбора можно организовать средствами PowerShell, используя оператор -or (или). Так предыдущее выражение можно записать следующим образом:
Get-Service | where {$_.Name -match ″vds″ -or $_.Name -match ″vss″}
Важный момент при использовании альтернатив — это их порядок. Для примера возьмем выражение:
″one, two, three″ -match ″(one)|(two)|(three)″
Если опустить тонкости, то проверка производится так — берется первое слово в строке (one) и сравнивается по очереди с каждой из альтернатив (one, two и three). Если совпадение, как в нашем примере, найдено, то выдается положительный результат, иначе процесс повторяется для следующего слова (two) и так до нахождения соответствия или окончания строки. Поскольку в нашем примере слово one стоит в начале строки и альтернатива one находится на первом месте, то поиск завершается максимально быстро. Теперь возьмем такое выражение:
″one, two, three″ -match ″(three)|(two)|(one)″
Как видите, теперь поиск пойдет несколько медленнее, поскольку one сравнивается сначала с three, потом с two и только затем с one. А если взять такое выражение:
″one, two, three″ -match ″(five)|(four)|(three)″
то процесс будет идти еще дольше, поскольку сначала пойдет сравнение one с five, four и three, затем то-же для two уже затем для three.
К чему я все это рассказываю? К тому, что простое изменение порядка альтернатив может в разы повысить скорость работы регулярного выражения. Конечно для данного примера это несущественно, но при обработке больших объемов данных может быть очень заметно.
Альтернатива на основании условия
Альтернативу можно организовать так, что выбор между альтернативными вариантами будет осуществляться в зависимости от некоторого условия. Так конструкция (?if (then|else)) означает, что сначала проверяется регулярное выражение if, если оно истинно — то выполняется проверка выражения then, иначе проверяется выражение else.
Для примера возьмем выражение, которое ищет в строке IP или Mac-адрес:
″192.168.0.1″ -match «(?(^\d{1,3}\.)((\d{1,3}\.){3}\d{1,3})|(\w+-){5}(\w+))
Сначала идет условие (?(^\d{1,3}\.), в котором проверяется наличие в начале строки от 1 до 3 цифр и точки (начало IP-адреса). Если совпадение найдено, то ищем IP-адрес с помощью выражения (\d{1,3}\.){3}\d{1,3}), иначе ищем Mac-адрес выражением (\w+-){5}(\w+).
Примечание. Напомню, что IP представляет из себя 4 группы символов разделенных точкой, в каждой группе от 1 до 3 цифр (напр. 192.168.0.1). Mac-адрес состоит из 12 символов (букв или цифр), как правило сгруппированных по 2 и разделенных дефисом (напр. 1A-2B-3C-4D-5E-6F).
Альтернатива на основании захваченной группы
В качестве условия может выступать захваченная группа. Синтаксис выглядит как (?(name) then|else)) или (?(number) then|else)), где name и number — соответственно имя или номер захваченной группы. Если имя\номер группы соответствует захваченной группе то выполняется выражение then, иначе выполняется выражение else.
Немного изменим предыдущее выражение, в качестве условия используем именованную группу mac:
″1A-2B-3C-4D-5E-6F″ -match «(?<mac>^\w{2}-))(?(mac))(\w{2}-){5}\w{2}|(\d{1,3}\.?){4}″
Конструкция (?<mac>^\w{2}-) проверяет, что в начале строки идут 2 символа и дефис (начало Mac-адреса) и помещает результат в группу mac. Если группа есть, то выражение (\w{2}-){5}\w{2} ищет полный Mac-адрес, иначе используется выражение (\d{1,3}\.?){4} для поиска IP-адреса.
То же самое, но с неименованной группой будет выглядеть так:
″1A-2B-3C-4D-5E-6F″ -match «(^\w{2}-))(?(1))(\w{2}-){5}\w{2}|(\d{1,3}\.?){4}″
Позиционная проверка
Как вы помните, уточнить положение искомого объекта в строке и обозначить его границы позволяют якоря. Но если этого недостаточно, то с помощью позиционной проверки можно задать расположение объекта относительно других объектов в строке. Другими словами, позиционная проверка дает возможность указать, что именно должно (или не должно) находится слева (или справа) от объекта. Всего существует четыре типа позиционной проверки:
• Позитивная опережающая проверка (?=…) — должно совпасть справа;
• Позитивная ретроспективная проверка (?<=…) — должно совпасть слева;
• Негативная опережающая проверка (?!…) — не должно совпасть справа;
• Негативная ретроспективная проверка (?<!…) — не должно совпасть слева.
Для примера используем позитивную опережающую проверку для поиска слова, справа от которого идет слово four:
″one two three four five″ -match ″(\b\w+)\s(?=four)\b″
А теперь с помощью позитивной ретроспективной проверки найдем слово, слева от которого есть слово two:
″one two three four five″ -match ″(?<=two)\s(\w+\b)″
Проверку можно ставить как до, так и после выражения, например так мы ищем слово, за которым стоит слово four, используя позитивную опережающую проверку:
″one two three four five″ -match ″(?=\b\w+\sfour)(\w+\b)″
В одном выражении может быть несколько проверок. Для примера найдем позицию в строке, слева от которой находится three, а справа four и поставим между ними дефис:
″one two three four five″ -replace ″\b(?<=three)\s(?=four\b)″,″-″
С негативными проверками (на мой взгляд) работать несколько сложнее, поскольку с их помощью мы указываем не то, что должно быть, а то, чего быть не должно. Так следующее выражение с помощью негативной опережающей проверки должно найти словосочетание, перед которым не стоит слово rubles:
″100 rubles, 50 dollars, 20 pounds″ -match ″\b(?!\d+\srubles).?\s(\d+\s\w+\b)″
А так, применив негативную ретроспективную проверку, мы найдем словосочетание, после которого нет слова pounds:
″100 rubles, 50 dollars, 20 pounds″ -match ″\b(?<!\d+\spounds).?\s(\d+\s\w+\b)″
Использовать позиционные проверки можно множеством различных вариантов. К примеру, следующее выражение выбирает строки, не начинающиеся на un:
″unique″,″unit″,″you″ -match ″\b(?!un)\w+\b″
Практический пример
Сделать так, чтобы регулярное выражение совпало с тем, чем нужно — это довольно просто. Гораздо сложнее сделать так, чтобы выражение не совпадало с тем, с чем не нужно, т.е. исключить все нежелательные совпадения. В качестве примера возьмем текстовый файл, и попробуем выбрать из него строки, содержащие IP-адрес.
Для удобства поместим содержимое файла в переменную $ip и начнем поиск. Cначала попробуем такое выражение:
$ip | where {$_ -match ″[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*″}
Конструкцию [0-9] можно заменить на \d, получится чуть короче:
$ip | where {$_ -match ″\d*\.\d*\.\d*\.\d*″}
На первый взгляд вроде все верно, но в результате получаем не только нормальные IP-адреса, но и кучу непонятных значений. Здесь стоит вспомнить о том, что квантификатор * означает ″любое количество совпадений или отсутствие совпадений″, т.е. в выражении \d*\.\d*\.\d*\.\d* необходимыми являются только точки, все остальное необязательно.
Уберем необязательность, заменив квантификатор * на +, и на всякий случай обозначим начало и конец искомой строки:
$ip | where {$_ -match ″^\d+\.\d+\.\d+\.\d+$″}
Совсем неподходящие строки типа …? отсеялись, но все равно в результате полно мусора. Поскольку каждое число может содержать от 1 до 3 цифр, попробуем более точно ограничить количество символов, заменив квантификатор + на {1,3}:
$ip | where {$_ -match ″^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$″}
И еще немного сократим выражение:
$ip | where {$_ -match ″^(\d{1,3}\.){3}\d{1,3}$″}
или так:
$ip | where {$_ -match ″^(\d{1,3}\.?){4}$″}
Получившееся выражение уже вполне можно использовать для поиска IP, однако оно все еще пропускает неправильные варианты типа 521.467.09.11 или 999.999.999.999. На этом этапе можно остановиться, признав некоторую неточность выражения, а можно пойти по пути уточнения, что приведет к усложнению выражения. Можно сказать что это компромисс между простотой и точностью.
Для дальнейшего уточнения вспоминаем, что в IP-адресе могут быть числа от 0 до 255, поэтому нам необходимо проследить за тем, какие цифры допускаются в числе и в каких позициях они находятся. Пойдем по порядку:
• Если число состоит из 1 или 2 цифр, то максимальное возможное число это 99, т.е принадлежность к диапазону 0-255 проверять не нужно. Проверить это условие можно выражением \d|\d\d;
• Если число начинается с 0 или 1, то оно заведомо принадлежит к интервалу 0-199 и тоже не нуждается в проверке. Суммируя с первым условием, получаем выражение \d|\d\d|[01]\d\d;
• Число, состоящее из трех цифр и начинающееся с 2 допустимо в том случае, если оно не больше 255, следовательно, если вторая цифра меньше 5, то число правильное, а если равна 5, то третья цифра должна быть меньше 6. Выразить это можно как 2[0-4]\d|25[0-5].
Объединив все требования, получаем выражение \d|\d\d|[01]\d\d|2[0-4]\d|25[0-5]. Сократим его, объединив первые три альтернативы, в результате получится выражение [01]?\d\d?|2[0-4]|25[0-5], которое описывает число от 0 до 255. Остается заключить его в скобки и подставить в выражение вместо \d{1,3}:
$ip | where {$_ -match ″^(([01]?\d\d?|2[0-4]|25[0-5])\.){3}([01]?\d\d?|2[0-4]|25[0-5])$″}
Результат доcтаточно точен, но в нем равно присутствует вариант из одних нулей. Для исключения этого варианта вставим проверку:
$ip | where {$_ -match ″^(?!(0+\.?){4})(([01]?\d\d?|2[0-4]|25[0-5])\.){3}([01]?\d\d?|2[0-4]|25[0-5])$″}
Вот теперь в результате только валидные IP-адреса, однако выражение получилось довольно сложным. Стоит ли стремиться за точностью и усложнять выражение или пойти на компромисс — все зависит от конкретной ситуации.
Заключение
В в своих статьях я описал лишь базовые возможности регулярных выражений. На этом мои знания иссякли 🙂 поэтому для дальнейшего изучения можно зайти на MSDN, где есть целый раздел, посвященный регулярным выражениям — .NET Framework Regular Expressions. Также в качестве справочника рекомендую «Regular Expressions Cookbook» издательства O′Reilly и книгу Дж. Фридла «Регулярные выражения», в которой очень подробно объясняются тонкости работы регулярных выражений.
$ip = «213.154.5.6»
$ip | where {$_ -match ″^(?!(0+\.?){4})(([01]?\d\d?|2[0-4]|25[0-5])\.){3}([01]?\d\d?|2[0-4]|25[0-5])$″}