Що таке групи балансування регулярних виразів?


91

Я просто читав питання про те, як отримати дані всередині подвійних фігурних дужок ( це питання ), а потім хтось підняв групи балансування. Я все ще не зовсім впевнений, що вони собою являють і як ними користуватися.

Я прочитав Визначення Балансувальної Групи , але за поясненням важко слідувати, і я все ще досить розгублений щодо питань, які я згадав.

Хтось може просто пояснити, що таке балансуючі групи та чим вони корисні?


Мені цікаво, скільки розробників регулярних виразів насправді підтримується.
Mike de Klerk

2
@MikedeKlerk Він підтримується принаймні в .NET Regex engine.
ЦеNotALie.

Відповіді:


173

Наскільки мені відомо, групи балансування унікальні для регулярного виразу .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)(?!))$

У своїй відповіді Кобі надав цю пряму демонстрацію

Тож, взявши всі ці речі разом, ми можемо:

  • Запам'ятайте довільно багато захоплень
  • Перевірити вкладені структури
  • Захопіть кожен рівень вкладеності

Все в одному регулярному виразі. Якщо це не захоплююче ...;)

Деякі ресурси, які я знайшов корисними, коли вперше про них дізнався:


6
Цю відповідь було додано до поширених запитань щодо регулярних виразів переповнення стеку в розділі "Розширений регулярний вираз-Fu".
aliteralmind

39

Лише невелике доповнення до чудової відповіді М. Бюттнера:

Яка справа з (?<A-B>)синтаксисом?

(?<A-B>x)тонко відрізняється від (?<-A>(?<B>x)). Вони отримують однаковий потік управління * , але захоплюють по- різному.
Наприклад, давайте розглянемо схему для збалансованих брекет-систем:

(?:[^{}]|(?<B>{)|(?<-B>}))+(?(B)(?!))

В кінці матчу у нас є збалансована струна, але це все, що ми маємо - ми не знаємо, де фігурні дужки, тому що Bстек порожній. Важка робота, яку зробив для нас двигун, зникла.
( приклад на Regex Storm )

(?<A-B>x)є рішенням цієї проблеми. Як Це НЕ захоплення xв $A: він фіксує вміст між попереднім захопленням Bі поточною позицією.

Давайте використаємо це за нашим зразком:

(?:[^{}]|(?<Open>{)|(?<Content-Open>}))+(?(Open)(?!))

Це зафіксувало б $Contentструни між фігурними дужками (та їх положеннями) для кожної пари на шляху.
Для рядка НЕ {1 2 {3} {4 5 {6}} 7}було б чотири захоплює: 3, 6, 4 5 {6}, і 1 2 {3} {4 5 {6}} 7- набагато краще , ніж нічого або } } } }.
( приклад - клацніть tableвкладку і подивіться ${Content}, знімки )

Насправді його можна використовувати без балансування взагалі: (?<A>).(.(?<Content-A>).)фіксує перші два символи, хоча вони розділені групами.
(lookahead тут використовується частіше, але він не завжди масштабується: це може дублювати вашу логіку.)

(?<A-B>)є сильною особливістю - вона дає вам точний контроль над своїми захопленнями. Майте це на увазі, коли ви намагаєтеся отримати більше з вашого шаблону.


@FYI, продовжуючи дискусію з питання, яке вам не сподобалось, у новій відповіді на це. :)
zx81

Я намагаюся знайти спосіб виконати перевірку регулярного виразу збалансованих фігурних дужок із екрануванням фігурних дужок всередині рядків. Наприклад, пройде такий код: public class Foo {private const char BAR = '{'; приватний рядок _qux = "{{{"; } Хтось це робив?
Містер Андерсон,

@MrAnderson - Вам просто потрібно додати |'[^']*'в потрібному місці: example . Якщо вам також потрібні екрановані символи, тут є приклад: (регулярний вираз для відповідності рядкових літералів C #) [ stackoverflow.com/a/4953878/7586] .
Кобі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.