Java: Bộ định lượng (Quantifier)
Quantifier (bộ định lượng) có thể được sử dụng để chỉ định số lần xuất hiện phù hợp.
Thoạt nhìn, có vẻ như các bộ định lượng X?, X??, và X?+ thực hiện những điều tương tự, vì chúng đều có vẻ khớp với X không hoặc một lần.
Tuy nhiên, có sự khác biệt nhỏ liên quan đến việc triển khai giữa mỗi bộ định lượng này.
Bảng sau cho thấy các định lượng tham lam (greedy), miễn cưỡng (reluctant và chiếm hữu (possessive):
Greedy | Reluctant | Possessive | Ý nghĩa |
---|---|---|---|
X? |
X?? |
X?+ |
X , không hoặc một lần |
X* |
X*? |
X*+ |
X , không hoặc nhiều lần |
X+ |
X+? |
X++ |
X , một hoặc nhiều lần |
X{n} |
X{n}? |
X{n}+ |
X , n lần |
X{n,} |
X{n,}? |
X{n,}+ |
X , ít nhất n lần |
X{n,m} |
X{n,m}? |
X{n,m}+ |
X , từ n đến m lần |
Hãy bắt đầu xem xét các định lượng tham lam bằng cách tạo ba biểu thức chính quy khác nhau: ký tự "a" theo sau là?, * Hoặc +. Hãy xem điều gì sẽ xảy ra khi các biểu thức này được kiểm tra với một chuỗi đầu vào trống "":
Enter your regex: a? Enter input string to search: I found the text "" starting at index 0 and ending at index 0. Enter your regex: a* Enter input string to search: I found the text "" starting at index 0 and ending at index 0. Enter your regex: a+ Enter input string to search: No match found.
Khớp với độ dài bằng 0
Trong ví dụ trên, kết hợp thành công trong hai trường hợp đầu tiên vì các biểu thức a? và a* đều cho phép không xuất hiện ký tự a.
Bạn cũng sẽ nhận thấy rằng cả chỉ số bắt đầu và kết thúc đều bằng 0, không giống như bất kỳ ví dụ nào mà ta đã thấy.
Chuỗi đầu vào trống "" không có độ dài, vì vậy việc kiểm tra chỉ đơn giản là không khớp với chỉ số nào ở chỉ số 0.
Các so khớp thuộc loại này được gọi là khớp có độ dài bằng 0.
Đối sánh độ dài bằng 0 có thể xảy ra trong một số trường hợp: trong một chuỗi đầu vào trống, ở đầu chuỗi đầu vào, sau ký tự cuối cùng của chuỗi đầu vào hoặc ở giữa hai ký tự bất kỳ của chuỗi đầu vào.
Các so khớp có độ dài bằng 0 có thể dễ dàng xác định được vì chúng luôn bắt đầu và kết thúc ở cùng một vị trí chỉ mục.
Hãy cùng tìm các so khớp có độ dài bằng 0 với một vài ví dụ khác nữa. Bạn hãy thay đổi chuỗi đầu vào thành một chữ cái duy nhất "a" và bạn sẽ nhận thấy điều thú vị:
Enter your regex: a? Enter input string to search: a I found the text "a" starting at index 0 and ending at index 1. I found the text "" starting at index 1 and ending at index 1. Enter your regex: a* Enter input string to search: a I found the text "a" starting at index 0 and ending at index 1. I found the text "" starting at index 1 and ending at index 1. Enter your regex: a+ Enter input string to search: a I found the text "a" starting at index 0 and ending at index 1
Cả ba bộ định lượng đều tìm thấy ký tự "a", nhưng hai bộ định lượng đầu tiên cũng tìm thấy khớp có độ dài bằng 0 ở chỉ số 1; nghĩa là, sau ký tự cuối cùng của chuỗi đầu vào. Hãy nhớ rằng trình so khớp nhìn thấy ký tự "a" đang ở trong ô giữa chỉ mục 0 và chỉ mục 1 và các vòng lặp khai thác thử nghiệm (test harness) của ta sẽ lặp lại cho đến khi không tìm thấy ký tự trùng khớp nữa. Tùy thuộc vào bộ định lượng được sử dụng, sự hiện diện của "nothing" ở chỉ mục sau ký tự cuối cùng có thể kích hoạt hoặc không thể kích hoạt so khớp.
Bây giờ thay đổi chuỗi đầu vào thành ký tự "a" năm lần liên tiếp và ta sẽ nhận được những điều sau:
Enter your regex: a? Enter input string to search: aaaaa I found the text "a" starting at index 0 and ending at index 1. I found the text "a" starting at index 1 and ending at index 2. I found the text "a" starting at index 2 and ending at index 3. I found the text "a" starting at index 3 and ending at index 4. I found the text "a" starting at index 4 and ending at index 5. I found the text "" starting at index 5 and ending at index 5. Enter your regex: a* Enter input string to search: aaaaa I found the text "aaaaa" starting at index 0 and ending at index 5. I found the text "" starting at index 5 and ending at index 5. Enter your regex: a+ Enter input string to search: aaaaa I found the text "aaaaa" starting at index 0 and ending at index 5.
Biểu thức a? tìm một kết quả khớp riêng lẻ cho từng ký tự vì nó khớp khi "a" xuất hiện không hoặc một lần. Biểu thức a* tìm thấy hai kết quả phù hợp riêng biệt: tất cả các ký tự "a" trong lần so khớp đầu tiên, sau đó khớp có độ dài bằng 0 sau ký tự cuối cùng ở chỉ số 5. Và cuối cùng, a+ khớp với tất cả các lần xuất hiện của ký tự "a" , bỏ qua sự hiện diện của "nothing" ở chỉ mục cuối cùng.
Tại thời điểm này, bạn có thể tự hỏi kết quả sẽ như thế nào nếu hai bộ định lượng đầu tiên gặp một chữ cái không phải là "a". Ví dụ: điều gì xảy ra nếu nó gặp phải chữ cái "b", như trong "ababaaaab"?
Hãy cùng xem các ví dụ:
Enter your regex: a? Enter input string to search: ababaaaab I found the text "a" starting at index 0 and ending at index 1. I found the text "" starting at index 1 and ending at index 1. I found the text "a" starting at index 2 and ending at index 3. I found the text "" starting at index 3 and ending at index 3. I found the text "a" starting at index 4 and ending at index 5. I found the text "a" starting at index 5 and ending at index 6. I found the text "a" starting at index 6 and ending at index 7. I found the text "a" starting at index 7 and ending at index 8. I found the text "" starting at index 8 and ending at index 8. I found the text "" starting at index 9 and ending at index 9. Enter your regex: a* Enter input string to search: ababaaaab I found the text "a" starting at index 0 and ending at index 1. I found the text "" starting at index 1 and ending at index 1. I found the text "a" starting at index 2 and ending at index 3. I found the text "" starting at index 3 and ending at index 3. I found the text "aaaa" starting at index 4 and ending at index 8. I found the text "" starting at index 8 and ending at index 8. I found the text "" starting at index 9 and ending at index 9. Enter your regex: a+ Enter input string to search: ababaaaab I found the text "a" starting at index 0 and ending at index 1. I found the text "a" starting at index 2 and ending at index 3. I found the text "aaaa" starting at index 4 and ending at index 8.
Mặc dù ký tự "b" xuất hiện trong các ô 1, 3 và 8, nhưng kết quả cho thấy có khớp độ dài bằng 0 tại các vị trí đó.
Biểu thức chính quy a? không đặc biệt tìm kiếm chữ cái "b"; nó chỉ đơn thuần là tìm kiếm sự hiện diện (hoặc không) của chữ cái "a".
Nếu bộ định lượng cho phép đối sánh "a" bằng 0 lần, bất kỳ thứ gì trong chuỗi đầu vào không phải là "a" sẽ hiển thị dưới dạng khớp có độ dài bằng 0.
Các a còn lại được so khớp theo các quy tắc đã thảo luận trong các ví dụ trên.
Để khớp một mẫu chính xác n số lần, chỉ cần chỉ định số bên trong một bộ dấu ngoặc nhọn:
Enter your regex: a{3} Enter input string to search: aa No match found. Enter your regex: a{3} Enter input string to search: aaa I found the text "aaa" starting at index 0 and ending at index 3. Enter your regex: a{3} Enter input string to search: aaaa I found the text "aaa" starting at index 0 and ending at index 3.
Ở đây, biểu thức chính quy a{3} đang tìm kiếm ba lần xuất hiện của chữ cái "a" liên tiếp.
Thử nghiệm đầu tiên không thành công vì chuỗi đầu vào không có đủ a để so khớp. Thử nghiệm thứ hai chứa chính xác 3 a trong chuỗi đầu vào, sẽ kích hoạt một kết quả khớp.
Phép thử thứ ba cũng kích hoạt kết hợp vì có chính xác 3 chữ a ở đầu chuỗi nhập.
Bất cứ điều gì sau đó đều không liên quan đến đối sánh đầu tiên.
Nếu mẫu xuất hiện lại sau thời điểm đó, nó sẽ kích hoạt các kết quả phù hợp tiếp theo:
Enter your regex: a{3} Enter input string to search: aaaaaaaaa I found the text "aaa" starting at index 0 and ending at index 3. I found the text "aaa" starting at index 3 and ending at index 6. I found the text "aaa" starting at index 6 and ending at index 9.
Để yêu cầu một mẫu phải xuất hiện ít nhất n lần thì ta thêm dấu (,) như ví dụ dưới đây:
Enter your regex: a{3,} Enter input string to search: aaaaaaaaa I found the text "aaaaaaaaa" starting at index 0 and ending at index 9
Với cùng một chuỗi đầu vào, thử nghiệm này chỉ tìm thấy một kết quả phù hợp, vì 9 a liên tiếp thỏa mãn "ít nhất" 3 a.
Cuối cùng, để chỉ định giới hạn trên về số lần khớp, hãy thêm một số thứ hai như ví dụ sau:
Enter your regex: a{3,6} // tìm từ 3 đến 6 a Enter input string to search: aaaaaaaaa I found the text "aaaaaa" starting at index 0 and ending at index 6. I found the text "aaa" starting at index 6 and ending at index 9.
Ở đây so khớp đầu tiên buộc phải dừng ở giới hạn trên là 6 ký tự.
So khớp thứ hai bao gồm bất cứ thứ gì còn lại, xảy ra là ba chữ a - số ký tự tối thiểu được phép cho lần so khớp này.
Nếu chuỗi đầu vào ngắn hơn một ký tự, sẽ không có kết quả khớp thứ hai vì chỉ còn lại hai chữ a.
Nhóm thu thập và lớp ký tự với Bộ định lượng
Trong các ví dụ trên chúng ta chỉ thử nghiệm các bộ định lượng trên các chuỗi đầu vào chứa một ký tự.
Trên thực tế, các bộ định lượng chỉ có thể gắn vào một ký tự tại một thời điểm, vì vậy biểu thức chính quy "abc+" sẽ có nghĩa là "a, theo sau là b, theo sau là một hoặc nhiều lần c". Nó không có nghĩa là "abc" một hoặc nhiều lần. Tuy nhiên, bộ định lượng cũng có thể gắn vào lớp Character và nhóm thu thập, chẳng hạn như [abc]+ (a hoặc b hoặc c, một hoặc nhiều lần) hoặc (abc)+ (nhóm "abc", một hoặc nhiều lần).
Dưới đây minh họa bằng cách chỉ định nhóm (dog), ba lần liên tiếp.
Enter your regex: (dog){3} Enter input string to search: dogdogdogdogdogdog I found the text "dogdogdog" starting at index 0 and ending at index 9. I found the text "dogdogdog" starting at index 9 and ending at index 18. Enter your regex: dog{3} Enter input string to search: dogdogdogdogdogdog No match found
Ở đây, ví dụ đầu tiên tìm thấy ba kết quả phù hợp, vì bộ định lượng áp dụng cho toàn bộ nhóm thu thập.
Tuy nhiên, khi xóa dấu ngoặc thì kết quả khớp không thành công vì bộ định lượng {3} hiện chỉ áp dụng cho chữ cái "g".
Tương tự, chúng ta có thể áp dụng một bộ định lượng cho toàn bộ lớp ký tự:
Enter your regex: [abc]{3} Enter input string to search: abccabaaaccbbbc I found the text "abc" starting at index 0 and ending at index 3. I found the text "cab" starting at index 3 and ending at index 6. I found the text "aaa" starting at index 6 and ending at index 9. I found the text "ccb" starting at index 9 and ending at index 12. I found the text "bbc" starting at index 12 and ending at index 15. Enter your regex: abc{3} Enter input string to search: abccabaaaccbbbc No match found.
Ở đây bộ định lượng {3}
áp dụng cho toàn bộ lớp ký tự trong ví dụ đầu tiên, nhưng chỉ cho chữ "c" trong ví dụ thứ hai.
Sự khác nhau giữa Greedy, Reluctant, và Possessive
Có sự khác biệt nhỏ giữa các bộ định lượng tham lam, miễn cưỡng và chiếm hữu.
- Các bộ định lượng tham lam được coi là "tham lam" vì chúng buộc trình so khớp phải đọc hoặc "ăn" toàn bộ chuỗi đầu vào trước khi thử so khớp đầu tiên. Nếu lần thử so khớp đầu tiên (toàn bộ chuỗi đầu vào) không thành công, trình so khớp sẽ lùi chuỗi đầu vào theo một ký tự và thử lại, lặp lại quy trình cho đến khi tìm thấy khớp hoặc không còn ký tự nào để lùi lại. Tùy thuộc vào bộ định lượng được sử dụng trong biểu thức, điều cuối cùng nó sẽ thử so khớp với 1 hoặc 0 ký tự.
- Tuy nhiên, các bộ định lượng miễn cưỡng có cách tiếp cận ngược lại: Chúng bắt đầu ở đầu chuỗi nhập, sau đó miễn cưỡng "ăn" từng ký tự một để tìm kiếm một kết quả phù hợp. Điều cuối cùng chúng thử là toàn bộ chuỗi đầu vào.
- Cuối cùng, bộ định lượng sở hữu luôn "ăn" toàn bộ chuỗi đầu vào, thử một lần (và chỉ một lần) cho một so khớp. Không giống như các bộ định lượng tham lam, các bộ định lượng sở hữu không bao giờ lùi bước, ngay cả khi làm như vậy sẽ cho phép so khớp tổng thể thành công.
Để minh họa, hãy xét chuỗi đầu vào xfooxxxxxxfoo.
Enter your regex: .*foo // greedy quantifier Enter input string to search: xfooxxxxxxfoo I found the text "xfooxxxxxxfoo" starting at index 0 and ending at index 13. Enter your regex: .*?foo // reluctant quantifier Enter input string to search: xfooxxxxxxfoo I found the text "xfoo" starting at index 0 and ending at index 4. I found the text "xxxxxxfoo" starting at index 4 and ending at index 13. Enter your regex: .*+foo // possessive quantifier Enter input string to search: xfooxxxxxxfoo No match found
Ví dụ đầu tiên sử dụng bộ định lượng tham lam .* dùng để tìm "bất kỳ thứ gì", không hoặc nhiều lần, theo sau là các chữ cái "f" "o" "o". Vì bộ định lượng là tham lam, nên phần .* của biểu thức trước tiên sẽ "ăn" toàn bộ chuỗi đầu vào. Tại thời điểm này, biểu thức tổng thể không thể thành công, vì ba chữ cái cuối cùng ("f" "o" "o") đã được sử dụng. Vì vậy, trình so khớp từ từ lùi từng chữ cái một cho đến khi xuất hiện "foo" ở ngoài cùng bên phải, lúc này so khớp thành công và kết thúc tìm kiếm.
Ví dụ thứ hai là miễn cưỡng, nó bắt đầu bằng việc sử dụng "nothing". Vì "foo" không xuất hiện ở đầu chuỗi, nó buộc phải nuốt chữ cái đầu tiên (một "x"), ký tự này sẽ kích hoạt kết quả khớp đầu tiên ở 0 và 4. Test harness tiếp tục quá trình cho đến khi chuỗi đầu vào không còn. Nó tìm thấy một so khớp khác ở 4 và 13.
Ví dụ thứ ba không tìm thấy kết quả phù hợp vì bộ định lượng có tính sở hữu. Trong trường hợp này, toàn bộ chuỗi đầu vào được sử dụng bởi .*+, lhông còn lại gì để thỏa mãn "foo" ở cuối biểu thức. Sử dụng bộ định lượng sở hữu cho các tình huống mà bạn muốn nắm bắt tất cả thứ gì đó mà không bao giờ lùi bước; nó sẽ hoạt động tốt hơn bộ định lượng tham lam tương đương trong trường hợp không tìm thấy kết quả phù hợp ngay.