Câu 1re.match(r"\d+", "abc 123") trả về cái gì?
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àm | Phạm vi tìm kiếm | Trả về | Khi không khớp |
|---|---|---|---|
| re.match | Chỉ đầu chuỗi | Match object | None |
| re.search | Trận khớp đầu tiên ở bất kỳ đâu | Match object | None |
| re.findall | Tất cả các trận khớp | Danh sách chuỗi | Danh sách rỗng [] |
Từ Match object mà re.match và re.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ó.
| Metacharacter | Ý nghĩa | Ví dụ |
|---|---|---|
| \d | Một chữ số (0-9) | \d+ → một hoặc nhiều chữ số |
| \w | Một ký tự word (alphanumeric + gạch dưới) | \w+ → ID và keyword |
| \s | Mộ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òng | Wildcard |
| * | Không hoặc nhiều của ký tự trước | a* → rỗng cũng OK |
| + | Một hoặc nhiều của ký tự trước | a+ → ít nhất một |
| ? | Không hoặc một của ký tự trước | Tùy chọn |
| [abc] | Một trong a / b / c | Lựa chọn |
| ^ / $ | Đầu / cuối chuỗi | Anchor |
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.
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.
.group(1) / .group(2). .group(0) là 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(...): ....
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ề).
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
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.
.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>
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
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ì?