Regex sâu: bên trong biểu thức chính quy
Sê-ri kỹ thuật về regex cho lập trình viên: máy trạng thái, backtracking, ReDoS, RE2 — mỗi bài đo output thật.
12/12 phần đã đăng
Lập trình
1
Regex thật ra là một cỗ máy trạng thái: engine khớp chuỗi thế nào
Nhiều người dùng regex như phép thuật mà không biết bên trong nó là gì. Thực ra engine làm hai việc: biên dịch mẫu thành một máy trạng thái, rồi chạy máy đó trên input. Bài này mở máy ra xem: python re.DEBUG in opcode thật của mẫu a(b|c)*d, và Go regexp (RE2) chứng minh nó duyệt input tuyến tính — input tăng gấp 5 thì thời gian cũng tăng gấp 5, ns mỗi ký tự gần như hằng số.
22/09/2026
· 6 phút đọc
2
Tham lam vs lười trong regex: vì sao .* nuốt cả chuỗi còn .*? thì không
Cùng một mẫu, chỉ thêm một dấu ?, mà kết quả khác hẳn — và tốc độ cũng vậy. Bài này đo thật bằng python re: greedy .* nuốt cả '<b>xin</b> và <i>bạn</i>' thành một match, lazy .*? tách đúng bốn thẻ; và trên input 200k ký tự, greedy chậm hơn lazy 785 lần vì phải nhả dần (backtrack). Hiểu cơ chế để chọn đúng và tránh lỗi trích sai.
22/09/2026
· 5 phút đọc
3
Neo và ranh giới trong regex: khớp thứ không phải là ký tự nào
^ $ \b \B không khớp ký tự nào, nhưng chúng quyết định mẫu của bạn đúng hay sai chỗ. Bài này đo thật: 'cat' khớp 4 chỗ (kể cả trong scatter, category), thêm \b thì chỉ còn 2 từ độc lập; và ^ $ đổi hẳn nghĩa khi bật cờ MULTILINE — từ 'đầu/cuối cả chuỗi' thành 'đầu/cuối mỗi dòng'. Hiểu anchor zero-width để tìm-thay-thế không dính nhầm.
22/09/2026
· 6 phút đọc
4
Lớp ký tự và quantifier: hai viên gạch nền, và cái bẫy \w không phải [a-zA-Z]
Lớp ký tự [...] và quantifier {m,n} là nền của mọi regex, nhưng chúng giấu vài cái bẫy. Bài này đo thật: trên 'Hoà', \w khớp cả chữ à (Unicode) còn [a-zA-Z] thì đứt ở à thành 'Ho'; và trên '1234567', {2,4} greedy lấy 4 trước rồi 3, còn {2,4}? lazy lấy 2 mỗi lần. Xem cả opcode MAX_REPEAT + IN RANGE mà engine biên dịch ra.
22/09/2026
· 5 phút đọc
5
Backreference: viên gạch khiến regex "không còn regular" và buộc phải backtracking
Chỉ một ký hiệu \1 mà đổi cả bản chất lý thuyết của regex. Bài này đo thật: python dùng (\w+)\s+\1 bắt đúng từ lặp 'the the', <(\w+)>...</\1> khớp thẻ đóng đúng thẻ mở; nhưng Go RE2 từ chối biên dịch \1 với lỗi 'invalid escape sequence'. Vì sao khả năng 'nhớ cái đã khớp' vượt khỏi máy trạng thái hữu hạn, buộc engine phải backtracking — và vì sao RE2 cố tình không hỗ trợ.
22/09/2026
· 6 phút đọc
6
ReDoS: vì sao một regex 6 ký tự có thể treo cả server của bạn
Mẫu (a+)+$ trông vô hại, nhưng gặp đúng input độc nó chạy chậm theo cấp số nhân. Bài này đo thật: với chuỗi 'aaaa...!', python re mất 9 giây chỉ với 28 ký tự, và thời gian gấp đôi mỗi khi thêm một 'a' — n=40 sẽ mất hàng ngàn năm. Cùng mẫu đó trên Go RE2 chỉ mất 3 micro-giây, kể cả với 100.000 ký tự. Đây là ReDoS, và cách phòng nó.
22/09/2026
· 5 phút đọc
7
RE2 và Go regexp: cỗ máy đánh đổi tính năng để không bao giờ nổ
RE2 (Go, Rust, Google) chạy tuyến tính bằng cách mô phỏng mọi trạng thái cùng lúc thay vì thử-và-quay-lui. Bài này đo thật: cùng mẫu độc (a+)+b, RE2 xử lý 20 triệu ký tự trong 443ms với ns/ký tự hằng số ~22 — trong khi python treo ở 28 ký tự. Cái giá: RE2 từ chối lookahead, lookbehind và backreference, mỗi cái báo một lỗi khác nhau. Khi nào nên chọn nó.
22/09/2026
· 6 phút đọc
8
Lookahead và lookbehind: nhìn quanh mà không nuốt, và cái giá của nó
Lookaround cho regex khả năng khẳng định điều kiện về ngữ cảnh mà không tiêu thụ ký tự. Bài này đo thật bằng python: chèn dấu phẩy nghìn '1234567' thành '1,234,567' bằng (?<=\d)(?=(\d{3})+$), kiểm mật khẩu mạnh bằng ba lookahead cùng vị trí, lấy số sau $ bằng (?<=\$)\d+, và bỏ 'cats' bằng cat(?!s). Vì sao những phép này mạnh — và vì sao RE2 từ chối chúng.
22/09/2026
· 6 phút đọc
9
Unicode trong regex: vì sao \w cắt cụt chữ tiếng Việt và 'à' đôi khi không khớp
Regex và tiếng Việt là mỏ bug ít người lường trước. Bài này đo thật: python \w bắt 'chào' nguyên vẹn nhưng [a-zA-Z] cắt thành 'ch','o'; Go \w lại chỉ ASCII nên phải dùng \p{L}. Chữ 'à' có hai cách mã hóa (NFC 1 codepoint, NFD 2 codepoint) trông giống hệt mà regex khớp khác nhau. Và . khớp 1 rune chứ không phải 1 byte. Cách xử lý cho đúng.
22/09/2026
· 6 phút đọc
10
Nhóm bắt và thay thế: dùng regex để biến đổi chuỗi, không chỉ để tìm
Regex không chỉ tìm — nó trích và dựng lại. Bài này đo thật: nhóm (...) cắt '2026-09-22' thành ba phần, nhóm đặt tên (?P<nam>) đọc bằng tên, re.sub đổi 'yyyy-mm-dd' thành 'dd/mm/yyyy' bằng \3/\2/\1, sed đổi 'Nguyen Van A' thành 'Van A, Nguyen'. Và vì sao (?:...) không được đếm lại quan trọng: thêm một cặp () thừa làm \1 trong thay thế trỏ sai.
22/09/2026
· 6 phút đọc
11
Tối ưu regex: bốn mẹo đo được, và một mẹo biến 2 giây thành 0.33 micro-giây
Regex chậm hiếm khi vì máy yếu — thường vì mẫu viết chưa khéo. Bài này đo thật bốn kỹ thuật: biên dịch một lần (nhanh 1.84x), lớp phủ định thay lazy (chênh nhỏ nhưng an toàn hơn), atomic/possessive chặn backtracking (2222ms xuống 0.33µs), và neo để loại sớm (68ms xuống 0.21µs, nhanh hơn 329 nghìn lần). Kèm nguyên tắc quan trọng nhất: đo trước khi tối ưu.
22/09/2026
· 6 phút đọc
12
Regex thực chiến: phân tích log bằng grep -P, ripgrep và awk — và khi nào nên dừng
Regex sáng nhất khi mổ log. Bài này đo thật trên 500 dòng access.log: grep -oP trích IP và status code, pipe qua sort|uniq -c xếp hạng (200 chiếm 359, 404 chiếm 75), ripgrep lọc request chậm và POST lỗi, awk tính latency trung bình 405ms. Và ripgrep quét 40MB nhanh gấp đôi grep. Kèm nguyên tắc quan trọng nhất và tổng kết cả loạt 12 phần.
22/09/2026
· 6 phút đọc