Продолжаем разговор о регулярных выражениях, начатый в первой части статьи. Сегодня речь пойдет о таких важных понятиях, как квантификаторы и группы, а также о парочке новых операторов PowerShell для работы с регулярными выражениями.
Квантификаторы
Как вы помните из предыдущей части, с помощью символьных классов можно описать то, какие именно символы надо искать, но нельзя указать их количество. Так символьный класс [a-z] описывает одну любую букву латинского алфавита, соответственно для описания большего количества букв потребуется конструкция [a-z][a-z]…[a-z]. Для примера выведем системные процессы с именем, состоящим из трех символов:
Get-Process | where {$_.ProcessName -match ″^[a-z][a-z][a-z]$″}
Даже для трех символов это выглядит неаккуратно и громоздко, однако данную конструкцию можно значительно сократить. Для этого в регулярных выражениях существуют специальные количественные модификаторы (квантификаторы). Квантификатор ставится справа от символа (или класса) и указывает их необходимое количество. Например квантификатор {3} означает 3 символа, соответственно предыдущий пример с использованием квантификаторов будет выглядеть так:
Get-Process | where {$_.ProcessName -match ″^[a-z]{3}$″}
Указывать точное число вовсе необязательно. Квантификаторы позволяют указывать диапазон в формате {x,y}, где х — минимально необходимое, а у — максимально возможное количество символов. Например выведем системные процессы с именем, содержащие в названии от 1 до 4 символов:
Get-Process | where {$_.ProcessName -match ″^[a-z]{1,4}$″}
Максимальное значение можно опустить, задав только минимальное, например {3, } означает ″3 и более символов″. Для наиболее общих квантификаторов есть сокращенное написание:
+ (плюс) — один или более символов, эквивалент {1, };
* (звездочка) — любое количество символов или полное их отсутствие, эквивалент {0, };
? (знак вопроса) — один символ или отсутствие символа, эквивалент {0,1}.
Обратите внимание, что, в отличие от подстановочных символов, квантификаторы в регулярных выражениях не используются сами по себе, а всегда применяются к символу или группе, например (.?) — один любой символ, (.*) — любое количество любых символов. Следующей командой выведем процессы с именем, начинающемся и заканчивающиеся на s, между которыми может быть любое количество символов:
Get-Process | where {$_.ProcessName -match ″^s.*s$″}
И еще, стоит помнить о необязательности квантификаторов, у которых в качестве минимального значения стоит ноль (напр. ? и *). Это значит, что регулярные выражения с их использованием для положительного результата не нуждаются хотя-бы в одном совпадении, они совпадают даже при отсутствии символов. Например выражения ″.?″ , ″.*″ или ″[a-z]*″ совпадают с чем угодно, включая пустую строку.
Операторы replace и split
Работа с регулярными выражениями может включать в себя не только поиск, но и обработку найденного. Для этих целей в PowerShell имеются операторы replace и split, которые могут не только анализировать строки, но и производить в них некоторые изменения.
Оператор replace находит часть строки, подходящую под регулярное выражение, и заменяет ее. Для примера запросим список файлов в директории и поместим его в переменную, а затем выведем имя одного из файлов:
$files = Get-ChildItem C:\Files\PS Books
$files[1].Name
А теперь возьмем полученное имя и поменяем в нем ″PowerShell″ на ″CMD″:
$files[1].Name -replace ″PowerShell″, ″CMD″
Еще одна особенность replace в том, что можно не указывать строку замены. В этом случае будет произведена замена найденного объекта на пустое место, или попросту удаление. Например так мы удалим слово ″PowerShell″ из названия файла:
$files[1].Name -replace ″PowerShell″
Оператор split рабивает исходную строку на части и возвращает массив строк. Граница, по которой производится разбиение, указывается с помощью регулярного выражения. Например, возьмем многострадальный файл и разделим его имя, используя в качестве разделителя пробел:
$files[1].Name -split ″\s″
При необходимости через запятую можно указать максимальное количество частей, на которые можно разбить строку:
$files[1].Name -split ″\s″, 2
Примечание. Так же как и match, операторы replace и split не зависят от регистра символов. Для этого у них имеются регистрозависимые версии creplace и csplit.
Лень и жадность
В данном случае лень и жадность — это не людские пороки, а всего лишь свойства квантификаторов. Дело в том, что по умолчанию все квантификаторы ″жадные″, то есть они всегда стараются захватить как можно больше символов. Для примера возьмем такое выражение:
″Greedy and Lazy Quantifiers″ -replace ″L.*″
Как видите, в данном случае квантификатор * отработал максимуму, захватив часть строки Lazy Quantifiers, т.е. все что следует после L и до конца строки. Поскольку такой подход не всегда приемлем, существует возможность ограничить квантификатор необходимым минимумом. Для этого есть ″ленивые″ версии квантификаторов, получаемые с помощью добавления вопросительного знака, напр. *?, +?, ??, {1,10}?. Немного изменим предыдущую команду:
″Greedy and Lazy Quantifiers″ -replace ″L.*?″
В отличие от своего жадного собрата ленивый квантификатор ограничился минимально возможным совпадением — одной буквой L. Впрочем и это поведение можно изменить и ленивый квантификатор можно заставить захватить больше, не оставив ему выбора:
″Greedy and Lazy Quantifiers″ -replace ″L.*?\s″
В этом примере квантификатор вынужден захватить минимум, но до ближайшего пробела.
Захватывающие группы
С помощью круглых скобок символы в регулярных выражениях можно объединять в группы. Группы можно использовать для упорядочения и группировки, к ним можно применять квантификаторы, например:
″Test grouping in regular expressions.″ -match ″(\w+\s){4}(\w+)\.$″
Однако основное предназначение групп — это захват содержимого. Принцип захвата заключается в том, что часть строки, совпавшая с выражением внутри группы, помещается в специальную переменную $Matches. Эта переменная является массивом, в котором находиться содержимое всех групп, входящих в выражение. Для примера найдем в строке два последних слова, разделенных пробелом:
″Test grouping in regular expressions.″ -match ″(\w+)\s(\w+)\.$″
Если теперь вывести содержимое переменной $Matches, то под индексом 0 там находится вся совпавшая строка целиком, под индексом 1 — содержимое первой группы, под индексом 2 — содержимое второй группы и т.д. Что особенно важно, можно обращаться к каждому элементу отдельно, например:
$Matches[2]
Это позволяет не просто найти, но и извлечь полученный результат.
Группы могут быть вложенными одна в другую, например так:
″Test grouping in regular expressions.″ -match ″((\w+)\s(\w+))\.$″
В этом случае под первым номером идет содержимое общей группы, а уже затем вложенные в нее группы. Впрочем не стоит излишне увлекаться подобными конструкциями, в них очень легко запутаться.
При необходимости группе можно присвоить имя, чтобы было удобнее к ней обращаться. Именованные группы имеют синтаксис (?<name> subexpression) или (?’name’ subexpression), где name — имя группы, а subexpression — регулярное выражение. Имя группы не должно начинаться с цифры и содержать знаков пунктуации. Например, предыдущее выражение с использованием именованных групп будет выглядеть так:
″Test grouping in regular expressions.″ -match ″(?’first’\w+)\s(?’second’\w+)\.$″
Обращаться к именованным группам можно как по их номеру, так и по имени, например:
$Matches[‘first’]
или так:
$Matches.first
Обратные ссылки
Еще одной особенностью групп является то, что внутри регулярного выражения на них можно ссылаться с помощью конструкций, называемых обратными ссылками (backreference). В некоторых случаях это очень удобно, например для поиска повторяющихся элементов в строке. Ссылаться на неименованные группы можно с помощью конструкции \n, где n является порядковым номером группы в регулярном выражении. Для примера найдем повторяющиеся слова в строке:
″Test grouping in regular regular expressions.″ -match ″(\w+)\s(\1)″
Здесь в первую группу попадает слово regular, ссылка на которое (\1) затем используется во второй группе.
Поскольку подобная запись используется не только в обратных ссылках, но и для обозначения восьмеричных escape-кодов, то существуют следующие правила:
• Выражения от \1 до \9 всегда интерпретируются как обратные ссылки, а не как восьмеричные числа;
• Выражения от \10 и больше считаются обратными ссылками в том случае, если имеется обратная ссылка, соответствующая данному номеру. В противном случае выражение интерпретируется как восьмеричный код;
• Если первая цифра многоразрядного выражения 8 или 9 (напр. \85), то выражение интерпретируется как литерал.
Избавиться от неоднозначности можно с помощью именованных ссылок типа \k<name> или \k’name’ , где name — имя именованной группы. Такие ссылки однозначно указывают на группу и их невозможно спутать с восьмеричными символами. С их помощью выражение изменится следующим образом:
″Test grouping in regular regular expressions.″ -match ″(?<first>\w+)\s(\k<first>)″
Незахватывающие группы
При использовании захватывающих групп на сохранение их содержимого тратится дополнительное время и ресурсы, что при обработке больших объемов данных может отрицательно сказаться на производительности. Поэтому, если группы нужны исключительно для группировки символов, а в захвате нет необходимости, то можно использовать ″незахватывающие″ группы. Эти группы получаются с помощью переключателя (?: ), например:
″Test grouping in regular expressions.″ -match ″(?:\w+\s)(\w+)\.$″
Как видно из примера, поскольку первая группа является незахватывающей, в переменную $Matches попало только содержимое второй группы.
Практический пример
И в завершение статьи небольшой пример из практики. Предположим, имеется лог веб-сервера IIS, из которого мне необходимо достать ответы сервера на запросы по адресу www.site.ru. Для удобства выгрузим содержимое файла в переменную, а затем посмотрим формат записи на примере одной из строк:
$file = Get-Content C:\Files\err.log
$file[5]
Как видите, в строке идет имя сайта, а сразу за ним код ответа сервера. Выведем необходимую информацию такой командой:
$file | where {$_ -match ″(www\.site\.ru)\s([\d]{3})″} | foreach {$Matches[0]}
На этом примере завершим вторую часть увлекательного 🙂 повествования о регулярных выражениях. Поскольку это далеко не все, то видимо будет и третья.
Познавательно, большое спасибо!