Apa itu Grup Penyeimbang ekspresi reguler?

92

Saya baru saja membaca pertanyaan tentang bagaimana mendapatkan data di dalam kurung kurawal ganda ( pertanyaan ini ), dan kemudian seseorang mengemukakan kelompok penyeimbang. Saya masih belum yakin apa itu dan bagaimana menggunakannya.

Saya membaca Definisi Balancing Group , namun penjelasannya sulit untuk diikuti, dan saya masih cukup bingung dengan pertanyaan yang saya sebutkan.

Bisakah seseorang menjelaskan secara sederhana apa itu kelompok penyeimbang dan bagaimana kelompok itu berguna?

IniNotALie.
sumber
Saya ingin tahu berapa banyak mesin regex yang sebenarnya didukung.
Mike de Klerk
2
@MikedeKlerk Ini didukung setidaknya di mesin .NET Regex.
It'sNotALie.

Jawaban:

175

Sejauh yang saya tahu, grup penyeimbang adalah unik untuk rasa regex .NET.

Selain: Grup Berulang

Pertama, Anda perlu tahu bahwa .NET adalah (sekali lagi, sejauh yang saya tahu) satu-satunya ragam regex yang memungkinkan Anda mengakses beberapa tangkapan dari satu grup penangkap (tidak dalam referensi latar tetapi setelah pertandingan selesai).

Untuk mengilustrasikan hal ini dengan sebuah contoh, perhatikan polanya

(.)+

dan tali itu "abcd".

di semua ragam ekspresi reguler lainnya, grup penangkap 1hanya akan menghasilkan satu hasil: d(perhatikan, pertandingan lengkap tentu saja akan abcdseperti yang diharapkan). Ini karena setiap penggunaan baru grup penangkap menimpa tangkapan sebelumnya.

.NET di sisi lain mengingat semuanya. Dan itu dilakukan dalam tumpukan. Setelah mencocokkan regex seperti di atas

Match m = new Regex(@"(.)+").Match("abcd");

Anda akan menemukannya

m.Groups[1].Captures

Adalah CaptureCollectionyang elemennya sesuai dengan empat tangkapan

0: "a"
1: "b"
2: "c"
3: "d"

dimana nomor tersebut adalah indeks ke dalam CaptureCollection. Jadi pada dasarnya setiap kali grup digunakan lagi, tangkapan baru didorong ke tumpukan.

Akan lebih menarik jika kita menggunakan grup penangkap bernama. Karena. NET memungkinkan penggunaan berulang dari nama yang sama kita bisa menulis seperti regex

(?<word>\w+)\W+(?<word>\w+)

untuk memasukkan dua kata ke dalam kelompok yang sama. Sekali lagi, setiap kali grup dengan nama tertentu ditemukan, tangkapan didorong ke tumpukannya. Jadi menerapkan regex ini ke input "foo bar"dan pemeriksaan

m.Groups["word"].Captures

kami menemukan dua tangkapan

0: "foo"
1: "bar"

Hal ini memungkinkan kita untuk mendorong sesuatu ke dalam satu tumpukan dari bagian ekspresi yang berbeda. Tapi tetap saja, ini hanya fitur .NET untuk dapat melacak beberapa tangkapan yang tercantum di sini CaptureCollection. Tapi saya katakan, koleksi ini bertumpuk . Jadi bisakah kita meletuskan sesuatu darinya?

Masuk: Grup Penyeimbang

Ternyata kita bisa. Jika kita menggunakan grup seperti (?<-word>...), maka tangkapan terakhir akan muncul dari tumpukan wordjika subekspresi ...cocok. Jadi jika kita mengubah ekspresi sebelumnya menjadi

(?<word>\w+)\W+(?<-word>\w+)

Kemudian grup kedua akan memunculkan tangkapan grup pertama, dan kami akan menerima kosong CaptureCollectionpada akhirnya. Tentu saja, contoh ini sangat tidak berguna.

Tapi ada satu detail lagi pada sintaks minus: jika tumpukan sudah kosong, grup gagal (terlepas dari subpola). Kita dapat memanfaatkan perilaku ini untuk menghitung tingkat bersarang - dan dari sinilah nama grup penyeimbang berasal (dan di mana hal itu menjadi menarik). Katakanlah kita ingin mencocokkan string yang diberi tanda kurung dengan benar. Kami mendorong setiap kurung buka pada tumpukan, dan memunculkan satu tangkapan untuk setiap kurung tutup. Jika kita menemukan satu kurung tutup terlalu banyak, itu akan mencoba memunculkan tumpukan kosong dan menyebabkan pola gagal:

^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*$

Jadi kami memiliki tiga alternatif dalam pengulangan. Alternatif pertama mengkonsumsi semua yang bukan tanda kurung. Alternatif kedua cocok (sambil mendorongnya ke tumpukan. Alternatif ketiga cocok dengan )s saat memunculkan elemen dari tumpukan (jika memungkinkan!).

Catatan: Hanya untuk memperjelas, kami hanya memeriksa bahwa tidak ada tanda kurung yang tidak cocok! Ini berarti bahwa string yang tidak mengandung tanda kurung sama sekali akan cocok, karena mereka masih valid secara sintaks (dalam beberapa sintaks di mana Anda membutuhkan tanda kurung yang cocok). Jika Anda ingin memastikan setidaknya satu set tanda kurung, cukup tambahkan lookahead (?=.*[(])tepat setelah ^.

Pola ini tidak sempurna (atau seluruhnya benar).

Finale: Pola Bersyarat

Ada satu tangkapan lagi: ini tidak memastikan bahwa tumpukan kosong di akhir string (karenanya (foo(bar)akan valid). .NET (dan banyak varian lainnya) memiliki satu konstruksi lagi yang membantu kita di sini: pola bersyarat. Sintaks umumnya adalah

(?(condition)truePattern|falsePattern)

di mana falsePatternadalah opsional - jika dihilangkan, kasus palsu akan selalu cocok. Kondisi dapat berupa pola, atau nama grup penangkap. Saya akan fokus pada kasus terakhir di sini. Jika itu adalah nama grup penangkap, maka truePatterndigunakan jika dan hanya jika tumpukan tangkapan untuk grup tertentu itu tidak kosong. Artinya, pola bersyarat seperti (?(name)yes|no)membaca "jika nametelah cocok dan menangkap sesuatu (yang masih di tumpukan), gunakan pola yesjika tidak gunakan pola no".

Jadi pada akhir pola kita di atas, kita bisa menambahkan sesuatu seperti (?(Open)failPattern)yang menyebabkan seluruh pola gagal, jika Open-stack tidak kosong. Hal paling sederhana untuk membuat pola gagal tanpa syarat adalah (?!)(tampilan negatif kosong). Jadi kami memiliki pola terakhir kami:

^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*(?(Open)(?!))$

Perhatikan bahwa sintaksis bersyarat ini tidak ada hubungannya dengan grup penyeimbang tetapi perlu untuk memanfaatkan kekuatan penuh mereka.

Dari sini, langit adalah batasnya. Banyak penggunaan yang sangat canggih dimungkinkan dan ada beberapa gotcha ketika digunakan dalam kombinasi dengan fitur .NET-Regex lainnya seperti lookbehinds panjang-variabel ( yang harus saya pelajari sendiri dengan susah payah ). Namun pertanyaan utamanya selalu: apakah kode Anda masih dapat dipelihara saat menggunakan fitur ini? Anda perlu mendokumentasikannya dengan sangat baik, dan memastikan bahwa setiap orang yang mengerjakannya juga mengetahui fitur-fitur ini. Jika tidak, Anda mungkin lebih baik, cukup berjalan string secara manual karakter demi karakter dan menghitung level bersarang dalam integer.

Tambahan: Ada apa dengan (?<A-B>...)sintaks?

Penghargaan untuk bagian ini diberikan kepada Kobi (lihat jawabannya di bawah untuk lebih jelasnya).

Sekarang dengan semua hal di atas, kita dapat memvalidasi bahwa string diberi tanda kurung dengan benar. Tapi akan jauh lebih berguna, jika kita benar-benar bisa menangkap (bersarang) untuk semua konten tanda kurung itu. Tentu saja, kami dapat mengingat membuka dan menutup tanda kurung di tumpukan tangkapan terpisah yang tidak dikosongkan, lalu melakukan beberapa ekstraksi substring berdasarkan posisinya dalam langkah terpisah.

Tetapi .NET menyediakan satu lagi fitur kenyamanan di sini: jika kita menggunakan (?<A-B>subPattern), tidak hanya tangkapan yang muncul dari tumpukan B, tetapi juga segala sesuatu antara tangkapan yang muncul dari Bdan grup saat ini didorong ke tumpukan A. Jadi jika kami menggunakan grup seperti ini untuk tanda kurung penutup, saat memunculkan level bersarang dari tumpukan kami, kami juga dapat mendorong konten pasangan ke tumpukan lain:

^(?:[^()]|(?<Open>[(])|(?<Content-Open>[)]))*(?(Open)(?!))$

Kobi memberikan Live-Demo ini sebagai jawabannya

Jadi dengan menggabungkan semua hal ini kita bisa:

  • Ingat sembarangan banyak tangkapan
  • Validasi struktur bertingkat
  • Tangkap setiap level bersarang

Semuanya dalam satu ekspresi reguler. Jika itu tidak menarik ...;)

Beberapa sumber yang menurut saya berguna ketika saya pertama kali mempelajarinya:

Martin Ender
sumber
7
Jawaban ini telah ditambahkan ke FAQ Ekspresi Reguler Stack Overflow , di bawah "Advanced Regex-Fu".
aliteralmind
40

Hanya sedikit tambahan untuk jawaban bagus M. Buettner:

Apa masalahnya dengan (?<A-B>)sintaks?

(?<A-B>x)sedikit berbeda dari (?<-A>(?<B>x)). Mereka menghasilkan aliran kontrol yang sama * , tetapi menangkap secara berbeda.
Misalnya, mari kita lihat pola kawat gigi yang seimbang:

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

Pada akhir pertandingan kami memiliki string yang seimbang, tetapi hanya itu yang kami miliki - kami tidak tahu di mana kurung kurawal karena Btumpukan kosong. Kerja keras yang dilakukan mesin untuk kami hilang.
( contoh di Regex Storm )

(?<A-B>x)adalah solusi untuk masalah tersebut. Bagaimana? Itu tidak menangkap xke $A: itu menangkap konten antara pengambilan sebelumnya Bdan posisi saat ini.

Mari kita gunakan dalam pola kita:

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

Ini akan menangkap $Contentstring antara kawat gigi (dan posisinya), untuk setiap pasangan di sepanjang jalan.
Untuk string {1 2 {3} {4 5 {6}} 7}akan ada empat menangkap: 3, 6, 4 5 {6}, dan 1 2 {3} {4 5 {6}} 7- jauh lebih baik daripada apa-apa atau } } } }.
( contoh - klik tabletab dan lihat ${Content}, menangkap )

Faktanya, ini dapat digunakan tanpa menyeimbangkan sama sekali: (?<A>).(.(?<Content-A>).)menangkap dua karakter pertama, meskipun dipisahkan oleh grup.
(sebuah lookahead lebih umum digunakan di sini tetapi tidak selalu menskalakan: itu mungkin menduplikasi logika Anda.)

(?<A-B>)adalah fitur yang kuat - ini memberi Anda kontrol yang tepat atas tangkapan Anda. Ingatlah hal itu saat Anda mencoba memaksimalkan pola Anda.

Kobi
sumber
@FYI, melanjutkan pembahasan dari pertanyaan yang tidak kamu suka di jawaban baru yang satu ini. :)
zx81
Saya mencoba mencari cara untuk melakukan pemeriksaan regex kawat gigi seimbang dengan melepaskan kawat gigi di dalam string. EG kode berikut akan lulus: public class Foo {private const char BAR = '{'; string pribadi _qux = "{{{"; } Apakah ada yang melakukan ini?
Mr Anderson
@MrAnderson - Anda hanya perlu menambahkan |'[^']*'di tempat yang tepat: contoh . Jika Anda juga membutuhkan karakter yang lolos, ada contohnya di sini: (Regex untuk pencocokan literal string C #) [ stackoverflow.com/a/4953878/7586] .
Kobi