Trang chủ » Regular Expression (Biểu thức chính quy)
Thuật toán

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)

#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ệ:

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é)

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ệ

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!

About the author

Võ Hoài Sơn

Tính tình bất định
Chọc vào là bịnh
Rất yêu lập trình
Luôn code hết mình
Mình hiện đang là sinh viên của trường ĐH Khoa học tự nhiên TPHCM. Bản thân rất thích code, kiêm luôn cả mần thơ nên thường hơi hâm hâm dở dở. Ngoài ra chém gió, chém chuối, chém trái cây các kiểu cũng là sở trường của mình. Rất mong được làm quen với các bạn :D

Add Comment