design
Regex Tester & Pattern Builder: Hướng Dẫn Thực Hành Viết Regex Xịn Xò, Chạy Chuẩn
Cách viết regex thực sự hiệu quả: từ anchor, quantifier, cái bẫy khác biệt giữa các engine, đến lỗi ReDoS có thể làm sập server của bạn. Hãy luôn test khi viết.
Published 2026-06-22 · 9 min read
Affiliate disclosure
Some links below are affiliate links. I may earn a commission from qualifying purchases at no extra cost to you. I only recommend tools I have used or tested.

Tóm tắt (TL;DR)
- Test trước khi đưa lên production: một trình kiểm tra regex sẽ giúp bạn bắt lỗi mà việc đoán mò bỏ sót. Các pattern thường hỏng theo những cách bạn không thể lường trước nếu không chạy thử.
- Mỗi engine một khác:
\dkhớp với hơn 600 chữ số Unicode trong Python nhưng lại chỉ khớp với ASCII 0–9 trong PCRE nếu bạn không bật cờ. Cú pháp lookbehind và mức độ hỗ trợ cũng khác nhau. Hãy luôn test regex trên engine mà bạn thực sự định dùng.- Cẩn thận với catastrophic backtracking: các pattern như
^(a+)+$có thể bị treo hoặc timeout khi chuỗi đầu vào không khớp, vì engine sẽ duyệt qua vô số nhánh theo cấp số nhân. Hãy dùng engine thời gian tuyến tính như RE2 cho dữ liệu không đáng tin.
Tại sao dùng regex tester lại hơn đoán mò?
Regex thực chất chỉ là "chỉ để đọc" cho đến khi bạn chạy thử nó. Một pattern có vẻ hoàn hảo trong đầu bạn rất dễ hỏng hóc khi đụng vào chuỗi thực tế: nó có thể khớp quá nhiều, quá ít, hoặc khiến engine bị timeout. Phương án "test trước" sẽ giúp bạn lòi ra những lỗi này trước khi chúng kịp phá production. Một trình kiểm tra regex cho phép bạn dán pattern vào, ném vài chuỗi test vào, và xem chính xác cái gì khớp, cũng như các nhóm trích xuất (capture groups) hoạt động ra sao. Công cụ anytools chạy hoàn toàn trên engine JavaScript ngay trong trình duyệt của bạn (không có dữ liệu nào rời khỏi trang), hiển thị các capture group có tên và số, cho phép bạn bật tắt các cờ như g, i, m, và s, kèm theo chế độ thay thế (replace mode) để test phép substitution. Bạn quan sát hành vi thực tế thay vì mù tưởng.
Một điểm cộng nữa là tốc độ. Thay vì cứ viết code, chạy bộ test rồi chờ kết quả, bạn nhận được feedback ngay lập tức trên trình duyệt. Sửa một pattern, xem nó đánh giá lại, và lặp lại vòng đời trong vài giây. Đến khi bạn port regex vào code production, bạn đã chắc chắn nó chạy ngon lành với các đầu vào quan trọng rồi.
Các khối xây dựng nền tảng — pattern thực sự làm gì?
Trước khi viết được một regex hoạt động tốt, bạn cần hiểu các thành phần làm gì và chỗ nào chúng hay gây hiểu lầm.
Các Anchor (điểm neo) khóa khớp của bạn vào các biên. ^ khớp với đầu chuỗi (hoặc đầu dòng, nếu cờ multiline được bật), và $ khớp với cuối chuỗi. Nếu thiếu chúng, 123 sẽ bị khớp ngay bên trong chuỗi "prefix123suffix". Có chúng, ^123$ sẽ chỉ khớp chính xác với chuỗi "123". Một lỗi kinh điển là quên neo và tự hỏi sao pattern lại khớp những thứ không nên khớp. Cái bẫy ở đây là ý nghĩa của ^ và $ sẽ thay đổi theo cờ multiline — JavaScript yêu cầu cờ /m để khiến chúng khớp với ranh giới dòng thay vì ranh giới chuỗi theo MDN.
Character classes (lớp ký tự) định nghĩa cái gì được phép khớp. [a-z] có nghĩa là "một chữ cái thường." [0-9] nghĩa là một chữ số. \d là cách viết tắt của chữ số, nhưng cái bẫy ở đây là: trong Python 3, \d khớp với bất kỳ chữ số thập phân Unicode nào, bao gồm cả Arabic-Indic (٠١٢) và Devanagari (०१२), chứ không chỉ 0–9. Trong PCRE, mặc định nó chỉ là ASCII [0-9] trừ khi cờ PCRE2_UCP được đặt theo docs Python. Nếu code của bạn đang kỳ vọng \d là 0–9 nhưng bạn port nó sang Python, bạn sẽ âm thầm bắt đầu khớp với các ký tự mà bạn chưa từng dự định.
Quantifier (bộ đếm) cho biết số lần lặp lại. + nghĩa là một hoặc nhiều. * nghĩa là không hoặc nhiều. ? nghĩa là không hoặc một. {2,5} nghĩa là từ 2 đến 5 lần. Theo mặc định, chúng là greedy (tham lam) — chúng khớp càng nhiều ký tự càng tốt. Pattern <div>.*</div> sẽ khớp từ <div> đầu tiên đến </div> cuối cùng trong chuỗi, nuốt chửng mọi thứ ở giữa. Tài liệu của Rexegg ghi lại việc thêm ? sẽ biến quantifier thành lazy (lười): <div>.*?</div> sẽ dừng ngay tại </div> đầu tiên. Trong các trường hợp đơn giản, quantifier lười có vẻ là cách sửa, nhưng nó không phải là đạn bạc. Một quantifier lười vẫn bị backtracking (quay lui), và nếu thẻ đóng vắng mặt hoàn toàn, engine vẫn sẽ khám phá nhiều đường dẫn khác trước khi bỏ cuộc, điều này có thể kích hoạt cái bẫy ReDoS bên dưới.
Capture group trích xuất các phần của kết quả khớp. Bọc một pattern trong dấu ngoặc đơn như (\d+)-(\w+) sẽ tạo ra hai capture group. Sau khi khớp, bạn có thể lấy toàn bộ kết quả và từng nhóm riêng biệt. Các capture group có tên như (?<day>\d+)-(?<month>\w+) giúp dán nhãn các nhóm để bạn khỏi phải đếm chúng — code sáng sủa hơn, ít bug off-by-one hơn. JavaScript hỗ trợ group có tên và lookbehind (với chiều rộng cố định), Python hỗ trợ cả hai, PCRE hỗ trợ cả hai, nhưng POSIX ERE thì không theo đặc tả POSIX. Lưu ý rằng: regex của bạn phải chạy trên một engine có các tính năng này.
Vấn đề về "flavor" — code chạy trên máy này, lỗi trên máy kia
Các flavor (phiên bản phương ngữ) regex là những biến thể của cùng một ngôn ngữ cốt lõi, và chúng khác nhau theo những cách tạo ra các lỗi âm thầm.
POSIX ERE là nền tảng cơ bản. Nó hỗ trợ các quantifier +, *, ?, {m,n}, gom nhóm và phép xen kẽ (alternation). Nó không có lookahead, lookbehind, backreference, hay group có tên — đây là những phần mở rộng của PCRE/Perl/Python/JavaScript. Nếu bạn đang viết regex cho một công cụ hệ thống như grep hoặc sed, hoặc cho một thư viện xác thực đơn giản, POSIX là thứ bạn sẽ nhận được.
Module re của Python khớp với Unicode theo mặc định. Như đã nói ở trên, \d là lớp thập phân Unicode trong Python 3. \w khớp với bất kỳ ký tự từ Unicode nào, không chỉ ASCII. Điều này rất tiện cho quốc tế hóa nhưng sẽ phá vỡ các giả định của bạn nếu bạn đang port code từ vùng PCRE (nơi \d chỉ là ASCII 0–9).
PCRE (và Perl) cung cấp nhiều tính năng nhất. Lookbehind, backreference, group có tên, atomic group, possessive quantifier — nếu nó có cảm giác giống như một siêu năng lực regex, thì PCRE chắc chắn có nó. Cái giá phải trả là sự phức tạp và tính kém khả chuyển (portability). Một pattern PCRE sẽ không chạy trong JavaScript hoặc Python mà không cần tinh chỉnh.
JavaScript có hầu hết các tính năng hiện đại nhưng lookbehind phải có độ dài cố định. Group có tên, lookahead, lookbehind (tương đối) — đều được hỗ trợ. Cái bẫy là: một assertion lookbehind như (?<=user_) phải có pattern với độ dài cố định, nghĩa là bạn không thể dùng + hoặc * bên trong nó. (?<=[a-z]+) sẽ văng lỗi cú pháp trong JavaScript theo MDN. PCRE cho phép lookbehind có độ dài thay đổi, mạnh mẽ hơn nhưng chậm hơn.
RE2 và regexp của Go ưu tiên tốc độ hơn tính năng. Chúng đảm bảo thời gian khớp tuyến tính O(mn) (trong đó m là kích thước pattern và n là đầu vào), giúp chúng miễn nhiễm với catastrophic backtracking. Đánh đổi là: không lookbehind, không backreference, không possessive quantifier. Nếu bạn quan tâm đến bảo mật và tốc độ hơn là đầy đủ tính năng, RE2 là lựa chọn đúng đắn. Google đã xây dựng nó để xử lý các pattern do người dùng cung cấp một cách an toàn.
Cái bẫy ReDoS — tại sao ^(a+)+$ có thể làm sập server của bạn
Đây là mối đe dọa regex bị đánh giá thấp nhất. Một pattern có thể hợp lệ về mặt cú pháp và vô nghĩa về mặt ngữ nghĩa, nhưng vẫn khiến engine regex bị treo hoặc timeout khi nó cố gắng khớp một số đầu vào nhất định. Điều này được gọi là catastrophic backtracking hoặc ReDoS (Regular expression Denial of Service), và nó xảy ra khi các quantifier lồng nhau ép engine phải khám phá vô số nhánh theo cấp số nhân.
Ví dụ điển hình là ^(a+)+$. Hãy cung cấp cho nó chuỗi aaaaaaaaaaaaaaa! (mười lăm chữ a theo sau là !). Engine cố gắng khớp pattern. + ở ngoài là tham lam (greedy), nên nó tiêu thụ tất cả mười lăm chữ a. + ở bên trong cũng vậy, nên nó muốn thêm nữa. Engine lùi lại (backtrack), cấp cho + bên trong ít ký tự hơn, và thử lại. Nó liên tục thử các cách chia khác nhau cho các chữ a giữa hai quantifier. Chỉ với 16 chữ a theo sau là một ký tự không khớp, engine đã phải khám phá 65,536 đường dẫn backtracking khả thi — theo cấp số nhân. Gấp đôi độ dài đầu vào, và số lượng đường dẫn sẽ tăng vọt.
Nếu bạn dùng một trình kiểm tra regex và cấp cho pattern này một chuỗi test gồm hơn 20 chữ a theo sau là một ký tự không khớp, bạn sẽ thấy tình trạng treo hoặc timeout theo thời gian thực. Cách khắc phục là tránh các quantifier lồng nhau. ^a+$ làm cùng một công việc mà không gặp bẫy. Đối với các pattern phức tạp hơn, giải pháp thực sự là sử dụng một engine như RE2 - loại bỏ hoàn toàn backtracking và đảm bảo thời gian tuyến tính.
Điều đáng nói là: các quantifier lồng nhau thường là tình cờ. Một pattern như (col|column)umn? có vẻ vô hại, nhưng nếu người dùng cung cấp đầu vào không khớp, engine có thể bị mắc kẹt khi thử các cách khác nhau để chia tách đầu vào giữa hai lựa chọn giống quantifier. Đó là lý do tại sao việc chấp nhận các pattern regex do người dùng cung cấp là nguy hiểm. Hãy luôn xác thực bằng RE2 hoặc hạn chế pattern trong một danh sách trắng (whitelist).
Đối với các pattern do chính bạn viết và test, rủi ro thấp hơn, nhưng nó vẫn rất thật nếu bạn chấp nhận các pattern regex từ các nguồn bên ngoài (tệp cấu hình, tham số API, đầu vào của người dùng). Hãy test chúng trong một trình kiểm tra regex với các trường hợp đặc biệt (edge cases) trước nhé.
Cái bẫy kinh điển — validate email khó hơn bạn tưởng
Xác thực email nghe có vẻ đơn giản: ^[a-zA-Z0-9.+]+@[a-zA-Z0-9]+\.[a-z]{2,}$. Nhưng sự thật không phải vậy. RFC 5322 (đặc tả của email) cho phép =, _và các ký tự khác ở phần cục bộ (local part) mà hầu hết các regex không bao gồm. Tinh tế hơn nữa, một quantifier tham lam (greedy) như.+sẽ khớp quá giới hạn. Tronguser.email@example.com, pattern ^[a-zA-Z0-9.+]+@sẽ tiêu thụ tất cả các ký tự cho đến dấu@` cuối cùng nếu có nhiều dấu, điều này là sai. Regex "chính xác" để xử lý mọi trường hợp của RFC 5322 thì dài ngoằng và rắc rối tới mức các chuyên gia cho rằng chẳng值得一 viết.
Giải pháp thực tế là: đừng cố validate email bằng regex. Hãy cứ nhận đầu vào, gửi một email xác nhận đến địa chỉ đó, và chốt lại xem người dùng có nhận được không. Đó là cách xác thực thực sự duy nhất. Nếu bạn chỉ muốn kiểm tra nhanh bằng regex để bắt các lỗi chính tả rõ ràng — như thiếu @, thiếu domain — hãy dùng một pattern lỏng lẻo như ^[^\s@]+@[^\s@]+$ (bất cứ thứ gì ngoại trừ khoảng trắng hoặc @ ở cả hai bên) và để bước xác nhận email lo phần còn lại. Hãy test một vài email thực tế (bao gồm cả first.last+tag@example.co.uk) trong một trình kiểm tra regex trước khi bạn quyết định một pattern là đủ tốt.
Các flavor regex khác biệt như thế nào — ma trận nhanh
| Engine / Flavor | Lookbehind | Group có tên | Backreferences | Unicode \p{} | Thời gian tuyến tính (An toàn ReDoS) |
|---|---|---|---|---|---|
| POSIX ERE | ✗ | ✗ | ✗ | ✗ | ✓ (đơn giản) |
Python 3 re | ✓ (cố định) | ✓ ((?P<name>)) | ✓ | Một phần (\w/\d hiểu Unicode; \p{} chỉ ở module regex) | ✗ |
| PCRE | ✓ | ✓ (?<name>) | ✓ | ✓ | ✗ |
| JavaScript | ✓ (chỉ cố định) | ✓ (?<name>) | ✓ | ✓ (với cờ u) | ✗ |
| RE2 / Go | ✗ | ✗ | ✗ | Hạn chế | ✓ (đảm bảo O(mn)) |
Điểm cốt lõi: tính năng nhiều hơn không có nghĩa là an toàn hay nhanh hơn. RE2 bị giới hạn có chủ đích để tránh ReDoS và đảm bảo thời gian tuyến tính. Python và JavaScript mang đến sự linh hoạt cho các pattern phức tạp. POSIX là mức độ thấp nhất (common denominator). Hãy chọn engine phù hợp với các ràng buộc của bạn — nếu tốc độ và sự an toàn quan trọng hơn tính năng, hãy chọn RE2 hoặc regexp của Go. Nếu bạn cần các tính năng, hãy chấp nhận rủi ro backtracking và xác thực các pattern trong trình kiểm tra trước.
Các pattern phổ biến — và lý do tại sao mỗi cái đều chưa trọn vẹn
Một bảng tham khảo nhanh về các pattern mà bạn sẽ dùng đến, với lời khuyên chân thành rằng mỗi cái đều không hoàn hảo và còn phụ thuộc vào trường hợp sử dụng cụ thể của bạn.
Số điện thoại (kiểu Mỹ): ^\d{3}-\d{3}-\d{4}$ khớp với 555-123-4567. Nhưng nó không xử lý phần mở rộng (ext), mã quốc gia, hay dấu cách. Và hãy nhớ rằng: \d là Unicode trong Python, là ASCII trong PCRE. Khéo thì nên phân tích cú pháp (parse) và xác thực riêng.
Ngày ISO (YYYY-MM-DD): ^\d{4}-\d{2}-\d{2}$ khớp với định dạng nhưng không kiểm tra xem ngày đó có hợp lệ không (có ngày 30 tháng 2 tồn tại không?). Hãy xác thực định dạng bằng regex, sau đó phân tích cú pháp và xác thực logic bằng code.
URL (đơn giản hóa): ^https?://[^\s]+$ cho phép bất kỳ ký tự nào sau tên miền ngoại trừ khoảng trắng. URL thực tế sẽ lằng nhằng hơn nhiều (query param, fragment, encoding). Hãy dùng thư viện parse URL nếu bạn cần độ chính xác cao.
Dòng CSV: ^[^,]*,[^,]*,[^,]*$ khớp với ba trường phân tách bằng dấu phẩy mà không xử lý các trường trong ngoặc kép hoặc dấu phẩy thoát (escaped commas). Regex sẽ không bao giờ xử lý CSV chính xác hoàn toàn; hãy dùng trình parse CSV.
Mỗi pattern ở trên đều chạy được cho trường hợp cụ thể của nó nhưng sẽ hỏng với các biến thể. Luôn chạy các chuỗi test thực tế (trường hợp đặc biệt, ký tự quốc tế, định dạng kỳ lạ) thông qua một trình kiểm tra regex trước khi bạn cam kết.
Các công cụ đi kèm sau khi bạn có pattern. Sau khi bạn chế tạo xong một pattern regex, bạn thường phải xử lý các chuỗi được khớp. Một bộ chuyển đổi chữ hoa thường sẽ lo việc chuyển đổi output khớp thành chữ hoa/thường theo ý bạn. Để debug hoặc log kết quả regex, một trình định dạng JSON sẽ làm đẹp các object khớp để bạn thấy rõ các capture group. Và nếu regex của bạn dùng để xác thực các thành phần URL, một bộ mã hóa URL sẽ kiểm tra xem pattern của bạn có hoạt động tốt với các ký tự an toàn cho URL hay không.
Vậy bạn nên thực sự chọn engine nào?
Ba quy tắc nhanh bao quát hầu hết các trường hợp. Khớp pattern từ những người dùng không đáng tin (một ô tìm kiếm, một trường API, tệp cấu hình do người khác chỉnh sửa)? Hãy dùng RE2 hoặc regexp của Go — đảm bảo thời gian tuyến tính đáng giá hơn việc đánh mất lookbehind. Viết một pattern cho chính code của bạn bằng Python, JavaScript, hoặc công cụ PCRE? Hãy dùng engine mà ngôn ngữ của bạn đã có sẵn; chỉ cần nhớ các khác biệt về chữ số viết tắt và lookbehind khi bạn sao chép một pattern giữa chúng. Thực hiện khớp nhanh trong công cụ shell như grep hoặc sed? Bạn đang dùng POSIX, vậy nên hãy bỏ qua lookaround và backreferences hoàn toàn đi.
Dù bạn chọn cái nào, hãy xây dựng pattern từng bước và xem nó chạy trước khi đưa nó đụng đến production.
Lời tóm lại
Tóm lại: Hãy xây dựng pattern từng bước một trong anytools regex tester, test chúng trên engine mà bạn thực sự định triển khai, và tìm hiểu các cạm bẫy của phiên bản đó. Tránh các quantifier lồng nhau, ưu tiên phép phủ định (
[^<]) hơn quantifier lazy, và không bao giờ xác thực email bằng một regex duy nhất — hãy nhận đầu vào, xác nhận nó riêng biệt, và để code kiểm tra logic. Đối với các pattern không đáng tin, hãy tìm đến RE2; còn đối với pattern của riêng bạn, hãy dán chúng vào trình kiểm tra với một vài trường hợp cận biên xấu xí trước khi bạn cam kết.





