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 "^ [az] {3} $"}
Вказувати точне число зовсім не обов'язково. Квантіфікатори дозволяють вказувати діапазон в форматі {x, y}, де х - мінімально необхідне, а у - максимально можлива кількість символів. Наприклад виведемо системні процеси з ім'ям, що містять у назві від 1 до 4 символів:
Get-Process | where {$ _.ProcessName -match "^ [az] {1,4} $"}
Максимальне значення можна опустити, задавши тільки мінімальне, наприклад {3,} означає "3 і більше символів". Для найбільш загальних квантіфікаторов є скорочене написання:
+ (плюс) - один або більше символів, еквівалент {1,};
* (зірочка) - будь-яка кількість символів або повна їх відсутність, еквівалент { 0,};
? (Знак питання) - один символ або відсутність символу, еквівалент {0,1}.
Зверніть увагу, що, на відміну від групових символів, квантіфікатори в регулярних виразах не використовуються самі по собі, а завжди мати перевагу над символу або групі, наприклад (.?) - один будь-який символ, (. *) - будь-яку кількість будь-яких символів. Наступною командою виведемо процеси з ім'ям, що починається і закінчуються на s, між якими може бути будь-яка кількість символів:
Get-Process | where {$ _.ProcessName -match "^ s. * S $"}
І ще, варто пам'ятати про необов'язковості квантіфікаторов, у яких в якості мінімального значення стоїть нуль (напр.? і *). Це означає, що регулярні вирази з їх використанням для позитивного результату не потребують хоча-б в одному збігу, вони збігаються навіть при відсутності символів.Наприклад вираження ".?", ". *" Або "[az] *" збігаються з чим завгодно, включаючи порожній рядок.
Оператори 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 +)) \.$ "
В цьому випадку під першим номером йде вміст загальної групи, а вже потім вкладені в неї групи. Втім не варто надмірно захоплюватися подібними конструкціями, в них дуже легко заплутатися.
При необхідності групі можна привласнити ім'я, щоб було зручніше до неї звертатися.Іменовані групи мають синтаксис (? 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 або \ k`name`, де name - ім'я іменованої групи.Такі посилання однозначно вказують на групу і їх неможливо сплутати з вісімковими символами. З їх допомогою вираз зміниться наступним чином:
"Test grouping in regular regular expressions." -Match "(? \ W +) \ s (\ k)"
Незахвативающіе групи
при використанні захоплюючих груп на збереження їх вмісту витрачається додатковий час і ресурси, що при обробці великих обсягів даних може негативно позначитися на продуктивності.Тому, якщо групи потрібні виключно для угруповання символів, а в захопленні немає необхідності, то можна використовувати "незахвативающіе" групи. Ці групи виходять за допомогою перемикача (?:), Наприклад:
"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]}
На цьому прикладі завершимо другу частину захоплюючого