Theo như tôi biết, các nhóm cân bằng là duy nhất đối với hương vị regex của .NET.
Bên cạnh: Nhóm lặp lại
Trước tiên, bạn cần biết rằng .NET là (một lần nữa, theo như tôi biết) là hương vị regex duy nhất cho phép bạn truy cập nhiều bản chụp của một nhóm chụp duy nhất (không phải trong tài liệu tham khảo mà sau khi kết thúc trận đấu).
Để minh họa điều này bằng một ví dụ, hãy xem xét mẫu
(.)+
và chuỗi "abcd"
.
trong tất cả các phiên bản regex khác, nhóm bắt 1
sẽ chỉ mang lại một kết quả: d
(lưu ý, kết quả phù hợp tất nhiên sẽ abcd
như mong đợi). Điều này là do mỗi lần sử dụng nhóm chụp mới sẽ ghi đè lên lần chụp trước đó.
.NET mặt khác ghi nhớ tất cả chúng. Và nó làm như vậy trong một ngăn xếp. Sau khi khớp với regex ở trên như
Match m = new Regex(@"(.)+").Match("abcd");
bạn sẽ thấy rằng
m.Groups[1].Captures
Là một CaptureCollection
có các phần tử tương ứng với bốn lần chụp
0: "a"
1: "b"
2: "c"
3: "d"
trong đó số là chỉ mục vào CaptureCollection
. Vì vậy, về cơ bản mỗi khi nhóm được sử dụng lại, một bản chụp mới được đẩy lên ngăn xếp.
Sẽ thú vị hơn nếu chúng ta sử dụng các nhóm chụp có tên. Vì .NET cho phép sử dụng lặp lại cùng một tên nên chúng ta có thể viết một regex như
(?<word>\w+)\W+(?<word>\w+)
để bắt hai từ vào cùng một nhóm. Một lần nữa, mỗi khi gặp một nhóm có tên nhất định, một bản chụp được đẩy lên ngăn xếp của nó. Vì vậy, áp dụng regex này cho đầu vào "foo bar"
và kiểm tra
m.Groups["word"].Captures
chúng tôi tìm thấy hai bức ảnh
0: "foo"
1: "bar"
Điều này cho phép chúng tôi thậm chí đẩy mọi thứ lên một ngăn xếp từ các phần khác nhau của biểu thức. Tuy nhiên, đây chỉ là tính năng của .NET có thể theo dõi nhiều lần chụp được liệt kê trong phần này CaptureCollection
. Nhưng tôi đã nói, bộ sưu tập này là một đống . Vậy chúng ta có thể bật ra mọi thứ từ nó không?
Nhập: Các nhóm cân bằng
Hóa ra là chúng ta có thể. Nếu chúng ta sử dụng một nhóm như thế (?<-word>...)
, thì lần chụp cuối cùng sẽ xuất hiện từ ngăn xếp word
nếu biểu thức con ...
khớp. Vì vậy, nếu chúng ta thay đổi biểu thức trước đó thành
(?<word>\w+)\W+(?<-word>\w+)
Sau đó, nhóm thứ hai sẽ bật ảnh chụp của nhóm đầu tiên, và cuối cùng chúng ta sẽ nhận được một ô trống CaptureCollection
. Tất nhiên, ví dụ này là khá vô dụng.
Nhưng có một chi tiết nữa đối với cú pháp trừ: nếu ngăn xếp đã trống, nhóm sẽ không thành công (bất kể chất liệu con của nó là gì). Chúng ta có thể tận dụng hành vi này để đếm các cấp độ lồng nhau - và đây là nơi bắt nguồn của nhóm cân bằng tên (và nơi nó trở nên thú vị). Giả sử chúng ta muốn so khớp các chuỗi được đặt trong ngoặc đơn chính xác. Chúng tôi đẩy từng dấu ngoặc mở vào ngăn xếp và bật một chụp cho mỗi dấu ngoặc đóng. Nếu chúng ta gặp một dấu ngoặc đóng quá nhiều, nó sẽ cố gắng làm bật một ngăn xếp trống và làm cho mẫu không thành công:
^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*$
Vì vậy, chúng tôi có ba lựa chọn thay thế trong một lần lặp lại. Phương án đầu tiên sử dụng mọi thứ không phải là dấu ngoặc đơn. Phương án thứ hai khớp với (
s trong khi đẩy chúng lên ngăn xếp. Phương án thay thế thứ ba khớp với )
các phần tử s trong khi bật ra từ ngăn xếp (nếu có thể!).
Lưu ý: Chỉ để làm rõ, chúng tôi chỉ kiểm tra xem không có dấu ngoặc đơn nào chưa khớp! Điều này có nghĩa là chuỗi không chứa dấu ngoặc đơn nào sẽ khớp, vì chúng vẫn hợp lệ về mặt cú pháp (trong một số cú pháp mà bạn cần dấu ngoặc đơn của mình khớp). Nếu bạn muốn đảm bảo có ít nhất một tập hợp các dấu ngoặc đơn, chỉ cần thêm một lookahead (?=.*[(])
ngay sau dấu ^
.
Tuy nhiên, mô hình này không hoàn hảo (hoặc hoàn toàn đúng).
Phần cuối: Các mẫu có điều kiện
Còn một lỗi nữa: điều này không đảm bảo rằng ngăn xếp trống ở cuối chuỗi (do đó (foo(bar)
sẽ hợp lệ). .NET (và nhiều phiên bản khác) có một cấu trúc khác giúp chúng ta ở đây: các mẫu có điều kiện. Cú pháp chung là
(?(condition)truePattern|falsePattern)
trong đó falsePattern
tùy chọn - nếu nó bị bỏ qua, trường hợp sai sẽ luôn khớp. Điều kiện có thể là một mẫu hoặc tên của một nhóm chụp. Tôi sẽ tập trung vào trường hợp thứ hai ở đây. Nếu đó là tên của một nhóm chụp, thì truePattern
được sử dụng nếu và chỉ khi ngăn xếp chụp cho nhóm cụ thể đó không trống. Đó là, một mẫu có điều kiện như (?(name)yes|no)
đọc "nếu name
đã khớp và nắm bắt được thứ gì đó (vẫn còn trên ngăn xếp), hãy sử dụng mẫu yes
nếu không thì hãy sử dụng mẫu no
".
Vì vậy, ở cuối mẫu trên, chúng ta có thể thêm một cái gì đó tương tự như (?(Open)failPattern)
vậy khiến toàn bộ mẫu bị lỗi, nếu Open
-stack không trống. Điều đơn giản nhất để làm cho mô hình thất bại vô điều kiện là (?!)
(một cái nhìn tiêu cực trống rỗng). Vì vậy, chúng tôi có mô hình cuối cùng của chúng tôi:
^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*(?(Open)(?!))$
Lưu ý rằng cú pháp có điều kiện này không liên quan gì đến việc cân bằng các nhóm nhưng nó cần thiết để khai thác toàn bộ sức mạnh của chúng.
Từ đây, bầu trời là giới hạn. Có thể có nhiều cách sử dụng rất phức tạp và có một số lỗi khi được sử dụng kết hợp với các tính năng .NET-Regex khác như lookbehinds có độ dài thay đổi ( mà tôi đã phải tự học một cách khó khăn ). Tuy nhiên, câu hỏi chính luôn là: mã của bạn có còn bảo trì được khi sử dụng các tính năng này không? Bạn cần phải ghi lại nó một cách thực sự tốt và chắc chắn rằng tất cả những người làm việc trên nó cũng biết về những tính năng này. Nếu không, bạn có thể tốt hơn, chỉ cần dạo chuỗi theo cách thủ công từng ký tự và đếm các mức lồng vào một số nguyên.
Phụ lục: (?<A-B>...)
Cú pháp là gì?
Tín dụng cho phần này thuộc về Kobi (xem câu trả lời của anh ấy bên dưới để biết thêm chi tiết).
Bây giờ với tất cả những điều trên, chúng ta có thể xác nhận rằng một chuỗi được đặt trong ngoặc đơn chính xác. Nhưng nó sẽ hữu ích hơn rất nhiều, nếu chúng ta thực sự có thể lấy được (lồng nhau) các bản ghi cho tất cả nội dung của dấu ngoặc đơn đó. Tất nhiên, chúng ta có thể nhớ việc mở và đóng dấu ngoặc đơn trong một ngăn xếp chụp riêng biệt không được làm trống, và sau đó thực hiện một số trích xuất chuỗi con dựa trên vị trí của chúng trong một bước riêng biệt.
Nhưng .NET cung cấp một tính năng tiện lợi hơn ở đây: nếu chúng ta sử dụng (?<A-B>subPattern)
, không chỉ một bản chụp được bật ra từ ngăn xếp B
mà còn mọi thứ giữa bản chụp được bật lên đó B
và nhóm hiện tại này được đẩy lên ngăn xếp A
. Vì vậy, nếu chúng ta sử dụng một nhóm như thế này cho các dấu ngoặc đóng, trong khi bật các mức lồng nhau từ ngăn xếp của chúng ta, chúng ta cũng có thể đẩy nội dung của cặp vào một ngăn xếp khác:
^(?:[^()]|(?<Open>[(])|(?<Content-Open>[)]))*(?(Open)(?!))$
Kobi đã cung cấp Live-Demo này trong câu trả lời của mình
Vì vậy, kết hợp tất cả những điều này lại với nhau, chúng ta có thể:
- Nhớ tùy tiện nhiều lần chụp
- Xác thực cấu trúc lồng nhau
- Nắm bắt từng cấp độ lồng nhau
Tất cả trong một biểu thức chính quy. Nếu điều đó không thú vị ...;)
Một số tài nguyên mà tôi thấy hữu ích khi lần đầu tiên biết về chúng: