Học Regular Expression và cuộc đời bạn sẽ bớt khổ (Updated v2.2)

2181

Người viết:

Regular Expression (RegEx) à? Nghe quen quen.

Đã bao giờ bạn ở vào các tính huống sau đây? Bạn cần xử lý validate (kiểm tra tính hợp lệ) các trường dữ liệu nhập vào ô Text

Bạn cần copy và paste rất nhiều text từ chỗ này sang chỗ kia

Ví dụ từ XML sang C#

hoặc CSV, Excel sang C#

Tìm kiếm một chuỗi nằm ở nhiều dòng trong Visual Studio hoặc Notepad++

Ví dụ tôi cần tìm và xóa bỏ (replace all) các comment như thế này ở code cũ.

Kiểm tra bằng câu lệnh SQL xem đã insert đủ các trường vào DB hay chưa?

Bạn cần bóc tách dữ liệu của một trang web.

Ví dụ extract (crawling) tách lấy dữ liệu từ một trang web để lưu lại vào cơ sở dữ liệu của bạn

Cơn ác mộng đọc một chuỗi string từ DB và cố gắng chuyển đổi nó sang DateTime

Chuỗi ngày tháng lưu vào CSDL rất đa dạng. Rất khó để dùng hàm DateTime.TryParse() để tự động chuyển đổi một chuỗi thành Datetime ngon lành cành đào.

Tìm kiếm một chuỗi, lưu chuỗi đó lại và lấy chuỗi đã lưu chèn vào chỗ nào đó

Hay nói cách khác, ta cần thay một chuỗi bằng một chuỗi khác, trong chuỗi mới đó lại có chứa cả chuỗi vừa bị thay thế. Như ví dụ dưới đây sẽ cho các bạn thấy. Ta cần tìm ra ClassName, nhưng không phải thay thế ClassName đó bằng Class khác mà ta sẽ nhét nó vào 2 vị trí thay vì chỉ có 1 vị trí như hiện tại.

Vậy RegEx có thể giúp gì cho chúng ta trong các trường hợp trên?

Regular Expression sinh ra là để giúp cho cuộc đời của bạn bớt khổ. Hãy tưởng tượng xem bạn phải nai lưng ra copy-paste bao nhiêu code. Bạn hoàn toàn có thể download các tool về để replace giúp bạn. Nhưng bạn cần bao nhiêu tool cho đủ đây.

Thay vào đó, bạn hãy thử học RegEx. Bộ cú pháp này sẽ giúp cho bạn thao tác với chuỗi như dao chém chuối. Như ta thấy các ví dụ bên trên, tất cả đều là XỬ LÝ CHUỖI ví dụ Cut Copy Paste hoặc Replace chuỗi. RegEx là ngôn ngữ giúp xử lý chuỗi rất mạnh.

RegEx không phải là một ngôn ngữ lập trình. Nó chỉ là một BỘ CÚ PHÁP dùng để bắt chuỗi. Nhưng nó cực kỳ phổ biến và bất kỳ ngôn ngữ lập trình nào cũng hỗ trợ. Nó có cả trăm ngàn ứng dụng và công cụ ăn theo.

Hãy lần lượt xem qua các ví dụ sau đây để học cách sử dụng Regular Expression nhé.

Bắt đầu sử dụng Regular Expression

Để bắt đầu sử dụng và học cách dùng RegEx, mình hay dùng trang web https://regex101.com/. Giao diện như sau:

Trước hết các bạn hãy đọc qua một lượt các quy tắc bắt chuỗi (matching) đơn giản nhất của RegEx

  • [xyz] Tìm và so sánh tất cả ký tự nằm trong dấu ngoặc vuông và trùng khớp với 1 ký tự trong dấu ngoặc vuông. Ví dụ: [31] sẽ trùng khớp với 3 hoặc 1, [0123456789] sẽ trùng khớp với bất kỳ một ký tự nào trong khoảng từ 0 đến 9.
  • [a-z] So sánh và trùng khớp với một ký tự nằm trong khoảng chỉ định. Ví dụ: [a-z] sẽ trùng khớp với một ký tự trong khoảng từ a đến z nằm trong chuỗi cần test. [0-9] sẽ trùng khớp với bất kỳ một ký tự nào trong khoảng từ 0 đến 9.
  • [^xyz] So sánh và không trùng khớp với những ký tự nằm trong khoảng chỉ định. Dấu ^ (dấu mũ) nằm trong dấu ngoặc vuông là một dấu phủ định. Ví dụ: [^a-z] sẽ không trùng khớp với tất cả các ký tự nằm trong khoảng từ a đến z.
  • ^ Trùng khớp với phần đầu của chuỗi đích. Ví dụ: ^a sẽ trùng khớp với chữ a trong chuỗi abc, ^\w+ sẽ trùng khớp với chữ đầu tiên – chữ “the” của chuỗi “The quick brown fox jumps over the lazy dog”.
  • $ Trùng khớp với phần cuối của chuỗi đích. Ví dụ: c$ sẽ trùng khớp với chữ c trong chuỗi abc, \w+$ sẽ trùng khớp với chữ cuối – chữ “dog” của chuỗi “The quick brown fox jumps over the lazy dog”.
  • + Trùng khớp với 1 hoặc nhiều lần ký tự đứng trước nó. Ví dụ \d+ sẽ chỉ trùng với chuỗi có từ 1 con số trở lên.
  • * Trùng khớp với 0 hoặc nhiều lần ký tự đứng trước nó. Ví dụ \d* sẽ trùng với chuỗi có chứa 1 chữ số hoặc k có chữ số nào cũng đc.
  • ? Trùng khớp với 0 hoặc 1 lần ký tự đứng trước nó. Tương tự như * nhưng nó lại chỉ nhân lên 1 lần. * thì nhân lên nhiều lần.
  • . Trùng khớp với 1 ký tự đơn bất kỳ ngoại trừ ký tự ngắt dòng (line-break) và cũng không lấy được ký tự có dấu (unicode). Ví dụ: . sẽ trùng khớp với ký tự a hoặc b hoặc c trong chuỗi abc. Nhưng . sẽ không bắt được các chữ ă hoặc ê.
  • x{n} Trùng khớp đúng với n lần ký tự đứng trước nó. n là một số không âm. Ví dụ \d{2} sẽ bắt đc các số có 2 chữ số đứng liền nhau.
  • x{n,} Trùng khớp với ít nhất n lần ký tự đứng trước nó. n là một số không âm.Ví dụ \d{2,} sẽ bắt đc các số có từ 2 chữ số trở lên đứng liền nhau.
  • x{n,m} Trùng khớp với ít nhất n lần và nhiều nhất là m lần ký tự đứng trước nó. n và m là một số không âm và n <= m. Ví dụ: a{1,3} sẽ khớp với hah, haah, haaah nhưng không khớp với haaaah.
  • x|y Trùng khớp với x hoặc y. Ví dụ: slow|fast sẽ khớp với chữ slow hoặc fast trong chuỗi đích.
  • \b Trùng khớp với toàn bộ ký tự đứng trước nó. Ví dụ: hello\b sẽ trùng khớp với toàn bộ từ hello trong chuỗi hello world nhưng sẽ không khớp với chuỗi helloworld.
  • \B Ngược lại với \b, \B sẽ không khớp với toàn bộ mà chỉ 1 phần ký tự đứng trước nó. Ví dụ: hello\B sẽ trùng khớp với chữ hello trong chuỗi helloworld nhưng sẽ không khớp với chuỗi hello world.
  • \d Trùng khớp 1 ký tự số (digit).
  • \D Trùng khớp 1ký tự không phải số (non-digit).
  • \s Trùng khớp 1 ký tự khoảng trắng (whitespace) bao gồm khoảng trắng tạo ra bởi phím Tab.
  • \S Trùng khớp với 1 ký tự không phải là khoảng trắng (non-whitespace).
  • \w Trùng khớp với các ký tự là từ (word) bao gồm dấu _ (underscore) và chữ số.
  • \W Trùng khớp với các ký tự không phải là từ (non-word). Ví dụ: \W sẽ khớp với ký tự % trong chuỗi “100%”.
  • \uxxxx Trùng khớp với 1 ký tự unicode. Ví dụ: \u00FA sẽ khớp với ký tự “ú”, \u00F9 sẽ khớp với ký tự “ù”.
  • \pL Trùng khớp với một ký tự Unicode bất kỳ ngoại trừ dấu cách. Đây chính là cú pháp viết hoàn hảo hơn của dấu .,Ví dụ \pL+ sẽ lấy được chuỗi truyền, thuyết trong chuỗi “truyền thuyết”.

Đừng lo, hãy xem qua các ví dụ từ từ rồi bạn sẽ hiểu

Ví dụ đơn giản

Tìm chuỗi số

Ví dụ 1: Đang load...(Nếu load quá lâu hãy ấn F5)

Tìm ngày tháng

Đang load video...(Nếu load quá lâu hãy ấn F5)

Link demo: https://regex101.com/r/3dNzjU/1

Tìm ngày tháng chính xác

Trong ví dụ trên thì ta thấy có chuỗi “60/60/2018” cũng được coi là ngày tháng, như thế là không chính xác. Ta hãy viết lại cho chuẩn.

Đang load video...(Nếu load quá lâu hãy ấn F5)

Link demo: https://regex101.com/r/3dNzjU/2

Cách “tóm lấy” string cần tìm đưa vào Group

Đang load video...(Nếu load quá lâu hãy ấn F5)

Cách tạo ra code để dùng RegEx trong các ngôn ngữ lập trình

RegEx là bộ cú pháp, tuy nhiên để áp dụng nó vào các ngôn ngữ lập trình lại phải tuân thủ theo các thư viện và quy tắc lập trình để lấy ra được các Group đã capture (thu) được. Hãy xem các tạo code ngay sau đây:

Đang load video...(Nếu load quá lâu hãy ấn F5)

Cách bắt lấy chuỗi bất kỳ

Đang load video...(Nếu load quá lâu hãy ấn F5) Link demo: https://regex101.com/r/3dNzjU/3

Lấy một chuỗi nhưng không chứa 1 ký tự đặc biệt nào đó

Ví dụ bạn cần lấy 1 chuỗi dài, nhưng nếu chuỗi đó có chứa 1 ký tự đặt biệt nào đó, ví dụ ký tự / thì không được lấy dòng đó. Nghĩa là cần loại bỏ 1 ký tự đặc biệt ra khỏi tập kết quả (group matches) thu được.

Rất đơn giản, chúng ta hãy làm như sau để có thể loại bỏ dấu “/” ra khỏi kết quả thu được. https://regex101.com/r/ctuQwj/1

2018-11-27_22-58-46.jpg

Giải thích 1 chút, như mình đã nói bên trên, khi dùng [^ab] thì nó sẽ loại bỏ các kết quả có chứa ký tự a hoặc b ra khỏi tập đã lấy được. Ở đây bạn cần loại bỏ dấu / nên ta sẽ viết là [^\/]. Và vì chúng ta cần nhân bản nhiều lần các ký tự khác dấu / để tìm 1 chuỗi dài liên tiếp, cho đến khi gặp 1 số có 4 chữ số. Vậy thì ta sẽ viết thêm đuôi *? vào để thành [^\/]*?.

Bình thường nếu tìm một chuỗi dài liên tiếp, không quan tâm là ký tự hay chữ số, ta hay viết là .*?, do đó cách viết [^\/]*? thực ra là một biến thể của .*?.

Dấu chấm . sẽ bắt được một ký tự bất kỳ ngoại trừ dấu xuống dòng, do đó .*? sẽ chỉ bắt được chuỗi dài vô tận ở cùng 1 dòng (1 line). Do đó nếu bạn cần bắt được chuỗi nằm ở nhiều dòng cho đến khi gặp một đoạn chuỗi cố định nào đó, thì sẽ phải dùng đến chiêu sau đây.

Sử dụng [\s\S] để thay thế cho dấu . ta sẽ bắt được toàn bộ ký tự bất kỳ bao gồm cả dấu xuống dòng /n. Do đó để bắt được 1 chuỗi dài vô tận nằm ở nhiều dòng ta sẽ dùng [\s\S]*

Lấy một chuỗi nhưng không chứa 1 chuỗi con đặc biệt nào đó

Bây giờ bạn không chỉ cần exclude (loại bỏ) một ký tự ra khỏi danh sách chuỗi thu được, bạn còn cần loại bỏ hẳn “một chuỗi” thì sao. Vậy hãy dùng cách sau đây:

Vấn đề lúc này cần phải dùng đến chức năng Negative Lookahead của Regex, cách hoạt động như sau:

2018-11-28_23-09-49.jpg

Giả sử mình cần exclude (loại bỏ) một chuỗi (chứ không phải 1 ký tự như là ví dụ [^\/]*? như bên trên nữa) ra khỏi một chuỗi đã bắt được. Thì cần dùng cách viết (?!abc) (Chú ý là dấu () là bắt buộc phải viết để đủ cú pháp). Trong đó abc là 1 chuỗi các ký tự liền nhau cần loại bỏ. Nếu cần loại bỏ 2 hoặc nhiều chuỗi con thì sao? vậy ta sẽ viết là (?!abc|def|gho).

Vì chuỗi con nằm bên trong chuỗi cha, do đó chúng ta cần viết thêm 1 dấu chấm . ngay đằng sau (?!\/bc) và bọc toàn bộ khối đó lại thành 1 group. Rồi viết thêm *? bên ngoài để nhân bản các ký tự lên thành 1 chuỗi.

Như vậy để viết Regex này ta phải tư duy từ bên trong ra bên ngoài. Và việc viết thêm Negative Lookahead kèm với dấu chấm dạng ((?!abc).)*? thật ra cũng vẫn chính là một biến thể của .*? (bắt một chuỗi dài vô tận, nhưng không được chứa chuỗi con cần loại bỏ bên trong nó)

Link demo: https://regex101.com/r/SThqo2/2

Bắt 1 chuỗi bất kỳ nằm ở nhiều dòng (mutiline)

Biểu thức RegEx: <h1>[\s\S]*<\/h2>

Sẽ bắt được cả toàn bộ chuỗi nhiều dòng sau đây:

Giải thích tại sao lại bắt được. Rất đơn giản, là vì \s sẽ lấy được ký tự dấu cách, còn \S sẽ lấy được ký tự bất kỳ ngoại trừ dấu cách (và \S sẽ bắt được dấu xuống dòng \n). Nếu kết hợp 2 ký tự đó nhét vào một khung vuông [\s\S] tức là sẽ lấy được 1 trong 2 trường hợp. Sẽ bắt được toàn bộ các ký tự bất kỳ bao gồm cả dấu cách nếu gặp. Viết [\s\S]*? thật ra là nhân bản ký tự cần bắt lên nhiều lần để tìm một chuỗi dài vô tận.

Bình thường nếu tìm một chuỗi dài liên tiếp, không quan tâm là ký tự hay chữ số, ta hay viết là .*?, do đó cách viết [\s\S]*? thực ra là một biến thể của .*?. Dấu chấm . sẽ bắt được một ký tự bất kỳ ngoại trừ dấu xuống dòng, do đó .*? sẽ chỉ bắt được chuỗi dài vô tận ở cùng 1 dòng (1 line). Do đó nếu bạn cần bắt được chuỗi nằm ở nhiều dòng cho đến khi gặp một đoạn chuỗi cố định nào đó, thì sẽ phải dùng đến chiêu sau đây. Sử dụng [\s\S] để thay thế cho dấu . ta sẽ bắt được toàn bộ ký tự bất kỳ bao gồm cả dấu xuống dòng /n. Do đó để bắt được 1 chuỗi dài vô tận nằm ở nhiều dòng ta sẽ dùng [\s\S]*

Các bạn có thể xem link demo: https://regex101.com/r/irhKCk/1

Sử dụng RegEx trong thực tế

Trong thực tế RegEx có thể được gõ trực tiếp ở bất kỳ trình Editor nào. Ví dụ mình hay dùng Notepad++, hoặc Visual Studio

Đang load video...(Nếu load quá lâu hãy ấn F5)

Thêm thật nhiều ví dụ cho bạn tham khảo (để xem sức mạnh của RegEx)

Check một chuỗi ngày tháng bất kỳ có hợp lệ hay không tính theo năm nhuận

Biểu thức RegEx:

Sẽ đúng cho:

Và sẽ không trùng với:

Check 1 chuỗi ngày tháng phải có cả ngày và giờ

Biểu thức RegEx:

Sẽ đúng cho:

Và sẽ không trùng với:

Check 1 chuỗi bất kỳ xem có phải ngày hoặc giờ hay không

Biểu thức RegEx:

Sẽ đúng cho:

Và sẽ không trùng với:

Tóm lại:

  • Regular expressions (hay còn gọi là Biểu thức chính quy – viết tắt là RegEx) là một chuỗi ký tự đặc biệt được dùng làm mẫu (pattern) để phân tích sự trùng khớp (match) của một tập hợp các chuỗi con cần lấy ra từ một chuỗi cha.
  • Chuỗi cha sau khi kiểm tra và lấy ra được các chuỗi con thì kết quả được đưa vào các Nhóm kết quả (Matches).
  • Trong mỗi Matches có các Group chứa các chuỗi con có thể cut lấy ra khỏi chuỗi cha (do người dùng định nghĩa chuỗi con cần lấy hoặc không lấy bằng cách viết thêm cặp ngoặc tròn () bao bọc chuỗi con cần lấy)
  • RegEx có thể lấy chuỗi ra (cut) hoặc thay thế chuỗi (replace)
  • Hầu hết những ngôn ngữ lập trình (PHP, C#, Perl, Javascript…) đều cung cấp các thư viện hoặc hàm xử lý để lập trình viên có thể làm việc với Regular expression.

Tham khảo

Tham khảo thêm toàn bộ cú pháp RegEx bằng tiếng Anh tại: https://www.cheatography.com/davechild/cheat-sheets/regular-expressions/

Một số công cụ test RegEx Online

Bài tập về nhà

Bạn ngứa tay muốn thử học RegEx ngay và luôn cho nóng. Vậy hãy thử thực hành bằng một số bài tập từ dễ đến khó sau đây nhé.

Bài tập nhập môn:

  1. Lấy ra các chữ có chữ test trong chuỗi sau: “that tested test is testing the tester’s tests”
  2. Lấy ra các số điện thoại trong chuỗi sau: ** “p:444-555-1234 f:246.555.8888 m:1235554567”**
  3. Lấy ra các mã màu RGB trong chuỗi sau: “#FF006C ABC 99AAB7FF 0xF0F73611”
  4. Lấy ra các chữ có 4 ký tự trong chuỗi sau: “drink beer, it’s very nice!”
  5. Lấy ra tên file trong chuỗi URL sau: rapidshare.com/asd/asd/File.avi.html”

Bài tập dành cho học sinh giỏi (lớp học thêm)

  1. Tìm cách lấy các URL trong chuỗi HTML sau:

  1. Tìm cách loại bỏ toàn bộ COMMENT trong đoạn code sau:

  1. Tìm cách lấy ra chuỗi tiếng Nhật trong chuỗi sau: “

  1. Lấy ra những file ảnh và độ phân giải của ảnh từ chuỗi sau:

 

Kết bài

Hi vọng bài viết nhỏ này đã giúp bạn học được cú pháp viết RegEx và áp dụng vào công việc thường ngày. Mình dùng Regex rất nhiều và nó là trợ thủ đắc lực trong quá trình code.

Các bạn thì sao? bạn đã dùng Regex vào những việc gì? Hãy kể tên bên dưới comment để mình cùng học hỏi với nhé.

Thanks các bạn đã đọc, hãy comment, upvote và share bạn bè thật nhiều nhé!

Update v2.2: Thêm các bài tập để các bạn luyện thêm (theo yêu cầu của nhiều bạn, nhớ tự làm trước khi dòm đáp án nhé ^^!)

BTU1. Tìm ra các chuỗi là số điện thoại trong chuỗi sau. Chú ý một số loại ra một số chuỗi không phải là số điện thoại

Lời giải https://regexr.com/38pvb

BTU2. Tìm ra chuỗi là địa chỉ email trong chuỗi sau, chú ý loại bỏ các chuỗi k phải địa chỉ emai thực sự.

Lời giải: https://regexr.com/3bcrb

BTU3. Tìm cách lấy ra và ngăn cách chuỗi số sau thành có phần trăm phần nghìn.

Lời giải: regexr.com/4409r

BTU4. Lấy ra các chuỗi ngăn cách bởi dấu phẩy theo định dạng của một file CSV như sau (chú ý là chuỗi bị xuống dòng):

Lời giải: https://regexr.com/3apuc

BTU5. Tìm và xóa đi toàn bộ các thẻ HTML trong đoạn text dưới đây:

Lời giải: https://regexr.com/3cak1

BTU6. Tìm cách loại bỏ các chuỗi console.log() do lập trình viên viết để test ra khỏi code trước khi bàn giao

Lời giải: https://regexr.com/3fi66

BTU7. Loại bỏ toàn bộ các đoạn comment code trong đoạn code sau đây:

Lời giải: https://regexr.com/3aeb7

BTU8. Lấy ra các chuỗi là các URL đầy đủ trong đoạn text dưới đây:

Lời giải: https://regexr.com/39nr7

BTU9. Lấy ra toàn bộ các URL của một video Youtube từ đoạn text test sau đây

Lời giải: https://regexr.com/3akf5

BTU10. Lấy ra các đoạn text là giá trị màu RBG

Lời giải: https://regexr.com/38lmo

Techtalk via viblo.asia

CHIA SẺ