Regular Expressions: RegEx không hề khó như những gì bạn thấy (II)

Bài viết này là phần tiếp theo của phần trước.

3. Lặp lại phép match để tìm ký tự lặp lại

Giả sử bạn được cho một chuỗi, và bài toán cho bạn là tìm hiểu xem liệu có ký tự nào bị lặp lại trong string không. Đây là giải pháp cho cho ký tự lặp lại ngay sau lần xuất hiện đầu tiên:

let e=/(\w)\1/; 
e.test("abc"); //false 
e.test("abb"); //true

Biểu thức e không match với bất kỳ phần nào của string abcabc không có ký tự nào bị trùng lặp, do đó kết quả trả về false. Nhưng e lại match với phần bb của string abb và trả về true. Bạn có thể test ví dụ này trong console của trình duyệt.

Dấu gạch chéo ngược \ Trong các biểu thức chính quy, dấu gạch chéo ngược \ là rất đặc biệt. Nó làm thay đổi ý nghĩa của các ký tự theo sau nó. Điều gì sẽ được thực hiện khi gặp \n trong 1 string? Đúng vậy, tạo 1 dòng mới. Bạn cũng sẽ làm những điều tương tự như thế đối với Regx. Trên thực tế, \n là những gì bạn dùng như một pattern khi bạn muốn tìm kiếm một dòng mới. Nó thay đổi ý nghĩa thông thường của ký tự n và tạo cho nó ý nghĩa mới là tạo dòng mới.

  • \d : Viết tắt cho tập các chữ số, chỉ match với một chữ số duy nhất.
  • \D : Viết tắt cho tập các ký tự không phải là số, là tất cả các ký tự khác với các số được match với \d.
  • \s : Viết tắt cho một khoảng trắng đơn như dấu space, dòng mới hoặc tab.
  • \S : Trái ngược với \s, tất cả các ký tự không phải khoảng trắng.
  • \w: Tập các ký tự chữ và số alpha, nó match với a-z, A-Z, 0-9.
  • \W : Trái ngược với \w.

Ghi nhớ và gọi lại Chúng ta đã bắt đầu phần này với việc tìm giải pháp cho bài toán tìm các ký tự trùng lặp trong string. /(\w)\1/ đã match với abb. Điều này thể hiện việc sử dụng bộ nhớ và gọi lại trong regx. Hãy chú ý việc sử dụng các dấu ngoặc đơn trong biểu thức trên, chuỗi kết quả đã match với biểu thức bên trong dấu ngoặc đơn được ghi nhớ để sử dụng trong các lần gọi sau. \1 ghi nhớ và sử dụng cho việc match từ biểu thức thứ nhất trong dấu ngoặc đơn, tương tự như thế, \2 sử dụng cho biểu thức thứ 2 trong dấu ngoặc đơn, vv. Giờ hãy thử dịch biểu thức /(\w)\1/: Match bất kỳ ký tự chữ và số nào trong string, ghi nhớ nó như là \1, kiểm tra xem liệu ký tự này có xuất hiện bên phải của lần gặp đầu tiên nữa không.

Mở rộng 1 - Cặp ký tự đảo ngược Giả sử chúng ta muốn tìm 2 ký tự mà nó xuất hiện theo thứ tự ngược lại ở bên phải mỗi ký tự đó ví dụ như abba, ab đảo ngược là ba và nó nằm bên phải của mỗi ký tự. Ví dụ:

let e=/(\w)(\w)\2\1/; 
e.test("aabb"); //false 
e.test("abba"); //true 
e.test("abab"); //false

(\w) đầu tiên match với a và ghi nhớ nó như là \1. (\w) thứ hai match với b và ghi nhớ nó như là \2, sau đó, biểu thức này trông đợi \2 sẽ xuất hiện trước và được theo sau bởi \1. Dó đó chỉ có abba match với biểu thức e.

Mở rộng 2 — Không có sự trùng lặp Lần này chúng ta sẽ xét chuỗi các ký tự mà không có sự trùng lặp nào trong đó, sẽ không có ký tự nào được theo sau bởi một ký tự giống nó. Hãy thử xét:

let e=/^(\w)(?!\1)$/; 
e.test("a"); //true 
e.test("ab"); //false 
e.test("aa"); //false

Ví dụ thứ 2 nhẽ ra không nên là false, nhưng chúng ta đã lại đưa ra thêm một vài ký hiệu cần được giải thích, điều đó có nghĩa là chúng ta lại phải đối mặt với chàng lính ngự lâm quyền lực nhất một lần nữa!

Quay trở lại về dấu hỏi chấm Hãy nhớ lại câu chuyện về 3 cháng lính ngự lâm trong phần trước, dấu hỏi chấm khiêm tốn thực sự lại là người quyền lực nhất, có thể khiến các ký hiệu khác thực hiện mệnh lệnh của nó, dấu gạch chéo so với nó là không đáng kể. Một tập hợp của dấu ngoặc đơn, dấu hỏi chấm và dấu chấm than (?!) được gọi là một biểu thức look ahead. Biểu thức (?!) là phủ định look ahead. a(?!b) match với a khi và chỉ khi nó không được theo sau bởi b. trong Javascript, dấu chấm than nghĩa là NOT nhưng trong CSS thì ngược lại, !important nghĩa là nó thực sự rất quan trọng và không được ghi đè. (?=) thì ngược lại với (?!), a(?=b) match với a khi và chỉ khi nó được theo sau bởi b. Như vậy, (\w)(?!\1) sẽ tìm kiếm 1 ký tự mà nó không có sự lặp lại trong string, nhưng nó chỉ tìm kiếm 1 ký tự thôi, chúng ta cần nhóm nó lại và tìm kiếm cho 1 hoặc nhiều ký tự hơn bằng cách sử dụng dấu cộng (+).

let e=/^((\w)(?!\1))+$/; 
e.test("madam"); //false 
e.test("maam"); //false

Nhưng có vẻ như nó không hoạt động. Nếu chúng ta nhóm các pattern ở trong dấu ngoặc đơn là ((\w)(?!\1)), \1 sẽ không còn là đại diện của (\w) nữa mà nó đại diện cho cặp dấu ngoặc đơn đã nhóm pattern lại, vì thế nên nó không đúng. Những gì chúng ta cần là một lựa chọn nhóm mà có thể quên, đây là lúc dấu hỏi chấm quay trở lại, nó cặp với dấu hai chấm (?:) và quên đi bất kỳ chức năng nào của bộ nhớ mà được đặc tả trong dấu ngoặc đơn.

let e=/^(?:(\w)(?!\1))+$/; 
e.test("madam"); //true 
e.test("maam"); //false

Trong ví dụ trên, dấu ngoặc đơn ngoài cùng sẽ không được ghi nhớ nhờ ?:\1 sẽ ghi nhớ kết quả trả về của phép match với \w.

Review

  • \w đại diện cho tất cả các ký tự chữ và số, nếu bạn viết hoa w thành W, điều đó có nghĩa là \W đại diện cho tất cả các ký tự không phải số và chữ.
  • ( ) biểu thức trong dấu ngoặc đơn được ghi nhớ cho lần sử dụng sau.
  • \1 ghi nhớ và sử dụng kết quả phép match của biểu thức đầu tiên ở trong dấu ngoặc đơn, \2 sử dụng cho cặp dấu ngoặc đơn thứ 2.
  • a(?!b) một tập hợp của dấu ngoặc đơn, dấu hỏi chấm và dấu chấm than (?!), gọi là biểu thức look ahead, nó match với a khi và chỉ khi a không được theo sau bởi b.
  • a(?=b) match với a khi và chỉ khi a được theo sau bởi b.
  • (?:a) nhóm mà nó có thể quên, nó tìm kiếm a nhưng không ghi nhớ nó, bạn không thể sử dụng \1 để dùng lại kết quả phép match.

4. Thứ tự xuất hiện xen kẽ nhau

Usecase này rất đơn giản: match 1 string mà nó chỉ sử dụng 2 ký tự và 2 ký tự này sẽ xuất hiện xen kẽ nhau trong suốt chiều dài của chuỗi, ví dụ: ababxyxyx. Đây là một giải pháp:

let e=/^(\S)(?!\1)(\S)(\1\2)*$/; 
e.test("abab"); //true 
e.test("$#$#"); //true 
e.test("#$%"); //false 
e.test("$ $ "); //false 
e.test("xyxyx"); //false

Trước tiên chúng ta hãy hiểu ý nghĩa của kết quả trước khi hiểu cách nó hoạt động. abab match với e, $#$# cũng thế, nó không khác gì abab. #$% fail vì nó có sự xuất hiện của ký tự thứ 3. $ $ mặc dù nó là một cặp nhưng nó có chứa dấu cách trong pattern nên nó trả về false. Tất cả đều trả về kết quả như mong đợi ngoại trừ xyxyx bởi vì pattern của chúng ta không biết cách nào để xử lý thằng cuối cùng x. Giờ chúng ta sẽ tìm cách.

Mỗi mảnh một thời điểm Bạn hầu như đã biết hầu hết các mảnh: \S trái ngược với \s. \S tìm kiếm các ký tự không phải là khoảng trắng. Giờ ta thử cắt nghĩa /^(\S)(?!\1)(\S)(\1\2)*$/:

  • Bắt đầu với /^.
  • tìm kiếm ký tự không phải là khoảng trắng (\S).
  • nhớ nó như là \1.
  • nhìn phía trước xem liệu ký tự đầu tiên có không được theo sau bởi ký tự giống nó không (?!\1).
  • nếu không có, tiếp tục tìm ký tự khác (\S).
  • nhớ nó như là \2.
  • sau đó, tìm 0 hoặc nhiều hơn các cặp của lần match thứ nhất và thứ 2 (\1\2)*.
  • tìm kiếm pattern như thế cho tới khi kết thúc string.

Áp dụng trong ví dụ của chúng ta, abab$#$# match.

Đuôi x ở cuối Nào cùng tìm giải pháp cho trường hợp xyxyx, như chúng ta đã thấy, vấn đề là ở cái đuôi cuối cùng x, chúng ta đã có giải pháp cho xyxy, giờ chúng ta cần 1 pattern để giả quyết bài toán: tìm kiếm một sự xuất hiện tùy chọn của ký tự đầu tiên. Hãy bắt đầu với giải pháp sau:

let e=/^(\S)(?!\1)(\S)(\1\2)*\1?$/; e.test("xyxyx"); //true 
e.test("$#$#$"); //true

Dấu hỏi chấm đã xuất hiện lại, một dấu hỏi chấm theo sau một ký tự hoặc 1 pattern nghĩa là 0 hoặc 1 phép match với pattern trước đó, trong trường hợp của chúng ta, \1? nghĩa là 0 hoặc 1 phép match của ký tự đầu tiên được ghi nhớ thông qua cặp dấu ngoặc đơn đầu tiên.

Review

  • \S : đại diện cho tất cả các ký tự ngoại trừ các khoảng trắng như dấu cách, xuống dòng mới.
  • a* : dấu *, tìm 0 hoặc nhiều hơn số lần xuất hiện của ký tự trước nó, trong trường hợp này là tìm 0 hoặc nhiều ký tự a, nó gần giống như dấu +.
  • a(?!b) một tập hợp của dấu ngoặc đơn, dấu hỏi chấm và dấu chấm than (?!), gọi là biểu thức look ahead, nó match với a khi và chỉ khi a không được theo sau bởi b. Ví dụ: nó match a trong aa, ax, a$ nhưng không match với ab.
  • \s: ký tự khoảng cách đơn như dấu cách, dấu xuống dòng mới
  • a(?=b) : nó match với a cái mà được theo sau bởi b.
  • ^ab*$ : nó có thể hiểu là: 0 hoặc nhiều lần xuất hiện của ab nhưng nó match a cái được theo sau bởi 0 hoặc nhiều ký tự b. Ví dụ: nó match abbb, a ab nhưng không match abab.
  • ^(ab)*$: nó match 0 hoặc nhiều cặp ab, nghĩa là nó sẽ match "", ab, abab nhưng không match abb.
  • a?: ? match 0 hoặc 1 lần xuất hiện của ký tự hoặc pattern trước nó. \1? match 0 hoặc nhiều lần tái xuất hiện của kết quả phép match đầu tiên đã ghi nhớ.

5. Match một địa chỉ email

Cảnh báo: Chỉ dùng biểu thức chính quy thôi thì có thể sẽ không đủ để validate email, một số thậm chí còn cho rằng các biểu thức chính quy không nên được sử dụng để validate email vì nó không bao giờ có thể match với 100% các email. Bạn phải xem xét tất cả các tên miền và cả các ký tự đặc biệt trong địa chỉ email như dấu . hoặc dấu +. Bạn cần phải validate 2 lần. Một ở client side để giúp người dùng không nhập sai email, đơn giản bạn có thể dùng thẻ input với type là email của html cho một số trình duyệt: <input type='email'>. Validate một lần nữa phía server bằng cách gửi một email xác nhận, sự xác nhận đó để chắc chắn email của bạn là hợp lệ. Bạn có thể xem biểu thức chính quy cho email tại đây: RegEx for Email

6. Match một password

Bây giờ, vấn đề đặt ra như sau: hãy nhớ lại một form đăng ký nào đó mà nó yêu cầu bạn phải nhập một password với nhiều cấp độ: yếu, tốt, mạnh, rất mạnh, giờ ta sẽ xây dựng một validate cho điều này. Password nên:

  • có ít nhất 4 ký tự.
  • bao gồm chữ in thường.
  • bao gồm chữ viết hoa.
  • bao gồm 1 chữ số.
  • bao gồm 1 ký tự đặc biêt.

Đây là 1 khó khăn, một khi bạn bắt đầu check các chữ cái, bạn không thể quay lại để check xem liệu nó có thỏa mãn bất kỳ điều kiện khác không. Đó là đầu mối của chúng ta.

Chiều dài của chuỗi Đầu tiên chúng ta sẽ test liệu pasword có chiều dài 4 ký tự hay không. Đơn giản! Sử dụng .length cho paswrod là xong đúng không? Không, chúng ta sẽ tăng độ khó của nó lên:

e1=/^(?=.{4,})$/; 
e1.test("abc") //false
e1.test("abcd") //false  

e2=/^(?=.{4,}).*$/; 
e2.test("abc") //false 
e2.test("abcd") //true
  • Bạn có nhớ (?=) ở phần trước?
  • dấu . là một ký tự thú vị, nó nghĩa là bất kỳ ký tự nào.
  • {4,} viết tắt cho: ít nhất 4 ký tự đứng trước và không có giới hạn tối đa.
  • \d{4} sẽ tìm kiếm chính xác 4 chữ số.
  • \w{4,20} sẽ tìm kiếm 4 đến 20 ký tự chữ và số.

Giờ hãy thử cắt nghĩa /^(?=.{4,})$/: bắt đầu từ đầu của string. Nhìn về phía trước ít nhất 4 ký tự. Không ghi nhớ phép match này. Quay lại lúc bắt đầu và check xem string có kết thúc ở đó không. Nghe có vẻ không đúng lắm, đó là lý do vì sao chúng ta thêm vào biến e2 một dấu chấm và một dấu sao *. Nó sẽ được đọc thế này: Đi từ đầu string, nhìn về phía trước 4 ký tự, không ghi nhớ phép match này, quay lại từ đầu, check tất cả các ký tự sử dụng .* và xem xem liệu bạn đã đang ở cuối string hay chưa. Đó là tại sao abc fail và abcd pass.

Ít nhất một chữ số

e=/^(?=.*\d+).*$/ 
e.test(""); //false 
e.test("a"); //false 
e.test("8"); //true 
e.test("a8b"); //true 
e.test("ab890"); //true

Bắt đầu từ đầu chuỗi /^, nhìn về phía trước 0 hoặc nhiều ký tự ?=.*, check xem liệu có 1 hoặc nhiều chữ số theo sau không \d+, nếu nó match, quay lại lúc đầu, check tất cả các ký tự trong string cho tới khi kết thúc .*$/.

Chứa ít nhất 1 chữ in thường Nó gần giống với pattern bên trên:

e=/^(?=.*[a-z]+).*$/; 
e.test(""); //false 
e.test("A"); //false 
e.test("a"); //true

Thay vì \d+, chúng ta có [a-z]+ nghĩa là 1 tập các chữ cái từ a đến z.

Chứa ít nhất 1 chữ in hoa Nó giống với chứa ít nhất một chữ in hoa, sử dụng [A-Z] thay vì [a-z].

Chứa ít nhất 1 ký tự đặc biệt Chúng ta phải tìm ra cách để match các ký hiệu được đặt trong một danh sách các ký hiệu trong 1 tập ký tự. /^(?=.*[-+=_)(\*&\^%\$#@!~”’:;|\}]{[/?.>,<]+).*$/.test(“$”). Đó là tất cả các ký hiệu trong tập ký tự.

//cân nhắc dấu cách như một ký hiệu 
let e1; 
e1=/^(?=.*[^a-zA-Z0-9])[ -~]+$/ 
e1.test("_"); //true 
e1.test(" "); //true  
//không gồm dấu cách
let e2; 
e2=/^(?=.*[^a-zA-Z0-9])[!-~]+$/ 
e2.test(" "); //false 
e2.test("_"); //true  

let e3; 
e3=/^(?=.*[\W])[!-~]+$/ 
e3.test("_"); //false

Trong 1 tập ký tự, ^ phủ định cho tập ký tự, tức là [^a-z] nghĩa là một ký tự bất kỳ nào đó khác với ký tự từ a đến z. Nên [^a-zA-Z0-9] nghĩa là một ký tự bất kỳ nào đó không phải là chữ cái viết thường, chữ cái viết hoa và chữ số. Chúng ta có thể sử dụng \W thay vì một tập ký tự dài như thế, nhưng \W viết tắt cho tất cả các ký tự chữ và số, bao gồm cả _, bạn có thể xem trong ví dụ thứ 3, nó không chấp nhận dấu _ là một ký hiệu hợp lệ. [!-~] bao gồm tất cả các ký hiệu, chữ cái và chữ số chúng ta cần. Nhóm tất cả với nhau, chúng ta có một biểu thức chính quy như sau: /^(?=.{5,})(?=.*[a-z]+)(?=.*\d+)(?=.*[A-Z]+)(?=.*[^\w])[ -~]+$/. Đó là cách chúng ta xây dựng các đối tượng biểu thức phức tạp, chúng ta xây dựng riêng lẻ từng phần trước và tập hợp chúng lại sau.

//start with prefix 
let p = "^"; 
//look ahead  
// min 4 chars 
p += "(?=.{4,})"; 
// lower case 
p += "(?=.*[a-z]+)"; 
// upper case 
p += "(?=.*[A-Z]+)"; 
// numbers 
p += "(?=.*\\d+)"; 
// symbols 
p += "(?=.*[^ a-zA-Z0-9]+)"; 
//end of lookaheads  
//final consumption 
p += "[ -~]+";  
//suffix 
p += "$"; 
//Construct RegEx 
let e = new RegEx(p); 
// tests 
e.test("aB0#"); //true  
e.test(""); //false 
e.test("aB0"); //false 
e.test("ab0#"); //false 
e.test("AB0#"); //false 
e.test("aB00"); //false 
e.test("aB!!"); //false  
// space is in our control 
e.test("aB 0"); //false 
e.test("aB 0!"); //true

Hãy chú ý 2 cú pháp lạ ở bên trên:

  • chúng ta không sử dụng /^, thay vì đó chúng ta dùng ^. Chúng ta không dùng $/ để kết thúc biểu thức mà chúng ta dùng $. Lý do là vì cấu trúc RegEx tự động thêm dấu gạch chéo đánh dấu bắt đầu và kết thúc biểu thức cho chúng ta.
  • để match với các chữ số, chúng ta đã sử dụng \\d thay vì \d như lúc trước. Đó là bởi vì biến p chỉ là một string bình thường trong dấu nháy kép "". Để thêm 1 dấu gạch chéo ngược, bạn phải thoát nó ra khỏi dấu gạch chéo ngược trước.

Và tất nhiên, chúng ta cũng cần có sự validate cho pasword bên phía server nữa. Hãy nghĩ đến các lỗ hổng của SQL injection nếu framework hoặc ngôn ngữ lập trình bạn dùng không xử lý được nó.

Kết luận

Chúng ta vừa tìm hiểu qua một số pattern trong RegEx sử dụng phương thức test. Ngoài ra, phương thức exec xây dựng trên nên tảng này được sử dụng để trả về chuỗi con dựa trên pattern. Một số phương thức như match, search, replacesplit cũng được sử dụng rộng rãi trong regex. Hy vọng bạn có thể khám phá và hiểu thêm về regex để tự xây dựng được các pattern cho Regex.


All Rights Reserved