Khám phá kỹ thuật đối sánh mẫu nâng cao trong JavaScript bằng biểu thức chính quy. Tìm hiểu cú pháp regex, ứng dụng thực tế và các kỹ thuật tối ưu hóa để viết mã hiệu quả và mạnh mẽ.
Đối Sánh Mẫu trong JavaScript với Biểu Thức Chính Quy: Hướng Dẫn Toàn Diện
Biểu thức chính quy (regex) là một công cụ mạnh mẽ cho việc đối sánh mẫu và xử lý văn bản trong JavaScript. Chúng cho phép các nhà phát triển tìm kiếm, xác thực và biến đổi chuỗi dựa trên các mẫu đã xác định. Hướng dẫn này cung cấp một cái nhìn tổng quan toàn diện về biểu thức chính quy trong JavaScript, bao gồm cú pháp, cách sử dụng và các kỹ thuật nâng cao.
Biểu Thức Chính Quy là gì?
Biểu thức chính quy là một chuỗi các ký tự xác định một mẫu tìm kiếm. Các mẫu này được sử dụng để đối sánh và xử lý chuỗi. Biểu thức chính quy được sử dụng rộng rãi trong lập trình cho các tác vụ như:
- Xác thực dữ liệu: Đảm bảo đầu vào của người dùng tuân thủ các định dạng cụ thể (ví dụ: địa chỉ email, số điện thoại).
- Trích xuất dữ liệu: Lấy thông tin cụ thể từ văn bản (ví dụ: trích xuất ngày tháng, URL hoặc giá cả).
- Tìm kiếm và Thay thế: Tìm và thay thế văn bản dựa trên các mẫu phức tạp.
- Xử lý Văn bản: Tách, nối hoặc biến đổi chuỗi dựa trên các quy tắc đã xác định.
Tạo Biểu Thức Chính Quy trong JavaScript
Trong JavaScript, biểu thức chính quy có thể được tạo theo hai cách:
- Sử dụng Biểu thức Chính quy Dạng chữ (Literal): Đặt mẫu trong dấu gạch chéo (
/). - Sử dụng Constructor
RegExp: Tạo một đối tượngRegExpvới mẫu là một chuỗi.
Ví dụ:
// Sử dụng biểu thức chính quy dạng chữ
const regexLiteral = /hello/;
// Sử dụng constructor RegExp
const regexConstructor = new RegExp("hello");
Sự lựa chọn giữa hai phương pháp phụ thuộc vào việc mẫu đã được biết tại thời điểm biên dịch hay được tạo động. Sử dụng ký pháp dạng chữ khi mẫu là cố định và đã biết trước. Sử dụng constructor khi mẫu cần được xây dựng theo chương trình, đặc biệt là khi kết hợp các biến.
Cú pháp Regex Cơ bản
Biểu thức chính quy bao gồm các ký tự đại diện cho mẫu cần được đối sánh. Dưới đây là một số thành phần regex cơ bản:
- Ký tự cố định (Literal Characters): Đối sánh chính các ký tự đó (ví dụ:
/a/đối sánh ký tự 'a'). - Siêu ký tự (Metacharacters): Có ý nghĩa đặc biệt (ví dụ:
.,^,$,*,+,?,[],{},(),\,|). - Lớp ký tự (Character Classes): Đại diện cho một tập hợp các ký tự (ví dụ:
[abc]đối sánh 'a', 'b', hoặc 'c'). - Lượng từ (Quantifiers): Chỉ định số lần một ký tự hoặc một nhóm nên xuất hiện (ví dụ:
*,+,?,{n},{n,},{n,m}). - Neo (Anchors): Đối sánh các vị trí trong chuỗi (ví dụ:
^đối sánh đầu chuỗi,$đối sánh cuối chuỗi).
Các Siêu ký tự Phổ biến:
.(dấu chấm): Đối sánh bất kỳ ký tự đơn nào ngoại trừ ký tự xuống dòng mới.^(dấu mũ): Đối sánh đầu chuỗi.$(dấu đô la): Đối sánh cuối chuỗi.*(dấu hoa thị): Đối sánh không hoặc nhiều lần xuất hiện của ký tự hoặc nhóm đứng trước nó.+(dấu cộng): Đối sánh một hoặc nhiều lần xuất hiện của ký tự hoặc nhóm đứng trước nó.?(dấu hỏi): Đối sánh không hoặc một lần xuất hiện của ký tự hoặc nhóm đứng trước nó. Được sử dụng cho các ký tự tùy chọn.[](dấu ngoặc vuông): Định nghĩa một lớp ký tự, đối sánh bất kỳ ký tự đơn nào bên trong dấu ngoặc.{}(dấu ngoặc nhọn): Chỉ định số lần xuất hiện cần đối sánh.{n}đối sánh chính xác n lần,{n,}đối sánh n hoặc nhiều lần hơn,{n,m}đối sánh từ n đến m lần.()(dấu ngoặc đơn): Nhóm các ký tự lại với nhau và bắt giữ chuỗi con được đối sánh.\(dấu gạch chéo ngược): Thoát các siêu ký tự, cho phép bạn đối sánh chúng theo nghĩa đen.|(dấu gạch đứng): Hoạt động như một toán tử "hoặc", đối sánh biểu thức trước hoặc sau nó.
Lớp ký tự:
[abc]: Đối sánh bất kỳ ký tự nào trong số a, b, hoặc c.[^abc]: Đối sánh bất kỳ ký tự nào *không phải* là a, b, hoặc c.[a-z]: Đối sánh bất kỳ chữ cái thường nào từ a đến z.[A-Z]: Đối sánh bất kỳ chữ cái hoa nào từ A đến Z.[0-9]: Đối sánh bất kỳ chữ số nào từ 0 đến 9.[a-zA-Z0-9]: Đối sánh bất kỳ ký tự chữ và số nào.\d: Đối sánh bất kỳ chữ số nào (tương đương với[0-9]).\D: Đối sánh bất kỳ ký tự nào không phải là chữ số (tương đương với[^0-9]).\w: Đối sánh bất kỳ ký tự từ nào (chữ và số cộng với dấu gạch dưới; tương đương với[a-zA-Z0-9_]).\W: Đối sánh bất kỳ ký tự nào không phải là ký tự từ (tương đương với[^a-zA-Z0-9_]).\s: Đối sánh bất kỳ ký tự khoảng trắng nào (dấu cách, tab, xuống dòng, v.v.).\S: Đối sánh bất kỳ ký tự nào không phải là khoảng trắng.
Lượng từ:
*: Đối sánh phần tử đứng trước nó không hoặc nhiều lần. Ví dụ,a*đối sánh "", "a", "aa", "aaa", và cứ thế.+: Đối sánh phần tử đứng trước nó một hoặc nhiều lần. Ví dụ,a+đối sánh "a", "aa", "aaa", nhưng không đối sánh "".?: Đối sánh phần tử đứng trước nó không hoặc một lần. Ví dụ,a?đối sánh "" hoặc "a".{n}: Đối sánh phần tử đứng trước nó chính xác *n* lần. Ví dụ,a{3}đối sánh "aaa".{n,}: Đối sánh phần tử đứng trước nó *n* hoặc nhiều lần hơn. Ví dụ,a{2,}đối sánh "aa", "aaa", "aaaa", và cứ thế.{n,m}: Đối sánh phần tử đứng trước nó từ *n* đến *m* lần (bao gồm cả hai). Ví dụ,a{2,4}đối sánh "aa", "aaa", hoặc "aaaa".
Neo:
^: Đối sánh đầu chuỗi. Ví dụ,^Hellođối sánh các chuỗi *bắt đầu* bằng "Hello".$: Đối sánh cuối chuỗi. Ví dụ,World$đối sánh các chuỗi *kết thúc* bằng "World".\b: Đối sánh một ranh giới từ. Đây là vị trí giữa một ký tự từ (\w) và một ký tự không phải từ (\W) hoặc đầu hoặc cuối chuỗi. Ví dụ,\bword\bđối sánh toàn bộ từ "word".
Cờ (Flags):
Cờ regex sửa đổi hành vi của các biểu thức chính quy. Chúng được thêm vào cuối của biểu thức chính quy dạng chữ hoặc được truyền dưới dạng đối số thứ hai cho constructor RegExp.
g(global - toàn cục): Đối sánh tất cả các lần xuất hiện của mẫu, không chỉ lần đầu tiên.i(ignore case - bỏ qua chữ hoa/thường): Thực hiện đối sánh không phân biệt chữ hoa/thường.m(multiline - đa dòng): Bật chế độ đa dòng, trong đó^và$đối sánh đầu và cuối của mỗi dòng (được phân tách bằng\n).s(dotAll): Cho phép dấu chấm (.) đối sánh cả các ký tự xuống dòng mới.u(unicode): Bật hỗ trợ Unicode đầy đủ.y(sticky - dính): Chỉ đối sánh từ chỉ mục được chỉ định bởi thuộc tínhlastIndexcủa regex.
Các Phương thức Regex trong JavaScript
JavaScript cung cấp một số phương thức để làm việc với biểu thức chính quy:
test(): Kiểm tra xem một chuỗi có khớp với mẫu không. Trả vềtruehoặcfalse.exec(): Thực thi tìm kiếm một kết quả khớp trong một chuỗi. Trả về một mảng chứa văn bản khớp và các nhóm đã bắt giữ, hoặcnullnếu không tìm thấy kết quả khớp.match(): Trả về một mảng chứa kết quả của việc đối sánh một chuỗi với một biểu thức chính quy. Hoạt động khác nhau khi có và không có cờg.search(): Kiểm tra một kết quả khớp trong một chuỗi. Trả về chỉ mục của kết quả khớp đầu tiên, hoặc -1 nếu không tìm thấy.replace(): Thay thế các lần xuất hiện của một mẫu bằng một chuỗi thay thế hoặc một hàm trả về chuỗi thay thế.split(): Tách một chuỗi thành một mảng các chuỗi con dựa trên một biểu thức chính quy.
Ví dụ Sử dụng các Phương thức Regex:
// test()
const regex = /hello/;
const str = "hello world";
console.log(regex.test(str)); // Output: true
// exec()
const regex2 = /hello (\w+)/;
const str2 = "hello world";
const result = regex2.exec(str2);
console.log(result); // Output: ["hello world", "world", index: 0, input: "hello world", groups: undefined]
// match() với cờ 'g'
const regex3 = /\d+/g; // Đối sánh một hoặc nhiều chữ số trên toàn cục
const str3 = "There are 123 apples and 456 oranges.";
const matches = str3.match(regex3);
console.log(matches); // Output: ["123", "456"]
// match() không có cờ 'g'
const regex4 = /\d+/;
const str4 = "There are 123 apples and 456 oranges.";
const match = str4.match(regex4);
console.log(match); // Output: ["123", index: 11, input: "There are 123 apples and 456 oranges.", groups: undefined]
// search()
const regex5 = /world/;
const str5 = "hello world";
console.log(str5.search(regex5)); // Output: 6
// replace()
const regex6 = /world/;
const str6 = "hello world";
const newStr = str6.replace(regex6, "JavaScript");
console.log(newStr); // Output: hello JavaScript
// replace() với một hàm
const regex7 = /(\d+)-(\d+)-(\d+)/;
const str7 = "Today's date is 2023-10-27";
const newStr2 = str7.replace(regex7, (match, year, month, day) => {
return `${day}/${month}/${year}`;
});
console.log(newStr2); // Output: Today's date is 27/10/2023
// split()
const regex8 = /, /;
const str8 = "apple, banana, cherry";
const arr = str8.split(regex8);
console.log(arr); // Output: ["apple", "banana", "cherry"]
Các Kỹ thuật Regex Nâng cao
Nhóm Bắt giữ (Capturing Groups):
Dấu ngoặc đơn () được sử dụng để tạo các nhóm bắt giữ trong biểu thức chính quy. Các nhóm bắt giữ cho phép bạn trích xuất các phần cụ thể của văn bản đã khớp. Các phương thức exec() và match() trả về một mảng trong đó phần tử đầu tiên là toàn bộ kết quả khớp, và các phần tử tiếp theo là các nhóm đã bắt giữ.
const regex = /(\d{4})-(\d{2})-(\d{2})/;
const dateString = "2023-10-27";
const match = regex.exec(dateString);
console.log(match[0]); // Output: 2023-10-27 (Toàn bộ kết quả khớp)
console.log(match[1]); // Output: 2023 (Nhóm bắt giữ đầu tiên - năm)
console.log(match[2]); // Output: 10 (Nhóm bắt giữ thứ hai - tháng)
console.log(match[3]); // Output: 27 (Nhóm bắt giữ thứ ba - ngày)
Nhóm Bắt giữ được Đặt tên:
ES2018 đã giới thiệu các nhóm bắt giữ được đặt tên, cho phép bạn gán tên cho các nhóm bắt giữ bằng cú pháp (?. Điều này làm cho mã dễ đọc và dễ bảo trì hơn.
const regex = /(?\d{4})-(?\d{2})-(?\d{2})/;
const dateString = "2023-10-27";
const match = regex.exec(dateString);
console.log(match.groups.year); // Output: 2023
console.log(match.groups.month); // Output: 10
console.log(match.groups.day); // Output: 27
Nhóm Không Bắt giữ:
Nếu bạn cần nhóm các phần của regex mà không bắt giữ chúng (ví dụ, để áp dụng một lượng từ cho một nhóm), bạn có thể sử dụng một nhóm không bắt giữ với cú pháp (?:...). Điều này tránh việc cấp phát bộ nhớ không cần thiết cho các nhóm bắt giữ.
const regex = /(?:https?:\/\/)?([\w\.]+)/; // Đối sánh một URL nhưng chỉ bắt giữ tên miền
const url = "https://www.example.com/path";
const match = regex.exec(url);
console.log(match[1]); // Output: www.example.com
Đối sánh Lân cận (Lookarounds):
Lookarounds là các khẳng định không chiếm độ rộng (zero-width assertions) đối sánh một vị trí trong chuỗi dựa trên một mẫu đứng trước (lookbehind) hoặc đứng sau (lookahead) vị trí đó, mà không bao gồm mẫu lookaround trong chính kết quả khớp.
- Đối sánh Nhìn trước Dương (Positive Lookahead):
(?=...)Đối sánh nếu mẫu bên trong lookahead *theo sau* vị trí hiện tại. - Đối sánh Nhìn trước Âm (Negative Lookahead):
(?!...)Đối sánh nếu mẫu bên trong lookahead *không theo sau* vị trí hiện tại. - Đối sánh Nhìn sau Dương (Positive Lookbehind):
(?<=...)Đối sánh nếu mẫu bên trong lookbehind *đứng trước* vị trí hiện tại. - Đối sánh Nhìn sau Âm (Negative Lookbehind):
(? Đối sánh nếu mẫu bên trong lookbehind *không đứng trước* vị trí hiện tại.
Ví dụ:
// Positive Lookahead: Lấy giá chỉ khi theo sau là USD
const regex = /\d+(?= USD)/;
const text = "The price is 100 USD";
const match = text.match(regex);
console.log(match); // Output: ["100"]
// Negative Lookahead: Lấy từ chỉ khi không theo sau bởi một số
const regex2 = /\b\w+\b(?! \d)/;
const text2 = "apple 123 banana orange 456";
const matches = text2.match(regex2);
console.log(matches); // Output: null vì match() chỉ trả về kết quả khớp đầu tiên không có cờ 'g', không phải cái chúng ta cần.
// để sửa nó:
const regex3 = /\b\w+\b(?! \d)/g;
const text3 = "apple 123 banana orange 456";
const matches3 = text3.match(regex3);
console.log(matches3); // Output: [ 'banana' ]
// Positive Lookbehind: Lấy giá trị chỉ khi đứng trước là $
const regex4 = /(?<=\$)\d+/;
const text4 = "The price is $200";
const match4 = text4.match(regex4);
console.log(match4); // Output: ["200"]
// Negative Lookbehind: Lấy từ chỉ khi không đứng trước bởi từ 'not'
const regex5 = /(?
Tham chiếu Ngược (Backreferences):
Tham chiếu ngược cho phép bạn tham chiếu đến các nhóm đã bắt giữ trước đó trong cùng một biểu thức chính quy. Chúng sử dụng cú pháp \1, \2, v.v., trong đó số tương ứng với số thứ tự của nhóm bắt giữ.
const regex = /([a-z]+) \1/;
const text = "hello hello world";
const match = regex.exec(text);
console.log(match); // Output: ["hello hello", "hello", index: 0, input: "hello hello world", groups: undefined]
Ứng dụng Thực tế của Biểu thức Chính quy
Xác thực Địa chỉ Email:
Một trường hợp sử dụng phổ biến cho biểu thức chính quy là xác thực địa chỉ email. Mặc dù một regex xác thực email hoàn hảo là cực kỳ phức tạp, đây là một ví dụ đơn giản hóa:
const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
console.log(emailRegex.test("test@example.com")); // Output: true
console.log(emailRegex.test("invalid-email")); // Output: false
console.log(emailRegex.test("test@sub.example.co.uk")); // Output: true
Trích xuất URL từ Văn bản:
Bạn có thể sử dụng biểu thức chính quy để trích xuất URL từ một khối văn bản:
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
const text = "Visit our website at https://www.example.com or check out http://blog.example.org.";
const urls = text.match(urlRegex);
console.log(urls); // Output: ["https://www.example.com", "http://blog.example.org"]
Phân tích Dữ liệu CSV:
Biểu thức chính quy có thể được sử dụng để phân tích dữ liệu CSV (Comma-Separated Values). Đây là một ví dụ về việc tách một chuỗi CSV thành một mảng các giá trị, xử lý các trường có dấu ngoặc kép:
const csvString = 'John,Doe,"123, Main St",New York';
const csvRegex = /(?:"([^"]*(?:""[^"]*)*)")|([^,]+)/g; //Corrected CSV regex
let values = [];
let match;
while (match = csvRegex.exec(csvString)) {
values.push(match[1] ? match[1].replace(/""/g, '"') : match[2]);
}
console.log(values); // Output: ["John", "Doe", "123, Main St", "New York"]
Xác thực Số điện thoại Quốc tế
Xác thực số điện thoại quốc tế rất phức tạp do các định dạng và độ dài khác nhau. Một giải pháp mạnh mẽ thường liên quan đến việc sử dụng thư viện, nhưng một regex đơn giản có thể cung cấp xác thực cơ bản:
const phoneRegex = /^\+(?:[0-9] ?){6,14}[0-9]$/;
console.log(phoneRegex.test("+1 555 123 4567")); // Output: true (Ví dụ Mỹ)
console.log(phoneRegex.test("+44 20 7946 0500")); // Output: true (Ví dụ Anh)
console.log(phoneRegex.test("+81 3 3224 5000")); // Output: true (Ví dụ Nhật)
console.log(phoneRegex.test("123-456-7890")); // Output: false
Xác thực Độ mạnh Mật khẩu
Biểu thức chính quy hữu ích cho việc thực thi các chính sách về độ mạnh của mật khẩu. Ví dụ dưới đây kiểm tra độ dài tối thiểu, chữ hoa, chữ thường và một chữ số.
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;
console.log(passwordRegex.test("P@ssword123")); // Output: true
console.log(passwordRegex.test("password")); // Output: false (không có chữ hoa hoặc số)
console.log(passwordRegex.test("Password")); // Output: false (không có số)
console.log(passwordRegex.test("Pass123")); // Output: false (không có chữ thường)
console.log(passwordRegex.test("P@ss1")); // Output: false (ít hơn 8 ký tự)
Các Kỹ thuật Tối ưu hóa Regex
Biểu thức chính quy có thể tốn kém về mặt tính toán, đặc biệt đối với các mẫu phức tạp hoặc đầu vào lớn. Dưới đây là một số kỹ thuật để tối ưu hóa hiệu suất regex:
- Cụ thể hóa: Tránh sử dụng các mẫu quá chung chung có thể khớp nhiều hơn dự định.
- Sử dụng Neo: Neo regex vào đầu hoặc cuối chuỗi bất cứ khi nào có thể (
^,$). - Tránh Quay lui (Backtracking): Giảm thiểu việc quay lui bằng cách sử dụng các lượng từ sở hữu (ví dụ:
++thay vì+) hoặc các nhóm nguyên tử ((?>...)) khi thích hợp. - Biên dịch một lần: Nếu bạn sử dụng cùng một regex nhiều lần, hãy biên dịch nó một lần và tái sử dụng đối tượng
RegExp. - Sử dụng Lớp ký tự một cách khôn ngoan: Các lớp ký tự (
[]) thường nhanh hơn so với các phương án thay thế (|). - Giữ cho nó đơn giản: Tránh các regex quá phức tạp, khó hiểu và khó bảo trì. Đôi khi, việc chia một tác vụ phức tạp thành nhiều regex đơn giản hơn hoặc sử dụng các kỹ thuật xử lý chuỗi khác có thể hiệu quả hơn.
Những Lỗi Regex Phổ biến
- Quên thoát các Siêu ký tự: Không thoát các ký tự đặc biệt như
.,*,+,?,$,^,(,),[,],{,},|, và\khi bạn muốn đối sánh chúng theo nghĩa đen. - Lạm dụng
.(dấu chấm): Dấu chấm khớp với bất kỳ ký tự nào (ngoại trừ ký tự xuống dòng trong một số chế độ), điều này có thể dẫn đến các kết quả khớp không mong muốn nếu không được sử dụng cẩn thận. Hãy cụ thể hơn khi có thể bằng cách sử dụng các lớp ký tự hoặc các mẫu hạn chế hơn. - Tính Tham lam (Greediness): Theo mặc định, các lượng từ như
*và+là tham lam và sẽ khớp càng nhiều càng tốt. Sử dụng các lượng từ lười biếng (*?,+?) khi bạn cần khớp chuỗi ngắn nhất có thể. - Sử dụng Neo không chính xác: Hiểu sai hành vi của
^(đầu chuỗi/dòng) và$(cuối chuỗi/dòng) có thể dẫn đến việc đối sánh không chính xác. Hãy nhớ sử dụng cờm(đa dòng) khi làm việc với các chuỗi đa dòng và muốn^và$khớp với đầu và cuối của mỗi dòng. - Không xử lý các Trường hợp biên: Không xem xét tất cả các kịch bản đầu vào có thể xảy ra và các trường hợp biên có thể dẫn đến lỗi. Hãy kiểm tra regex của bạn kỹ lưỡng với nhiều loại đầu vào, bao gồm chuỗi rỗng, ký tự không hợp lệ và các điều kiện biên.
- Vấn đề về Hiệu suất: Xây dựng các regex quá phức tạp và không hiệu quả có thể gây ra các vấn đề về hiệu suất, đặc biệt là với các đầu vào lớn. Tối ưu hóa regex của bạn bằng cách sử dụng các mẫu cụ thể hơn, tránh quay lui không cần thiết và biên dịch các regex được sử dụng lặp đi lặp lại.
- Bỏ qua Bảng mã Ký tự: Không xử lý đúng các bảng mã ký tự (đặc biệt là Unicode) có thể dẫn đến kết quả không mong muốn. Sử dụng cờ
ukhi làm việc với các ký tự Unicode để đảm bảo việc đối sánh chính xác.
Kết luận
Biểu thức chính quy là một công cụ có giá trị cho việc đối sánh mẫu và xử lý văn bản trong JavaScript. Việc thành thạo cú pháp và các kỹ thuật regex cho phép bạn giải quyết hiệu quả một loạt các vấn đề, từ xác thực dữ liệu đến xử lý văn bản phức tạp. Bằng cách hiểu các khái niệm được thảo luận trong hướng dẫn này và thực hành với các ví dụ thực tế, bạn có thể trở nên thành thạo trong việc sử dụng biểu thức chính quy để nâng cao kỹ năng phát triển JavaScript của mình.
Hãy nhớ rằng biểu thức chính quy có thể phức tạp, và thường hữu ích khi kiểm tra chúng kỹ lưỡng bằng các công cụ kiểm tra regex trực tuyến như regex101.com hoặc regexr.com. Điều này cho phép bạn hình dung các kết quả khớp và gỡ lỗi bất kỳ vấn đề nào một cách hiệu quả. Chúc bạn lập trình vui vẻ!