Học bằng cách đọc theo thứ tự

Biểu thức chính quy re — Tìm kiếm và Thay thế Mẫu

Học module re của Python từ căn bản. Bao quát khi nào dùng re.match / re.search / re.findall, kết hợp các metacharacter \d / \w / \s / * / + / ?, bắt nhóm với ( ), thay thế với re.sub, và tái sử dụng mẫu với re.compile — kèm bài tập thực hành chạy được.

Bài này đi qua module re cho biểu thức chính quy"trích xuất và thay thế các chuỗi con khớp với một mẫu cụ thể". Những việc bạn làm liên tục trong dự án thực — phân tích số điện thoại, email, dòng log và URL — trở thành one-liner.

Một công cụ để thử regex trực tiếp

Biểu thức chính quy có nhiều thành phần và khó suy luận chỉ trong đầu. Để kiểm tra xem mẫu của bạn có khớp với điều bạn dự định hay không, Regex Extractor chạy hoàn toàn trong trình duyệt — gõ một mẫu và một đoạn text rồi xem các trận khớp theo thời gian thực. Mở nó kế bài này khiến việc theo dõi dễ hơn nhiều.

match, search và findall — Ba hàm tìm kiếm và khi nào dùng từng cái

Module re phơi bày nhiều hàm tìm kiếm, và bạn chọn trong ba hàm tùy nhu cầu. Tên gợi nhớ — match khớp ở đầu, search tìm một trận khớp ở bất kỳ đâu, và findall tìm tất cả. Phạm vi tìm kiếm chính xác, kiểu trả về và hành vi khi không khớp được tóm tắt ở bảng tiếp theo.

HàmPhạm vi tìm kiếmTrả vềKhi không khớp
re.matchChỉ đầu chuỗiMatch objectNone
re.searchTrận khớp đầu tiên ở bất kỳ đâuMatch objectNone
re.findallTất cả các trận khớpDanh sách chuỗiDanh sách rỗng []

Từ Match objectre.matchre.search trả về (một đối tượng giữ vị trí khớp, chuỗi khớp và thông tin nhóm), bạn đọc chuỗi khớp bằng cách gọi method `.group()` của nó — m.group() hoặc m.group(0) cho toàn bộ trận khớp, và (với nhóm bắt giới thiệu sau) m.group(1) cho chỉ thứ trong dấu ngoặc. Chỉ re.findall trả về một danh sách trực tiếp, nên bạn không gọi .group() trên nó.

Cách re.match / search / findall khác nhau
re.matchkhớp từ đầuNếu đầu không khớp→ Nonere.searchkhớp đầu tiên ở bất kỳ đâuMatch nếu tìm thấyNone nếu khôngre.findalltất cả các trận khớpDanh sách chuỗi khớp([] nếu không có)
match chỉ kiểm tra liệu mẫu có xuất hiện ở đầu chuỗi. search trả về trận khớp đầu tiên ở bất kỳ vị trí nào. findall trả về mọi trận khớp trong một danh sách.
MetacharacterÝ nghĩaVí dụ
\dMột chữ số (0-9)\d+ → một hoặc nhiều chữ số
\wMột ký tự word (alphanumeric + gạch dưới)\w+ → ID và keyword
\sMột ký tự khoảng trắng (space / tab / xuống dòng)Phân tách
.Bất kỳ ký tự nào trừ xuống dòngWildcard
*Không hoặc nhiều của ký tự trướca* → rỗng cũng OK
+Một hoặc nhiều của ký tự trướca+ → ít nhất một
?Không hoặc một của ký tự trướcTùy chọn
[abc]Một trong a / b / cLựa chọn
^ / $Đầu / cuối chuỗiAnchor
import re

text = "user_id: 12345, age: 30"

# match: từ đầu (\w+ là chuỗi ký tự word)
m = re.match(r"\w+", text)
print(m.group())            # user_id

# search: chuỗi chữ số đầu tiên ở bất kỳ đâu
s = re.search(r"\d+", text)
print(s.group())            # 12345

# findall: mọi chuỗi chữ số
nums = re.findall(r"\d+", text)
print(nums)                 # ['12345', '30']

Viết regex như raw string r"..."

Backslash xuất hiện khắp nơi trong regex. Một chuỗi thông thường "\d" có thể bị các escape của nó diễn giải bởi tầng string trước khi re thấy nó, nên an toàn hơn là viết raw string `r"\d"` với r ở đầu. Editor cũng có xu hướng tô màu raw string như regex, làm tăng khả năng đọc.

Lấy ID và số ra từ một dòng log. Thử re.match / re.search / re.findall trên cùng một chuỗi và quan sát kết quả khác nhau như thế nào.

① Import re.

② Đặt text = "order_id: 9876, qty: 3, price: 1500".

③ Lấy một chuỗi ký tự word từ đầu chuỗi và in ra dạng match: ◯◯.

④ Lấy chuỗi chữ số đầu tiên từ chuỗi và in ra dạng search: ◯◯.

⑤ Lấy mọi chuỗi chữ số dưới dạng danh sách và in ra dạng findall: ◯◯.

(Nếu code chạy đúng, phần giải thích sẽ hiện ra.)

Python Editor

Chạy code để xem đầu ra

Nhóm bắt — Lấy các phần cụ thể từ một mẫu

Bất cứ thứ gì bạn đặt trong `( )` trong regex trở thành một nhóm bắt — thay vì chỉ toàn bộ trận khớp, bạn có thể lấy từng mảnh ra riêng. Mẫu như r"#(\d+) on (\d{4})-(\d{2})-(\d{2})" cho phép bạn tách số đơn hàng và ngày từ một dòng log trong một lần.

Gọi `.group(N)` trên Match object để đọc nhóm thứ N (đánh số từ 1). .group(0) (hoặc .group() không tham số) trả về toàn bộ trận khớp.

Cách nhóm bắt hoạt động
#(\d+) on(\d{4})-(\d{2})-(\d{2}).group(0)toàn bộ trận khớp.group(1)số đơn hàng.group(2)năm.group(3)tháng
Mỗi `( )` trong regex trở thành một nhóm, có thể truy cập bằng chỉ số 1-based như .group(1) / .group(2). .group(0)toàn bộ trận khớp.
import re

text = "Order #1234 placed on 2024-03-15"

# Mẫu nghĩa là gì:
#   #         → '#' literal
#   (\d+)     → một hoặc nhiều chữ số → group(1) số đơn hàng
#   placed on → 'placed on' literal
#   (\d{4})   → 4 chữ số → group(2) năm
#   (\d{2})   → 2 chữ số → group(3) tháng
#   (\d{2})   → 2 chữ số → group(4) ngày
m = re.search(r"#(\d+) placed on (\d{4})-(\d{2})-(\d{2})", text)
if m:
    print("whole:", m.group(0))    # #1234 placed on 2024-03-15
    print("order #:", m.group(1))   # 1234
    print("year:", m.group(2))      # 2024
    print("month:", m.group(3))     # 03
    print("day:", m.group(4))       # 15

Gọi .group() khi Match là None sẽ ném lỗi

Khi re.search không tìm thấy mẫu nó trả về None. Gọi m.group() trên đó crash với AttributeError: 'NoneType' object has no attribute 'group'. Luôn kiểm tra với `if m:` trước khi .group(), hoặc làm cả hai trong một bước với toán tử walrus: if m := re.search(...): ....

Tách một địa chỉ email thành tên người dùng và tên miền. Dùng nhóm bắt để lấy cả hai phần trong một lần tìm kiếm.

① Import re.

② Đặt text = "liên hệ chúng tôi tại alice@example.com".

③ Viết một mẫu email bắt những gì ở mỗi phía của @ thành các nhóm riêng.

- Trái: ký tự word cộng ., +, -, một hoặc nhiều

- Phải: cùng loại ký tự, kết thúc bằng tên miền như .com

④ Khi tìm thấy trận khớp, in `username: ◯◯``domain: ◯◯`.

Python Editor

Chạy code để xem đầu ra

re.sub — Thay thế các trận khớp mẫu

"Che thông tin cá nhân khỏi log", "loại bỏ thẻ HTML và giữ lại văn bản nội dung", "chuẩn hóa hỗn hợp khoảng trắng full-width và half-width" — tất cả đều quy về "viết lại bất cứ gì khớp với mẫu thành thứ khác". Hàm replace của string chỉ xử lý chuỗi con cố định, nhưng re.sub làm theo mẫu.

`re.sub(mẫu, thay thế, gốc)` trả về một chuỗi mới với mỗi trận khớp được thay bằng chuỗi thay thế. Chuỗi gốc không thay đổi (chuỗi Python là bất biến, nên bạn luôn làm việc với giá trị trả về).

re.sub hoạt động ra sao
Chuỗi gốc"Tel: 03-1234-5678"re.sub(\d, *, ...)Chuỗi mới"Tel: **-****-****"
Trả về chuỗi mới với mỗi trận khớp được thay bằng chuỗi thay thế. Bản gốc bất biến; nhận kết quả qua giá trị trả về.
import re

# Che chữ số trong số điện thoại (thay mỗi \d bằng một *)
text = "Tel: 03-1234-5678"
masked = re.sub(r"\d", "*", text)
print(masked)
# Tel: **-****-****

# Loại bỏ thẻ HTML để chỉ giữ văn bản nội dung
html = "<p>Xin chào <b>thế giới</b></p>"
plain = re.sub(r"<[^>]+>", "", html)
print(plain)
# Xin chào thế giới

Che chữ số trong bất kỳ số điện thoại nào xuất hiện trong dòng log bằng cách thay chúng bằng ``.**

① Import re.

② Đặt text = "Liên hệ: 03-1234-5678 hoặc 090-9999-8888".

③ Dùng re.sub để *thay mỗi chữ số `\d` bằng một `**, và in kết quả dạng masked: ◯◯`.

④ In text gốc lại dạng original: ◯◯ để xác nhận nó không thay đổi (re.sub chỉ trả về chuỗi mới).

Python Editor

Chạy code để xem đầu ra

re.compile — Tái sử dụng một mẫu

Khi bạn dùng cùng một regex lặp đi lặp lại, viết re.search(r"...", text) mãi khiến engine phân tích (compile) mẫu mỗi lần, đó là công sức lãng phí. `re.compile(mẫu)` xây dựng đối tượng mẫu đã compile một lần, và bạn gọi method trên nó như pattern.search(...) / pattern.findall(...) / pattern.sub(...). Code đọc tốt hơn và chạy nhanh hơn.

Cách dùng re.compile
r"\d{2,4}-\d{4}-\d{4}"re.compile(...)phone_re(đối tượng mẫu)phone_re.search(text)phone_re.findall(text)phone_re.sub("*", text)
`re.compile(mẫu)` cho bạn đối tượng mẫu mà bạn có thể gọi .search / .findall / .sub bao nhiêu lần tùy thích. Compile khi tái sử dụng cùng một mẫu.
import re

# Tái sử dụng cùng mẫu số điện thoại
phone_re = re.compile(r"\d{2,4}-\d{4}-\d{4}")

print(phone_re.findall("03-1234-5678 hoặc 080-1111-2222"))
# ['03-1234-5678', '080-1111-2222']

print(phone_re.search("điện thoại của tôi là 03-9999-0000").group())
# 03-9999-0000

print(phone_re.sub("<phone>", "Liên hệ: 03-1234-5678"))
# Liên hệ: <phone>

Xây dựng mẫu số điện thoại một lần với `re.compile`, rồi đếm và thay thế trên cùng text liên tiếp.

① Import re.

② Đặt text = "Liên hệ: 03-1234-5678 hoặc 090-9999-8888".

③ Compile một mẫu số điện thoại là 2-4 chữ số + 4 chữ số + 4 chữ số với re.compile, và lưu nó là phone_re.

④ Dùng phone_re.findall(text) để đếm có bao nhiêu số điện thoại, và in dạng phone count: ◯.

⑤ Dùng phone_re.sub để thay mỗi số điện thoại đầy đủ bằng `<phone>`, và in kết quả dạng replaced: ◯◯.

Python Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1re.match(r"\d+", "abc 123") trả về cái gì?

Câu 2Regex đúng cho một hoặc nhiều chữ số liên tiếp là gì?

Câu 3Từ re.search(r"(\w+)@(\w+)", "alice@example"), lệnh gọi nào trả về chỉ tên miền?

Câu 4Lý do chính để dùng raw string `r"..."` khi viết regex trong Python là gì?