Đến phần nội dung
Toova
Tất cả công cụ

10 Mẹo Regex Mọi Nhà Phát Triển Nên Biết

Toova

Biểu thức chính quy là một trong những công cụ ban đầu cảm thấy khó tiếp cận, rồi đột nhiên mọi thứ trở nên rõ ràng. Khi điều đó xảy ra, bạn bắt đầu thấy chúng ở khắp nơi: kiểm tra đầu vào, phân tích log, pipeline tìm kiếm và thay thế, định tuyến URL. Nhưng đa số nhà phát triển chỉ dùng một vài tính năng — lớp ký tự, bộ lượng từ, neo — và bỏ qua phần còn lại của đặc tả.

Hướng dẫn này bao quát mười tính năng regex vượt ra ngoài những điều cơ bản. Mỗi tính năng giải quyết một vấn đề thực tế mà các mẫu đơn giản hơn không thể xử lý gọn gàng. Tất cả ví dụ đều dùng cú pháp JavaScript, cũng hợp lệ cho mọi môi trường tương thích ECMAScript.

Bạn có thể kiểm tra mọi mẫu trong bài viết này bằng Toova Regex Tester mà không cần viết một dòng cấu hình nào.

1. Lookahead: Khớp Mà Không Tiêu Thụ

Lookahead khẳng định rằng một mẫu phải (hoặc không được) theo sau vị trí hiện tại, mà không khiến engine khớp di chuyển qua các ký tự đó. Văn bản được khớp không bao gồm phần mà lookahead kiểm tra.

Cú pháp lookahead dương: (?=...)

// Lookahead dương: khớp "foo" chỉ khi theo sau là "bar"
const re1 = /foo(?=bar)/;
re1.test('foobar'); // true
re1.test('foobaz'); // false

Cú pháp lookahead âm: (?!...)

// Lookahead âm: khớp "foo" KHÔNG theo sau bởi "bar"
const re2 = /foo(?!bar)/;
re2.test('foobaz'); // true
re2.test('foobar'); // false

Một ứng dụng thực tế: khớp một số giá chỉ khi nó được theo sau bởi ký hiệu tiền tệ, mà không gộp ký hiệu vào giá trị đã bắt. Hoặc kiểm tra rằng mật khẩu chứa ít nhất một chữ số bằng (?=.*\d) mà không cần chỉ định vị trí của chữ số.

Lookahead có độ rộng bằng không — không tiêu thụ ký tự nào. Bạn có thể xếp chồng nhiều lookahead ở cùng một vị trí để áp đặt nhiều điều kiện độc lập đồng thời.

2. Lookbehind: Kiểm Tra Những Gì Đến Trước

Lookbehind là tấm gương của lookahead: nó kiểm tra văn bản đứng trước vị trí hiện tại mà không gộp nó vào kết quả khớp.

Cú pháp lookbehind dương: (?<=...)

// Lookbehind dương: khớp "bar" chỉ khi đứng trước là "foo"
const re3 = /(?<=foo)bar/;
re3.test('foobar'); // true
re3.test('bazbar'); // false

Cú pháp lookbehind âm: (?<!...)

// Lookbehind âm: khớp "bar" KHÔNG đứng sau "foo"
const re4 = /(?<!foo)bar/;
re4.test('bazbar'); // true
re4.test('foobar'); // false

Lookbehind xuất hiện trong ECMAScript 2018 và được hỗ trợ trong mọi trình duyệt hiện đại cùng Node.js 10+. Một trường hợp sử dụng phổ biến: trích xuất phần giá trị của cặp khóa-giá trị như name=Alice bằng cách khớp mọi thứ sau name= mà không gộp khóa vào kết quả.

Lưu ý: không giống lookahead, biểu thức lookbehind trong JavaScript không thể chứa mẫu có độ dài biến đổi — biểu thức lookbehind phải có độ dài cố định hoặc tối đa giới hạn.

3. Nhóm Bắt Được Đặt Tên: Mẫu Tự Tài Liệu Hóa

Nhóm bắt tiêu chuẩn được tham chiếu bằng số: $1, $2, v.v. Khi bạn thêm hoặc xóa một nhóm, mọi tham chiếu phía sau sẽ vỡ. Nhóm được đặt tên giải quyết điều này bằng cách cho phép bạn gắn nhãn cho mỗi nhóm.

const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const m = '2026-05-10'.match(dateRe);
console.log(m.groups.year);  // "2026"
console.log(m.groups.month); // "05"
console.log(m.groups.day);   // "10"

Nhóm được đặt tên cũng có sẵn trong chuỗi thay thế qua $<tên>:

// Tham chiếu ngược dùng nhóm được đặt tên trong mẫu
const quoteRe = /(?<q>['"]).*?\k<q>/;
quoteRe.test('"hello"'); // true
quoteRe.test('"hello''); // false

Tên nhóm phải là định danh JavaScript hợp lệ. Sử dụng tên mô tả phản ánh điều nhóm bắt được — year, port, protocol — và các mẫu của bạn trở nên gần như tự tài liệu hóa. Bạn cũng có thể dùng \k<tên> bên trong chính mẫu để tham chiếu ngược một nhóm được đặt tên, như minh họa ở trên.

4. Bộ Lượng Từ Không Tham Lam: Khớp Mức Tối Thiểu

Theo mặc định, các bộ lượng từ (*, +, ?) là tham lam — chúng khớp càng nhiều ký tự càng tốt. Thêm một ? sau bộ lượng từ làm nó trở thành không tham lam (còn gọi là lười hay miễn cưỡng), khớp càng ít ký tự càng tốt.

const html = '<a>click</a>';

// Tham lam (mặc định) — khớp chuỗi dài nhất có thể
/<.+>/.exec(html)?.[0]; // '<a>click</a>'

// Không tham lam — khớp chuỗi ngắn nhất có thể
/<.+?>/.exec(html)?.[0]; // '<a>'

Điều này quan trọng khi phân tích HTML, XML, hoặc bất kỳ định dạng nào mà cùng một dấu phân cách có thể xuất hiện nhiều lần. Phiên bản tham lam nuốt mọi thứ từ thẻ mở đầu tiên đến thẻ đóng cuối cùng trong toàn bộ chuỗi. Phiên bản không tham lam dừng lại ở kết quả khớp đóng hợp lệ đầu tiên.

Điều tương tự áp dụng cho +? (một hoặc nhiều, lười) và ?? (không hoặc một, lười). Bộ lượng từ không tham lam không thay đổi cái có thể được khớp — chúng thay đổi kết quả khớp hợp lệ nào được chọn khi tồn tại nhiều tùy chọn.

5. Tránh Quay Lui Thảm Họa

Quay lui là cách engine regex phục hồi từ một lần khớp thất bại — chúng thử một đường khác qua mẫu. Trong đa số trường hợp điều này vô hình và nhanh chóng. Nhưng một số mẫu có thể khiến engine khám phá số lượng đường tăng theo cấp số nhân, làm gục tiến trình Node.js ngay cả với một chuỗi đầu vào khiêm tốn.

Mẫu nguy hiểm kinh điển là các bộ lượng từ lồng nhau như (a+)+ áp dụng cho chuỗi như aaaaab. Engine thử mọi cách có thể để chia các ký tự a giữa nhóm bên trong và bên ngoài trước khi kết luận rằng không có kết quả khớp.

Nhóm atomic ((?>...)) ngăn chặn điều này bằng cách yêu cầu engine không quay lui vào một nhóm khi nó đã khớp. JavaScript không hỗ trợ nguyên bản nhóm atomic, nhưng bạn có thể mô phỏng hành vi chiếm hữu bằng một lookahead:

// Không có nhóm atomic — engine quay lui vào (\d+)
// Có nhóm atomic — khi (\d+) đã khớp, không cho phép quay lui
// JavaScript không hỗ trợ nguyên bản nhóm atomic,
// nhưng bạn có thể mô phỏng bằng mẹo lookahead:
const re5 = /(?=(\d+))\1(?!\d)/; // mô phỏng possessive \d++

Quy tắc an toàn hơn: tránh các bộ lượng từ được lồng trực tiếp bên trong các bộ lượng từ khác trừ khi bạn có lý do cụ thể. Viết lại các mẫu để cụ thể hơn về cái chúng khớp. Bạn cũng có thể dùng công cụ Text Diff để so sánh đầu ra của hai mẫu tương đương cạnh nhau khi tái cấu trúc.

6. Phép Toán Tập Hợp Trên Lớp Ký Tự (Cờ Unicode v)

ECMAScript 2024 đã giới thiệu cờ v, kích hoạt các phép toán tập hợp bên trong lớp ký tự. Điều này cho phép bạn biểu đạt "tất cả các chữ cái ngoại trừ nguyên âm" hoặc "chữ cái in hoa cũng thuộc ASCII" dưới dạng định nghĩa lớp sạch sẽ thay vì một chuỗi thay thế cồng kềnh.

// Phép trừ lớp ký tự POSIX không có trong JS,
// nhưng chế độ Unicode sets (cờ `v`) thêm các phép toán tập hợp:
const lettersNoVowels = /[a-z--[aeiou]]/v;
lettersNoVowels.test('b'); // true
lettersNoVowels.test('e'); // false

Cờ v hỗ trợ ba phép toán bên trong lớp ký tự:

  • Phép trừ: [A--B] — các ký tự trong A nhưng không trong B
  • Phép giao: [A&&B] — các ký tự trong cả A và B
  • Phép hợp: [AB] — các ký tự trong A hoặc B (giống như lớp ký tự tiêu chuẩn)

Node.js 20+ và mọi trình duyệt evergreen đều hỗ trợ cờ v. Đây là tập cha của cờ u — đừng kết hợp cả hai; chỉ dùng v khi bạn cần các tính năng của nó.

7. Ranh Giới Từ: Khớp Từ Nguyên

Neo \b khớp vị trí giữa một ký tự từ (\w) và một ký tự không phải từ. Nó không tiêu thụ ký tự — nó chỉ khẳng định vị trí. Ngược lại \B khớp bất kỳ vị trí nào không phải là ranh giới từ.

const sentence = 'cat concatenate';

// Không có \b — "cat" cũng được tìm thấy bên trong "concatenate"
/cat/g.exec(sentence); // khớp "cat" trong "cat" VÀ trong "concatenate"

// Có \b — chỉ khớp từ nguyên "cat"
/\bcat\b/g.exec(sentence); // chỉ khớp "cat" đứng độc lập
// \B là ngược lại: khớp bên trong một từ, không ở ranh giới
/\Bcat\B/.test('concatenate'); // true — "cat" nằm bên trong từ

Ranh giới từ rất cần thiết khi tìm kiếm định danh trong code hoặc văn bản. Không có chúng, việc tìm kiếm một biến tên id cũng sẽ tìm thấy indexOf, invalidgrid. Sử dụng \bthuật_ngữ\b để giới hạn kết quả khớp ở những lần xuất hiện độc lập.

Một lưu ý quan trọng: \b sử dụng định nghĩa ký tự từ của JavaScript ([a-zA-Z0-9_]). Các ký tự có dấu và chữ cái không thuộc Latin được xem là ký tự không phải từ. Để có ranh giới từ nhận biết Unicode, kết hợp cờ v với các lớp thuộc tính Unicode.

8. Chế Độ Đa Dòng: Neo Trên Mỗi Dòng

Theo mặc định, ^ chỉ khớp đầu tuyệt đối của chuỗi và $ chỉ khớp cuối tuyệt đối. Cờ m (đa dòng) thay đổi điều này: ^ khớp đầu của mỗi dòng và $ khớp cuối của mỗi dòng.

const text = 'dòng một\ndòng hai\ndòng ba';

// Không có cờ m — ^ chỉ khớp đầu của toàn bộ chuỗi
/^dòng/.test(text); // true (chỉ dòng đầu tiên)

// Với cờ m — ^ khớp đầu của MỖI dòng
const matches = text.match(/^dòng/gm);
console.log(matches); // ['dòng', 'dòng', 'dòng']

Điều này không thể thiếu khi xử lý văn bản nhiều dòng như tệp log, tệp cấu hình hoặc code. Các cách dùng phổ biến gồm trích xuất các dòng bắt đầu bằng từ khóa, thay thế token cuối dòng, hoặc xác thực rằng mỗi dòng trong một khối khớp với một mẫu.

Đừng nhầm cờ m với cờ s (dotAll). Cờ s cũng khiến . khớp ký tự xuống dòng. Cờ m hoàn toàn không ảnh hưởng đến . — chỉ ảnh hưởng đến hành vi của ^$.

9. Escape Thuộc Tính Unicode: Khớp Ký Tự Quốc Tế

Cờ u kích hoạt escape thuộc tính Unicode, cho phép bạn khớp ký tự dựa trên danh mục Unicode, bảng chữ cái hoặc thuộc tính khác. Đây là cách đúng đắn để khớp chữ cái, chữ số hoặc dấu câu trên mọi hệ thống viết của con người — không chỉ ASCII.

// Cờ u kích hoạt escape thuộc tính Unicode
const letters = /\p{L}+/u;
letters.test('Héllo');   // true
letters.test('你好');    // true
letters.test('12345');   // false

// Chỉ khớp chữ in hoa trên mọi bảng chữ cái
const upper = /\p{Lu}+/u;
upper.test('ABC');  // true
upper.test('abc');  // false
// Khớp emoji (danh mục Unicode chung: Symbol, Other)
const emoji = /\p{So}/u;
emoji.test('🚀'); // true

Các thuộc tính Unicode được sử dụng phổ biến nhất là:

  • \p{L} — bất kỳ chữ cái nào (mọi bảng chữ cái)
  • \p{Lu} — chữ in hoa
  • \p{Ll} — chữ thường
  • \p{N} — bất kỳ số nào
  • \p{Nd} — chữ số thập phân
  • \p{P} — dấu câu
  • \p{Script=Latin} — ký tự bảng chữ cái Latin
  • \p{Emoji} — ký tự emoji

Sử dụng \P{...} (P hoa) để phủ định — khớp mọi thứ không có thuộc tính được chỉ định. Danh sách đầy đủ các thuộc tính Unicode được hỗ trợ và giá trị của chúng được duy trì trong tài liệu MDN.

10. Mẫu Verbose Qua Lắp Ghép Chuỗi

Nhiều biến thể regex (Python, Ruby, .NET, PCRE) hỗ trợ chế độ verbose hoặc mở rộng (cờ x) cho phép khoảng trắng và bình luận bên trong mẫu. JavaScript không có cờ này — cờ x không hợp lệ trong ECMAScript.

Giải pháp tiêu chuẩn là lắp ghép các mẫu từ hằng chuỗi được đặt tên và kết hợp chúng với new RegExp():

// JavaScript không có cờ x nguyên bản,
// nhưng bạn có thể xây dựng các mẫu dễ đọc dưới dạng hằng chuỗi:
const YEAR  = '(?<year>\\d{4})';
const SEP   = '-';
const MONTH = '(?<month>\\d{2})';
const DAY   = '(?<day>\\d{2})';
const datePattern = new RegExp(YEAR + SEP + MONTH + SEP + DAY);

Mỗi hằng mô tả cái nó khớp, và mẫu cuối cùng đọc như một câu. Cách tiếp cận này cũng giúp dễ dàng kết hợp các mẫu con dùng chung trên nhiều định nghĩa regex trong codebase, và kiểm thử đơn vị từng thành phần một cách độc lập.

Đối với các mẫu ít phức tạp hơn, giữ toàn bộ regex trên một dòng với bình luận inline trong một khối bình luận gần đó thường đủ. Mục tiêu là đảm bảo nhà phát triển tiếp theo (bao gồm cả bạn trong tương lai) có thể hiểu mẫu mà không cần chạy nó qua bộ giải mã.

Tổng Hợp Tất Cả

Mười kỹ thuật này bao quát một phần lớn bề mặt "tại sao regex này thất bại ở trường hợp biên?". Lookahead và lookbehind cho phép bạn khẳng định ngữ cảnh mà không tiêu thụ nó. Nhóm được đặt tên giữ các mẫu dễ đọc khi tái cấu trúc. Bộ lượng từ không tham lam ngăn ngừa việc khớp dư thừa ngẫu nhiên. Escape thuộc tính Unicode xử lý đầu vào vượt ra ngoài ASCII.

Cách tốt nhất để thành thạo là thử nghiệm với các mẫu thực tế trên dữ liệu thực tế. Sử dụng Regex Tester để lặp lại nhanh chóng. Khi mẫu của bạn tạo ra đầu ra cần so sánh hoặc dọn dẹp, công cụ Text Diff cho thấy chính xác điều gì đã thay đổi giữa các lần chạy. Và nếu regex của bạn đang phân tích JSON, JSON Formatter cho phép bạn kiểm tra kết quả có cấu trúc mà không rời trình duyệt.

Để có tham khảo toàn diện về mọi cú pháp và cờ regex JavaScript, MDN Regex Cheatsheet là trang đơn tốt nhất để lưu vào dấu trang. Để gỡ lỗi mẫu tương tác với khả năng hiển thị toàn bộ kết quả khớp, regex101.com hỗ trợ chế độ JavaScript với giải thích tích hợp cho mọi thành phần trong một mẫu.