Наскільки мені відомо, групи балансування унікальні для регулярного виразу .NET.
Окрім: повторювані групи
По-перше, вам потрібно знати, що .NET є (знову ж таки, наскільки мені відомо) єдиним ароматом регулярного виразу, який дозволяє отримати доступ до кількох захоплень однієї групи захоплення (не в зворотних посиланнях, а після завершення матчу).
Щоб проілюструвати це на прикладі, розглянемо шаблон
(.)+
і рядок "abcd"
.
у всіх інших ароматах регулярного виразу група захоплення 1
просто дасть один результат: d
(зауважте, повний збіг, звичайно, буде abcd
таким, як очікувалося). Це пояснюється тим, що кожне нове використання групи захоплення замінює попереднє захоплення.
.NET, з іншого боку, пам’ятає їх усіх. І робить це в стосі. Після узгодження з вищевказаним регулярним виразом like
Match m = new Regex(@"(.)+").Match("abcd");
ви це знайдете
m.Groups[1].Captures
Це a CaptureCollection
, елементи якого відповідають чотирьом захопленням
0: "a"
1: "b"
2: "c"
3: "d"
де число є індексом CaptureCollection
. Отже, в основному кожного разу, коли групу використовують знову, на стек виштовхується новий знімок.
Це стає цікавішим, якщо ми використовуємо іменовані групи захоплення. Оскільки .NET дозволяє повторне використання однойменних назв, ми могли б написати регулярний вираз, подібний
(?<word>\w+)\W+(?<word>\w+)
щоб захопити два слова в одну групу. Знову ж таки, кожного разу, коли зустрічається група з певним іменем, захоплення висувається на її стек. Отже, застосовуючи цей регулярний вираз до вхідних даних "foo bar"
та перевіряючи
m.Groups["word"].Captures
ми знаходимо два захоплення
0: "foo"
1: "bar"
Це дозволяє нам навіть штовхати речі в один стек з різних частин виразу. Але все-таки це лише особливість .NET - можливість відстеження декількох захоплень, перелічених у цьому CaptureCollection
. Але я сказав, ця колекція - це стек . Так що ми можемо вискочити речі з нього?
Введіть: Групи балансування
Виявляється, ми можемо. Якщо ми використовуємо групу like (?<-word>...)
, тоді останнє захоплення вискакується зі стеку, word
якщо підвираз ...
відповідає. Тож якщо ми змінимо наш попередній вираз на
(?<word>\w+)\W+(?<-word>\w+)
Тоді друга група зробить захоплення першої групи, і CaptureCollection
в підсумку ми отримаємо пусте . Звичайно, цей приклад досить марний.
Але є ще одна деталь до мінус-синтаксису: якщо стек вже порожній, група виходить з ладу (незалежно від її підтипу). Ми можемо використати цю поведінку для підрахунку рівнів вкладеності - і саме звідси походить група балансування імен (і де вона стає цікавою). Скажімо, ми хочемо зіставити рядки, які правильно введені в дужки. Ми натискаємо кожну відкриваючу дужку на стек і виводимо по одній фіксації для кожної закриває дужки. Якщо ми зустрічаємо одну закриваючу дужку занадто багато, вона спробує вивести порожній стек і призведе до збою шаблону:
^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*$
Отже, у нас є три альтернативи повторення. Перша альтернатива споживає все, що не є дужкою. Друга альтернатива відповідає (
s, підштовхуючи їх до стека. Третя альтернатива відповідає )
s при вискакуванні елементів зі стеку (якщо можливо!).
Примітка: Щоб уточнити, ми лише перевіряємо, чи немає нерівних дужок! Це означає, що рядок, що взагалі не містить дужок , збігатиметься, оскільки вони все ще синтаксично дійсні (у якомусь синтаксисі, де вам потрібні ваші дужки). Якщо ви хочете забезпечити принаймні один набір дужок, просто додайте довідку (?=.*[(])
прямо після ^
.
Однак цей шаблон не є ідеальним (або цілком правильним).
Фінал: умовні візерунки
Є ще одна фішка: це не гарантує, що стек порожній у кінці рядка (отже (foo(bar)
, буде дійсним). .NET (та багато інших різновидів) має ще одну конструкцію, яка нам тут допомагає: умовні шаблони. Загальним синтаксисом є
(?(condition)truePattern|falsePattern)
де значення falsePattern
є необов’язковим - якщо його опустити, помилковий регістр завжди збігатиметься. Умовою може бути або шаблон, або назва групи захоплення. Тут я зупинюсь на останньому випадку. Якщо це ім'я групи захоплення, тоді truePattern
використовується тоді і тільки тоді, коли стек захоплення для цієї конкретної групи не порожній. Тобто умовний шаблон, такий як (?(name)yes|no)
читається, "якщо name
щось збіглося і захопило (що все ще знаходиться в стеку), використовуйте шаблон, yes
інакше використовуйте шаблон no
".
Отже, наприкінці наведеного вище шаблону ми можемо додати щось на зразок того, (?(Open)failPattern)
що спричиняє збій цілого шаблону, якщо Open
-stack не порожній. Найпростішим способом зробити безумовний збій шаблону є (?!)
(порожній негативний результат пошуку). Отже, ми маємо остаточну схему:
^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*(?(Open)(?!))$
Зауважте, що цей умовний синтаксис сам по собі не має нічого спільного з балансуючими групами, але необхідно використати їх повну потужність.
Звідси небо - це межа. Можливо багато дуже складних застосувань, і є деякі проблеми, коли вони використовуються в поєднанні з іншими функціями .NET-Regex, такими як lookbehinds змінної довжини ( котрий мені довелося навчитися самому ). Однак головне питання завжди полягає в тому: чи підтримується ваш код при використанні цих функцій? Вам потрібно це дуже добре задокументувати, і будьте впевнені, що всі, хто працює над цим, також знають про ці особливості. В іншому випадку вам може бути краще, просто пройдівшись рядком вручну, символ за символом і підрахувавши рівні вкладеності в ціле число.
Додаток: Що з (?<A-B>...)
синтаксисом?
Кредити за цю частину надходять до Кобі (докладніше див. Його відповідь нижче).
Тепер, маючи все вищесказане, ми можемо перевірити, що рядок правильно введено в дужки. Але було б набагато корисніше, якби ми могли насправді отримувати (вкладені) знімки для всього вмісту цих дужок. Звичайно, ми могли б пам’ятати, як відкривати та закривати дужки в окремому стеку захоплення, який не спорожняється, а потім виконувати деяке вилучення підрядків на основі їхніх позицій на окремому кроці.
Але .NET надає тут ще одну зручну функцію: якщо ми використовуємо (?<A-B>subPattern)
, не тільки виймається захоплення зі стеку B
, але й все, що між цим вискакуючим захопленням B
і поточною групою виштовхується в стек A
. Отже, якщо ми використовуємо подібну групу для закриття дужок, під час викидання рівнів вкладеності з нашого стека, ми можемо також перенести вміст пари в інший стек:
^(?:[^()]|(?<Open>[(])|(?<Content-Open>[)]))*(?(Open)(?!))$
У своїй відповіді Кобі надав цю пряму демонстрацію
Тож, взявши всі ці речі разом, ми можемо:
- Запам'ятайте довільно багато захоплень
- Перевірити вкладені структури
- Захопіть кожен рівень вкладеності
Все в одному регулярному виразі. Якщо це не захоплююче ...;)
Деякі ресурси, які я знайшов корисними, коли вперше про них дізнався: