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 1
hanya akan menghasilkan satu hasil: d
(perhatikan, pertandingan lengkap tentu saja akan abcd
seperti 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 CaptureCollection
yang 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 word
jika 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 CaptureCollection
pada 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 falsePattern
adalah 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 truePattern
digunakan jika dan hanya jika tumpukan tangkapan untuk grup tertentu itu tidak kosong. Artinya, pola bersyarat seperti (?(name)yes|no)
membaca "jika name
telah cocok dan menangkap sesuatu (yang masih di tumpukan), gunakan pola yes
jika 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 B
dan 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: