Cơ bản về Regular Expression trong C# P1

1. Giới thiệu:
RE là một ngôn ngữ cực mạnh dùng mô tả văn bản cũng như thao tác trên văn bản. Một RE thường được ứng dụng lên một chuỗi, nghĩa là lên một nhóm ký tự.
Chẳng hạn, ta có chuỗi sau:
Mot, Hai, Ba, Bon, NEVERLAND.
Bạn có thể trả về bất cứ hoặc tất cả các chuỗi con của nó (Hai hoặc Bon chẳng hạn) hoặc thay đổi phiên bản của những chuỗi con của nó (Mot hoặc BoN chẳng hạn).

Một RE là một kiểu mẫu văn bản gồm 2 phần: literal (trực kiện) và metacharacters (ký tự siêu). 
  • Một literal đơn thuần chỉ là một ký tự (a-z) mà bạn muốn đem so khớp với chuỗi đích.
  • Metacharacters là một ký tự đặc biệt hoạt động như là 1 mệnh lệnh đối với bộ phận phân tích ngữ nghĩa (parser) của RE

Bây giờ chúng ta thử tạo một regular expression như sau:

1^(From|To|Subject|Date):

RE này sẽ khớp với bất cứ chuỗi con nào miễn là chuỗi này là một hàng mới bắt đầu với “From” hoặc “To” hoặc các chữ “Subject” hoặc “Date” (Dấu ^ nghĩa là bắt đầu 1 hàng mới) chuỗi này kết thúc bởi dấu hai chấm (:).

Dấu ^ cho bộ parser của RE biết chuỗi mà ban đang truy tìm phải bắt đầu bởi một hàng mới. Các chữ “From”, “To”,.. là những literal, và những metacharacter “(“, “)”, và “|” được dùng để tạo nhóm literal và cho biết bất cứ những lựa chọn nào cũng phải khớp. Dấu ^ cũng là metacharacter, nó cho biết là khởi đầu 1 hàng. Do đó, bạn đọc hàng sau đây:

1^(From|To|Subject|Date):

Như sau: “cho khớp bất cứ chuỗi con nào bắt đầu bởi một hàng mới theo sau bởi bất cứ 4 chuỗi literal: From,To,Subject và Date rồi theo sau bởi dấu hai chấm
2. Các ký tự siêu thường dùng (vô cùng quan trọng cần phải nắm)

. : đại diện cho 1 ký tự bất kỳ trừ ký tự xuống dòng \n.
\d : ký tự chữ số tương đương [0-9]
\D : ký tự ko phải chữ số
\s : ký tự khoảng trắng tương đương [ \f\n\r\t\v]
\S : ký tự không phải khoảng trắng tương đương [ ^\f\n\r\t\v]
\w : ký tự word (gồm chữ cái và chữ số, dấu gạch dưới _ ) tương đương [a-zA-Z_0-9]
\W : ký tự không phải ký tự word tương đương [^a-zA-Z_0-9]
^ : bắt đầu 1 chuỗi hay 1 dòng
$ : kết thúc 1 chuỗi hay 1 dòng
\A : bắt đầu 1 chuỗi
\z : kết thúc 1 chuỗi
| : ký tự ngăn cách so trùng tương đương với phép or (lưu ý cái này nếu muốn kết hợp nhiều điều kiện)
[abc] : khớp với 1 ký tự nằm trong nhóm là a hay b hay c.
[a-z] so trùng với 1 ký tự nằm trong phạm vi a-z, dùng dấu - làm dấu ngăn cách.
[^abc] sẽ không so trùng với 1 ký tự nằm trong nhóm, ví dụ không so trùng với a hay b hay c.
() : Xác định 1 group (biểu thức con) xem như nó là một yếu tố đơn lẻ trong pattern .ví dụ ((a(b))c) sẽ khớp với b, ab, abc.
? : khớp với đứng trước từ 0 hay 1 lần. Ví dụ A?B sẽ khớp với B hay AB.
* : khớp với đứng trước từ 0 lần trở lên . A*B khớp với B, AB, AAB
+ : khớp với đứng trước từ 1 lần trở lên. A+B khớp với AB, AAB.
{n} : n là con số, Khớp đúng với n ký tự đúng trước nó . Ví dụ A{2}) khớp đúng với 2 chữ A.
{n, } : khớp đúng với n ký tự trở lên đứng trước nó , A{2,} khớp vói AA, AAA ...
{m,n} : khớp đùng với từ m->n ký tự đứng trước nó, A{2,4} khớp vói AA,AAA,AAAA

?=kitu : phải có kitu .VD: AA(?=x) là kết thúc sau AA là kí tự x
?!kitu : ko có kitu

Note: khi kết hợp các kí tự đặc biệt: 
*? là 0 lần
+? là 1 lần
?? là 0 lần  và các loại kết hợp đặc biệt khác sẽ bị lỗi !
3. Các lớp để thao tác với Regular Expression trong .NET:

.NET cung cấp một cách tiếp cận hướng đối tượng về việc so khớp chuỗi và thay thế theo RE. System.Text.RegularExpression là namespace trên thư viện các lớp chuẩn của .NET liên quan đến tất cả các đối tượng gắn liền với RE. Sau đây mình xin giới thiệu sơ lược về các lớp này:

1.Regex:
Lớp Regex tượng trưng cho 1 regular expression bất di bất dịch (read-only). Nó cũng chứa một phương thức tĩnh (static) cho phép chúng ta sử dụng những lớp rex khác mà khỏi khởi tạo 1 đối tượng khác. Ví dụ:

1string pattern = @"\s2000";
2Regex myRegex = new Regex(pattern);

Sau đây, mình sẽ kể ra vài thành phần của lớp Regex này:
-Thuộc tính:
+Options: trả về những mục chọn được trao qua cho constructor Regex.
+RightToLeft: nhận 1 trị cho biết liệu xem regular expression dò tìm từ phải qua trái hay không
-Phương thức:
+GetGroupNames: trả về mảng gồm toàn tên nhóm thu lượm đối với RE.
+GetGroupNumbers: trả về mảng gồm toàn số nhóm thu lượm tương ứng với tên nhóm trên 1 mảng.
+GroupNameFromNumber: đi lấy tên nhóm tương ứng với số nhóm được khai báo.
+IsMatch: trả về trị bool cho biết liệu xem RE có tìm thấy một so khớp hay không trên pattern.
+Match: dò tìm trên pattern xem có xuất hiện một RE hay không rồi trả về kết quả chính xác như là một đối tượng Match duy nhất.
+Matches: dò tìm trên pattern xem tất cả các xuất hiện của một RE có hay không rồi trả về tất cả những so khớp thành công xem như Match được gọi nhiều lần.
+Replace: cho thay thế những xuất hiện của một pattern được định nghĩa bởi một RE bởi một chuỗi ký tự thay thế được chỉ định.
+Split: chẻ một pattern thành một mảng gồm những chuỗi con ở những vị trí được chỉ định bởi một so khớp trên RE
+Unescape: cho unescape bất cứ những ký tự nào được escape trên pattern.

Sau đây là ví dụ sử dụng lớp Regex để tách chuỗi qua việc dùng phương thức Split của nó:

01string chuoi = "Mot, Hai, Ba, Bon, NEVERLAND.";
02                //tạo pattern
03                //luật: xem chuỗi nào có chứa khoảng trắng hay dấu phẩy
04                string pattern = " |, ";
05                Regex myRegex = new Regex(pattern);
06                string[] sKetQua = myRegex.Split(chuoi);
07                foreach (string subString in sKetQua)
08                {
09                    Console.WriteLine(subString);
10               }

Và đây là kết quả của nó:

1Mot
2
3Hai
4
5Ba
6
7Bon
8
9NEVERLAND.

Như bạn thấy , phương thức khởi tạo của class Regex sẽ nhận 1 chuỗi pattern làm đối số.
Phương thức Regex.Split() hoạt động cũng giống như String.Split(), trả về 1 mảng chuỗi như là kết quả việc so khớp pattern của RE trong lòng myRegex.
2.Lớp Match:

Lớp này tượng trưng cho những kết quả duy nhất của một tác vụ so khớp (match) RE. Sau đây mình có 1 ví dụ nhỏ sử dụng phương thức Match của lớp Regex để trả về 1 đối tượng kiểu Match để có thể tìm ra so khớp đầu tiên trên chuỗi nhập.
Sử dụng thuộc tính Match.Access của lớp Match báo cho biết liệu xem đã tìm ra 1 so khớp hay chưa.

01string chuoi = "123abcd456bdabc";
02            string pattern = "abc";
03            Regex myRegex = new Regex(pattern);
04            Match m = myRegex.Match(chuoi);
05            if (m.Success)
06            {
07                Console.WriteLine("Tim thay chuoi con {0} o vi tri thu {1} trong chuoi", m.Value, m.Index);
08            }
09            else
10                Console.WriteLine("Khong tim thay chi ca");

Kết quả như sau:

1Tim thay chuoi con abc o vi tri thu 3 trong chuoi

3.Lớp MatchCollection

Lớp này tượng trưng cho 1 loạt những so khớp thành công đè chồng lên nhau tạo thành một tập hợp bất di bất dịch và lớp này không có phương thức khởi tạo. Nhũng đối tượng MatchCollection sẽ do thuộc tính Regex.Matches của lớp Regex trả về.
Hiểu nôm na MatchCollection là mảng các đối tượng Match là được.
Ví dụ:

01static void Main(string[] args)
02        {
03            //tập hợp chứa những so khớp
04            MatchCollection mc;
05            //1 chuỗi thử nghiệm
06            string chuoi = "I like money, like woman and like C#";
07            //tạo pattern
08            string pattern = "like";
09            //khởi tạo 1 đối tượng của Regex
10            //truyền chuỗi pattern vào constructor
11            Regex myRegex = new Regex(pattern);
12            //dùng phương thức Matches của myRegex
13            //để tìm ra matches và chỉ mục của từng match
14            mc = myRegex.Matches(chuoi);
15            foreach (Match m in mc)
16            {
17                Console.WriteLine("Chuoi con '{0}' xuat hien o chi muc {1}", m.Value, m.Index);
18            }
19        }

Ta có kết quả sau:

1Chuoi con 'like' xuat hien o chi muc 2
2Chuoi con 'like' xuat hien o chi muc 14
3Chuoi con 'like' xuat hien o chi muc 29

Sử dụng Regex Match Collections:

Hai thuộc tính của đối tượng Match là chiều dài và vị trí của nó, mà ta có thể đọc như ví dụ sau:

01static void Main(string[] args)
02        {
03            //tập hợp chứa những so khớp
04            MatchCollection mc;
05            //1 chuỗi thử nghiệm
06            string chuoi = "This is a example string.";
07            //tạo pattern
08            //luật:cho tìm ra bất cứ những ký tự không phải ký tự khoảng trắng
09            //rồi theo sau nó là kí tự khoảng trắng
10            string pattern = @"\S+\s";
11            //khởi tạo 1 đối tượng của Regex
12            //truyền chuỗi pattern vào constructor
13            Regex myRegex = new Regex(pattern);
14            //dùng phương thức Matches của myRegex
15            //để tìm ra matches và chỉ mục của từng match
16            mc = myRegex.Matches(chuoi);
17            for (int i = 0; i < mc.Count; i++)
18            {
19                Console.WriteLine("The match[{0}]: '{1}' co chieu dai la {2}", i,mc[i].Value, mc[i].Length);

Chuỗi \S đi tìm những ký tự không phải khoảng trắng, và dấu + cho biết một hoặc nhiều ký tự ở đằng sau. Còn \s (s thường nhá) cho biết là khoảng trắng. Do đó, gộp lại ta có mệnh đề “hãy đi tìm bất cứ ký tự non-whitespace theo sau bởi whitespace”.

Kết quả của ví dụ trên là:

1The match[0]: 'This ' co chieu dai la 5
2The match[1]: 'is ' co chieu dai la 3
3The match[2]: 'a ' co chieu dai la 2
4The match[3]: 'example ' co chieu dai la 8

Lý do từ chót “string.” không được tìm thấy là vì nó có kết thúc là dấu chấm (không phải khoảng trắng).
4.Lớp Group
Đôi khi người ta cho là rất tiện khi cho gộp lại những biểu thức con so khớp với nhau như vậy bạn có thể phân tích ngữ nghĩa những đoạn của chuỗi khớp. 
Ví dụ, có thể bạn muốn so khớp dựa trên địa chỉ IP và cho gộp lại tất cả các IP tìm thấy được bất cứ nơi nào trên đoạn chuỗi.
Lớp Group cho phép bạn tạo những nhóm so khớp dựa trên cú pháp RE, và tượng trưng cho kết quả từ 1 biểu thức gộp nhóm duy nhất.
Một biểu thức gộp nhóm đặt tên cho một nhóm v2 cung cấp 1 RE; bất cứ chuỗi con nào khớp với RE sẽ được đưa vào nhóm. Ví dụ, muốn tạo 1 nhóm IP, bạn có thể viết một RE cho biết một hoặc nhiều digit hay dot theo sau bởi space như sau:

1@”(?(\d|\.)+)\s”
Lớp Match được dẫn xuất từ Group, và có một tập hợp mang tên Groups chứa tất cả các nhóm mà Match tìm thấy.
Lớp Group tượng trưng cho những kết quả thu hoạch được từ 1 thu lượm nhóm duy nhất. Ví Group có thể thu lượm 0, 1 hoặc nhiều chuỗi chữ trong một lần so khớp duy nhất, nó chứa một tập hợp gồm những đối tượng của Capture. Vì Group kế thừa từ Capture, substring bị thu lượm có thể được truy xuất trực tiếp.

Các thể hiện của Group sẽ được trả về bởi thuộc tính Match.Groups(số group) hoặc Match.Groups(“tên group”) nếu cấu trúc gộp nhóm “(?<groupname>)” được dùng đến.

Ví dụ sau đây sử dụng kiến trúc gộp nhóm lồng nhau để thu lượm những chuỗi con gộp thành nhóm:

01static void Main(string[] args)
02        {
03            string pattern = "(a(b))c";
04            string chuoi = "abdabc";
05            //định nghĩa những substring abc,ab,b
06            Regex myRegex = new Regex(pattern);
07            Match m = myRegex.Match(chuoi);
08  
09            for (int i = 0; m.Groups[i].Value != ""; i++)
10            {
11                Console.WriteLine("{0} co chieu dai {1}", m.Groups[i].Value, m.Groups[i].Length);
12            }
13        }

Kết quả:

1abc co chieu dai 3
2ab co chieu dai 2
3b co chieu dai 1

Đoạn mã sau đây sử dụng kiến trúc gộp nhóm có mang tên (name và value) để thu lượm những substrings từ một chuỗi chứa dữ liệu trên 1 dạng thức “DATANAME:VALUE” mà RE bị chẻ ở dâu dấu hai chấm (:)

01static void Main(string[] args)
02        {
03            string pattern = @"^(?\w+):(?\w+)";
04            Regex myRegex = new Regex(pattern);
05            Match m = myRegex.Match("Section:119900");
06            for (int i = 0; m.Groups[i].Value != ""; i++)
07            {
08                Console.WriteLine("{0} co chieu dai {1}", m.Groups[i].Value, m.Groups[i].Length);
09            }
10        }

Kết quả:

1Section:119900 co chieu dai 14
2Section co chieu dai 7
3119900 co chieu dai 6

RE sẻ trả về kết xuất sau đây:

1m.Groups[“name”].Value = “Section1”
2m.Groups[“value”].Value = “119900”

Sử dụng cụ thể lớp Group:

01static void Main(string[] args)
02        {
03            //một chuỗi ví dụ
04            string chuoi = "04:03:27 127.0.0.0 khanh.com.vn";
05            //group time = một hoặc nhiều digit hoặc dấu hai chấm
06            //theo sau bởi khoảng trắng
07            string timePattern = @"(?(\d|\:)+)\s";
08            string ipPattren = @"(?(\d|\.)+)\s";
09            string sitePattern = @"(?\S+)";
10            string pattern = timePattern +  ipPattren +  sitePattern;
11            Regex myRegex = new Regex(pattern);
12            //đi lấy tập hợp những so khớp
13            MatchCollection matches = myRegex.Matches(chuoi);
14  
15            foreach (Match match in matches)
16            {
17                if (match.Length != 0)
18                {
19                    Console.WriteLine("\nMatch: {0}", match.ToString());
20                    Console.WriteLine("\nTime: {0}", match.Groups["time"]);
21                    Console.WriteLine("\nIP: {0}", match.Groups["ip"]);
22                    Console.WriteLine("\nSite: {0}", match.Groups["site"]);
23                }
24            }
25        }

Kết quả:

1Match: 04:03:27 127.0.0.0 khanh.com.vn
2Time: 04:03:27
3 IP: 127.0.0.0
4Site: khanh.com.vn

Theo ví dụ trên, đầu tiên ta tạo một chuỗi để tiến hành dò khớp:

1string chuoi = "04:03:27 127.0.0.0 khanh.com.vn";
Chuỗi này có thể là 1 trong nhiều chuỗi được ghi nhận trên một tập tin log của web server như là kết quả dò tìm của CSDL. Trong ví dụ đơn giản này có 3 cột: time – IP – Site, mỗi cột cách nhau bởi một khoảng trắng.
Bạn muốn tạo một đối tượng Regex duy nhất để dò tìm những chuỗi kiểu này, và chặt chúng thành 3 nhóm: time,ip và site :

1string timePattern = @"(?(\d|\:)+)\s";
2            string ipPattren = @"(?(\d|\.)+)\s";
3            string sitePattern = @"(?\S+)";
4            string pattern = timePattern +  ipPattren +  sitePattern;
5            Regex myRegex = new Regex(pattern);

Ta tập trung xem các ký tự hình thành nhóm:
Các dấu ngoặc () sẽ tạo nên một nhóm. Những gì nằm giữa dấu ngoặc mở (ngay trước dấu ?) và dấu ngoặc đóng (sau dấu + trong trường hợp này) là 1 nhóm đơn độc chưa mang tên.

1@"(?(\d|\:)+)\s"

  • Chuỗi ?<time> đặt tên nhóm là time và nhóm gắn liền với đoạn văn bản so khớp, là regular expression “(\d|\:)+)\s”. RE này ược suy diễn như sau: “một hoặc nhiều digit hoặc dấu hai chấm theo sau bởi khoảng trắng”.
  • Chuỗi ?<ip> đặt tên cho nhóm ip, và ?<site> đặt tên cho nhóm site.
Như các ví dụ trước, ví dụ trên cũng đòi hỏi một tập hợp của tát cả các đoạn khớp:

1MatchCollection matches = myRegex.Matches(chuoi);
Tiếp theo, cho đi xuyên qua tập hợp matches để lôi ra từng phần tử match của nó:

1foreach (Match match in matches)
Nếu chiều dài Length của match lớn hơn 0 có nghĩa là đã tìm thấy một so khớp. Sau đó, thì cho in ra toàn bộ những mục so khớp:
Tiếp theo, là đi lấy nhóm time từ tập hợp Groups của match rồi cho in ra nội dung:

1Console.WriteLine("\nTime: {0}", match.Groups["time"]);
Kết quả
Tương tự như thế với các nhóm site và ip ta có kết quả

1IP: 127.0.0.0

2Site: khanh.com.vn

Comment