+8

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

Bạn có phải là một trong những người tránh xa những biểu thức chính quy (regular expressions - RegEx) vì nó trông giống như một ngoại ngữ không? Tôi cũng là một trong số đó!!! Hãy suy nghĩ về tất cả những âm thanh, ký hiệu giao thông và mùi vị mà bạn có thể nhận ra, RegEx cũng không khác gì những thứ trên. Nó giống như một ngôn ngữ ký hiệu dùng để phân tích string. Trong bài viết này, chúng ta sẽ tập trung vào những biểu thức chính quy đó, ít nhất là cách nó thường xuyên được sử dụng. Giống như bất kỳ ngôn ngữ lập trình nào, một biểu thức chính quy là một ngôn ngữ ngắn gọn theo chính cách của nó ^^.

0. Chuẩn bị môi trường

Trên thực tế, có một trang web có đầy đủ những gì bạn cần về RegEx: Regular Expressions from MDN, bạn có thể tham khảo trang web này và ngừng đọc bài viết này tại đây ^^. Nếu bạn vẫn tiếp tục, chúng ta cần một sandbox để thực hành các biểu thức này. Thật may mắn, chúng ra có thể sử dụng DevTools ngay trong console của trình duyệt của bạn. Làm quen với cú pháp Để bắt đầu, chúng ta sẽ sử dụng cú pháp /expression/.test('string'). Trong đó, expression là một biểu thức chính quy bất kỳ mà chúng ta tạo, string là string chúng ta muốn test. Phương thức test trả về true hoặc false tùy thuộc vào string có khớp với expression hay không. Dấu gạch chéo / đánh dấu điểm bắt đầu và kết thúc của expression, hãy hiểu nó giống như khi bạn dùng " hoặc ' để đánh dấu điểm bắt đầu và kết thúc của một string. Biểu thức ở giữa / là một chữ hoặc các ký tự chữ. Bây giờ, hãy mở console của trình duyệt và thử chạy:

/a/.test("a"); //true
/a/.test("b"); //false

Nếu code trên hoạt động, chúng ta đã sẵn sàng, đừng lo lắng chúng là gì, chúng ta sẽ tìm hiểu ngay bên dưới.

1. Bắt đầu nhỏ với các chữ cái

Hãy bắt đầu với các ví dụ nhỏ. Chúng ta cần phải tìm ra liệu một string có chứa ký tự được mô tả hay không. Ví dụ: tìm ký tự a trong một string

/a/.test("abc"); //true 
/a/.test("bcd"); //false 
/a/.test("cba"); //true

Biểu thức đã thực hiện những gì chúng ta muốn: Tìm ký tự a trong một string. Trong ví dụ, abccba có chứa ký tự a nhưng bcd thì không. Thực tế, chúng ta có thể gán /a/ vào một biến để dễ nhìn hơn:

let e = /a/; 
e.test("abc"); //true 
e.test("bcd"); //false 
e.test("cba"); //true

Trong ví dụ trên, biểu thức giữa 2 dấu gạch chéo / chỉ là một ký tự đơn, chúng ta đang chỉ tìm kiếm cho một ký tự.

Tìm kiếm nhiều ký tự Thế nếu bạn muốn tìm nhiều hơn một ký tự thì sao? Hãy đặt chúng theo đúng thứ tự và coi chúng như một chuỗi con (substring) của string. Đây là ví dụ:

/ab/.test("abacus"); //true 
/bac/.test("abacus"); //true  
/abc/.test("abacus"); //false 
/abas/.test("abacus"); //false

String được kiểm tra phải chứa chính xác cụm từ trong các dấu gạch chéo. Chúng ta có được một kết quả đúng nếu điều kiện đó được đáp ứng. bac nằm trong abacus nhưng abas không có trong abacus. Mặc dù chúng ta có đầy đủ các ký tự đó, nhưng chúng ta không nhận được kết quả chính xác do các ký tự không đúng thứ tự trong string.

Tóm tắt lại Ký hiệu /.../. Dấu gạch chéo (/) đánh dấu điểm bắt đầu và kết thúc của một biểu thức chính quy. Bỏ qua các dấu chấm, đó là nơi chúng ta đặt các pattern. Ký tự /a/ giữa các dấu gạch chéo là một pattern khớp với string được test. Các ký tự /abc/ giữa các dấu gạch chéo được tìm kiếm như một chuỗi con trong suốt quá trình kiểm tra với string được test.

2. Patterns với các chữ số

Hãy thêm gia vị lên một chút. Hãy nói rằng bạn muốn tìm hiểu xem liệu một string có đầy đủ các ký tự số hay không.

let e = /0|1|2|3|4|5|6|7|8|9/;
e.test("42"); //true 
e.test("The answer is 42"); //true

Trước hết, pattern trông khá dài. Nhưng dải dài các ký tự đó có thể được thể hiện chỉ trong hai ký tự. Chúng ta sẽ nói đến nó sau ở cuối phần này. Trường hợp thứ 2 nhẽ ra không phải là true. Chúng ta sẽ giải quyết nó một lát sau. Bây giờ, dấu sổ thẳng (|) ở đây nghĩa là OR. Bên ngoài của biểu thức chính quy, chúng được sử dụng như một toán tử thao tác trên bit (bitwise) OR và điều kiện OR với dấu sổ thẳng kép (||).

Vậy chúng ta có nên viết 9 dấu sổ thẳng không? Không! Giờ hãy viết lại:

e=/[0123456789]/; 
e.test("42"); //true 
e.test("The answer is 42"); //still true

Giờ chúng trông tốt hơn, 9 dấu sổ thẳng giờ được thay thế bởi 2 dấu ngoặc vuông, ta tiết kiệm được 7 ký tự! Bằng cách này, giá trị trong dấu ngoặc vuông sẽ được hiểu là: hoặc là cái này hoặc là cái kia. Đó là một tập hợp các ký tự. Trong trường hợp của chúng ta, string nên bao gồm hoặc là 0, hoặc là 1, hoặc là 2 hoặc là 3 hoặc là ...

Sao? Bạn đang nói gì? Trông nó vẫn dài? Bạn không hài lòng? Ok, giờ ta hãy thử lại:

e=/[0-9]/; 
e.test(42); //true 
e.test("42"); //true 
e.test("The answer is 42"); //true!

Thế này thì sao? Trông nó gọn gàng hơn đúng không? Mọi thứ trong dấu ngoặc vuông nghĩa là OR. 0-9 đánh dấu một range, nghĩa là từ 0 tới 9. Vì vậy, ví dụ thực hiện yêu cầu tìm kiếm các ký tự từ 0 tới 9 trong chuỗi cần test.

The prefix and suffix patterns - pattern với tiền tố và hậu tố Giờ chúng ta sẽ giải quyết trường hợp không thành công thứ 2. "The answer is 42" đã khớp với test của chúng ta bởi vì pattern của chúng ta tìm kiếm các ký tự số ở bất kỳ đâu trong string, không phải là ở đầu hay cuối. Và giờ ^$ sẽ giúp chúng ta.

  • ^ nghĩa là bắt đầu của string.
  • $ nghĩa là kết thúc của string.

Giờ là ví dụ với tiền tố pattern:

/^a/.test("abc"); //true 
/^a/.test("bca"); //false 
/^http/.test("https://pineboat.in"); //true 
/^http/.test("ftp://pineboat.in"); //false

Bất kỳ pattern nào theo sau ^ nên là bắt đầu của string đang test. String thứ 2 bắt đầu với b trong khi pattern của chúng ta đang tìm kiếm a. String thứ 4 tìm kiếm http trong khi nó bắt đầu với ftp. Đó là lý do kết quả là false.

The suffix patterns $ ở cuối của pattern nghĩa là tìm kiếm pattern ở cuối của string.

/js$/.test("regex.js"); //true 
/js$/.test("regex.sj"); //false

Điều này có thể khiến bạn nghĩ rằng: "Tìm kiếm js và sau đó kết thúc string", nhưng tốt hơn nên hiểu là "Hãy tìm một string có kết thúc là js".

Pattern match End to End

let e=/^[0-9]$/ 
e.test("42"); //false - NO! 
e.test("The answer is 42"); //false 
e.test("7"); //true

Thật ngạc nhiên, ví dụ đầu tiền thất bại khi chúng ta thêm ^$. /^[0-9]$/ được hiểu là: "Đi từ đầu string, tìm kiếm một ký tự số đơn và kiểm tra xem string có kết thúc tại đó không". Đó là lý do tại sao ví dụ cuối trả về true. String chỉ là một số đơn, bắt đầu và cũng là kết thúc. Nhưng đó không phải là điều chúng ta muốn, chúng ta muốn test xem liệu string có một hoặc nhiều hơn một chữ số hay không.

Câu chuyện về ba chàng lính ngự lâm Một dấu chấm hỏi (?), một dấu cộng (+) và một dấu hoa thị (* ) gặp nhau tại trận đấu, mỗi người trong đó có những khả năng khác biệt. Dấu hỏi chấm khiêm tốn nói: "Tôi có thể nhìn thấy không có thứ gì hoặc chỉ nhìn thấy một thứ". Dấu cộng (+) nói: " Tôi cần thấy ít nhất một thứ hoặc nhiều hơn". Dấu hoa thị (* ) nói: "Tôi có thể làm được cả hai, tôi có thể nhìn thấy không có thứ gì, chỉ một thứ hoặc nhiều hơn". Một trong số chúng đang khéo léo che giấu những gì chúng có thể làm. Dấu hỏi chấm biểu diễn đầu tiên:

/a?/.test(""); //true 
/a?/.test("a"); //true 
/a?/.test("b"); //true! 
/a?/.test("aa"); //true 
/^a?$/.test("aa"); //false
  • Tìm trong string rỗng "" : ? viết tắt cho 0 hoặc 1
  • Tìm trong a : một kết quả khớp
  • Tìm trong b : không có cái nào khớp
  • Tìm trong aa : a đầu tiên khớp với pattern và a thứ 2 không phải là một phần của pattern.
  • /^a?$/ không khớp với aa : Nó tìm kiếm 0 hoặc 1 ký tự a, đầu và cũng là cuối, không nhiều hơn, không ít hơn.

Dấu cộng (+) nhìn vào dấu hỏi chấm và nhận xét: "Tôi đã rất ấn tượng, nhưng trọng tâm của bạn giống như hệ nhị phân vậy". Và nó bước lên bục biểu diễn:

/a+/.test("a"); //true 
/a+/.test("aa"); //true 
/a+/.test("ba"); //true! 
/^a+$/.test("aa"); //true  

/a+/.test(""); //false 
/a+/.test("b"); //false 
/^a+$/.test("ab"); //false

Bạn có nhớ những gì dấu cộng đã nói không? Nó có thể bắt một hoặc nhiều lần xuất hiện của pattern đứng trước nó. Tất cả các trường hợp trả về true đều có một hoặc nhiều ký tự a. Chúng ta thậm chí tìm cách để có được một chuỗi toàn bộ chỉ bao gồm a trong trường hợp cuối cái mà trả lại true với / ^ a + $ /. Các trường hợp fasle bây giờ đã được làm rõ, nhưng một từ ở cuối lại trả về false. /^a+$/ tìm kiếm a ở đầu và cũng là ở cuối cùng, không có ký tự nào khác được cho phép ở đây, đó là lý do tại sao ab khi test bị fail. Cuối cùng, dấu sao (*) biểu diễn. Cậu ta tự hào rằng: "Tôi có thể đấu một mình hoặc đấu với cả 2 bạn cùng lúc" và nói "Tôi có thể match không cái nào, một cái hoặc nhiều cái".

/a*/.test("a"); //true 
/a*/.test("aa"); //true 
/a*/.test("ba"); //true 
/a*/.test(""); //true 
/a*/.test("b"); //true 
/^a*$/.test("aa"); //true 
/^a*$/.test(""); //true  

/^a*$/.test("ab"); //false

Ngoại trừ trường hợp cuối cùng, * có thể xử lý tất cả những cái khác. /^a*$/ có thể hiểu là: 0 hoặc nhiều hơn ký tự a là đầu cũng là cuối. Đó là tại sao string rỗng "" pass test trong khi ab fail.

Quay lại câu trả lời lúc trước Bạn có nhớ chúng ta đã từng gặp qua trường hợp ba chàng lính ngự lâm ở đâu trước đó không? Đúng rồi, đó là trường hợp “The answer is 42”. Bây giờ, nếu ta cần tìm kiếm chỉ các chữ số, một hoặc nhiều hơn các ký tự, chúng ta sẽ làm gì?

//Let's throw in a plus 
let e=/^[0-9]+$/ 
e.test("4"); //true 
e.test("42"); //true 
e.test("The answer 42"); //false - Hurray

Dấu cộng (+) trong [0-9]+ đã đưa cho chúng ta một giải pháp. Cộng nghĩa là nhiều hơn một lần xuất hiện của ký tự hoặc pattern ở đằng trước nó. Trong trường hợp của chúng ta, nhiều hơn một chữ số. Nó vẫn trả về false trong tường hợp The answer 42 bởi vì không có chữ số nào bắt đầu ở đầu string. Oh, mình quên mất, [0-9] viết tắt cho bất kỳ một tập chữ số nào và có cách viết ngắn hơn \d.

let e=/^\d+$/; 
e.test("4"); //true 
e.test("42"); //true 
e.test("The answer 42"); //false - Hurray

Chỉ cần 2 ký tự để biểu thị cho các chữ số và không có cách nào viết ngắn hơn nữa! Có một vài pattern đặc biệt đặc tả cho các tập khác nhau: tập các số (\d), tập các ký tự apha (\w), dấu cách (\s). Review

  • [123] Biểu thức trong dấu ngoặc vuông là một tập các ký tự, bất kỳ ký tự nào khớp với string sẽ pas test. Chỉ một ký tự thôi.
  • [0-9] Tìm kiếm một chữ số đơn từ 0 đến 9.
  • [0-5] Tìm kiếm một chữ số đơn từ 0 đến 5.
  • [a-z] Tìm kiếm một chữ cái đơn từ a đến z.
  • [A-F] Tìm kiếm một chữ cái đơn từ A đến Z.
  • [123]+ dấu + tìm kiếm một hoặc nhiều hơn số lần xuất hiện của ký tự trong tập.
  • | Dấu sổ thẳng viết tắt cho OR.
  • \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.

Trên đây là một số hiểu biết cơ bản về biểu thức chính quy, chúng ta sẽ tiếp tục tìm hiểu nhiều hơn về nó trong bài viết sau. Cảm ơn bạn đã đọc bài viết!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí