Regular Expression (Biểu thức chính quy)

cplusplusXin chào mọi người!

Vậy là sau nhiều ngày ngụp lặn trong deadline, hiện tại mình vẫn còn sống sót và cập nhật blog 😀 Tuy có hơi quá trễ so với quy định, nhưng mình chúc tất cả các bạn nữ theo dõi blog này có 1 ngày 20/10 thật vui vẻ, hạnh phúc, thành công trong cuộc sống, và đặc biệt với các bạn trong ngành CNTT thì cố gắng vượt lũ “deadline” nhé 😀

Lan man thế đủ rồi, mình xin mở màn về bài viết hôm nay 🙂

Có bao giờ bạn gặp 1 vấn đề như thế này: Giả sử người dùng nhập vào email, bạn muốn kiểm tra xem liệu rằng email có hợp lệ hay không, hoặc là số điện thoại chẳng hạn (đặc biệt với mấy bạn không bao giờ nhập chính xác mà toàn là asdasdasd @@ – mình lâu lâu cũng vậy 😀 ). Thông tin từ phía người dùng không bao giờ có thể tin tưởng hoàn toàn 100%, mà phải kiểm tra cụ thể và rõ ràng. Vậy để kiểm tra liệu rằng người dùng có nhập chính xác dữ liệu mình cần thì phải làm sao?

Bài viết hôm nay mình sẽ giới thiệu về Regular Expression, hay còn được gọi với tên “thuần Việt” là Biểu thức chính quy.

1. Khái niệm

Regular Expression (hay còn gọi là Biểu thức chính quy, RegEx, RegExp,…) là một chuỗi miêu tả một bộ các chuỗi khác, theo những quy tắc cú pháp nhất định. Ta có thể sử dụng regex để tìm kiếm, so sánh, cắt ghép,… chuỗi dựa trên các mẫu văn bản (pattern).

C++11 đã mở rất nhiều tiêu chuẩn, và regex là 1 trong số các tiêu chuẩn đó, vì vậy nên ở bài viết này mình sẽ sử dụng C++ làm chuẩn (đối với C# thì ta có lớp Regex, Java thì có package java.util.regex,… các bạn có thể tự tìm hiểu thêm nhé). Tài liệu về regex trong C++ các bạn có thể xem ở đây.

VD: Ta muốn kiểm tra xem chữ “Hom qua” có trong chuỗi “Hom qua qua noi qua qua ma qua khong qua, hom nay qua khong noi qua qua ma qua qua” (Hôm qua qua nói qua qua mà qua không qua, hôm nay qua không nói qua qua mà qua qua) hay không (hack não :v)

Mã nguồn:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
// Nhớ include thư viện regex nhé
#include <regex>
#include <string>
using namespace std;
void main()
{
  string source("Hom qua qua noi qua qua ma qua khong qua, hom nay qua khong noi qua qua ma qua qua");
  // regex nằm trong namespace std nhé
  regex pattern("Hom qua");
  if (regex_search(source, pattern))
    cout << "Tim thay!" << endl;
  else cout << "Khong tim thay!" << endl;
  system("pause");
}

Kết quả sẽ xuất ra “Tim thay!” nếu chuỗi “Hom qua” trong pattern có trong chuỗi source. Thật là tiện lợi phải không nào? Nhưng thế mạnh của nó không chỉ có như vậy 😀

2. Cách sử dụng

C++11 hỗ trợ script dạng ECMAScript để biểu diễn pattern. Trước khi sử dụng regex, các bạn cần xem qua một số kí tự đặc biệt được sử dụng tại đây. Dưới đây là danh sách các kí tự đặc biệt thường gặp:

  • .: đại diện cho bất kì kí tự nào ngoại trừ \
  • [abc]: khớp với 1 kí tự trong nhóm kí tự a, b hoặc c
  • [a-z]: khớp với 1 kí tự trong vùng từ a-z, ngăn cách bằng dấu 
  • [^abc]: khớp với 1 kí tự ngoại trừ nhóm kí tự a, b và c
  • \d: kí tự chữ số tương đương [0-9]
  • \D: kí tự không phải chữ số
  • \s: kí tự khoảng trắng (space)
  • \S: kí tự không phải khoảng trắng
  • \w: kí tự word (gồm chữ cái, chữ số và dấu _), tương đương [a-zA-Z0-9_]
  • \W: tương đương [^a-zA-Z0-9_]
  • ^: bắt đầu 1 chuỗi hay 1 dòng
  • $: kết thúc 1 chuỗi hay 1 dòng
  • \: biểu diễn 1 kí tự đặc biệt trong regex thành kí tự thường (ví dụ \. sẽ khớp với dấu chấm, nếu thiếu \ sẽ hiểu nhầm là kí tự đại diện . phía trên mình đã nói)
  • |: kí tự tương đương phép or (sử dụng nhiều trong cặp móc tròn)
  • (): khớp với 1 nhóm các kí tự (ví dụ Ste(v|ph)en sẽ khớp với Steven hoặc Stephen)
  • ?: khớp với kí tự đứng trước 0 – 1 lần (ví dụ S?even sẽ khớp Seven hoặc even)
  • *: khớp với kí tự đứng trước từ 0 lần trở lên (ví dụ Te*n sẽ khớp Tn, Ten, Teen, Teeen, Teeeen,…)
  • +: khớp với kí tự đứng trước từ 1 lần trở lên (ví dụ Te+n sẽ khớp Ten, Teen, Teeen,…)
  • {<n>}: khớp đúng với <n> kí tự trước đó (ví dụ Te{2}n sẽ khớp Teen)
  • {<n>,}: khớp với <n> kí tự trước đó trở lên (ví dụ Te{2,}n sẽ khớp Teen, Teeen, Teeeen,…)
  • {<n>,<m>}: khớp từ <n> đến <m> kí tự trước đó (ví dụ Te{1,3}n sẽ khớp Ten, Teen, Teeen,…)

VD: ta muốn kiểm tra 1 email có hợp lệ:

Mã nguồn:
1
2
3
4
5
6
7
string email = "vector.mic@gmail.com";
regex pattern("[a-zA-Z0-9_\.]+@[a-zA-Z]+\.[a-zA-Z]+(\.[a-zA-Z]+)*");
// Hàm regex_match() dùng để kiểm tra TOÀN BỘ chuỗi
// Hàm regex_search() dùng để kiểm tra CHUỖI CON trong chuỗi
if (regex_match(email, pattern)
  cout << "Match!" << endl;
else cout << "Not match!" << endl;

Cùng phân tích sâu nhé:

[a-zA-Z0-9_\.]+@[a-zA-Z]+\.[a-zA-Z]+(\.[a-zA-Z]+)*
  1. Đầu tiên ta kiểm tra phần tên email, tên email bao gồm kí tự thường, in hoa, chữ số, dấu _ và .. Vì vậy nên ta phải gom tất cả vào 1 nhóm [a-zA-Z_\.] (bởi vì giữa kí tự thường và in hoa có các kí tự đặc biệt nên ta không thể dùng A-z mà phải tách ra a-zA-Z). Tuy nhiên ta cần phải thêm dấu + vì nhóm đó chỉ đại diện cho 1 kí tự duy nhất.
  2. Kí tự @
  3. Phần tên miền, ở đây mình quy định tên miền chỉ bao gồm các kí tự thường và hoa, có dấu . phân cách. Vì vậy nên ta ghi vào [a-zA-Z]+\.[a-zA-Z]+. Tuy nhiên nó chỉ đúng với vài tên miền dạng như gmail.com, yahoo.com, live.com,… nhưng sẽ không đúng với những tên miền dạng như yahoo.com.vn, dauan.com.xyz.vn,… chẳng hạn. Vì vậy ta cần phải thêm vào 1 group bao gồm dấu và các kí tự chữ (\.[a-zA-Z]+). Group này có thể có cũng được, không có cũng được nên ta đặt dấu ngay đuôi (khớp với 0 lần trở lên).

Vậy là ta đã thiết kế xong 1 pattern kiểm tra email hợp lệ rồi! Đơn giản phải không nào?

VD: ta muốn kiểm tra 1 số điện thoại di động hợp lệ ở Việt Nam (không có khoảng trắng nhé)

Mã nguồn:
1
2
3
4
5
string input = "0987412354";
regex pattern("(\\+84|0)\\d{9,10}");
if (regex_match(email, pattern)
  cout << "Match!" << endl;
else cout << "Not match!" << endl;

Cùng phân tích sâu nhé:

(\\+84|0)\\d{9,10}
  1. Đầu tiên, do số điện thoại có phần mở đầu có thể là +84 (ở Việt Nam) hoặc là 0, nên ta cần đặt vào trong group và thêm dấu |
  2. Kế tiếp, 1 số điện thoại bao gồm 10 hoặc 11 chữ số, nhưng ta không tính phần đầu của số điện thoại nên chỉ còn khoảng 9 – 10 chữ số

Thật dễ phải không nào? Bây giờ thử 1 ví dụ khó nhé

VD: ta muốn kiểm tra 1 tên miền hợp lệ

Mã nguồn:
1
2
3
4
5
string url = "http://trachanhso.net/";
regex pattern("(http|https|ftp):\\/\\/(www\\.)?[\\w\\-]+\\.[\\w]+(\\.[\\w]+)*\\/?(([\\w\\-]+)\\/?)*([\\w\\-_]+\.(php|html|htm|jsp|aspx))?(\\?([\\w\\-]+=[\\w\\-]+)*(&([\\w\\-]+=[\\w\\-]+))*)?");
if (regex_match(email, pattern)
  cout << "Match!" << endl;
else cout << "Not match!" << endl;

Cùng phân tích sâu nhé:

(http|https|ftp):\\/\\/(www\\.)?[\\w\\-]+\\.[\\w]+(\\.[\\w]+)*\\/?(([\\w\\-]+)\\/)*([\\w\\-_]+\\.(php|html|htm|jsp|aspx))?(\\?([\\w\\-]+=[\\w\\-]+)?(&([\\w\\-]+=[\\w\\-]+))*)?

Do chuỗi trên quá dài, khó hiểu nên ta sẽ tách ra làm 3 phần: tên miền, path và biến. Các bạn có thể không cần xem hết cả Path lẫn Biến nếu không thật sự hiểu hết về regex 😀 hại não nhau lắm

  • Tên miền: (http|https|ftp):\\/\\/(www\\.)?[\\w\\-]+\\.[\\w]+(\\.[\\w]+)*\\/?
  • Path: (([\\w\\-]+)\\/)*([\\w\\-_]+\\.(php|html|htm|jsp|aspx))?
  • Biến: (\\?([\\w\\-]+=[\\w\\-]+)?(&([\\w\\-]+=[\\w\\-]+))*)?

Tên miền:

  1. Đầu tiên, protocol có rất nhiều dạng, nhưng mình kể đến là 3 dạng: http, https và ftp, vì vậy nên mình đặt nó vào group kèm theo dấu | để khớp với 1 trong 3 dạng protocol. Kế đến là dấu hai chấm và 2 dấu //, nhưng đó là kí tự đặc biệt trong regex nên ta phải thêm 2 dấu \\ ở trước mỗi kí tự thành \\/\\/.
  2. Do với mỗi tên miền có thể có www hoặc không, nên ta để group (www\\.) và thêm ? để chỉ khớp 0 – 1 lần duy nhất.
  3. Tên miền thì giống như email phía trên, các bạn có thể quay lại trên để đọc nhé 😀
  4. Cuối cùng là dấu / nhưng có thể không cần nên ta thêm ? vào

Path: VD: http://trachanhso.net/thuat-toan/ hoặc http://trachanhso.net/index.php hoặc http://trachanhso.net/thuat-toan/index.php

  1. Tên path bao gồm kí tự chữ, số, dấu gạch chân và dấu trừ, và kết thúc có thể có dấu / hoặc không, vì vậy ta sẽ viết: [\\w\\-]+)\\/? . Tuy nhiên ta cần phải thêm dấu * trong các trường hợp như http://trachanhso.net/ hoặc http://trachanhso.net/thuat-toan/abc-xyz/dauan-tuongan/, như vậy sẽ trở thành ([\\w\\-]+)\\/)*
  2. Nếu URL có tên file, tên file có dạng <tên file>.<đuôi>, đuôi có thể là php, html, htm, aspx, jsp,… nhưng mình chỉ liệt kê 5 thằng trên. Do vậy tên file sẽ có regex pattern là [\\w\\-]+\\.(php|html|htm|jsp|aspx). Nhưng do có nhiều URL ẩn đi tên file để tránh lộ thông tin, nên gom pattern thành 1 group và thêm ? vào phía sau, trở thành ([\\w\\-_]+\\.(php|html|htm|jsp|aspx))?

Biến: VD: ?id=6 hay ?cat_name=lap_trinh&cat_id=4

  1. Đầu tiên ta phải có kí tự ? ở đầu
  2. Kế tiếp ta sẽ có dạng <tên biến>=<giá trị>, vì vậy regex pattern sẽ là [\\w\\-]+=[\\w\\-]+, ngoài ra ta còn phải thêm dấu ? phía sau, thành ([\\w\\-]+=[\\w\\-]+)?
  3. Nếu URL có nhiều biến, ta phải thêm kí tự giữa các biến, vì vậy regex pattern sẽ là (&([\\w\\-]+=[\\w\\-]+))* (dấu * để biểu thị rằng biến có thể có nhiều hơn)
  4. Và cuối cùng, ta cần đặt 1 dấu ? cho toàn bộ regex pattern biến, để cho biết rằng phần biến có thể có hoặc không.

Regular Expression ban đầu thoạt thì nhìn rất khó, bởi vì cú pháp rất khó nắm vững, nhưng nếu các bạn chịu khó thực hành với những mẫu chuỗi căn bản như email, số điện thoại, thì những mẫu chuỗi khó hơn như URL sẽ không làm khó được bạn 😀

Trên đây là bài viết về Regular Expression trong C++, cảm ơn các bạn đã chú ý theo dõi. Hẹn gặp lại ở những bài viết tiếp theo!

 

LibreOffice of :

Ký tự Kết quả
Bất cứ ký tự nào Đại diện ký tự đưa ra nếu không được ghi rõ khác.
. Đại diện bất cứ ký tự đơn nào, trừ ký tự ngắt dòng hay ký tự ngắt đoạn văn. Thí dụ, chuỗi tìm kiếm « T.ng » sẽ trả lại cả « Tổng », « Tăng », « Tưng » v.v.
^ Chỉ tìm kết quả ở đầu của đoạn văn. Ở vị trí đó cũng bỏ qua các đối tượng đặc biệt, như trường rỗng hay ký tự thả neo vào ký tự. Thí dụ « ^Hướng » sẽ tìm các đoạn văn bắt đầu với « Hướng ».
$ Chỉ tìm kết quả ở cuối của đoạn văn. Ở vị trí đó cũng bỏ qua các đối tượng đặc biệt, như trường rỗng hay ký tự thả neo vào ký tự. Thí dụ « xong$ » tìm chỉ các đoạn văn kết thúc bằng « xong ».$ on its own matches the end of a paragraph. This way it is possible to search and replace paragraph breaks.
* Tìm ≥0 lần gặp ký tự phía trước dấu sao. Thí dụ, chuỗi « Ab*c » tìm cả « Ac », « Abc », « Abbc », « Abbbc » v.v.
+ Tìm ≥1 lần gặp ký tự phía trước dấu cộng. Thí dụ « AX.+4 » tìm « AXx4 », nhưng không phải « AX4 ».Trong đoạn văn, lúc nào cũng tìm chuỗi dài nhất có thể mà tương ứng với mẫu tìm kiếm này. Nếu đoạn văn chứa « AX 4 AX4 », toàn chuỗi được tô sáng.
 ? Tìm 0 hay 1 ký tự phía trước dấu hỏi. Thí dụ « Texts? » tìm cả hai « Text » và « Texts », và « x(ab|c)?y » tìm « xy », « xaby » hay « xcy ».
\ Chức năng tìm kiếm giải thích ký tự đặc biệt phía sau « \ » như một ký tự bình thường, không phải như một biểu thức chính quy (trừ khi dùng tổ hợp « \n », « \t », « \< » hay « \> »). Thí dụ « tree\. » tìm « tree. », không phải « treed » hay « trees ». Vậy ký tự dấu xuyệc ngược có thể được dùng để « thoát » các ký tự thường có nghĩa đặc biệt trong biểu thức.
\n Represents a line break that was inserted with the Shift+Enter key combination. To change a line break into a paragraph break, enter \n in the Find and Replace boxes, and then perform a search and replace.\n in the Find text box stands for a line break that was inserted with the Shift+Enter key combination.

\n in the Replace text box stands for a paragraph break that can be entered with the Enter or Return key.

\t Represents a tab. You can also use this expression in the Replace box.
\b Match a word boundary. For example, “\bbook” finds “bookmark” but not “checkbook” whereas “book\b” finds “checkbook” but not “bookmark”. The discrete word “book” is found by both search terms.
^$ Tìm một đoạn văn rỗng.
^. Tìm ký tự đầu của đoạn văn.
& hay $0 Adds the string that was found by the search criteria in the Find box to the term in the Replace box when you make a replacement.For example, if you enter “window” in the Find box and “&frame” in the Replace box, the word “window” is replaced with “windowframe”.

You can also enter an “&” in the Replace box to modify the Attributes or the Format of the string found by the search criteria.

[abc123] Đại diện một của những ký tự nằm bên trong dấu ngoặc vuông.
[a-e] Represents any of the characters that are between a and e, including both start and end charactersThe characters are ordered by their code numbers.
[a-eh-x] Đại diện bất cứ ký tự nào nằm giữa a và e, và giữa h và x, trong bảng chữ cái.
[^a-s] Đại diện bất cứ ký tự nào không nằm giữa a và s trong bảng chữ cái.
\uXXXX\UXXXXXXXX Represents a character based on its four-digit hexadecimal Unicode code (XXXX).For obscure characters there is a separate variant with capital U and eight hexadecimal digits (XXXXXXXX).

For certain symbol fonts the code for special characters may depend on the used font. You can view the codes by choosing Insert – Special Character.

| Finds the terms that occur before the “|” and also finds the terms that occur after the “|”. For example, “this|that” finds “this” and “that”.
{2} Xác định số lần gặp ký tự nằm trước dấu ngoặc móc mở. Thí dụ, « tre{2} » tìm « tree ».
{1,2} Xác định số lần tối thiểu có thể gặp ký tự nằm trước dấu ngoặc móc mở. Thí dụ, « tre{2,} » tìm cả « tree », « treee » và « treeeee ».
{1,} Xác định số lần tối thiểu có thể gặp ký tự nằm trước dấu ngoặc móc mở. Thí dụ, « tre{2,} » tìm cả « tree », « treee » và « treeeee ».
( ) In the Find box:Xác định các ký tự nằm bên trong dấu ngoặc làm tham chiếu. Vì vậy bạn có thể chỉ tới tham chiếu thứ nhất trong biểu thức hiện thời dùng « \1 », tới tham chiếu thớ hai dùng « \2 » v.v.

Chẳng hạn, nếu văn bản chứa con số 13487889 và bạn tìm kiếm bằng biểu thức chính quy « (8)7\1\1 » thì tìm được « 8788 ».

Cũng có thể sử dụng () để nhóm lại các từ, v.d. « a(bc)?d » sẽ tìm « ad » hay « abcd ».

In the Replace box:

Dùng dấu đồng đô-la ($) thay cho gạch chéo ngược (\) để thay thế tam chiếu. Dùng « $0 » để thay thế toàn chuỗi đã tìm.

[:alpha:] Đại diện một ký tự chữ cái. Dùng « [:alpha:]+ » để tìm chỉ một ký tự chữ cái.
[:digit:] Đại diện một chữ số thập phân. Dùng « [:digit:]+ » để tìm chỉ một chữ số.
[:alnum:] Đại diện một ký tự chữ số, chữ cái ([:alpha:] và [:digit:]).
[:space:] Đại diện một ký tự dấu cách (không phải ký tự khoảng trắng khác).
[:print:] Đại diện một ký tự có thể in được.
[:cntrl:] Đại diện một ký tự không thể in.
[:lower:] Đại diện một ký tự chữ thường nếu tùy chọn Phân biệt chữ hoa/thường đã được hiệu lực dưới mục Tùy chọn.
[:upper:] Đại diện một ký tự chữ hoa nếu tùy chọn Phân biệt chữ hoa/thường đã được hiệu lực dưới mục Tùy chọn.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s