Regular Expression – Phần 12: Java Regular Expression Engine (java.util.regex.*): Group và Subgroup

Trong các phần trước, chúng ta đã làm quen với khái niệm group cho phép gom nhóm một biểu thức mẫu con trong một mẫu cha để người dùng có thể tùy chọn hoặc phải nhập dữ liệu đúng với toàn bộ nhóm, hoặc có thể bỏ qua toàn bộ nhóm kí tự đó.

Hoặc chúng ta đã sử dụng qua khái niệm group để qui định trước danh sách các mục mà người dùng phải nhập, ví dụ như sau:


String pattern = (music|sport|book|movie)

Chúng ta dùng cặp dấu ngoặc tròn (()) và chỉ định một danh sách các mục, cách nhau bởi dấu gạch đứng (|). Như vậy, với mẫu trên, người dùng bắt buộc phải nhập một trong các giá trị: music, sport, book, hoặc movie.

Trong phần này, chúng ta sẽ tìm hiểu chi tiết hơn về khái niệm group (nhóm) và sub-group (nhóm con)

Group là một tập hợp các kí tự mẫu tuần tự. Hay nói một cách khác, group cho phép chúng ta xử lý một tập hợp các kí tự mẫu như một đơn vị duy nhất.

Trong java, một biểu thức Regular Expression có thể có nhiều group. Mỗi group đượ đánh một chỉ số (index) giống như trong mảng (array). Group đầu tiên cũng sẽ có chỉ số là 0.

Group được chia làm 2 loại: group ngầm định (implicit group) và group tường minh (explicit group):

–      Các kí tự được đặt trong cặp dấu ngoặc tròn (()) được gọi là group tường minh, như chúng ta đã sử dụng trong các phần trước

–      Các kí tự không nằm trong cặp dấu ngoặc tròn (()) được gọi là group ngầm định

Như vậy, khi chúng ta tạo ra một mẫu và không đặt vào trong cặp dấu ngoặc tròn (()) thì mẫu này thuộc về một nhóm ngầm định, và có chỉ số (index) là 0.

Ví dụ một mẫu trong hình sau:

1_implicit_group

Trong mẫu trên, chúng ta có 1 group ngầm định với chỉ số là 0.

Chúng ta xem ví dụ sau:

2_explicit_implicit_groups

Trong mẫu trên, chúng ta có 2 group:

–      1 group ngầm định có chỉ số 0, chính là nhóm chứa toàn bộ mẫu

–      1 group tường minh có chỉ số 1, chính là phần đặt trong cặp dấu ngoặc tròn ((\\d{3}))

Chúng ta xem ví dụ sau:

3_explicit_implicit_group

Trong mẫu trên, chúng ta có 3 group:

–      Một group ngầm định có chỉ số 0, chính là nhóm chứa toàn bộ mẫu

–      Một group tường minh có chỉ số 1, chính là phần đặt trong cặp dấu ngoặc tròn đầu tiên ((\\d{3}))

–      Một group tường minh có chỉ số 2, chính là phần đặt trong cặp dấu ngoặc tròn thứ 2 ((\\d{3}))

Như vậy, chú ý chúng ta có thể nói mẫu trên có tổng cộng 3 nhóm: 1 nhóm ngầm định, và 2 nhóm tường minh.

Chúng ta xem đoạn chương trình sau:


import java.util.regex.*;
public class Demo {
    public static void main(String[] args) throws Exception {
        Pattern pattern;
        Matcher matcher;
        String searchString;
        String text;
        text = "Monday 12-9-2013";
        searchString = "\\w+ \\d{1,2}-\\d{1,2}-\\d{4}";
        pattern = Pattern.compile(searchString, Pattern.CASE_INSENSITIVE);
        matcher = pattern.matcher(text);    
        while (matcher.find()) {
            System.out.println("found: " + matcher.group(0));
        }
    }
}

Trong đoạn chương trình trên, tôi định nghĩa một mẫu như sau:


searchString = "\\w+ \\d{1,2}-\\d{1,2}-\\d{4}";

Mẫu này khớp với bất kì định dạng ngày tháng:

–      Bắt đầu bằng các kí tự, theo sau là một khoảng trắng

–      Theo sau là 1 hoặc 2 số đại diện cho ngày, theo sau là dấu gạch nối (-)

–      Theo sau là 1 hoặc 2 số đại diện cho tháng, theo sau là dấu gạch nối (-)

–      Theo sau là 4 số đại diện cho năm

Sau đó tôi truyền mẫu này vào cho phương thức Pattern.compile() để tiến hành “biên dịch” mẫu, nghĩa là kiểm tra mẫu có hợp lệ hay không:


pattern = Pattern.compile(searchString, Pattern.CASE_INSENSITIVE);

Chú ý, tôi chỉ định thêm tham số thứ 2 cho phương thức Pattern.compile():


Pattern.CASE_INSENSITIVE

Giá trị này có nghĩa là các kí tượng trong chuỗi đầu vào có thể là chữ HOA hoặc chữ thường.

Tôi để sẵn một chuỗi ngày tháng khớp với mẫu như sau:


text = "Monday 12-9-2013";

Va trong vòng lặp while() tôi sử dụng phương thức Matcher.find() để tìm mẫu trùng trong chuỗi ban đầu, nếu tìm thấy, thì tôi gọi phương thức Matcher.group() để lấy ra nhóm trùng với mẫu:


while (matcher.find()) {
            System.out.println("found: " + matcher.group(0));
}

Chú ý, trong phương thức Matcher.group() tôi truyền vào giá trị 0, chính là muốn lấy ra chuỗi khớp với group có chỉ số là 0.

Hoặc nếu tôi tôi không truyền giá trị chỉ số cho phương thức Matcher.group() thì mặc định, phương thức này cũng sẽ trả về chuỗi khớp với group có chỉ số là 0.

Do vậy, dòng code:


System.out.println("found: " + matcher.group(0));

Tương đương với:


System.out.println("found: " + matcher.group());

Chạy chương trình, chúng ta có kết quả như sau:


found: Monday 12-9-2013

Chúng ta được toàn bộ chuỗi ban đầu, vì toàn bộ chuỗi này khớp với mẫu nằm trong group ngầm định có chỉ số 0.

Giả sử bây giờ tôi muốn lấy ra từng thành phần:

–      Phần ngày, bao gồm cả thứ

–      Phần tháng

–      Phần năm

Trong trường hợp này, tôi cần đặt từng thành phần trong một group để tôi có thể tách ra khi so trùng mẫu.

Tôi thay đổi chương trình như sau:


import java.util.regex.*;
public class Demo {
    public static void main(String[] args) throws Exception {
        Pattern pattern;
        Matcher matcher;
        String searchString;
        String text;
        text = "Monday 12-9-2013";
        searchString = "(\\w+ \\d{1,2})-(\\d{1,2})-(\\d{4})";
        pattern = Pattern.compile(searchString, Pattern.CASE_INSENSITIVE);
        matcher = pattern.matcher(text);
        while (matcher.find()) {
            System.out.println("found: " + matcher.group(1));
            System.out.println("found: " + matcher.group(2));
            System.out.println("found: " + matcher.group(3));
        }
        System.out.println("There are " + matcher.groupCount() + " groups in the pattern!");
    }
}

Chú ý trong đoạn chương trình trên, tôi đã đặt từng thành phần muốn lấy ra trong từng nhóm:


searchString = "(\\w+ \\d{1,2})-(\\d{1,2})-(\\d{4})";

Như vậy, trong mẫu trên, tôi có tổng cộng 4 nhóm:

–      Nhóm đầu tiên, có chỉ số 0, luôn là nhóm ngầm định không nằm trong dấu ngoặc tròn nào

–      Nhóm thứ 2, có chỉ số 1, chính là phần nằm trong cặp dấu ngoặc tròn đầu tiên: (\\w+ \\d{1,2})

–      Nhóm thứ 3, có chỉ số 2, chính là phần nằm trong cặp dấu ngoặc tròn thứ 2: (\\d{1,2})

–      Nhóm thứ 4, có chỉ số 3, chính là phần nằm trong cặp dấu ngoặc tròn thứ 3: (\\d{4})

Như vậy, sau khi so trùng khớp với mẫu, chúng ta muốn lấy ra từng thành phần, tức là từng nhóm, chúng ta chỉ cần cung cấp chỉ số tương ứng cho phương thức Matcher.group() như sau:


while (matcher.find()) {
            System.out.println("found: " + matcher.group(1));
            System.out.println("found: " + matcher.group(2));
            System.out.println("found: " + matcher.group(3));
}

Và để đếm xem có bao nhiêu group trong mẫu, chúng ta gọi phương thức Matcher.groupCount():


System.out.println("There are " + matcher.groupCount() + " groups in the pattern!");

Tuy nhiên, lưu ý là phương thức Matcher.groupCount() chỉ trả về số lượng group tường minh, tức 3 group trong ví dụ hiện tại.

Chạy chương trình, chúng ta được kết quả như sau:


found: Monday 12
found: 9
found: 2013
There are 3 groups in the pattern!

Như vậy là chúng ta đã tách ra được từng thành phần trong chuỗi ban đầu.

Chúng ta có thể lồng một group vào trong một group khác. Trong trường hợp này, group nằm trong được gọi là sub group (group con).

Ví dụ trong trường hợp trên, tôi muốn tách riêng phần thứ trong phần ngày ra, thì tôi cần đặt toàn bộ phần mẫu khớp ngày vào một group nữa:


searchString = "((\\w+) \\d{1,2})-(\\d{1,2})-(\\d{4})";

Như vậy, hiện tại trong mẫu của tôi đang có tổng cộng 5 nhóm:

–      Nhóm đầu tiên, có chỉ số 0, luôn là nhóm ngầm định không nằm trong dấu ngoặc tròn nào

–      Nhóm thứ 2, có chỉ số 1, chính là phần nằm trong cặp dấu ngoặc tròn đầu tiên: ((\\w+) \\d{1,2})

–      Nhóm thứ 3, có chỉ số 2, bây giờ chính là nhóm con nằm trong nhóm thứ 2: (\\w+)

–      Nhóm thứ 4, có chỉ số 3,chính là nhóm nằm trong cặp dấu ngoặc tròn thứ 3: (\\d{1,2})

–      Nhóm thứ 5, có chỉ số 4, chính là nhóm nằm trong cặp dấu ngoặc tròn cuối cùng: (\\d{4})

Do chúng ta đã có thêm một nhóm con, vì vậy chúng ta cần gọi thêm phương thức Matcher.group() và cung cấp thêm chỉ số thứ 4 để lấy ra tất cả các nhóm:


while (matcher.find()) {
            System.out.println("found: " + matcher.group(1));
            System.out.println("found: " + matcher.group(2));
            System.out.println("found: " + matcher.group(3));
            System.out.println("found: " + matcher.group(4));
}

Chạy chương trình, chúng ta có kết quả như sau:


found: Monday 12
found: Monday
found: 9
found: 2013
There are 4 groups in the pattern!

Như vậy, chúng ta đã tách ra được từng thành phần mong muốn. Và số lượng nhóm tường minh hiện tại là 4 nhóm.