Tới giờ cả loạt bài chủ yếu bàn về tìm — regex khớp hay không, khớp ở đâu. Nhưng sức mạnh thực tế lớn nhất của regex trong công việc hằng ngày là biến đổi: trích một phần dữ liệu ra rồi dựng lại thành định dạng khác. Đổi 2026-09-22 thành 22/09/2026, tách họ và tên rồi đảo thứ tự, lấy tên miền từ URL — tất cả dựa trên hai cơ chế: nhóm bắt (capture group) để cắt chuỗi thành từng phần, và thay thế (substitution) để dán các phần đó lại theo ý mình. Bài này (phần 10 loạt Regex) đo thật cả hai bằng python re và sed, và làm rõ một điểm hay nhầm: \1 trong mẫu và \1 trong chuỗi thay thế là hai thứ khác nhau.

Nhóm bắt: cắt chuỗi thành từng phần

Đặt một phần mẫu trong ngoặc (...) là bắt (capture) đoạn khớp được vào một nhóm đánh số 1, 2, 3... theo thứ tự dấu ( mở. Sau khi khớp, bạn lấy từng nhóm ra bằng group(n) hoặc cả tuple bằng groups(). Có ba biến thể quan trọng:

  • Nhóm bắt (...): đánh số tự động, đọc bằng group(1).
  • Nhóm đặt tên (?P<ten>...) (Python; các engine khác dùng (?<ten>...)): đọc bằng group('ten') hoặc groupdict() — rõ nghĩa hơn số, và không vỡ khi bạn chèn nhóm mới vào giữa.
  • Nhóm không bắt (?:...): chỉ gom để áp quantifier hoặc |, không tạo nhóm và không được đánh số.

Ảnh chụp đoạn mã nền tối minh hoạ nhóm bắt và thay thế từ khớp sang biến đổi chuỗi ngoặc tròn dấu hỏi P tên re sub sed dấu hỏi hai chấm không bắt, nhóm bắt ngoặc tròn cắt chuỗi khớp thành từng phần m bằng re search nhóm d 4 gạch d 2 gạch d 2 m group 0 toàn bộ đoạn khớp m group 1 nhóm 1 bằng năm group 2 bằng tháng group 3 bằng ngày m groups tuple tất cả nhóm, nhóm đặt tên dấu hỏi P tên đọc bằng tên thay vì số re search dấu hỏi P nam d 4 gạch dấu hỏi P thang d 2 gạch dấu hỏi P ngay d 2 m group nam m groupdict dict tên giá trị tham chiếu tên trong mẫu dấu hỏi P bằng nam giống backslash 1 nhưng rõ nghĩa hơn, thay thế backslash 1 ở đây là nội suy không phải backreference re sub nhóm d 4 gạch d 2 gạch d 2 backslash 3 gạch chéo backslash 2 gạch chéo backslash 1 yyyy-mm-dd sang dd trên mm trên yyyy re sub bằng tên backslash g ngay trên g thang trên g nam quan trọng backslash 1 trong mẫu bằng backreference bài 05 đòi khớp lại backslash 1 trong thay thế bằng dán lại nội dung nhóm 1 khác nhau echo Nguyen Van A sed đổi thứ tự, nhóm không bắt dấu hỏi hai chấm gom mà không tạo nhóm dấu hỏi hai chấm https dấu hỏi hai gạch chéo w cộng chấm w cộng dấu hỏi hai chấm https chỉ để gom http https không được đếm nhóm 1 2 là w cộng w cộng nếu viết https dấu hỏi thì nó thành nhóm 1 đẩy mọi thứ lùi đánh số nhóm lệch hết

Hình 1: Nhóm bắt (...) cắt chuỗi thành phần đọc bằng group(n)/groups(); nhóm đặt tên (?P<ten>...) đọc bằng tên; thay thế re.sub/sed dán lại nội dung nhóm bằng \1 (nội suy, khác backreference trong mẫu); nhóm không bắt (?:...) gom mà không đánh số.

Đo thật: trích, đổi định dạng, và đếm nhóm

Ảnh chụp bảng kết quả chạy thật nhóm bắt và thay thế output thật go-lab python re cộng sed, a nhóm bắt cắt 2026-09-22 thành ba phần group 0 cả match 2026-09-22 group 1 2 3 2026 09 22 groups tuple 2026 09 22, b nhóm đặt tên đọc bằng tên và tham chiếu dấu hỏi P bằng w group nam 2026 groupdict nam 2026 thang 09 ngay 22 dấu hỏi P w chấm chấm dấu hỏi P bằng w the on tự lặp dùng tên thay backslash 1, c thay thế đổi định dạng ngày hai cách cho cùng kết quả input Han 2026-09-22 2025-01-05 bằng backslash 3 trên 2 trên 1 Han 22 trên 09 trên 2026 05 trên 01 trên 2025 bằng nhóm tên cùng kết quả backslash g d trên g m trên g y echo Nguyen Van A sed s w cộng chấm cộng backslash 2 backslash 1 Van A Nguyen sed đổi thứ tự bằng backslash 2 backslash 1, d dấu hỏi hai chấm không được đếm giữ số nhóm sạch dấu hỏi hai chấm https hai gạch chéo w cộng chấm w cộng groups coffeecode vn 2 nhóm https hai gạch chéo w cộng chấm w cộng groups https coffeecode vn 3 nhóm thay dấu hỏi hai chấm bằng ngoặc làm nhóm 1 2 bị đẩy thành 2 3 backslash 1 trong thay thế trỏ sai

Hình 2: Chạy thật — (a) nhóm bắt cắt 2026-09-22 thành ('2026','09','22'); (b) nhóm đặt tên cho groupdict()={'nam':'2026',...}, (?P<w>..) (?P=w) bắt từ lặp ['the','on']; (c) re.sub đổi ngày sang '22/09/2026, 05/01/2025' (bằng \3/\2/\1 hoặc tên), sed đổi Nguyen Van A→Van A, Nguyen; (d) (?:https?) cho 2 nhóm, (https?) cho 3 nhóm.

  • (a) Nhóm bắt cắt chuỗi: (\d{4})-(\d{2})-(\d{2}) trên 2026-09-22 cho group(0)='2026-09-22' (cả match) và groups()=('2026','09','22') (ba phần riêng). Đây là cách trích cấu trúc ra khỏi một chuỗi phẳng.
  • (b) Nhóm đặt tên rõ nghĩa hơn: (?P<nam>\d{4})-(?P<thang>\d{2})-(?P<ngay>\d{2}) cho groupdict()={'nam':'2026','thang':'09','ngay':'22'} — đọc bằng tên thay vì đếm số. Và (?P<w>\w+) (?P=w) bắt từ lặp ['the','on'] bằng cách tham chiếu tên (?P=w) (đây là backreference trong mẫu, kiểu phần 5, nhưng dùng tên cho dễ đọc).
  • (c) Thay thế biến đổi chuỗi: re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\3/\2/\1', s) đổi mọi ngày yyyy-mm-dd thành dd/mm/yyyy → 'Han: 22/09/2026, 05/01/2025'. Cùng kết quả khi dùng nhóm tên \g<d>/\g<m>/\g<y>. Với sed, echo "Nguyen Van A" | sed -E 's/^(\w+) (.+)$/\2, \1/' cho Van A, Nguyen — đảo thứ tự bằng \2 \1.
  • (d) (?:...) giữ số nhóm sạch: (?:https?)://(\w+)\.(\w+) cho groups()=('coffeecode','vn') — chỉ 2 nhóm, vì (?:https?) không được đếm. Nhưng nếu viết (https?)://(\w+)\.(\w+) thì thành 3 nhóm ('https','coffeecode','vn'). Điều này quan trọng: nếu bạn đang dùng \1 để chỉ tên miền mà lỡ đổi (?:) thành (), mọi số nhóm bị đẩy lùi một bậc và \1 trỏ sai.

Điểm hay nhầm: \1 trong mẫu vs trong thay thế

Đây là chỗ dễ lẫn nhất, nối tiếp phần 5. \1 trong mẫu là backreference: "khớp lại đúng chuỗi mà nhóm 1 đã bắt" — nó phá vỡ tính regular và cần backtracking ((\w+) \1 tìm từ lặp). \1 trong chuỗi thay thế hoàn toàn khác: nó chỉ là nội suy — "dán lại nội dung nhóm 1 vào đây" — không liên quan gì tới backtracking, chỉ là ghép chuỗi. Cùng ký hiệu, hai vai trò tách biệt: một cái là điều kiện khớp, một cái là công thức dựng chuỗi kết quả.

Đánh đổi cần cân nhắc

Nhóm đặt tên đáng dùng cho mẫu phức tạp — vì số nhóm dễ vỡ. Khi mẫu có nhiều nhóm và bạn còn phải chỉnh sửa nó, dùng \1, \2 là mời lỗi: chèn một nhóm mới vào giữa là mọi số sau đó lệch, và không có lỗi nào báo — chỉ ra kết quả sai. Nhóm đặt tên (?P<...>) miễn nhiễm chuyện này: thêm nhóm không đổi tên nhóm cũ. Với mẫu quá 2-3 nhóm hoặc mẫu sẽ còn sửa, ưu tiên đặt tên.

Nhóm bắt không cần thiết làm chậm và tốn bộ nhớ — dùng (?:...). Mỗi nhóm bắt buộc engine ghi nhớ vị trí bắt đầu/kết thúc của đoạn khớp để trả về sau. Nếu bạn dùng () chỉ để gom (áp | hay quantifier) mà không cần trích, hãy dùng (?:...) — nó bỏ phần ghi nhớ đó, nhanh hơn chút và giữ số nhóm sạch. Thói quen "mọi ngoặc đều là (?:) trừ khi thực sự cần bắt" giúp mẫu vừa nhanh vừa dễ bảo trì.

Cú pháp nhóm tên và thay thế khác nhau giữa các công cụ. Python dùng (?P<ten>) và \g<ten>; JavaScript/.NET/PCRE dùng (?<ten>) và $<ten>/${ten}; sed cơ bản cần escape ngoặc \(...\) (hoặc -E cho ngoặc thường) và dùng \1 khi thay thế. Đừng bê nguyên mẫu từ ngôn ngữ này sang ngôn ngữ khác — đặc biệt phần thay thế, nơi \1 (sed/Python) và $1 (JS/PCRE) khác nhau.

Ba ý mang về

  1. Nhóm bắt cắt chuỗi, thay thế dán lại: đo thật (\d{4})-(\d{2})-(\d{2}) trích ('2026','09','22'), rồi re.sub với \3/\2/\1 đổi yyyy-mm-dd thành dd/mm/yyyy (22/09/2026), sed đảo Nguyen Van A→Van A, Nguyen — regex biến đổi chứ không chỉ tìm.
  2. Nhóm đặt tên bền hơn số, (?:...) giữ số nhóm sạch: đo thật groupdict() đọc bằng tên; (?:https?) cho 2 nhóm còn (https?) cho 3 — thêm một cặp () thừa đẩy lệch mọi số nhóm và làm \1 trỏ sai.
  3. \1 trong mẫu vs trong thay thế là hai thứ: \1 trong mẫu là backreference (khớp lại, cần backtracking — phần 5); \1 trong chuỗi thay thế chỉ là nội suy (dán lại nội dung nhóm), không liên quan backtracking.

Nguồn

Phần sau ta gom mọi thứ đã học thành nghệ thuật tối ưu regex: biên dịch một lần, neo và lớp phủ định để cắt backtracking, atomic group/possessive quantifier — và đo thật mỗi mẹo giúp nhanh lên bao nhiêu.