Menu

Regexp trong Golang: match, find, nhóm bắt và replace

Cách dùng biểu thức chính quy trong Go với package regexp: MustCompile, MatchString, FindString và FindAllString, nhóm bắt và nhóm có tên, ReplaceAllString, cùng giới hạn của cú pháp RE2 như không có lookbehind.

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ên dịch một lần, rồi so khớp

Hãy viết mẫu dưới dạng literal chuỗi thô trong dấu backtick. Trong chuỗi nháy kép thông thường, mọi dấu gạch chéo ngược phải nhân đôi ("\\d+"), và mẫu nhanh chóng trở nên khó đọc.

regexp.MustCompile gây panic nếu mẫu không hợp lệ, và đó là điều bạn muốn với một mẫu nằm trong mã nguồn: lỗi gõ nhầm thất bại ngay khi khởi động, không phải ở request đầu tiên. Với các mẫu đến từ người dùng hoặc cấu hình, hãy dùng regexp.Compile và xử lý lỗi:

re, err := regexp.Compile(userPattern)
if err != nil {
	return fmt.Errorf("bad pattern: %w", err)
}

Biên dịch một lần. Biên dịch tốn kém hơn so khớp rất nhiều. Một lời gọi regexp.MustCompile bên trong hàm chạy cho mỗi request hay mỗi dòng sẽ lặp lại công việc đó mọi lúc. Hãy đặt *regexp.Regexp đã biên dịch vào một var cấp package hoặc một trường của struct. Một regexp đã biên dịch dùng an toàn từ nhiều goroutine cùng lúc.

regexp.MatchString(pattern, s) biên dịch và so khớp trong một lời gọi. Dùng cho một lần kiểm tra lẻ thì được, nhưng trong vòng lặp thì lãng phí.

Tìm kết quả khớp

Tên các phương thức theo một khuôn mẫu: Find + All? + String? + Submatch? + Index?.

PhầnÝ nghĩa
Allmọi kết quả khớp không chồng lấn; nhận giới hạn n (-1 để lấy tất cả)
Stringlàm việc trên string; nếu không có phần này, phương thức nhận và trả về []byte
Submatchtrả về cả các nhóm bắt
Indextrả về vị trí byte thay vì văn bản

FindString trả về "" khi không khớp, và điều này không phân biệt được với việc khớp một chuỗi rỗng. Khi mẫu có thể khớp văn bản rỗng, hãy dùng FindStringIndex (trả về nil khi không khớp) hoặc gọi MatchString trước. Các biến thể All trả về nil khi không có gì khớp.

Nhóm bắt

Dấu ngoặc đơn dùng để bắt. FindStringSubmatch trả về toàn bộ chuỗi khớp ở chỉ số 0, sau đó mỗi nhóm một phần tử:

Luôn kiểm tra nil trước khi truy cập kết quả theo chỉ số; khi không khớp, m[1] sẽ panic.

(?:...) nhóm lại mà không bắt, dùng cho phép chọn hoặc lặp: (?:ab)+. Nhóm có tên được viết là (?P<name>...), và từ Go 1.22 có thêm cách viết (?<name>...).

Thay thế

Một cái bẫy kinh điển khi tham chiếu nhóm: "$1x" được hiểu là nhóm có tên 1x, nhóm này không tồn tại, nên được thay bằng chuỗi rỗng. Hãy viết "${1}x".

Với một chuỗi cố định, strings.ReplaceAll, strings.Contains và strings.Split đơn giản và nhanh hơn regexp. Chỉ dùng regexp khi văn bản bạn tìm có một hình dạng, chứ không phải một giá trị cố định.

Tra cứu nhanh cú pháp

Go dùng cú pháp RE2, bao gồm các tính năng quen thuộc kiểu Perl:

Cú phápKhớp với
.mọi ký tự trừ xuống dòng (với (?s) thì cả xuống dòng)
\d \w \schữ số, ký tự từ [0-9A-Za-z_], khoảng trắng (chỉ ASCII)
\D \W \Sphủ định của các lớp trên
[abc] [^abc] [a-z]lớp ký tự
\pL \p{Greek}lớp Unicode: mọi chữ cái, mọi ký tự Hy Lạp
* + ? {n,m}lặp, kiểu tham lam
*? +? ??lặp, kiểu lười
^ $đầu và cuối văn bản (đầu và cuối dòng, với (?m))
\branh giới từ (ASCII)
a|bphép chọn
(?i)không phân biệt hoa thường từ vị trí đó đến hết nhóm bao quanh; thường được viết ở đầu mẫu

\d và \w chỉ khớp ASCII. Để khớp "mọi chữ cái của mọi ngôn ngữ", hãy dùng \pL, và với mọi chữ số Unicode, dùng \p{Nd}. regexp.QuoteMeta(s) escape mọi ký tự đặc biệt trong s, điều bạn cần khi tạo mẫu từ đầu vào của người dùng.

Những gì RE2 không làm được

regexp của Go bảo đảm việc so khớp mất thời gian tuyến tính theo độ dài đầu vào. Những tính năng có thể phá vỡ bảo đảm đó không được hỗ trợ:

  • Không có lookahead hay lookbehind: (?=...), (?!...), (?<=...), (?<!...) là lỗi biên dịch. Vì Go 1.22 bắt đầu chấp nhận (?<name>...) cho nhóm có tên, một lookbehind giờ thất bại với thông báo khó hiểu invalid named capture, như chương trình bên dưới cho thấy.
  • Không có backreference: không thể dùng (\w)\1 để khớp một chữ cái lặp đôi.
  • Không có lượng từ possessive hay nhóm atomic.

Cách khắc phục gần như luôn giống nhau: bắt nhiều hơn một chút so với cần, rồi lọc hoặc cắt bằng code Go. Bù lại, regexp của Go không thể bị đẩy vào backtracking thảm họa. Một mẫu như (a+)+$, có thể làm đóng băng engine PCRE chỉ với một đầu vào ngắn, ở đây chạy trong thời gian tuyến tính, điều rất quan trọng khi mẫu hoặc đầu vào đến từ bên ngoài.

Lỗi thường gặp

  • Biên dịch bên trong vòng lặp hay handler. Hãy biên dịch một lần vào một biến cấp package.
  • Mẫu trong nháy kép. "\d" thậm chí không phải chuỗi Go hợp lệ; hãy dùng backtick.
  • Quên neo khi kiểm tra hợp lệ. \d{5} khớp bên trong "abc123456xyz". Hãy dùng ^\d{5}$.
  • Truy cập kết quả submatch theo chỉ số mà không kiểm tra nil.
  • Trông chờ \w hay \b hiểu văn bản không phải ASCII. Hãy dùng các lớp Unicode như \pL.
  • Dùng regexp cho chuỗi cố định. Package strings rõ ràng và nhanh hơn.

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

Làm sao kiểm tra một chuỗi có khớp regex trong Go không?

Biên dịch mẫu một lần bằng regexp.MustCompile, viết nó dưới dạng chuỗi thô trong dấu backtick để không phải escape dấu gạch chéo ngược, rồi gọi re.MatchString(s). Nếu không có neo (^ và $), MatchString trả về true khi mẫu khớp ở bất kỳ đâu trong chuỗi.

regexp của Go có hỗ trợ lookahead và lookbehind không?

Không. regexp của Go dùng cú pháp RE2, không có lookahead, lookbehind hay backreference. Đổi lại, việc so khớp chạy trong thời gian tuyến tính theo kích thước đầu vào, nên một mẫu hay đầu vào độc hại không thể làm treo chương trình. Hãy dùng nhóm bắt kèm một chút code Go, hoặc chia phép kiểm tra thành hai regex.

regexp.Compile và regexp.MustCompile khác nhau thế nào?

Compile trả về mẫu đã biên dịch và một lỗi. MustCompile thì panic khi mẫu không hợp lệ. Dùng MustCompile cho các mẫu viết trong mã nguồn, thường là trong một biến cấp package, để lỗi gõ nhầm lộ ra ngay khi khởi động. Dùng Compile cho các mẫu đến từ người dùng hoặc tệp cấu hình.

Làm sao lấy nhóm bắt từ một regex trong Go?

Dùng FindStringSubmatch, hàm này trả về một slice trong đó chỉ số 0 là toàn bộ chuỗi khớp và chỉ số 1, 2, ... là các nhóm, hoặc nil nếu không khớp. Để lấy mọi kết quả khớp, dùng FindAllStringSubmatch(s, -1). Nhóm có tên (?P<year>\d{4}) có thể tra bằng re.SubexpIndex("year").

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