[Regex] Học rồi mà như chưa học

31
Nguyễn Trọng Giap viết hơn 6 năm trước

alt text

1. Tựa đề

Mình vẫn còn nhớ cái hôm đấy. Trong khi mãi ngắm nhìn những dòng code đến hoa mắt là lúc mặt trời đã lặn khuất sau những toà nhà đô thị từ lúc nào cũng không ai hay. Bên ngoài mưa gió bão bùng, lúc mà mình đang cố release cho cái dự án lụt không cần nước, cho nó có thể chạy được.
alt text
Nhưng hôm đấy cũng không nằm ngoài dự tính, nó vẫn bị fail như bao lần trước, lần này nguyên nhân là do validate dữ liệu bị sai. Lúc đấy phải cần fix luôn ngay nhưng thật không may là anh phụ trách làm phần đấy đã tốc biến về nhà lúc nào không rõ. Còn bản thân thì chả biết tí tẹo về grex cả. Mọi thứ dường như bế tắc!
alt text
Đã đến lúc mình phải đọc cái regex mà anh kia đã viêt!. Nhìn những dòng đấy chỉ có thể thốt lên "wth, mình đang đọc cái gì thế này". Thật không thể nếu bạn chả có kiến thức gì về nó cả. Thực ra mình và nhiều người hay mắc tật lúc cần làm gì là chỉ cần search từ khoá trên anh google rồi copy vào code của mình, tuy không hiểu rõ nhưng thấy nó chạy ổn thì ok. Đến lúc lỗi lên thì chả biết fix thế nào. Cũng vì nguyên nhân đấy mà mình mới biết là đang làm một cách sai lầm và mù quáng và bắt đầu học một cách đúng đắn hơn. Trước mắt là phải tìm hiểu về regex xem nó có gì mà nhìn vào đã thấy sợ rồi.
alt text

Bạn đã từng bao giờ rơi vào trường hợp như mình chưa, cần kiểm tra định dạng một chuỗi có hợp lệ, hoặc tìm kiếm một chuỗi nằm trong một chuỗi dài khác. Đa số các bạn cần dùng đến việc validate (xác minh) tính hợp lệ của dữ liệu đều đã từ gặp phải câu hỏi đấy.

Nếu bạn gặp trường hợp lúc đấy giải pháp đầu tiên bạn nghĩ đến là gì nếu chưa biết gì về regex?

Có thể nó sẽ trở thành một bài toán khá hóc búa nếu bạn chỉ xử lý thuần tuý bằng cách duyệt chuỗi mà không có công cụ và thư viện xử lý.

Vậy regex là gì? Nó giải quyết bài toán không tưởng ở trên như thế nào? Nó cao siêuảo diệu ra sao?

Chúng ta hãy cùng đi tìm hiểu nào!

alt text

2. Giới thiệu

Theo anh wikipedia:

Biểu thức chính quy (regular expression, viết tắt là regexp, regex hay regxp) 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.

Cùng điểm một chút về lịch sử nhỉ.

Khái niệm regex được bắt đầu đưa ra từ những năm 1950 khi mà nhà toán học người Mỹ Stephen Cole Kleene chính thức mô tả một ngôn ngữ chính quy Khái niệm này được sử dụng phổ biến trong các tiện ích xử lý văn bản Unix. Sau đấy, từ những năm 1980, tồn tại các cú pháp khác nhau để tạo ra các biểu thức chính quy. Các chuẩn được đưa ra và sử dụng rộng rãi nhất là cú pháp Perl

Mỗi lần mình tìm hiểu xong một thời gian không dùng đến, lúc quay lại đọc đều có cảm giác như mới :).

3. Công dụng

Regex được sử dụng với rất nhiều mục đích khác nhau nhưng thường được dùng nhiều với mục đích đối sánh văn bản và kiểm tra cú pháp trong các trình biên tập văn bản và các tiện ích tìm kiếm và xử lý văn bản dựa trên các mẫu được quy định.

Ví dụ: Nếu bạn muốn tạo bộ lọc và muốn loại bỏ dữ liệu là những trang web mà người khác tạo ra, bạn có thể sử dụng biểu thức chính quy để loại trừ bất kỳ dữ liệu nào từ toàn bộ dải địa chỉ IP người khác sử dụng. Giả sử các địa chỉ IP đó có dải từ 198.51.100.1 - 198.51.100.25. Thay vì nhập 25 địa chỉ IP khác nhau, bạn có thể tạo biểu thức chính quy như 198\.51\.100\.\d* để đối sánh với toàn bộ dải địa chỉ.

4. Cách dùng

Các kí tự đặc biệt

Bảng 4.1 Các kí tự đặc biệt trong biểu thức chính quy.
Kí tự (kí hiệu, cờ) Ý nghĩa
\

Tìm với luật dưới đây:

Một dấu gạch chéo ngược sẽ biến một kí tự thường liền kế phía sau thành một kí tự đặc biệt, tức là nó không được sử dụng để tìm kiếm thông thường nữa. Ví dụ,  trường hợp kí tự 'b' không có dấu gạch chéo ngược này sẽ được khớp với các kí tự 'b' in thường, nhưng khi nó có thêm dấu gạch chéo ngược, '\b' thì nó sẽ không khớp với bất kì kí tự nào nữa, lúc này nó trở thành kí tự đặc biệt. Xem thêm phần word boundary character để biết thêm chi tiết.

Tuy nhiên nếu đứng trước một kí tự đặc biệt thì nó sẽ biến kí tự này thành một kí tự thường, tức là bạn có thể tìm kiếm kí tự đặc biệt này trong xâu chuỗi của bạn như các kí tự thường khác. Ví dụ, mẫu /a*/ có '*' là kí tự đặc biệt và mẫu này sẽ bị phụ thuộc vào kí tự này, nên được hiểu là sẽ tìm khớp  với 0 hoặc nhiều kí tự a. Nhưng, với mẫu /a\*/ thì kí tự '*' lúc này được hiểu là kí tự thường nên mẫu này sẽ tìm kiếm xâu con là 'a*'.

Đừng quên \ cũng là một kí tự đặc biệt, khi cần so khớp chính nó ta cũng phải đánh dấu nó là kí tự đặc biệt bằng cách đặt \ ở trước (\\).

^

Khớp các kí tự đứng đầu một chuỗi. Nếu có nhiều cờ này thì nó còn khớp được cả các kí tự đứng đầu của mỗi dòng (sau kí tự xuống dòng).

Ví dụ, /^A/ sẽ không khớp được với 'A' trong "an A" vì 'A' lúc này không đứng đầu chuỗi, nhưng nó sẽ khớp "An E" vì lúc này 'A' đã đứng đầu chuỗi.

Ý nghĩa của '^' sẽ thay đổi khi nó xuất hiện như một kí tự đầu tiên trong một lớp kí tự, xem phần complemented character sets để biết thêm chi tiết.

$

So khớp ở cuối chuỗi. Nếu gắn cờ multiline (đa dòng), nó sẽ khớp ngay trước kí tự xuống dòng.

Ví dụ, /t$/ không khớp với 't' trong chuỗi "eater" nhưng lại khớp trong chuỗi "eat".

*

Cho phép kí tự trước nó lặp lại 0 lần hoặc nhiều lần. Tương đương với cách viết {0,}.

Ví dụ, /bo*/ khớp với 'boooo' trong chuỗi "A ghost booooed" nhưng không khớp trong chuỗi "A birth warbled".

+

Cho phép kí tự trước nó lặp lại 1 lần hoặc nhiều lần. Tương đương với cách viết {1,}.

Ví dụ, /a+/ khớp với 'a' trong chuỗi "candy" và khớp với tất cả kí tự a liền nhau trong chuỗi "caaaaaaandy".

?

Cho phép kí tự trước nó lặp lại 0 lần hoặc 1 lần duy nhất. Tương đương với cách viết {0,1}.

Ví dụ, /e?le?/ khớp với 'el' trong chuỗi "angel" và 'le' trong chuỗi "angle" hay 'l' trong "oslo".

Nếu sử dụng kí tự này ngay sau bất kì kí tự định lượng nào trong số *,+,? hay {}, đều làm bộ định lượng "chán ăn" (dừng so khớp sau ngay khi tìm được kí tự phù hợp), trái ngược với đức tính "tham lam" vốn sẵn của chúng (khớp tất cả kí tự chúng tìm thấy). Ví dụ, áp dụng biểu mẫu /\d+/ cho "123abc" ta được "123". Nhưng áp /\d+?/ cho chính chuỗi trên ta chỉ nhận được kết quả là "1".

Bạn có thể đọc thêm trong mục x(?=y) và x(?!y) của bảng này.

.

Dấu . khớp với bất kì kí tự đơn nào ngoại trừ kí tự xuống dòng.

Ví dụ, /.n/ khớp với 'an' và 'on' trong chuỗi "no, an apple is on the tree", nhưng không khớp với 'no'.

(x)

Khớp 'x' và nhớ kết quả so khớp này, như ví dụ ở dưới. Các dấu ngoặc tròn được gọi là các dấu ngoặc có nhớ.

Biểu mẫu /(foo) (bar) \1 \2/ khớp với 'foo' và 'bar' trong chuỗi "foo bar foo bar". \1\2 trong mẫu khớp với 2 từ cuối.

Chú ý rằng \1, \2, \n được sử dụng để so khớp các phần trong regex, nó đại diện cho nhóm so khớp đằng trước. Ví dụ: /(foo) (bar) \1 \2/ tương đương với biểu thức /(foo) (bar) foo bar/. 

Cú pháp $1, $2, $n còn được sử dụng trong việc thay thế các phần của một regex. Ví dụ: 'bar foo'.replace(/(...) (...)/, '$2 $1') sẽ đảo vị trí 2 từ 'bar' và 'foo' cho nhau.

(?:x)

Khớp 'x' nhưng không nhớ kết quả so khớp. Những dấu ngoặc tròn được gọi là những dấu ngoặc không nhớ, nó cho phép bạn định nghĩa những biểu thức con cho những toán tử so khớp. Xem xét biểu thức đơn giản /(?:foo){1,2}/. Nếu biểu thức này được viết là /foo{1,2}/{1,2} sẽ chỉ áp dụng cho kí tự 'o' ở cuối chuỗi 'foo'. Với những dấu ngoặc không nhớ, {1,2} sẽ áp dụng cho cả cụm 'foo'.

x(?=y)

Chỉ khớp 'x' nếu 'x' theo sau bởi 'y'.

Ví dụ, /Jack(?=Sprat)/ chỉ khớp với 'Jack' nếu đằng sau nó là 'Sprat'. /Jack(?=Sprat|Frost)/ chỉ khớp 'Jack' nếu theo sau nó là 'Sprat' hoặc 'Frost'. Tuy nhiên, cả 'Sprat' và 'Frost' đều không phải là một phần của kết quả so khớp trả về.

x(?!y)

Chỉ khớp 'x' nếu 'x' không được theo sau bởi 'y'.

Ví dụ: /\d+(?!\.)/ chỉ khớp với số không có dấu . đằng sau. Biểu thức /\d+(?!\.)/.exec("3.141")​ cho kết quả là '141' mà không phải '3.141'.

x|y

Khớp 'x' hoặc 'y'

Ví dụ, /green|red/ khớp với 'green' trong chuỗi "green apple" và 'red' trong chuỗi "red apple".

{n}

Kí tự đứng trước phải xuất hiện n lần. n phải là một số nguyên dương.

Ví dụ, /a{2}/ không khớp với 'a' trong "candy", nhưng nó khớp với tất cả kí tự 'a' trong "caandy", và khớp với 2 kí tự 'a' đầu tiên trong "caaandy".

{n,m}

Kí tự đứng trước phải xuất hiện từ n đến m lần. n và m là số nguyên dương và n <= m. Nếu m bị bỏ qua, nó tương đương như ∞.

Ví dụ, /a{1,3}/ không khớp bất kì kí tự nào trong "cndy", kí tự 'a' trong "candy", 2 kí tự 'a' đầu tiên trong "caandy", và 3 kí tự 'a' đầu tiên trong "caaaaaaandy". Lưu ý là "caaaaaaandy" chỉ khớp với 3 kí tự 'a' đầu tiên mặc dù chuỗi đó chứa 7 kí tự 'a'.

[xyz]

Lớp kí tự. Loại mẫu này dùng để so khớp với một kí tự bất kì trong dấu ngoặc vuông, bao gồm cả escape sequences. Trong lớp kí tự, dấu chấm (.) và dấu hoa thị (*) không còn là kí tự đặc biệt nên ta không cần kí tự thoát đứng trước nó. Bạn có thể chỉ định một khoảng kí tự bằng cách sử dụng một kí tự gạch nối (-) như trong ví dụ dưới đây:

Mẫu [a-d] so khớp tương tự như mẫu [abcd], khớp với 'b' trong "brisket" và 'c' trong "city". Mẫu /[a-z.]+/ và /[\w.]+/ khớp với toàn chuỗi "test.i.ng".

[^xyz]

Lớp kí tự phủ định. Khi kí tự ^ đứng đầu tiên trong dấu ngoặc vuông, nó phủ định mẫu này.

Ví dụ, [^abc] tương tự như [^a-c], khớp với 'r' trong "brisket" và 'h' trong "chop" là kí tự đầu tiên không thuộc khoảng a đến c.

[\b]

Khớp với kí tự dịch lùi - backspace (U+0008). Bạn phải đặt trong dấu ngoặc vuông nếu muốn so khớp một kí tự dịch lùi. (Đừng nhầm lẫn với mẫu \b).

\b

Khớp với kí tự biên. Kí tự biên là một kí tự giả, nó khớp với vị trí mà một kí tự không được theo sau hoặc đứng trước bởi một kí tự khác. Tương đương với mẫu (^\w|\w$|\W\w|\w\W). Lưu ý rằng một kí tự biên được khớp sẽ không bao gồm trong kết quả so khớp. Nói cách khác, độ dài của một kí tự biên là 0. (Đừng nhầm lẫn với mẫu [\b])

Ví dụ:
/\bm/ khớp với 'm' trong chuỗi "moon";
/oo\b/ không khớp  'oo' trong chuỗi "moon", bởi vì 'oo' được theo sau bởi kí tự 'n';
 /oon\b/ khớp với 'oon' trong chuỗi "moon", bởi vì 'oon' ở cuối chuỗi nên nó không được theo sau bởi một kí tự; 
/\w\b\w/ sẽ không khớp với bất kì thứ gì, bởi vì một kí tự không thể theo sau một kí tự biên và một kí tự thường.

Chú ý: Engine biên dịch biểu thức chính quy trong Javascript định nghĩa một lớp kí tự là những kí tự thường. Bất kỳ kí tự nào không thuộc lớp kí tự bị xem như một kí tự ngắt. Lớp kí tự này khá hạn chế: nó bao gồm bộ kí tự La-tinh cả hoa và thường, số thập phân và kí tự gạch dưới. Kí tự có dấu, như "é" hay "ü", không may, bị đối xử như một kí tự ngắt.

\B

Khớp với kí tự không phải kí tự biên. Mẫu này khớp tại vị trí mà kí tự trước và kí tự sau nó cùng kiểu: hoặc cả hai là kí tự hoặc cả hai không phải là kí tự. Bắt đầu và kết thúc chuỗi không được xem là những kí tự.

Ví dụ, /\B../ khớp với 'oo' trong "noonday", và /y\B./ khớp với 'ye' trong "possibly yesterday."

\cX

X là một kí tự trong khoảng A tới Z. Mẫu này khớp với một kí tự điều khiển trong một chuỗi.

Ví dụ: /\cM/ khớp với control-M (U+000D) trong chuỗi.

\d

Khớp với một kí tự số. Tương đương với mẫu [0-9].

Ví dụ: /\d/ hoặc /[0-9]/ khớp với '2' trong chuỗi "B2 is the suite number."

\D

Khớp với một kí tự không phải là kí tự số. Tương đương với mẫu [^0-9].

Ví dụ; /\D/ hoặc /[^0-9]/ khớp với 'B' trong "B2 is the suite number."

\f Khớp với kí tự phân trang - form feed (U+000C).
\n Khớp với kí tự xuống dòng - line feed (U+000A).
\r Khớp với kí tự quay đầu dòng -  carriage return (U+000D).
\s

Khớp với một kí tự khoảng trắng, bao gồm trống - space, tab, phân trang - form feed, phân dòng - line feed. Tương đương với [ \f\n\r\t\v​\u00a0\u1680​\u180e\u2000​\u2001\u2002​\u2003\u2004​\u2005\u2006​\u2007\u2008​\u2009\u200a​\u2028\u2029​​\u202f\u205f​\u3000].

Ví dụ: /\s\w*/ khớp với ' bar' trong "foo bar."

\S

Khớp với một kí tự không phải khoảng trắng. Tương đương với [^ \f\n\r\t\v​\u00a0\u1680​\u180e\u2000​\u2001\u2002​\u2003\u2004​\u2005\u2006​\u2007\u2008​\u2009\u200a​\u2028\u2029​\u202f\u205f​\u3000].

Ví dụ: /\S\w*/ khớp với 'foo' trong chuỗi "foo bar."

\t Khớp với kí tự tab (U+0009).
\v Khớp với kí tự vertical tab (U+000B).
\w

Khớp với tất cả kí tự là chữ, số và gạch dưới. Tương đương với mẫu [A-Za-z0-9_].

ví dụ, /\w/ khớp với 'a' trong "apple," '5' trong "$5.28," và '3' trong "3D."

\W

Khớp với tất cả kí tự không phải là chữ. Tương đương với mẫu [^A-Za-z0-9_].

ví dụ, /\W/ hoặc /[^A-Za-z0-9_]/ khớp với '%' trong "50%."

\n

Trong đó, n là một số nguyên dương, một tham chiếu ngược tới chuỗi khớp thứ n trong biểu thức (đếm từ trái sang, bắt đầu bằng 1).

ví dụ, /apple(,)\sorange\1/ hay /apple(,)\sorange,/ khớp với 'apple, orange,' trong chuỗi "apple, orange, cherry, peach."

\0 Khớp với kí tự NULL (U+0000). Lưu ý: không được thêm bất kì một kí tự số nào sau 0, vì \0<các-kí-tự-số> là một biểu diễn hệ bát phân escape sequence.
\xhh Khớp với kí tự với mã code là hh (2 số trong hệ thập lục phân)
\uhhhh Khớp với kí tự có mã hhhh (4 số trong hệ thập lục phân).

Làm việc với biểu thức chính quy

Bảng 4.2 Những phương thức được sử dụng trong biểu thức chính quy
Phương thức Mô tả
exec Một phương thức của RegExp dùng để tìm kiếm chuỗi phù hợp với mẫu so khớp. Nó trả về một mảng chứa kết quả tìm kiếm.
test Một phương thức của RegExp dùng để kiểm tra mẫu có khớp với chuỗi hay không. Nó trả về giá trị true hoặc false.
match Một phương thức của chuỗi dùng để tìm kiếm chuỗi phù hợp với mẫu so khớp. Nó trả về một mảng chứa kết quả tìm kiếm hoặc null nếu không tìm thấy.
search Một phương thức của chuỗi dùng để tìm kiếm chuỗi phù hợp với mẫu so khớp và trả về vị trí của chuỗi đó hoặc -1 nếu không tìm thấy.
replace Một phương thức của chuỗi dùng để tìm kiếm một chuỗi theo mẫu so khớp và thay thế chuỗi con được khớp với một chuỗi thay thế.
split Một phương thức của chuỗi dùng một biểu mẫu chính quy hoặc một chuỗi bất biến để ngắt chuỗi đó thành một mảng các chuỗi con.

Ví dụ

Mã hóa escapse chuỗi người dùng nhập vào bằng một hàm thay thế đơn giản sử dụng biểu thức chính quy:

function escapeRegExp(string){
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

alt text

Bạn có thể sử dụng trang web https://regex101.com/, dán một đoạn regex vào đấy, nó sẽ giải thích đoạn mã đấy làm gì.

Lưu ý

Giữ biểu thức chính quy của bạn đơn giản. Regex đơn giản giúp người dùng khác hiểu và sửa đổi dễ dàng hơn.
Biểu thức chính quy về bản chất đối sánh rất nhiều dữ liệu: nếu bạn không chỉ định cho regex biết không nên đối sánh dữ liệu nào, chúng sẽ đối sánh với những gì bạn chỉ định và bất kỳ ký tự liền kề nào. Ví dụ: site đối sánh với mysite, yoursite, theirsite, parasite--bất kỳ chuỗi có chứa “site”. Nếu bạn cần thực hiện đối sánh cụ thể, hãy thiết lập cấu trúc regex của bạn cho phù hợp. Ví dụ: nếu bạn chỉ cần đối sánh với chuỗi “site”, hãy thiết lập cấu trúc regex của bạn để “site” vừa là bắt đầu chuỗi vừa là kết thúc chuỗi: ^site$.

5. Tham khảo

https://support.google.com/analytics/answer/1034324?hl=vi&ref_topic=1034375
https://developer.mozilla.org/vi/docs/Web/JavaScript/Guide/Regular_Expressions
https://en.wikipedia.org/wiki/Regular_expression

Bình luận


White
{{ comment.user.name }}
Hay Bỏ hay
{{ comment.like_count}}
White

Nguyễn Trọng Giap

55 bài viết.
1 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}

{{like_count}}

kipalog

{{ comment_count }}

Bình luận


White
{{userFollowed ? 'Following' : 'Follow'}}
55 bài viết.
1 người follow

 Đầu mục bài viết