Menu

Regex trong C#: Match, Replace, Split và group kèm ví dụ

Cách dùng biểu thức chính quy trong C# với System.Text.RegularExpressions: IsMatch, Match và Matches, group đánh số và group có tên, Replace với chuỗi thay thế và lambda, Split, RegexOptions, chuỗi verbatim cho pattern, kiểm tra đầu vào, timeout và attribute GeneratedRegex của .NET 7.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Biểu thức chính quy (regular expression, regex) là một pattern mô tả văn bản: "bốn chữ số, một dấu gạch ngang, hai chữ số", "một từ bắt đầu bằng chữ in hoa", "bất cứ thứ gì nằm giữa hai dấu ngoặc vuông". Trong C#, lớp Regex thuộc System.Text.RegularExpressions tìm, trích xuất, thay thế và tách văn bản bằng những pattern như vậy.

IsMatch, Match và Matches

Ba phương thức đáp ứng hầu hết các lần tìm kiếm. IsMatch trả lời có hoặc không, Match trả về kết quả khớp đầu tiên, và Matches trả về tất cả:

Đầu ra:

True
2026-03-14 at index 22
4 matches:
  1042
  2026
  1043
  2026
Success: False, Value: []

Match không khớp sẽ không trả về null: nó trả về một Match có Success là false và Value rỗng. Luôn kiểm tra Success trước khi dùng một kết quả khớp.

Lần tìm bốn chữ số cho thấy pattern có thể bắt nhiều hơn dự định. \b\d{4}\b nghĩa là "đúng bốn chữ số tạo thành một từ trọn vẹn", và năm trong các ngày tháng cũng thỏa mãn, vì - là ranh giới từ. Hãy làm pattern chặt hơn để nói đúng điều bạn muốn, ví dụ (?<=Order |order )\d+, hoặc dùng group, ở phần tiếp theo.

Chuỗi verbatim cho pattern

Cú pháp regex dùng dấu gạch chéo ngược khắp nơi (\d, \s, \b), và ký tự thoát trong chuỗi C# cũng vậy. Trong chuỗi thông thường, "\d" thậm chí không biên dịch được (CS1009, chuỗi thoát không hợp lệ) còn "\b" lại biên dịch thành ký tự backspace, âm thầm làm thay đổi pattern. Hãy viết pattern dưới dạng chuỗi verbatim với @, nơi dấu gạch chéo ngược chỉ là dấu gạch chéo ngược:

var a = new Regex("\\d+\\.\\d{2}");   // escaped twice: hard to read
var b = new Regex(@"\d+\.\d{2}");     // verbatim: what the regex engine sees

Trong chuỗi verbatim, dấu ngoặc kép được viết là "". Raw string literal của C# 11 ("""...""") còn tránh được cả điều đó.

Group: trích xuất các phần của kết quả khớp

Dấu ngoặc đơn tạo ra một group, và văn bản của từng group có sẵn sau khi khớp. Group được đánh số từ 1 theo thứ tự dấu ngoặc mở; group 0 là toàn bộ kết quả khớp. Group có tên, (?<name>...), dễ đọc hơn và không bị ảnh hưởng khi bạn sửa pattern:

Đầu ra:

2026-03-14
ERROR
payments
Card declined for order 1042
60 by 90, area 5400

[ và ] có ý nghĩa riêng trong regex (lớp ký tự), nên muốn khớp đúng dấu ngoặc vuông thì cần \[ và \]. Điều tương tự áp dụng cho . * + ? ( ) { } ^ $ | \. Để khớp nguyên văn một chuỗi do người dùng nhập bên trong pattern, hãy cho nó qua Regex.Escape, phương thức này thoát các ký tự đặc biệt giúp bạn: Regex.Escape("price (USD)") trả về price\ \(USD\) (khoảng trắng cũng được thoát, điều này vô hại).

Dấu ngoặc chỉ để nhóm mà không capture được viết là (?:...). Chúng giữ cho việc đánh số group gọn gàng và nhanh hơn một chút.

Replace: chuỗi thay thế và lambda

Regex.Replace thay thế mọi kết quả khớp. Trong chuỗi thay thế, $1 chèn group 1, ${name} chèn group có tên và $0 chèn toàn bộ kết quả khớp. Với những gì mẫu thay thế không diễn đạt được, hãy truyền một hàm (thường là lambda) nhận từng Match và trả về chuỗi thay thế cho nó:

Đầu ra:

Invoiced 14/03/2026, paid 02/04/2026.
too many spaces
Card **** **** **** 1234
Tea 2.75, Scone 3.52, Jam 1.10

Phần che số thẻ dùng lookahead, (?=...): nó chỉ khớp một chữ số nếu theo sau là bốn chữ số nữa, mà không tiêu thụ chúng. Các lookaround ((?=...), (?!...), (?<=...), (?<!...)) kiểm tra ngữ cảnh mà không đưa ngữ cảnh đó vào kết quả khớp, nhờ vậy chỉ một lần Replace là giữ nguyên được bốn chữ số cuối.

Với văn bản thuần không cần pattern, string.Replace đơn giản và nhanh hơn; hãy dùng Regex.Replace khi thứ cần thay thế có thể thay đổi.

Split

Regex.Split tách chuỗi tại mỗi kết quả khớp của pattern, nhờ đó xử lý được các dấu phân cách không cố định:

Đầu ra:

csharp / dotnet / regex / tutorial / beginner
Order Shipped Event Handler

Nếu pattern chứa group có capture, Regex.Split sẽ đưa cả các dấu phân cách đã capture vào kết quả; hãy dùng (?:...) khi bạn không muốn chúng.

RegexOptions

Các tùy chọn thay đổi cách cả pattern hoạt động. Kết hợp chúng bằng |:

Đầu ra:

1
2
error INFO Error
True

Những tùy chọn bạn sẽ dùng:

  • IgnoreCase: khớp không phân biệt hoa thường (dạng viết trong pattern là (?i)).
  • Multiline: ^ và $ khớp ở đầu và cuối mỗi dòng, không chỉ ở đầu và cuối cả chuỗi.
  • Singleline: . khớp cả \n (mặc định nó khớp mọi ký tự trừ ký tự xuống dòng).
  • IgnorePatternWhitespace: khoảng trắng trong pattern bị bỏ qua và # bắt đầu một chú thích, dành cho pattern dài viết trên nhiều dòng.
  • CultureInvariant: khi dùng cùng IgnoreCase, so sánh mà không theo quy tắc của culture hiện tại.
  • Compiled: biên dịch pattern sang IL một lần, dành cho regex được dùng rất nhiều lần.

Kiểm tra đầu vào: neo và câu chuyện email

Khi kiểm tra đầu vào, hãy neo pattern bằng ^ và $, nếu không nó sẽ chấp nhận ngay khi một phần đầu vào khớp:

Đầu ra:

PT-1000    loose=True  strict=True
pt-1000    loose=False strict=False
XPT-1000Y  loose=True  strict=False
PT-10      loose=False strict=False
ana@example.com   True
ana@example       False
ana @example.com  False
ana@@example.com  False

Pattern email chỉ kiểm tra hình dạng: một chuỗi không có khoảng trắng hay @, một dấu @, một tên miền có dấu chấm. Đó là chủ ý. Ngữ pháp thực sự của địa chỉ email (RFC 5322) cho phép phần local nằm trong ngoặc kép, chú thích và tên miền dạng địa chỉ IP; những pattern cố gắng bao quát hết dài đến hàng trăm ký tự mà vẫn từ chối các địa chỉ hợp lệ người ta thực sự dùng. Hơn nữa, một địa chỉ hoàn hảo về cú pháp vẫn có thể bị trả về. Hãy kiểm tra hình dạng, rồi gửi email xác nhận. System.Net.Mail.MailAddress là một cách kiểm tra hình dạng khác nếu bạn không muốn tự viết pattern.

Hiệu năng: phương thức tĩnh, instance, compiled

Các phương thức tĩnh (Regex.IsMatch(input, pattern)) phân tích pattern và giữ nó trong một bộ nhớ đệm nhỏ (mặc định 15 pattern), nên gọi lặp lại với cùng một pattern thì rất nhẹ. Với pattern dùng trong vòng lặp nóng, hãy tạo một instance Regex và giữ nó trong một field static readonly; thêm RegexOptions.Compiled nếu nó chạy hàng nghìn lần, chấp nhận khởi động chậm hơn để khớp nhanh hơn.

Timeout và catastrophic backtracking

Một số pattern tốn thời gian theo cấp số mũ với một vài đầu vào nhất định. Ví dụ kinh điển là lượng từ lồng nhau như ^(a+)+$ gặp "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!": engine thử mọi cách chia các chữ a giữa hai dấu + trước khi chịu thua. Trên máy chủ web, chỉ một đầu vào như vậy từ người dùng cũng có thể chiếm trọn một nhân CPU (tấn công "ReDoS"). Hãy đặt timeout cho mọi regex xử lý đầu vào không đáng tin cậy:

var pattern = new Regex(@"^(\w+\s?)*$", RegexOptions.None, TimeSpan.FromMilliseconds(100));
try
{
    bool ok = pattern.IsMatch(userInput);
}
catch (RegexMatchTimeoutException)
{
    // treat as invalid input
}

Mọi phương thức tĩnh cũng có overload nhận timeout. .NET 7 còn bổ sung RegexOptions.NonBacktracking, một engine đảm bảo thời gian tuyến tính với cái giá là mất một số tính năng (lookaround, backreference).

GeneratedRegex (.NET 7)

.NET 7 bổ sung một source generator biến pattern thành code C# thông thường ngay lúc biên dịch. Bạn có tốc độ của Compiled mà không tốn chi phí khởi động, code được sinh ra đọc được trong IDE, và nó hoạt động với trimming và Native AOT:

public static partial class Patterns
{
    [GeneratedRegex(@"^[A-Z]{2}-\d{4}$", RegexOptions.IgnoreCase)]
    public static partial Regex ProductCode();
}

bool ok = Patterns.ProductCode().IsMatch("pt-1000");   // True

Từ .NET 7 trở đi, đây là dạng được khuyến nghị cho mọi pattern đã biết lúc biên dịch.

Lỗi thường gặp

  • Thiếu neo khi kiểm tra đầu vào. Không có ^...$, IsMatch chấp nhận đầu vào chỉ cần chứa một kết quả khớp.
  • Dùng chuỗi thông thường cho pattern. "\b" là ký tự backspace, không phải ranh giới từ. Hãy dùng @"...".
  • Không thoát ký tự đặc biệt. . khớp mọi ký tự; pattern 3.50 khớp cả 3x50. Hãy thoát bằng \., hoặc bằng Regex.Escape với đầu vào của người dùng.
  • Lượng từ tham lam (greedy). <.*> trên <b>bold</b> khớp cả chuỗi. Hãy dùng dạng lười .*? hoặc lớp phủ định [^>]*.
  • Không đặt timeout cho đầu vào không đáng tin cậy. Một lượng từ lồng nhau có thể làm treo cả request.
  • Dùng regex cho định dạng có cấu trúc. HTML, JSON và CSV có dấu ngoặc kép cần một parser, không phải một pattern.

Câu hỏi thường gặp

Dùng regex trong C# như thế nào?

Thêm using System.Text.RegularExpressions; và gọi các phương thức tĩnh của Regex: Regex.IsMatch(input, pattern) trả về bool, Regex.Match trả về kết quả khớp đầu tiên, Regex.Matches trả về tất cả, Regex.Replace thay thế và Regex.Split tách chuỗi. Hãy viết pattern dưới dạng chuỗi verbatim, @"\d+", để dấu gạch chéo ngược đến được engine regex nguyên vẹn.

Làm sao lấy giá trị của group từ kết quả regex trong C#?

Đặt dấu ngoặc đơn quanh phần bạn cần và đọc match.Groups[1].Value (group được đánh số từ 1; group 0 là toàn bộ kết quả khớp). Group có tên rõ ràng hơn: (?<year>\d{4}) được đọc bằng match.Groups["year"].Value. Hãy kiểm tra match.Success trước, vì khi không khớp, các group là rỗng chứ không phải null.

Làm sao thay thế văn bản bằng regex trong C#?

Regex.Replace(input, pattern, replacement) thay thế mọi kết quả khớp. Chuỗi thay thế có thể tham chiếu đến group: $1 cho group đánh số, ${name} cho group có tên, $0 cho toàn bộ kết quả khớp. Với logic mà mẫu thay thế không diễn đạt được, hãy truyền một lambda: Regex.Replace(text, @"\d+", m => (int.Parse(m.Value) * 2).ToString()).

Làm sao để regex trong C# không phân biệt hoa thường?

Truyền RegexOptions.IgnoreCase: Regex.IsMatch(input, "error", RegexOptions.IgnoreCase). Các tùy chọn kết hợp với nhau bằng |, ví dụ RegexOptions.IgnoreCase | RegexOptions.Multiline. Bạn cũng có thể bật nó ngay trong pattern bằng (?i).

Làm sao kiểm tra địa chỉ email bằng regex trong C#?

Một pattern như ^[^@\s]+@[^@\s]+\.[^@\s]+$ bắt được các lỗi gõ hiển nhiên (thiếu @, có khoảng trắng, tên miền không có dấu chấm) và thường là đủ cho một form. Regex không thể thực sự xác thực một địa chỉ: ngữ pháp đầy đủ cho phép những dạng mà không pattern thực tế nào xử lý được, và một địa chỉ đúng cú pháp vẫn có thể không tồn tại. Hãy kiểm tra hình dạng cơ bản, rồi xác nhận bằng cách gửi email xác minh.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu