Regex trong java với ví dụ cụ thể – Part 1

Regular expresstions được sử dụng để định nghĩa các chuỗi mô hình dùng trong tìm kiếm, cập nhật etc. Regular expresstions hay gọi ngắn là java regex.

Lý do sử dụng Java Regex

Lấy ví dụ nhỏ rằng cho một chuỗi str, kiểm tra nếu chuỗi có chứa các ký số thì báo là không hợp lệ, ngược lại hợp lệ.

Mã 1: Lặp từng ký tự của chuỗi và kiểm tra xem nếu có bất kỳ một ký số nào thì chuỗi không hợp lệ.

public class Main {

    public static void main(String[] args) {
        String str = "shareprog1ramming";
        if (isValid(str)) {
            System.out.println("Hop le");
        } else {
            System.out.println("Khong hop le");
        }
    }

    private static boolean isValid(String str) {
        for (int i = 0; i < str.length() - 1; i++) {
            if (str.charAt(i) >= '0' && str.charAt(i) <= '9') {
                return false;
            }
        }
        return true;
    }
}

Output:

Khong hop le

Mã 2: Vẫn là ý tưởng nếu chuỗi có bất kỳ ký số nào thì là không hợp lệ. Sử dụng java regex.

import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) {
        String str = "shareprog1ramming";
        String pattern = "\\D+";
        if (Pattern.matches(pattern, str)) {
            System.out.println("Hop le");
        } else {
            System.out.println("Khong hop le");
        }
    }
}

Output: 

Khong hop le

Ở trên chuỗi pattern được gọi là một chuỗi mẫu dùng để áp dụng trong java regex. Ý nghĩa “\\D+” chuỗi không được chứ các ký số [0-9].

So với Mã 1 thì Mã 2 có phần ngắn gọn hơn. Đó chỉ là một trong các ứng dụng của java regex. Giờ thì đi tìm hiểu xem các tính năng và cách sử dụng của java regex nào.

Pattern class

Pattern.matches()

Ở phần đầu mình đã dùng matches() để kiểm tra xem chuỗi đầu vào không được chứa các ký số. Chuỗi mẫu pattern \\D+ dùng để kiểm tra chuỗi chỉ chứa các các ký tự.

Như vậy method matches() là một method cho phép kiểm tra tính hợp lệ của chuỗi theo một chuỗi mẫu được định nghĩa trước.

Pattern.compile()

Giả sử mình có danh sách các chuỗi ký tự, mình muốn tìm các chuỗi chứ ký tự a, b, c không phân biệt chữ hoa hay chữ thường. Cho ví dụ  str1 = ‘faaBcf’, str2 = ‘abcfd’ hoặc str3 = ‘ABC’ đều hợp lệ.

Vậy phải làm cách nào đây? Liệt kê các trường hợp có thể có rồi lập thành các chuỗi mẫu để matches()?

Không nhé, hãy xem compile()Matcher.matches() giúp gì cho mình nào. Matcher class sẽ được giới thiệu ở phần sau nhé.

// File Main.java
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) {

        List<String> strs = new ArrayList<>();
        strs.add("agabc");
        strs.add("agg1c");
        strs.add("klA13c");
        strs.add("bgaBc");
        strs.add("afABC");
        String patternStr = ".*abc.*";
        Pattern pattern = Pattern.compile(patternStr, Pattern.CASE_INSENSITIVE);
        for (String str : strs) {
            Matcher matcher = pattern.matcher(str);
            if (matcher.matches()) {
                System.out.println(str);
            }
        }

    }
}

Output:

agabc
bgaBc
afABC

Khi chúng ta sử dụng compile() sẽ nhận được một instance của Pattern. Sử dụng Matcher.matches() để kiểm tra tính hợp lệ của chuỗi.

Các trường hợp tìm kiếm nó sẽ tìm kiếm các chuỗi có chứa ký tự đó mà không phân biệt chữ hoa hay chữ thường. Ngoài ra chúng ta còn có các param khác, các bạn có thể xem tại flag pattern.

 

Pattern.split()

Ngoài công dụng kiểm tra và tìm kiếm, Pattern cũng cung cấp cơ chế cắt chuỗi dựa trên chuỗi mẫu regex. Giống như matcher() split() cũng được dùng từ instance được tạo ra từ compile().

Nếu các bạn dùng cách cắt chuỗi bình thường thì chỉ cắt theo ký tự ngắt xác định. Còn với split() cắt với chuỗi mẫu sẽ giúp cho việc cắt chuỗi linh hoạt hơn.

Ví dụ mình có chuỗi “share235programming52.net” không biết vì lý do gì mà nó bị chèn các chữ số vào, bây giờ mình muốn lọc các chữ số ra để lấy lại chuỗi vốn có.

Mình sẽ dùng split() với đoạn chuỗi mẫu khớp với các chữ số, để khi gặp các chữ số thì nó sẽ cắt chuỗi. Sau đó nối mảng nhận được từ split() sẽ được chuỗi ban đầu.

import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) {
        String str = "share235programming52.net";
        String patternStr = "\\d+";
        Pattern pattern = Pattern.compile(patternStr, Pattern.CASE_INSENSITIVE);
        String[] results = pattern.split(str);
        StringBuilder primitive = new StringBuilder();
        for (String s : results) {
            primitive.append(s);
        }

        System.out.println(primitive.toString());

    }
}

Output:

shareprogramming.net

Matcher Class

Ở phần trên mình đã giới thiệu sơ qua Matcher class bằng việc sử dụng method matches(). Phần  này chúng ta sẽ tìm hiểu xem thử Matcher class còn có những thứ gì nữa nào.

matches()

Để kiểm tra tính hợp lệ dựa trên chuỗi mẫu của một Matcher instance được tạo ra từ Pattern.matcher()

// ...
Matcher matcher = pattern.matcher(str);
   if (matcher.matches()) {
       System.out.println(str);
    }

lookingAt()

Tương tự matches(), chỉ khác một điểm duy nhất là lookingAt() sẽ kiểm tra chuỗi regex tại đầu chuỗi, trong khi matches() sẽ kiểm tra trên toàn bộ chuỗi.

find()

Method find() thường được dùng để tìm kiếm chuỗi con hoặc chuỗi regex trong chuỗi input

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) {
        String str = "share235programming52.net";
        String patternStr = "program";
        Pattern pattern = Pattern.compile(patternStr, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(str);
        boolean found = matcher.find();
        System.out.println(found ? "Tim thay" : "Khong tim thay");

    }
}

Output

Tim thay

start() và end()

Cả 2 method này được dùng chung với find() để lấy vị đầu và cuối của chuỗi tìm kiếm được.

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) {
        String content = "ZZZ AA PP AA QQQ AAA ZZ";

        String string = "AA";
        Pattern pattern = Pattern.compile(string);
        Matcher matcher = pattern.matcher(content);

        while(matcher.find()) {
            System.out.println("Index: "+ matcher.start()
                    +
                    " - " + matcher.end());
        }
    
    }
}

Output


Index: 4 – 6
Index: 10 – 12
Index: 17 – 19

Chuỗi ký tự

Nếu bạn muốn tìm một chuỗi trong mảng ứng với mỗi chuỗi cho trước. Hoặc chúng ta loop qua các phần tử của mảng và sử dụng equals() để so sánh, hoặc chúng ta có thể dùng regex như sau: 

Pattern.matches("shareprogramming", "shareprogramming");

Character classes

Character class cho phép định nghĩa một tập các ký tự và cho phép trùng khớp với một ký tự trong chuỗi input đầu vào.

Cho ví dụ:

Pattern.matches(“[pqr]”, “abcd”) chúng ta sẽ nhận được false vì p, q, r đều không có trong chuỗi input. 
Pattern.matches(“[pqr]”, “r”) true vì r được tìm thấy trong chuỗi input.
Pattern.matches(“[pqr]”, “pq”) false vì r không chứa một trong hai.

Cách sử dụng character class:

[abc]: Phù hợp với chuỗi chỉ chứa duy nhất một trong ba ký tự a, b hoặc c.

[^abc]: Phù hợp với chuỗi chỉ chứa một ký tự ngoại trừ a, b hoặc c.

[a-zA-Z]: Phù hợp với chuỗi chỉ chứa một ký tự trong khoảng a-z và A-Z.

[a-d[m-p]]: Phù hợp với chuỗi chỉ chứa một ký tự trong khoảng a-d và m-p. (phép hội)

[a-z&&[abc]]: Phù hợp với chuỗi chỉ chứa một ký tự trong các ký tự a, b hoặc c. (phép giao)

[a-z&&[^bc]]: Phù hợp với chuỗi chỉ chứa một ký tự trong trong khoảng a-z ngoại trừ b và c.

[a-z&&[^m-p]]: Phù hợp với chuỗi chỉ chứa một ký tự trong trong khoảng a-z ngoại các ký tự trong khoảng m-p.

Các Character class dựng sẵn

Để dễ dàng cho chúng ta, java xây dựng một số character class:

Cấu trúc Mô tả Ví dụ
. Bất ký ký tự nào Pattern.matches(“.”, “*”)) => true
\d Một chữ số Pattern.matches(“\\d”, “1”)) => true  
\D Không phải chữ số Pattern.matches(“\\D”, “1”)) =>   
\s Một ký tự khoảng trắng Pattern.matches(“\\s”, ” “)) => true
\S Không phải là một khoảng trắng Pattern.matches(“\\S”, ” “)) => false
\w Một ký tự hoặc ký số

Pattern.matches(“\\w”, “#”) => false

Pattern.matches(“\\w”, “1”) => true

Pattern.matches(“\\w”, “a”) => true

\W Không phải một ký tự hoặc một ký số

Pattern.matches(“\\w”, “a”) => false

Pattern.matches(“\\w”, “*”) => true

Note: Ở trên các bạn thấy các chuỗi regex đều chỉ chứa một dấu \. Ấy vậy mà trong ví dụ thì lại có đến 2 dấu \ ?. Thật ra trong java một dấu \ là một ký tự escape. Thế nên để biểu diễn dấu \ chúng ta phải sử dụng đến 2 cấu \ đấy.

Boundary Matchers

Ở các phần trên chúng ta đã biết làm sao để tìm so khớp một chuỗi. Giờ đây, boundary matchers giúp chúng ta tìm kiếm chính xác vị trí xuất hiện trong chuỗi input. 

Chúng ta một danh sách các boundary matchers sau:

  • ^ – Đặt trước từ được khớp
  • $ – Đặt cuối từ đựợc khớp
  • \b – Kiểm tra mẫu bắt đầu hay kết thúc trên word boundary.
  • \B – Kiểm tra mẫu bắt đầu hay kết thúc không trên word boundary.
  • \A – Bắt đầu chuỗi input.
  • \G – Yêu cầu đối sánh chỉ xảy ra một lần vào cuối lần đối sánh trước.
  • \Z – Cuối cùng của chuỗi input nhưng có thể cho cùng các terminators.
  • \z – Cuối cùng của chuỗi input.

Ví dụ: Input = ”hgaishga” đi tìm vị trí của str = “hga” trong chuỗi input. 

^ – đặt trước từ được khớp

// File Main.java
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) {
        String txt = "hgaishga";

        String regex = "^hga";
        Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(txt);
        while (matcher.find())
        {
            System.out.println("Start: " + matcher.start());
            System.out.println("End: " + matcher.end());
        }
    }
}

Output: start: 0 – end: 3

$ – đặt sau từ được khớp

//File Main.java
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) {
        String txt = "hgaishga";

        String regex = "hga$";
        Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(txt);
        while (matcher.find())
        {
            System.out.println("Start: " + matcher.start());
            System.out.println("End: " + matcher.end());
        }
    }
}

Output: start 5 – end: 8

\b – Kiểm tra mẫu bắt đầu hay kết thúc trên word boundary

 

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) {
        String txt = "hgaishga";

        String regex1 = "\\bhga";
        Pattern pattern1 = Pattern.compile(regex1, Pattern.CASE_INSENSITIVE);
        Matcher matcher1 = pattern1.matcher(txt);
        while (matcher1.find())
        {
            System.out.println("Start: " + matcher1.start());
            System.out.println("End: " + matcher1.end());
        }
        System.out.println();

        String regex2 = "hga\\b";
        Pattern pattern2 = Pattern.compile(regex2, Pattern.CASE_INSENSITIVE);
        Matcher matcher2 = pattern2.matcher(txt);
        while (matcher2.find())
        {
            System.out.println("Start: " + matcher2.start());
            System.out.println("End: " + matcher2.end());
        }
    }
}

Output:

Start: 0
End: 3

Start: 5
End: 8

\G – Yêu cầu đối sánh chỉ xảy ra một lần vào cuối lần đối sánh trước

Mình chỉ lấy ví dụ cho các bạn hiểu phần này thôi nhé, các  bạn tự code lấy =)); 

Input = hgahga hga , regex = \\Dhga

Output => start -end: 0 – 3 và 3 – 6

Quantifiers

Quantifiers cho phép chỉ định số lần xuất hiện trong chuỗi input. Chúng ta có 3 loại quantifiers sau: Greedy, Reluctant, Possesive.

Greedy Reluctant Possessive  Meaning
X? X?? X?+ X, xuất hiện một lần hoặc không
X* X*? X*+ X không xuất hiện hoặc xuất hiện nhiều lần
X+ X+? X++ X xuất hiện một hoặc nhiều lần
X{n} X{n}? X{n}+ X xuất hiện đúng n lần
X{n, } X{n, }? X{n, }+ X xuất hiện ít nhất n lần
X{n, m} X{n, m}? X{n, m}+ X xuất hiện trong khoảng n đến m. (m >= n)

Với 3 loại trên về mặt ý nghĩa là giống nhau, thế nhưng trong một các trường hợp cụ thể chúng sẽ thực hiện theo cách riêng của nó. Chúng ta sẽ đi qua các ví dụ để xem chúng khác nhau thế nào!

Chuỗi regex: a?
Input string: a
Tìm thấy “a” bắt đầu tại 0 kết thúc tại 1
Tìm thấy “” bắt đầu tại 1 kết thúc 1

Chuỗi regex: a*
Input string: a
Tìm thấy “a” bắt đầu tại 0 kết thúc taị 1
Tìm thấy “” bắt đầu tại 1 kết thúc 1

Chuỗi regex: a+
Input string: a
Tìm thấy “a” bắt đầu tại 0 kết thúc taị 1

Qua ví dụ trên ta thấy a? và a* đều có kết quả Tìm thấy “” bắt đầu tại 1 kết thúc 1. Tương ứng với không xuất hiện trong chuỗi input.

Chuỗi regex: a?
Input string: aaaaa
Tìm thấy “a” bắt đầu tại 0 kết thúc tại 1
Tìm thấy “a” bắt đầu tại 1 kết thúc tại 2
Tìm thấy “a” bắt đầu tại 2 kết thúc tại 3
Tìm thấy “a” bắt đầu tại 3 kết thúc tại 4
Tìm thấy “a” bắt đầu tại 4 kết thúc tại 5
Tìm thấy “” bắt đầu tại 5 kết thúc 5

Chuỗi regex: a*
Input string: aaaaa
Tìm thấy “a” bắt đầu tại 0 kết thúc taị 5
Tìm thấy “” bắt đầu tại 5 kết thúc 5

Chuỗi regex: a+
Input string: a
Tìm thấy “a” bắt đầu tại 0 kết thúc taị 5

 

Sự khác nhau giữa Greedy, Reluctant, Possessive

Greedy: Mặc định nó sẽ cố gắng chuỗi dài nhất phù hợp với pattern đã cho. Trước khi xứ lý Greedy sẽ đọc toàn bộ chuỗi và cố gắng so khớp, nếu không thoả nó tiến hành bỏ ký tự cuối ra và tiếp tục so sánh cho đến khi tìm thấy.

Reluctant: Nó ngược lại với greedy. Nó bắt đầu từ ký tự đầu tiên và xử lý một ký tự tại một thời điểm.

Possessive: Tương tự như Greedy, điểm khác nhau lớn nhất là Possessive không xoá ký tự cuối cùng và tiến hành kiếm tra lại.

Chuỗi Regex: .*foo // Greedy quantifier
Input: xfooxxxxxxfoo
Tìm thấy xfooxxxxxxfoo bắt đầu tại 0 kết thúc tại 13

Chuỗi Regex: .*?foo // reluctant quantifier 
Input: xfooxxxxxxfoo
Tìm thấy xfoo bắt đầu tại 0 kết thúc tại 4
Tìm thấy xxxxxxfoo tại vị trí 4 kết thúc 13

Chuỗi Regex: .*+foo // pose
Input: xfooxxxxxxfoo
Không tìm thấy

Nhìn vào ví dụ đầu tiên sử dụng greedy quantifier, .* dùng để tìm “anything”, cho nên riêng .* nó đã tìm ra chuỗi xfooxxxxxxfoo, nhưng còn ký chuỗi foo đằng sau nên nó sẽ tiến hành bỏ đi ký tự cuối cùng, đến khi bỏ được 3 ký tự cuối chúng ta sẽ tìm thấy kết quả so sánh khớp đầu tiên là (xfooxxxxxx cho .* và foo). Tiếp tục đến cuối ta có xfoo.

Ở ví dụ thứ hai, reluctant quantifier xử lý tuần tự từ phần tử đầu tiên đến hết ta cũng có 2 chuỗi thoả xfoo, xfooxxxxxxfoo.

Và ví dụ cuối cùng chúng ta không tìm thấy bất kỳ chuỗi nào, vì .* chiếm trọn chuỗi input, vì Possessive quantifier không có cơ chế bỏ đi ký tự cuối và tiếp tục so sánh nên ta không có kết quả nào thoả mãn.

Code demo

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {
    public static void main(String[] args)
    {
//        Pattern p = Pattern.compile(".*foo");
        Pattern p = Pattern.compile(".*?foo");
//        Pattern p = Pattern.compile(".+foo");

        Matcher m = p.matcher("xfooxxxxxxfoo");

        while (m.find())
            System.out.println("Start: " + m.start() +
                    " to " + (m.end()));

    }

}

Bài tập

1, Viết chương trình kiểm tra ký tự đầu tiên của chuỗi có phải là chữ in hoa hay không?
2, Viết chương trình kiểm tra định dạng của địa chỉ email. Cho email đinh dạng đính thoả mãn các điều kiện sau:
  • Bắt đầu bằng ký tự a-z hoặc A-Z – (^[a-zA-Z]+).
  • Email không được chứa các ký tự đặt biệt – ([a-zA-Z0-9]*)
  • @ xuất hiện 1 lần trong sau nó là các chữ cái, ví dụ @gmail, @yahoo etc – (@{1}[a-zA-Z]+).
  • Email kết thúc với .com – mail.com$

Source code tham khảo

3, Kiểm tra định dạng của số điện thoại. Biết rằng số điện thoại có 10 chữ số, bắt đầu bằng số 0. Số tiếp theo không được là số 0.

Source code tham khảo

 

‹Previous Next›

 

4 1 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x