Nhóm Cân bằng biểu thức chính quy là gì?


91

Tôi vừa đọc một câu hỏi về cách lấy dữ liệu bên trong dấu ngoặc nhọn kép ( câu hỏi này ), và sau đó ai đó đã đưa ra các nhóm cân bằng. Tôi vẫn không chắc chắn chúng là gì và cách sử dụng chúng.

Tôi đã đọc qua Định nghĩa Nhóm Cân bằng , nhưng phần giải thích khó làm theo và tôi vẫn khá bối rối về các câu hỏi mà tôi đã đề cập.

Ai đó có thể giải thích đơn giản các nhóm cân bằng là gì và chúng hữu ích như thế nào không?


Tôi tự hỏi có bao nhiêu regex engiens này thực sự được hỗ trợ.
Mike de Klerk

2
@MikedeKlerk Nó được hỗ trợ ít nhất trong công cụ .NET Regex.
Không phải đâu.

Câu trả lời:


173

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 1sẽ chỉ mang lại một kết quả: d(lưu ý, kết quả phù hợp tất nhiên sẽ abcdnhư 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 CaptureCollectioncó 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 wordnế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 đó falsePatterntù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 yesnế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 Bmà còn mọi thứ giữa bản chụp được bật lên đó Bvà 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:


6
Câu trả lời này đã được thêm vào Câu hỏi thường gặp về Cụm từ Thông dụng Tràn Ngăn xếp , trong "Regex-Fu Nâng cao".
aliteralmind

39

Chỉ là một bổ sung nhỏ cho câu trả lời xuất sắc của M. Buettner:

Thỏa thuận với (?<A-B>)cú pháp là gì?

(?<A-B>x)là một cách tinh tế khác với (?<-A>(?<B>x)). Chúng dẫn đến cùng một luồng điều khiển * , nhưng chúng nắm bắt khác nhau.
Ví dụ, chúng ta hãy xem xét một mô hình cho niềng răng cân đối:

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

Vào cuối trận đấu, chúng tôi có một chuỗi cân bằng, nhưng đó là tất cả những gì chúng tôi có - chúng tôi không biết các dấu ngoặc nhọn ở đâuBngăn xếp trống. Công việc khó khăn mà động cơ đã làm cho chúng ta không còn nữa.
( ví dụ trên Regex Storm )

(?<A-B>x)là giải pháp cho vấn đề đó. Làm sao? Nó không chụp xvào $A: nó ghi lại nội dung giữa lần chụp trước Bvà vị trí hiện tại.

Hãy sử dụng nó trong mô hình của chúng tôi:

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

Điều này sẽ bắt vào $Contentcác chuỗi giữa các dấu ngoặc nhọn (và vị trí của chúng), cho mỗi cặp trên đường đi.
Đối với các chuỗi {1 2 {3} {4 5 {6}} 7}có muốn có bốn chụp: 3, 6, 4 5 {6}, và 1 2 {3} {4 5 {6}} 7- tốt hơn nhiều so với không có gì hay } } } }.
( ví dụ - nhấp vào tabletab và xem ${Content}, chụp )

Trên thực tế, nó có thể được sử dụng mà không cần cân bằng: (?<A>).(.(?<Content-A>).)chụp hai ký tự đầu tiên, mặc dù chúng được phân tách theo nhóm.
(lookahead thường được sử dụng ở đây hơn nhưng không phải lúc nào nó cũng mở rộng quy mô: nó có thể trùng lặp logic của bạn.)

(?<A-B>)là một tính năng mạnh - nó cho phép bạn kiểm soát chính xác các ảnh chụp của mình. Hãy ghi nhớ điều đó khi bạn đang cố gắng vượt ra khỏi khuôn mẫu của mình.


@FYI, tiếp tục thảo luận từ câu hỏi bạn không thích trong một câu trả lời mới cho câu hỏi này. :)
zx81

Tôi đang cố gắng tìm ra một cách để thực hiện việc kiểm tra regex niềng răng cân đối với việc thoát mắc cài bên trong. EG đoạn mã sau sẽ truyền: public class Foo {private const char BAR = '{'; chuỗi riêng _qux = "{{{"; } Có ai đã làm điều này?
Mr Anderson

@MrAnderson - Bạn chỉ cần thêm |'[^']*'vào đúng chỗ: ví dụ . Nếu bạn cũng cần các ký tự thoát, có một ví dụ ở đây: (Regex để đối sánh các ký tự chuỗi C #) [ stackoverflow.com/a/4953878/7586] .
Kobi
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.