Заметки о Windows и других программных продуктах Microsoft...

PowerShell и регулярные выражения (часть 2)

PowerShell и регулярные выражения (часть 2)

Продолжаем разговор о регулярных выражениях, начатый в первой части статьи.  Сегодня речь пойдет о таких важных понятиях, как квантификаторы и группы, а также о парочке новых операторов 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}$″}

квантификаторы, min и max

 

Максимальное значение можно опустить, задав только минимальное, например {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″

оператор replace

 

Оператор split рабивает исходную строку на части и возвращает массив строк. Граница, по которой производится разбиение, указывается с помощью регулярного выражения. Например, возьмем многострадальный файл и разделим его имя, используя в качестве разделителя пробел:

$files[1].Name -split ″\s″

При необходимости через запятую можно указать максимальное количество частей, на которые можно разбить строку:

$files[1].Name -split ″\s″, 2

оператор split

 

Примечание. Так же как и 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]}

пример парсинга логов

 

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

 
 
Комментарии
Виталий

Познавательно, большое спасибо!

Леколь

«Восток — дело тонкое».
Формулировки условий — тоже.

Да-да, я опять про строгость формулировок.

Вот Кирилл («адманщик») завлекает нас:

— — — — —
«Давайте выведем процессы с именами, начинающиеся и заканчивающиеся на s, между которыми может быть любое количество символов:

$x -match ″^s.*s$″ (то есть когда выражение истинно по изложенной формулировке)
— — — — —

Минуточку! Это и аккуратно и компактно, но не строго, и поэтому «в общем случае неверно».

«Патамушта»… при $x -eq «s», это выражение даст false.

А ведь «на словах всё выполняется»:

Ну рассудите сами! В выражении $x
— символ ‘s’ стоит в начале строки
— одновременно после него стоит любое количество символов, хотя бы и нулевое
— одновременно ‘s’ стоит в конце строки
— одновременно перед ним стоит любое число символов, хотя бы и нулевое.

Дискриминация, Кирилл!
Ведь выражение $x выполнило все твои условия!

Леколь

Уточняю для читателей вопрос.

Как изменить формулу запроса,
чтобы на вывод пошли и те имена,
которые автор «имел ввиду», и это
имя файла (вариант с одним ‘s’), который является
их «софистическим» аналогом.

Леколь

«Эва что вытворяют, озорники!» — бурчу я на квантификаторы.
Меня просто восхитило лаконичное предупреждение Кирилла,
что выражения «с нуль-включающими квантификаторами»
(то есть вида «.?» или «.*») ведут себя «непредсказуемо правильно».
Эти коварные вольнолюбцы совпадают с чем угодно, включая пустую строку.

«» -match «.?» —> true
«» -match «.*» —> true

Вот квантификатор (+), ведет себя приличнее, предсказумее.
«» -match «.+» —> false

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

По предложенному образцу вывожу:

«Test grouping in regular expressions.» -match «(\w+)\s(\w+)\s(\w+)\s(\w+)\s»

Набираем $matches

Получаем более-менее предсказуемый результат, а именно
— четыре однословные группы
— две четырехсловных группы (хотя ожидалась одна!)

5 regular
4 in
3 grouping
2 Test
1 Test grouping in regular
0 Test grouping in regular

Нормально! Выбирай любую группу! И властвуй!
Нефик больше умничать!

Ну а… почему бы не побурчать:

«Фи, как некрасиво и громоздко!
Зачем повторять 4 раза один и тот же фрагмент,
когда есть приличный-предсказуемый квантификатор (плюс).
Сделаем красиво и компактно.»

Ну ладно, сделаем.

«Test grouping in regular expressions.» -match «((\w+)\s)+»

Ожидается аналогичный результат из шести подвластных групп.

Но набираем $matches и получаем… только 3 группы:

Одна из них нулевая — ожидаемая, четырехсловная
А остальные — «не пойми что».

2 regular
1 regular
0 Test grouping in regular

Какой вывод?
Нефик бурчать на некрасивых и громоздких!

Или — «по классику»:
«Если вам подают молоко, то не ищите в нем пива.»
// Антон Палыч Чехов

Леколь

К слову. Или «примечание к примечанию».

У меня на компе стоит PowerShell-5.1
При наборе оператора split или replace, обнаружил что…

Кроме явно определенных регистрозависимых операторов
(creplace и csplit), когда-то для полноты картины
добавлены и явно определенные регистронезависимые операторы
isplit и ireplace.

Круто полируют фирмачи язык!
Полируют!!