Regular Expression là nỗi sợ hãi của nhiều lập trình viên. Những cái gì khó nhớ, ít làm, lập trình viên sẽ Google, Stack Overflow cho nhanh. Chạy được là ok còn không cần hiểu và nhớ kỹ làm gì cho mệt. Lầu dần, ai cũng dùng Regular Expression nhưng chả có mấy ai hiểu quy tắc viết Regular Expression. Trong bài này tôi chia sẻ mấy quy tắc để hiểu và nhớ Regular Expression

1. Regular Expression là gì?

Regular Expression là kỹ thuật viết biểu thức để tìm kiếm chuỗi trùng hợp theo yêu cầu. Regular = thường xuyên, Expression = biểu thức. Chúng ta tạm gọi nó là biểu thức thường xuyên hoặc vài nơi gọi là biểu thức chính quy. Còn tôi đơn giản gọi là regex. Mọi ngôn ngữ lập trình đều hỗ trợ regex. Ứng dụng của regex:

  • Kiểm tra tính hợp lệ dữ liệu: email, số điện thoại...
  • Tìm những chuỗi phù hợp trong một chuỗi lớn theo biểu thức. Thay vì so sánh từng ký tự mà sử dụng biểu thức, đó mới là sức mạnh của regex

Để học nhanh Regex hiệu quả, thực dụng tôi xin giới thiệu một số nguồn:

Một biểu thức regex đầy đủ sẽ gồm hai phần:

  • Phần biểu thức (expression): chúng ta cần phải viết
  • Phần cờ điều khiển cách tìm kiếm chuỗi phù hợp (flags): chỉ cần chọn cho phù hợp

2. Giải thích

2.0 Flags - cờ điều khiển

Hãy thử ví dụ này

/^Chào thế giới/gmui

https://regex101.com/r/8ZC0sc/1

Giải thích:

  • g: global
  • m: multiline
  • u: unicode
  • i: case insensitive

2.1 Matching Symbol - biểu tượng để khớp

Biểu thức Giải thích
. Khớp với mọi ký tự
^hello Khớp chuỗi ở đầu dòng
hello$ Khớp chuỗi ở cuối dòng
XY X tiếp sau đó phải Y
[abc] Khớp 1 ký tự hoặc a hoặc b hoặc c. [] định nghĩa một dải các lựa chọn
[abc][xy] Ký tự đầu hoặc a hoặc b hoặc c, nối tiếp theo là ký tự x hoặc y
\bhello\b Danh giới của bên trái và phải. Chuỗi khớp phải là một từ chứ không phải là chuỗi con
[^abc] ^ phủ định dải tiếp theo. Là bất kỳ ký tự nào không phải a hoặc b hoặc
(color|colour) Hoặc color hoặc colour. () nhóm các ký tự thành một khối
countr(y|ies) Hoặc country hoặc countries

2.2 Meta character - Ký tự đại diện

Ký tự Giải thích
\d Bất kỳ chữ số nào [0-9]
\D Ký tự không phải chữ số [^0-9]
\s Ký tự cách white space
\w Ký tự [a-zA-Z_0-9]
\W Ký tự khác, không phải \w

2.3 Quantifier - đếm số lần khớp

Biểu tượng Giải thích
* Xuất hiện 0 đến nhiều lần. Tương đương {0,}
+ Xuất hiện 1 đến nhiều lần. Tương đương {1,}
? Xuất hiện 0 hoặc 1 lần. Tương đương {0,1}
{X} Xuất hiện X lần
{X,Y} Xuất hiện từ X đến Y lần

2.4 Range [] và Group ()

Range [] tạo ra một dải các lựa chọn. Ví dụ:

  • [nl] : hoặc n hoặc l
  • [a-z] một ký tự bất kỳ nằm trong dải a đến z
  • [a-z|A-Z] cũng không khác gì so với [a-zA-Z]. Việc thêm ký tự OR | trong [] không có tác dụng gì

Group () nhóm các ký tự bên trong thành một khối.

  • (png|jpeg|gif|jpg): nhóm ký tự tạo thành file extension. Dùng thêm | để tạo ra các lựa chọn nối bằng OR
  • () có thể chứa [] và nhiều ký tự, biểu tượng bên trong.

3.Thực hành theo tình huống

Hãy sử dụng Regex101 để viết regex và xem kỹ giải thích. Ở đây tôi dùng từ khớp có nghĩa là match. Chuỗi khớp có nghĩa là match string.

Trong các biểu thức regex dưới đây, phần cuối cùng /gmi chính là cờ điều khiển.

3.1 Cần khớp bất kỳ chuỗi chứa country hay countries

/\bcountr(y|ies)\b/gmi

https://regex101.com/r/iKoIaX/1

Giải thích:

  • / bắt đầu biểu thức
  • /gmi cờ điều khiển: g (global search) tìm tất cả chuỗi phù hợp, m (multilines) tìm trên nhiều dòng, i (case insensitive) không quan tâm chữ hoa chứ thường
  • (y|ies): khớp ký tự y hoặc chuỗi ies

3.2 Số di động ở Việt nam gồm 10 đến 11 chữ số

Yêu cầu:

  • Có từ 10-11 chữ số
  • Phải có chữ số 0 đầu tiên
/\b0[0-9]{9,10}\b/gmi

https://regex101.com/r/YjmkFm/1

Giải thích:

  • \b: word boundry yêu cầu chuỗi khớp phải là một word đầy đủ chứ không phải chuỗi con. Chúng ta phải chặn hai đầu chuỗi di động bằng \b

  • 0: chữ số đầu tiên buộc phải là số 0

  • [0-9] : một ký tự nằm trong danh sách từ 0 đến 9

  • {9,10}: số lượng chữ số từ 9 đến 10 ký tự

3.3 Kiểm tra email có hợp lệ

(?:[a-z0-9!#$%&'*+\=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])

Đây là regex tôi lấy nguyên từ Stack Overflow mà không chút nghĩ ngợi. Link chạy thử đây https://regex101.com/r/rtuOuV/1

3.4 Hà nội, Hà lội, ha noi

Cần tìm tất cả các từ Hà nội, Hà lội, ha noi, Ha noi, ha Noi...

/\b(h[aà] [nl][oộô|]i)\b/gmi

https://regex101.com/r/WRIEeX/1

Giải thích:

  • Luôn phải chặn hai đầu biểu thức bằng \b để yêu cầu chuỗi khớp phải là một từ đầy đủ chứ không phải chuỗi con
  • Dùng range [aà], [oộô] để chấp nhận ký tự có dấu và không dấu
  • Dùng range [nl] để chấp nhận khi có người nói ngọn nhầm lẫn giữa n với l

3.5 FirstName:xxx,LastName:yyyy

Cần khớp chuỗi có dạng FirstName:xxx,LastName:yyy. Giữa FirstName: và xxx có 0 hoặc nhiều khoảng trắng.

Giữa LastName: và yyy có 0 hoặc nhiều khoảng trắng.

/\bFirstName:[\s]*(\w+),[\s]*LastName:[\s]*(\w+)\b/gm

https://regex101.com/r/arUAJw/1

Giải thích:

  • \b: bắt đầu một từ

  • [\s]*: có 0 hoặc nhiều khoảng trắng

  • (\w+): nhóm các ký tự [a-zA-Z_0-9]

3.6 Tên file ảnh

Cần tìm ra tất cả những tên file ảnh có đuôi png, jpeg, jpg, gif

/(\w+\.(png|jpeg|gif|jpg))/gm

https://regex101.com/r/XRyE7d/2/

Giải thích:

  • .(png|jpeg|gif|jpg) buộc chuỗi cần tìm kiếm phải có extenstion chứa hoặc png hoặc jpeg hoặc gif hoặc jpg

3.7 Địa chỉ IPv4 aaa.bbb.ccc.ddd

Địa chỉ IPv4 gồm 4 khối cách nhau bằng 3 dấu . ví dụ: 192.168.1.1, hay 127.0.0.1

/\b(\d+).(\d+).(\d+).(\d+)\b/gm

https://regex101.com/r/hjKDOX/1

Chặt chẽ hơn nữa, mỗi khối không được vượt quá 255

/\b(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\b/gm

https://regex101.com/r/ypHIfa/1

3.8 URL http:// hoặc https://

/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#()?&\/=]*)/gm

https://regex101.com/r/N3R9M0/1/

3.9 Chuỗi năm-tháng-ngày

/\b([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))\b/gm

https://regex101.com/r/FBqI8q/1

Giải thích:

  • [12]: ký tự đầu tiên của năm phải là 1 hoặc 2. Còn lâu mới đến năm 3xxx
  • \d{3}: tiếp theo đó là 3 chữ số còn lại của năm
  • (0[1-9]|1[0-2]): nhóm chữ số thể hiện tháng có 2 khả năng:
    • dưới 10: 0[1-9]: 01,02,...,09
    • trên 10: 1[0-2]: 11, 12
  • (0[1-9]|[12]\d|3[01])): nhóm chữ số thể hiện ngày có 3 khả năng:
    • dưới 10: 0[1-9]
    • 11 đến 29: [12]\d
    • 30 và 31: 3[01]

Kết luận

Biểu thức thường xuyên - Regex giúp bạn xử lý chuỗi, tìm kiếm, kiểm tra hợp lệ một cách nhanh chóng, dễ dàng. Một ngôn ngữ lập trình có thể không hỗ trợ Class (Go), không hỗ trợ Generic (Go), không hỗ trợ Reflection (C++), không hỗ trợ kiểu lỏng lẻo (Duckling type) nhưng nếu không hỗ trợ Regex thì nó chưa thể trở thành một ngôn ngữ lập trình. Bởi xử lý số và chuỗi là hai thao tác tối cần thiết trong mọi ngôn ngữ lập trình.

Regex không khó để hiểu. Bạn có thể quên regex nhưng một khi đã hiểu, và xem lại Cheat sheet, cùng với công cụ prototype biểu thức https://regex101.com/ bạn có thể tạo regex trong 15 phút.

Cần phải nhớ rằng, biểu thức regex không thể đảm bảo mọi trường hợp. Ví dụ trường hợp yyyy-mm-dd, Regex sẽ chấp nhận chuỗi 2021-02-31. Thực tế không thể có ngày 31 trong tháng 2, nhưng trong Regex chúng ta không thể viết logic để xử lý trường hợp này. Tỷ lệ khớp (matching) càng chính xác thì biểu thức regex càng dài, càng phức tạp, càng khó debug và tốn thời gian khi chạy. Do đó bạn cần kết hợp viết thành 2 cấp độ: lần đầu dùng regex, lần hai dùng code logic để xử lý những ngoại lệ rất phức tạp.

Lập trình Regex thật là đơn giản phải không các bạn. Hãy chịu khó vào blog Techmaster để xem và đóng góp bài viết nhé.