- Dùng
open()với đúng chế độ mở tệp:r,w,a,x,b,+. - Luôn dùng
with— hiểu vì sao nó an toàn hơntry/finallythủ công. - Đọc tệp theo 4 cách và biết khi nào dùng cách nào (đặc biệt: tệp lớn hơn RAM).
- Hiểu encoding: vì sao luôn ghi
encoding="utf-8", và cách gỡUnicodeDecodeError. - Thành thạo
pathlib.Path— chuẩn hiện đại thay choos.path. - Đọc/ghi JSON và CSV bằng stdlib, kể cả trường hợp có tiếng Việt.
- Nắm cú pháp regex: ký tự đại diện, lượng từ, nhóm, nhóm có tên, lớp ký tự.
- Dùng đúng
search/match/fullmatch/findall/finditer/sub/split. - Biết khi nào KHÔNG nên dùng regex — và dùng gì thay thế.
Complex is better than complicated.
1. open() — cánh cửa vào tệp tin
Hàm built-in open() trả về một đối tượng tệp (file object) — thứ bạn đọc, ghi,
và cuối cùng phải đóng. Ba tham số quan trọng nhất:
open(
file, # đường dẫn (str hoặc Path)
mode="r", # chế độ mở — xem bảng bên dưới
encoding=None, # ⚠️ LUÔN đặt "utf-8" cho tệp văn bản
newline=None, # cách xử lý ký tự xuống dòng
errors=None, # hành vi khi giải mã lỗi
)
1.1. Chế độ mở tệp — bảng đầy đủ
| Chế độ | Tên | Tệp chưa tồn tại | Tệp đã tồn tại | Vị trí con trỏ | Đọc | Ghi |
|---|---|---|---|---|---|---|
"r" | read (mặc định) | FileNotFoundError | Giữ nguyên | Đầu tệp | ✅ | ❌ |
"w" | write | Tạo mới | Xoá sạch nội dung! | Đầu tệp | ❌ | ✅ |
"a" | append | Tạo mới | Giữ nguyên | Cuối tệp | ❌ | ✅ |
"x" | exclusive create | Tạo mới | FileExistsError | Đầu tệp | ❌ | ✅ |
"r+" | read + write | FileNotFoundError | Giữ nguyên | Đầu tệp | ✅ | ✅ |
"w+" | write + read | Tạo mới | Xoá sạch! | Đầu tệp | ✅ | ✅ |
"a+" | append + read | Tạo mới | Giữ nguyên | Cuối tệp | ✅ | ✅ |
Thêm hậu tố b vào bất kỳ chế độ nào để làm việc ở dạng nhị phân:
"rb", "wb", "ab", "rb+". Ở chế độ nhị phân, bạn đọc/ghi
bytes thay vì str, và không được truyền encoding.
"w" xoá sạch tệp ngay lúc mở
Không phải lúc ghi — mà ngay khoảnh khắc open(path, "w") chạy. Kể cả khi
chương trình sập ngay dòng sau, tệp cũ đã mất. Đây là cách phá dữ liệu phổ biến nhất của người mới. Ba
cách phòng:
- Dùng
"x"khi bạn biết tệp phải chưa tồn tại — Python sẽ chặn thay vì ghi đè. - Dùng
"a"khi chỉ muốn thêm vào. - Ghi ra tệp tạm rồi
Path.replace()— thao tác nguyên tử, không bao giờ để lại tệp ghi dở.
2. with — luôn luôn dùng
# ❌ TỆ — quên đóng, hoặc lỗi giữa chừng thì không bao giờ đóng
f = open("data.txt", encoding="utf-8")
noi_dung = f.read()
f.close()
# ⚠️ TẠM ĐƯỢC — đúng nhưng dài dòng
f = open("data.txt", encoding="utf-8")
try:
noi_dung = f.read()
finally:
f.close()
# ✅ ĐÚNG — with tự đóng, kể cả khi có exception
with open("data.txt", encoding="utf-8") as f:
noi_dung = f.read()
# ra khỏi khối → f.close() đã được gọi
close() lại nguy hiểm- Dữ liệu chưa được ghi xuống đĩa. Python đệm nội dung trong bộ nhớ;
close()mới xả (flush) xuống. Chương trình sập trước đó → mất dữ liệu. - Cạn file descriptor. Hệ điều hành giới hạn số tệp mở đồng thời (thường 256-1024). Vòng lặp mở 10.000 tệp mà không đóng →
OSError: Too many open files. - Khoá tệp trên Windows. Tệp đang mở không xoá/đổi tên được.
CPython có bộ đếm tham chiếu nên thường tự đóng tệp khi biến ra khỏi phạm vi — nhưng đó là
chi tiết cài đặt của CPython, không phải cam kết của ngôn ngữ. PyPy hay các bản cài đặt khác
không đảm bảo điều đó. Luôn dùng with.
# Nối hai tệp thành một
with (
open("a.txt", encoding="utf-8") as f1,
open("b.txt", encoding="utf-8") as f2,
open("gop.txt", "w", encoding="utf-8") as out,
):
out.write(f1.read())
out.write(f2.read())
# Cả 3 tệp đều được đóng, kể cả nếu open("b.txt") ném lỗi
Cú pháp ngoặc tròn bao nhiều context manager là tính năng của Python 3.10+ (PEP 617), cho phép xuống dòng
gọn gàng. Bản cũ hơn viết trên một dòng: with open(a) as f1, open(b) as f2:. Cơ chế bên trong
của with — __enter__/__exit__ — sẽ được mổ xẻ ở
Chương 10.
3. Bốn cách đọc tệp
| Cách | Trả về | Bộ nhớ | Dùng khi |
|---|---|---|---|
f.read() | str toàn bộ tệp | Bằng cỡ tệp | Tệp nhỏ, cần xử lý cả khối |
f.read(n) | str tối đa n ký tự | n ký tự | Đọc theo khối cố định |
f.readline() | Một dòng (kèm \n) | Một dòng | Cần kiểm soát thủ công từng dòng |
f.readlines() | list[str] mọi dòng | Bằng cỡ tệp + overhead list | Hiếm khi cần — thường có cách tốt hơn |
for line in f ⭐ | Lặp lười từng dòng | Một dòng | Mặc định — mọi kích thước tệp |
# ① Đọc toàn bộ — tệp nhỏ
with open("config.txt", encoding="utf-8") as f:
noi_dung = f.read()
# ② Lặp từng dòng — CÁCH MẶC ĐỊNH, chạy được với tệp 100 GB
with open("nhat_ky.log", encoding="utf-8") as f:
for so_dong, dong in enumerate(f, start=1):
if "ERROR" in dong:
print(f"{so_dong}: {dong.rstrip()}") # rstrip bỏ \n cuối dòng
# ③ Đọc theo khối — tệp nhị phân lớn, tính checksum...
with open("video.mp4", "rb") as f:
while khoi := f.read(8192): # walrus := gán và kiểm tra cùng lúc
xu_ly(khoi)
# ④ Danh sách dòng đã làm sạch (chỉ khi tệp nhỏ)
with open("ten.txt", encoding="utf-8") as f:
ten = [d.strip() for d in f if d.strip()] # bỏ dòng trống
Đối tượng tệp là một iterator
for dong in f không phải phép màu — đối tượng tệp cài đặt giao thức iterator, mỗi lần
next() đọc thêm đúng một dòng từ đĩa. Nhờ vậy bạn xử lý được tệp lớn hơn RAM mà không cần
nghĩ gì. Hệ quả cần nhớ: iterator dùng một lần — đọc hết rồi thì vòng
for thứ hai không thấy gì nữa, phải f.seek(0) để tua về đầu.
Chương 9 sẽ mổ xẻ giao thức này.
4. Ghi tệp
# ① write() — KHÔNG tự thêm \n
with open("ra.txt", "w", encoding="utf-8") as f:
f.write("Dòng một\n")
f.write("Dòng hai\n")
f.write(f"Kết quả: {42}\n")
# ② writelines() — cũng KHÔNG tự thêm \n (tên gây hiểu lầm!)
dong = ["a\n", "b\n", "c\n"]
with open("ra.txt", "w", encoding="utf-8") as f:
f.writelines(dong)
# ③ Cách Pythonic khi có sẵn list không có \n
ten = ["An", "Bình", "Chi"]
with open("ten.txt", "w", encoding="utf-8") as f:
f.write("\n".join(ten))
# ④ print() ghi thẳng vào tệp — tiện vì tự thêm \n
with open("bao_cao.txt", "w", encoding="utf-8") as f:
print("=== BÁO CÁO ===", file=f)
for t in ten:
print(f"- {t}", file=f)
# ⑤ Thêm vào cuối, không ghi đè
with open("nhat_ky.log", "a", encoding="utf-8") as f:
f.write("[INFO] khởi động\n")
from pathlib import Path
def ghi_an_toan(duong_dan: Path, noi_dung: str) -> None:
"""Ghi nguyên tử: hoặc tệp cũ nguyên vẹn, hoặc tệp mới hoàn chỉnh."""
tam = duong_dan.with_suffix(duong_dan.suffix + ".tmp")
tam.write_text(noi_dung, encoding="utf-8")
tam.replace(duong_dan) # thao tác nguyên tử ở tầng hệ điều hành
Nếu chương trình sập giữa lúc ghi, tệp gốc vẫn nguyên vẹn — chỉ còn lại một tệp .tmp dở dang
vô hại. Mẫu này bắt buộc với tệp cấu hình và dữ liệu quan trọng.
5. Encoding — nguồn gốc của mọi lỗi tiếng Việt
Trên đĩa, tệp chỉ là dãy byte. Chuỗi Python là dãy ký tự Unicode. Encoding là quy tắc chuyển đổi giữa hai thứ đó. Chọn sai quy tắc → chữ biến thành ký tự lạ, hoặc chương trình sập.
s = "Xin chào"
print(len(s)) # 8 — tám KÝ TỰ
print(len(s.encode("utf-8"))) # 9 — chín BYTE (chỉ "à" có dấu, tốn 2 byte: \xc3\xa0)
print(s.encode("utf-8")) # b'Xin ch\xc3\xa0o'
# Giải mã sai bảng mã → chữ rác (mojibake)
byte_utf8 = s.encode("utf-8")
print(byte_utf8.decode("latin-1")) # 'Xin chà o' — sai nhưng KHÔNG báo lỗi!
# Bảng mã không biểu diễn được → lỗi
s.encode("ascii")
# UnicodeEncodeError: 'ascii' codec can't encode character '\xe0'
encoding="utf-8" — mọi lúc
Không truyền encoding thì Python dùng bảng mã mặc định của hệ thống
(locale.getpreferredencoding()). Trên macOS/Linux thường là UTF-8 — có vẻ ổn. Nhưng trên
Windows tiếng Việt, mặc định có thể là cp1258. Kết quả: code chạy hoàn hảo trên máy bạn,
sập trên máy đồng nghiệp, hoặc tệ hơn — chạy được nhưng lưu ra chữ rác.
# ❌ phụ thuộc hệ thống
open("data.txt")
# ✅ tường minh, giống nhau trên mọi máy
open("data.txt", encoding="utf-8")
Python 3.15 sẽ đổi mặc định thành UTF-8 (PEP 686), nhưng đến lúc đó vẫn nên viết tường minh —
code của bạn phải chạy được trên cả bản cũ. Bật cảnh báo bằng python -X warn_default_encoding
để tìm những chỗ còn thiếu.
errors= | Hành vi khi gặp byte không giải mã được | Dùng khi |
|---|---|---|
"strict" (mặc định) | Ném UnicodeDecodeError | Mặc định — bạn muốn biết dữ liệu hỏng |
"ignore" | Bỏ qua byte lỗi | Chấp nhận mất dữ liệu để chương trình chạy tiếp |
"replace" | Thay bằng � (U+FFFD) | Hiển thị cho người xem, thấy rõ chỗ hỏng |
"surrogateescape" | Giữ byte lỗi ở dạng đặc biệt, khôi phục được khi ghi lại | Đọc-sửa-ghi tệp có byte lạ mà không phá phần còn lại |
# Gặp UnicodeDecodeError? Ba bước chẩn đoán:
# 1. Xem vài byte đầu để đoán bảng mã
with open("bi_an.txt", "rb") as f:
print(f.read(50))
# b'\xef\xbb\xbfXin ch\xc3\xa0o' ← \xef\xbb\xbf là BOM của UTF-8
# 2. Có BOM → dùng utf-8-sig để Python tự bỏ BOM
with open("bi_an.txt", encoding="utf-8-sig") as f:
print(f.read())
# 3. Không rõ bảng mã → thử lần lượt
for ma in ("utf-8", "utf-8-sig", "cp1258", "latin-1"):
try:
with open("bi_an.txt", encoding=ma) as f:
print(ma, "→", f.read(80))
except UnicodeDecodeError as e:
print(ma, "→ hỏng ở byte", e.start)
Byte Order Mark — ba byte EF BB BF mà một số công cụ Windows (Notepad,
Excel) chèn vào đầu tệp UTF-8 để đánh dấu. Đọc bằng "utf-8" thì ký tự đầu tiên của bạn sẽ là
'\ufeff' vô hình — gây lỗi kiểu KeyError: '\ufeffTên' khi đọc CSV. Dùng
encoding="utf-8-sig" để Python tự bỏ nó. Khi ghi CSV cho Excel mở đúng tiếng Việt,
ngược lại bạn cần ghi bằng "utf-8-sig".
6. pathlib — cách hiện đại làm việc với đường dẫn
Trước Python 3.4, đường dẫn là chuỗi và bạn dùng module os.path với hàng chục hàm rời rạc.
pathlib gói tất cả vào một class Path — hướng đối tượng, gọn, và tự lo khác biệt
giữa Windows (\) và Unix (/).
| Việc cần làm | os.path (cũ) | pathlib (nên dùng) |
|---|---|---|
| Nối đường dẫn | os.path.join(a, b, c) | Path(a) / b / c |
| Tệp có tồn tại | os.path.exists(p) | p.exists() |
| Lấy tên tệp | os.path.basename(p) | p.name |
| Lấy thư mục cha | os.path.dirname(p) | p.parent |
| Lấy phần mở rộng | os.path.splitext(p)[1] | p.suffix |
| Tên không có đuôi | os.path.splitext(os.path.basename(p))[0] | p.stem |
| Đường dẫn tuyệt đối | os.path.abspath(p) | p.resolve() |
| Tạo thư mục lồng nhau | os.makedirs(p, exist_ok=True) | p.mkdir(parents=True, exist_ok=True) |
Liệt kê tệp .txt | glob.glob(os.path.join(p, "*.txt")) | p.glob("*.txt") |
| Đọc cả tệp | open(p).read() | p.read_text(encoding="utf-8") |
from pathlib import Path
p = Path("/home/anh/du_an/bao_cao.thang_5.csv")
print(p.name) # 'bao_cao.thang_5.csv' — tên đầy đủ
print(p.stem) # 'bao_cao.thang_5' — bỏ đuôi CUỐI CÙNG
print(p.suffix) # '.csv'
print(p.suffixes) # ['.thang_5', '.csv'] — mọi đuôi
print(p.parent) # Path('/home/anh/du_an')
print(p.parents[1]) # Path('/home/anh') — ông nội
print(p.parts) # ('/', 'home', 'anh', 'du_an', 'bao_cao.thang_5.csv')
# Toán tử / để nối — đọc rất tự nhiên
goc = Path("/du_lieu")
dich = goc / "2026" / "thang_08" / "ban_hang.csv"
print(dich) # /du_lieu/2026/thang_08/ban_hang.csv
# Đổi tên/đuôi mà không cắt chuỗi thủ công
print(p.with_suffix(".json")) # .../bao_cao.thang_5.json
print(p.with_name("tong_hop.csv")) # .../tong_hop.csv
from pathlib import Path
thu_muc = Path("du_lieu")
# --- Kiểm tra ---
thu_muc.exists() # tồn tại?
thu_muc.is_file() # là tệp?
thu_muc.is_dir() # là thư mục?
# --- Tạo / xoá ---
thu_muc.mkdir(parents=True, exist_ok=True) # tạo cả cây, không lỗi nếu đã có
(thu_muc / "tam.txt").unlink(missing_ok=True) # xoá tệp
thu_muc.rmdir() # xoá thư mục RỖNG
# --- Đọc / ghi một phát ---
(thu_muc / "ghi_chu.txt").write_text("Xin chào", encoding="utf-8")
noi_dung = (thu_muc / "ghi_chu.txt").read_text(encoding="utf-8")
byte_du_lieu = (thu_muc / "anh.png").read_bytes()
# --- Duyệt ---
for f in thu_muc.iterdir(): # mọi mục con, 1 cấp
print(f)
for f in thu_muc.glob("*.csv"): # theo mẫu, 1 cấp
print(f)
for f in thu_muc.rglob("*.py"): # ĐỆ QUY mọi cấp (r = recursive)
print(f)
# --- Thông tin ---
p = thu_muc / "ghi_chu.txt"
print(p.stat().st_size) # kích thước (byte)
print(p.stat().st_mtime) # thời điểm sửa cuối (timestamp)
# --- Đường dẫn hữu ích ---
print(Path.cwd()) # thư mục làm việc hiện tại
print(Path.home()) # thư mục home của người dùng
print(Path(__file__).resolve().parent) # thư mục chứa CHÍNH FILE NÀY
from pathlib import Path
# ❌ SAI — phụ thuộc thư mục người dùng đang đứng khi chạy
cau_hinh = Path("config.json")
# ✅ ĐÚNG — luôn trỏ đúng, chạy từ đâu cũng được
THU_MUC_NAY = Path(__file__).resolve().parent
cau_hinh = THU_MUC_NAY / "config.json"
du_lieu = THU_MUC_NAY.parent / "data" / "input.csv"
Path("config.json") là đường dẫn tương đối — nó phụ thuộc
Path.cwd(), tức nơi người dùng gõ lệnh. Chạy python src/app.py từ thư mục gốc
sẽ tìm ./config.json chứ không phải ./src/config.json. Dùng
__file__ là cách duy nhất chắc chắn.
7. JSON — định dạng trao đổi dữ liệu phổ biến nhất
import json
from pathlib import Path
du_lieu = {
"ten": "Nguyễn Việt Anh",
"tuoi": 22,
"ky_nang": ["Python", "TypeScript"],
"dang_hoc": True,
"diem_gpa": None,
}
# --- Ghi ra tệp: dump (có chữ s = string, không s = file) ---
with open("ho_so.json", "w", encoding="utf-8") as f:
json.dump(du_lieu, f,
ensure_ascii=False, # ⚠️ BẮT BUỘC cho tiếng Việt
indent=2) # xuống dòng cho người đọc
# --- Đọc từ tệp ---
with open("ho_so.json", encoding="utf-8") as f:
doc_lai = json.load(f)
# --- Chuỗi ↔ object (dumps / loads) ---
chuoi = json.dumps(du_lieu, ensure_ascii=False)
lai = json.loads(chuoi)
# --- Ngắn gọn nhất với pathlib ---
Path("ho_so.json").write_text(
json.dumps(du_lieu, ensure_ascii=False, indent=2), encoding="utf-8"
)
doc_lai = json.loads(Path("ho_so.json").read_text(encoding="utf-8"))
ensure_ascii=True là mặc định — và nó phá tiếng Việtprint(json.dumps({"ten": "Việt Anh"}))
# {"ten": "Vi\u1ec7t Anh"} ← đúng về kỹ thuật, không ai đọc nổi
print(json.dumps({"ten": "Việt Anh"}, ensure_ascii=False))
# {"ten": "Việt Anh"} ← ✅
Mặc định này có từ thời JSON hay bị truyền qua kênh chỉ chấp nhận ASCII. Ngày nay mọi thứ đều UTF-8, nên
hãy luôn đặt ensure_ascii=False — trừ khi bạn thực sự cần tương thích hệ thống cũ.
| JSON | Python | Ghi chú chiều ngược lại |
|---|---|---|
object | dict | Key JSON luôn là chuỗi — {1: "a"} ghi ra thành {"1": "a"} |
array | list | tuple cũng ghi thành array, nhưng đọc lại ra list |
string | str | |
number (int) | int | |
number (real) | float | Decimal không ghi được — phải tự chuyển |
true / false | True / False | |
null | None | |
| — | set, datetime, Path, class tự viết | Ném TypeError — cần default= |
import json
from datetime import datetime
from dataclasses import dataclass, asdict
from pathlib import Path
@dataclass
class SuKien:
ten: str
luc: datetime
def ma_hoa(o: object):
"""Được gọi cho MỌI object json không biết cách xử lý."""
if isinstance(o, datetime):
return o.isoformat()
if isinstance(o, set):
return sorted(o)
if isinstance(o, Path):
return str(o)
raise TypeError(f"Không serialize được {type(o).__name__}")
sk = SuKien("Họp nhóm", datetime(2026, 8, 14, 9, 30))
print(json.dumps(asdict(sk), default=ma_hoa, ensure_ascii=False))
# {"ten": "Họp nhóm", "luc": "2026-08-14T09:30:00"}
# Bắt lỗi cú pháp JSON đúng cách
try:
json.loads('{"a": 1,}') # JSON không cho dấu phẩy thừa
except json.JSONDecodeError as e:
print(f"Sai cú pháp tại dòng {e.lineno}, cột {e.colno}: {e.msg}")
8. CSV — bảng dữ liệu dạng văn bản
CSV trông đơn giản đến mức nhiều người tự tách bằng line.split(","). Đừng làm thế: một ô chứa
dấu phẩy ("Hà Nội, Việt Nam"), chứa dấu nháy kép, hay chứa xuống dòng sẽ phá tan cách làm đó.
Module csv xử lý đủ mọi trường hợp biên.
import csv
# --- ĐỌC bằng DictReader (khuyên dùng — truy cập theo tên cột) ---
with open("sinh_vien.csv", encoding="utf-8", newline="") as f:
for hang in csv.DictReader(f):
print(hang["ho_ten"], hang["diem"]) # hang là dict, GIÁ TRỊ LUÔN LÀ str
# --- ĐỌC bằng reader (theo chỉ số) ---
with open("sinh_vien.csv", encoding="utf-8", newline="") as f:
doc = csv.reader(f)
tieu_de = next(doc) # bỏ qua dòng tiêu đề
for hang in doc:
print(hang[0], hang[2]) # hang là list[str]
# --- GHI bằng DictWriter ---
sinh_vien = [
{"ho_ten": "Nguyễn Việt Anh", "lop": "CNTT-K18", "diem": 8.5},
{"ho_ten": "Trần Thị Bình", "lop": "CNTT-K18", "diem": 9.0},
]
with open("ket_qua.csv", "w", encoding="utf-8-sig", newline="") as f:
ghi = csv.DictWriter(f, fieldnames=["ho_ten", "lop", "diem"])
ghi.writeheader()
ghi.writerows(sinh_vien)
csv-
newline=""là bắt buộc. Không có nó, trên Windows tệp ghi ra sẽ có dòng trống xen kẽ (vì cả modulecsvlẫn tầng văn bản đều dịch ký tự xuống dòng). Đây là yêu cầu nêu rõ trong tài liệu Python, không phải mẹo. -
Mọi giá trị đọc ra đều là
str.hang["diem"]trả"8.5"chứ không phải8.5. Phải tự chuyển:float(hang["diem"]). Cột rỗng cho"", không phảiNone. -
Excel và tiếng Việt. Ghi bằng
encoding="utf-8"thuần thì Excel mở lên thấy chữ rác. Dùng"utf-8-sig"(có BOM) để Excel nhận đúng.
import csv
from collections import defaultdict
from pathlib import Path
def diem_trung_binh_theo_lop(duong_dan: Path) -> dict[str, float]:
tong: dict[str, float] = defaultdict(float)
dem: dict[str, int] = defaultdict(int)
with duong_dan.open(encoding="utf-8-sig", newline="") as f:
for so_dong, hang in enumerate(csv.DictReader(f), start=2):
try:
diem = float(hang["diem"])
except (ValueError, KeyError) as e:
print(f"⚠️ Bỏ qua dòng {so_dong}: {e}")
continue
lop = hang["lop"].strip()
tong[lop] += diem
dem[lop] += 1
return {lop: round(tong[lop] / dem[lop], 2) for lop in tong}
print(diem_trung_binh_theo_lop(Path("sinh_vien.csv")))
# {'CNTT-K18': 8.75, 'CNTT-K19': 7.9}
Lưu ý duong_dan.open(...) — Path có method open riêng, nhận đúng các
tham số như hàm built-in. Và enumerate(..., start=2) để số dòng khớp với thứ tự trong Excel
(dòng 1 là tiêu đề).
9. Regex — module re
Biểu thức chính quy (regular expression) là một ngôn ngữ nhỏ để mô tả mẫu văn bản.
Thay vì hỏi "chuỗi này có bằng abc không", bạn hỏi "chuỗi này có khớp với ba chữ cái theo
sau là hai chữ số không".
import re
van_ban = "Liên hệ: an@example.com hoặc binh.tran@company.vn"
# r"" = raw string — BẮT BUỘC với regex (xem callout bên dưới)
mau = r"[\w.]+@[\w.]+\.\w+"
print(re.findall(mau, van_ban))
# ['an@example.com', 'binh.tran@company.vn']
r"..."# Chuỗi thường: Python xử lý \ TRƯỚC, regex nhận được thứ khác
print("\d") # SyntaxWarning: invalid escape sequence — may mắn còn cảnh báo
print("\n") # thành ký tự xuống dòng thật, regex không thấy \n nữa
print("\\d") # phải escape đôi mới ra \d — rối mắt
# Raw string: Python KHÔNG xử lý \, đưa nguyên xi cho regex
print(r"\d") # \d — đúng thứ regex cần
print(r"\n") # \n (hai ký tự), regex hiểu là "xuống dòng"
Quy tắc: mọi mẫu regex đều viết trong r"...", kể cả khi mẫu không có dấu
\. Nhất quán quan trọng hơn tiết kiệm một ký tự.
9.1. Bảng cú pháp regex
| Ký tự đại diện | ||
|---|---|---|
| Mẫu | Khớp với | Ví dụ |
. | Bất kỳ ký tự nào trừ xuống dòng | a.c → abc, a1c |
\d | Chữ số [0-9] | \d\d → 42 |
\D | Không phải chữ số | |
\w | Chữ, số, gạch dưới (kể cả tiếng Việt có dấu!) | \w+ → Việt_Anh1 |
\W | Không phải \w | |
\s | Khoảng trắng (space, tab, xuống dòng) | |
\S | Không phải khoảng trắng | |
[abc] | Một trong các ký tự liệt kê | [aeiou] → nguyên âm |
[^abc] | Bất kỳ ký tự nào ngoài danh sách | [^0-9] |
[a-z] | Khoảng ký tự | [A-Za-z0-9] |
| Lượng từ (quantifier) — "bao nhiêu lần" | ||
|---|---|---|
| Mẫu | Số lần | Ví dụ |
* | 0 trở lên | ab* → a, ab, abbb |
+ | 1 trở lên | ab+ → ab, abbb (không khớp a) |
? | 0 hoặc 1 (tuỳ chọn) | colou?r → color, colour |
{n} | Đúng n lần | \d{4} → 2026 |
{n,} | Ít nhất n lần | \d{2,} |
{n,m} | Từ n đến m lần | \d{2,4} |
*? +? ?? | Lười — khớp ít nhất có thể | xem callout bên dưới |
| Neo (anchor) và nhóm | ||
|---|---|---|
| Mẫu | Ý nghĩa | Ví dụ |
^ | Đầu chuỗi (hoặc đầu dòng với cờ M) | ^Xin |
$ | Cuối chuỗi (hoặc cuối dòng với cờ M) | \.$ |
\b | Ranh giới từ | \bcat\b khớp cat nhưng không khớp trong category |
(...) | Nhóm bắt (capturing group) | (\d{4})-(\d{2}) |
(?:...) | Nhóm không bắt — chỉ để gom | (?:ab)+ |
(?P<ten>...) | Nhóm có tên ⭐ | (?P<nam>\d{4}) |
| | Hoặc | mèo|chó |
(?=...) | Lookahead khẳng định — "theo sau phải là" | \d+(?=đ) |
(?!...) | Lookahead phủ định — "theo sau không được là" | \d+(?!%) |
(?<=...) | Lookbehind khẳng định — "phía trước phải là" | (?<=\$)\d+ |
import re
html = "<b>đậm</b> và <i>nghiêng</i>"
# THAM LAM (mặc định) — .* nuốt càng nhiều càng tốt
print(re.findall(r"<.*>", html))
# ['<b>đậm</b> và <i>nghiêng</i>'] ← một kết quả, nuốt cả chuỗi!
# LƯỜI — thêm ? sau lượng từ
print(re.findall(r"<.*?>", html))
# ['<b>', '</b>', '<i>', '</i>'] ← ✅ bốn thẻ riêng biệt
# Cách thứ ba, thường tốt nhất: cấm ký tự kết thúc trong lớp
print(re.findall(r"<[^>]*>", html))
# ['<b>', '</b>', '<i>', '</i>'] ← ✅ và nhanh hơn .*?
Mặc định lượng từ *, +, {n,m} đều tham lam: chúng
khớp dài nhất có thể rồi mới lùi dần. Thêm ? để đổi sang lười: khớp ngắn
nhất rồi mới tiến dần.
9.2. Bảy hàm của module re
| Hàm | Trả về | Tìm ở đâu | Dùng khi |
|---|---|---|---|
re.search(p, s) | Match hoặc None | Bất kỳ đâu trong chuỗi | Mặc định — tìm lần khớp đầu tiên |
re.match(p, s) | Match hoặc None | Chỉ từ đầu chuỗi | Kiểm tra tiền tố |
re.fullmatch(p, s) | Match hoặc None | Phải khớp toàn bộ | Kiểm tra hợp lệ (validate) |
re.findall(p, s) | list[str] (hoặc list tuple nếu có nhóm) | Mọi lần khớp | Lấy nhanh danh sách |
re.finditer(p, s) | Iterator các Match | Mọi lần khớp | Cần vị trí, nhóm; tệp lớn |
re.sub(p, r, s) | str mới | Mọi lần khớp | Thay thế |
re.split(p, s) | list[str] | Mọi lần khớp | Tách theo mẫu phức tạp |
import re
s = "Đơn DH-2026-0042 ngày 14/08/2026, tổng 1500000đ"
# search — tìm ở bất kỳ đâu
m = re.search(r"DH-\d{4}-\d{4}", s)
print(m.group()) # 'DH-2026-0042'
print(m.start(), m.end()) # 4 16 — vị trí trong chuỗi
# match — chỉ từ đầu chuỗi → None vì chuỗi bắt đầu bằng "Đơn"
print(re.match(r"DH-\d+", s)) # None
# fullmatch — validate: phải khớp TOÀN BỘ
print(re.fullmatch(r"\d{10}", "0912345678")) # khớp
print(re.fullmatch(r"\d{10}", "0912345678x")) # None ✅
# findall — lấy nhanh mọi số
print(re.findall(r"\d+", s))
# ['2026', '0042', '14', '08', '2026', '1500000']
# finditer — khi cần cả vị trí
for m in re.finditer(r"\d+", s):
print(f"{m.group()} tại {m.start()}")
# sub — thay thế (che số điện thoại chẳng hạn)
print(re.sub(r"\d{7,}", "[ẩn]", s))
# 'Đơn DH-2026-0042 ngày 14/08/2026, tổng [ẩn]đ'
# split — tách theo nhiều loại dấu phân cách
print(re.split(r"[,;|]\s*", "a, b;c | d"))
# ['a', 'b', 'c', 'd']
9.3. Nhóm và nhóm có tên
import re
# --- Nhóm đánh số ---
m = re.search(r"(\d{2})/(\d{2})/(\d{4})", "Hạn: 14/08/2026")
print(m.group(0)) # '14/08/2026' — nhóm 0 = toàn bộ khớp
print(m.group(1)) # '14'
print(m.groups()) # ('14', '08', '2026')
# --- Nhóm CÓ TÊN — dễ đọc hơn nhiều, dùng cái này ---
mau = r"(?P<ngay>\d{2})/(?P<thang>\d{2})/(?P<nam>\d{4})"
m = re.search(mau, "Hạn: 14/08/2026")
print(m.group("nam")) # '2026'
print(m.groupdict()) # {'ngay': '14', 'thang': '08', 'nam': '2026'}
# --- Tham chiếu nhóm trong chuỗi thay thế ---
# Đổi dd/mm/yyyy → yyyy-mm-dd
print(re.sub(mau, r"\g<nam>-\g<thang>-\g<ngay>", "Hạn: 14/08/2026"))
# 'Hạn: 2026-08-14'
# --- Nhóm không bắt (?:...) — gom mà không tạo nhóm ---
print(re.findall(r"(?:https?://)?(\w+\.com)", "https://a.com và b.com"))
# ['a.com', 'b.com'] — nếu dùng (...) thay (?:...) sẽ ra list tuple lộn xộn
# --- sub với hàm thay thế: xử lý phức tạp ---
def tang_gia(m: re.Match) -> str:
return str(int(m.group()) * 2)
print(re.sub(r"\d+", tang_gia, "Giá 100 và 250"))
# 'Giá 200 và 500'
findall đổi kiểu trả về khi có nhóms = "a1 b2 c3"
re.findall(r"\w\d", s) # ['a1', 'b2', 'c3'] — không nhóm → list[str]
re.findall(r"(\w)\d", s) # ['a', 'b', 'c'] — 1 nhóm → list nội dung nhóm
re.findall(r"(\w)(\d)", s) # [('a','1'), ('b','2'), ...] — nhiều nhóm → list[tuple]
Ba mẫu gần giống nhau cho ba kiểu dữ liệu khác nhau — nguồn của rất nhiều TypeError khó
hiểu. Khi có nhóm, hãy dùng finditer và tự lấy m.group() để kiểm soát rõ ràng.
9.4. Cờ và biên dịch trước
| Cờ | Viết tắt | Tác dụng |
|---|---|---|
re.IGNORECASE | re.I | Không phân biệt hoa/thường |
re.MULTILINE | re.M | ^/$ khớp đầu/cuối mỗi dòng |
re.DOTALL | re.S | . khớp cả ký tự xuống dòng |
re.VERBOSE | re.X | Cho phép khoảng trắng và chú thích trong mẫu ⭐ |
re.ASCII | re.A | \w, \d, \s chỉ khớp ASCII |
import re
# --- Biên dịch trước: dùng lại nhiều lần, đọc rõ hơn ---
MAU_EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+", re.IGNORECASE)
for dong in danh_sach:
if MAU_EMAIL.search(dong): # gọi method trên object đã compile
...
# --- VERBOSE: viết regex phức tạp mà vẫn đọc được ---
MAU_SDT_VN = re.compile(r"""
^ # đầu chuỗi
(?:\+84|0) # mã quốc gia +84 hoặc số 0
\s?
(3|5|7|8|9) # đầu số di động Việt Nam hợp lệ
\d{8} # 8 chữ số còn lại
$ # cuối chuỗi
""", re.VERBOSE)
print(bool(MAU_SDT_VN.match("0912345678"))) # True
print(bool(MAU_SDT_VN.match("+84912345678"))) # True
print(bool(MAU_SDT_VN.match("0112345678"))) # False — đầu số 1 không hợp lệ
# --- MULTILINE ---
log = """INFO khởi động
ERROR không kết nối được
INFO thử lại"""
print(re.findall(r"^ERROR.*", log, re.MULTILINE))
# ['ERROR không kết nối được']
\w và tiếng Việt
Trong Python 3, mẫu regex trên chuỗi str mặc định hoạt động ở chế độ
Unicode. Vì vậy \w khớp cả ệ, ữ, Đ — khác với
nhiều ngôn ngữ khác. Nếu bạn muốn chỉ ASCII (ví dụ kiểm tra tên biến), thêm cờ
re.ASCII. Ngược lại, để kiểm tra "chỉ chữ cái tiếng Việt", dùng
[^\W\d_] (là \w trừ chữ số và gạch dưới).
10. Khi nào không nên dùng regex
| Việc cần làm | Đừng dùng regex | Dùng cái này |
|---|---|---|
| Kiểm tra chứa chuỗi con | re.search(r"abc", s) | "abc" in s |
| Kiểm tra tiền tố/hậu tố | re.match(r"^http", s) | s.startswith("http") |
| Tách theo một ký tự | re.split(r",", s) | s.split(",") |
| Thay thế chuỗi cố định | re.sub(r"a", "b", s) | s.replace("a", "b") |
| Phân tích HTML | Bất kỳ regex nào | BeautifulSoup, lxml |
| Phân tích JSON/XML/CSV | Bất kỳ regex nào | json, xml, csv |
| Phân tích URL | Regex tự chế | urllib.parse.urlparse |
| Kiểm tra email chuẩn RFC | Regex 300 ký tự trên mạng | Kiểm tra thô + gửi email xác thực |
import re
# Mẫu có lượng từ lồng nhau — "catastrophic backtracking"
re.match(r"(a+)+$", "a" * 30 + "b")
# Treo vài phút đến vài giờ — engine thử 2^30 tổ hợp
Bộ máy regex của Python dùng thuật toán quay lui (backtracking). Mẫu có lượng từ lồng nhau
((a+)+, (a|a)*, (.*)*) trên chuỗi không khớp sẽ khiến số bước tăng
theo hàm mũ. Nếu mẫu regex đến từ người dùng, đây là lỗ hổng từ chối dịch vụ thật.
Phòng tránh: tránh lượng từ lồng nhau; ưu tiên lớp ký tự phủ định
([^"]* thay .*?); với dữ liệu không tin cậy, cân nhắc thư viện
regex có tham số timeout.
11. Cheat sheet chương 7
| Tình huống | Cách viết |
|---|---|
| Đọc cả tệp văn bản | Path(p).read_text(encoding="utf-8") |
| Ghi cả tệp văn bản | Path(p).write_text(s, encoding="utf-8") |
| Đọc tệp lớn | with open(p, encoding="utf-8") as f: for dong in f: |
| Thêm vào cuối tệp | open(p, "a", encoding="utf-8") |
| Tạo tệp, lỗi nếu đã có | open(p, "x", encoding="utf-8") |
| Tệp nhị phân | open(p, "rb") / Path(p).read_bytes() |
| Ghi an toàn | Ghi .tmp rồi tmp.replace(dich) |
| Nối đường dẫn | Path(a) / b / c |
| Thư mục chứa file hiện tại | Path(__file__).resolve().parent |
| Tìm tệp đệ quy | Path(p).rglob("*.py") |
| Tạo cây thư mục | p.mkdir(parents=True, exist_ok=True) |
| JSON có tiếng Việt | json.dump(d, f, ensure_ascii=False, indent=2) |
| CSV đọc theo tên cột | csv.DictReader(f) + newline="" |
| CSV cho Excel tiếng Việt | encoding="utf-8-sig" |
| Regex — kiểm tra hợp lệ | re.fullmatch(r"...", s) |
| Regex — tìm tất cả kèm vị trí | re.finditer(r"...", s) |
| Regex — mẫu phức tạp | re.compile(r"""...""", re.VERBOSE) |
| Regex — khớp ngắn nhất | Thêm ?: .*? — hoặc tốt hơn: [^x]* |
Bài tập
Công cụ phân tích tệp nhật ký
Cho tệp app.log với định dạng mỗi dòng:
2026-08-14 09:15:32 INFO [api] Khởi động thành công
2026-08-14 09:15:45 ERROR [db] Kết nối thất bại sau 3000ms
2026-08-14 09:16:02 WARN [api] Phản hồi chậm: 1520ms
Viết phan_tich(duong_dan: Path) -> dict trả về:
- Số dòng theo từng cấp độ (
{"INFO": 12, "ERROR": 3, ...}) — dùngCounter. - Danh sách
(thoi_gian, module, thong_diep)của mọi dòngERROR. - Thời gian phản hồi trung bình, lấy từ mọi con số có hậu tố
ms.
Yêu cầu: một regex duy nhất có nhóm có tên; đọc lười từng dòng để chạy được với tệp 5 GB.
Gợi ý
Khung mẫu: r"(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(?P<cap>\w+)\s+\[(?P<mod>\w+)\]\s+(?P<msg>.*)".
Nhớ re.compile ngoài vòng lặp. Dòng không khớp mẫu (log nhiều dòng, traceback) phải
được xử lý — m sẽ là None, đừng để nổ AttributeError.
Lấy số ms bằng regex thứ hai r"(\d+)ms" trên nhóm msg.
Chuyển đổi CSV ↔ JSON hai chiều
Viết hai hàm và một CLI nhỏ:
def csv_sang_json(nguon: Path, dich: Path, kieu: dict[str, type] | None = None) -> int: ...
def json_sang_csv(nguon: Path, dich: Path) -> int: ...
# Cả hai trả về số bản ghi đã xử lý
# Ví dụ: ép kiểu cột khi đọc CSV
csv_sang_json(Path("sv.csv"), Path("sv.json"),
kieu={"diem": float, "tuoi": int})
Yêu cầu: tiếng Việt hiển thị đúng ở cả hai chiều; Excel mở được tệp CSV sinh ra; dòng CSV hỏng thì báo số dòng và bỏ qua thay vì dừng.
Gợi ý
Ba điểm quyết định: ensure_ascii=False khi ghi JSON;
encoding="utf-8-sig" khi ghi CSV (cho Excel); newline="" cho mọi tệp CSV.
Với json_sang_csv, lấy fieldnames từ union các khóa của mọi bản ghi
(không phải chỉ bản ghi đầu) để không mất cột.
Đổi tên hàng loạt bằng pathlib
Viết doi_ten(thu_muc: Path, khuon: str, khô_chạy: bool = True) đổi tên mọi tệp ảnh
(.jpg, .jpeg, .png) trong thư mục theo khuôn mẫu, ví dụ
"anh_{i:03d}{ext}" → anh_001.jpg, anh_002.png…
- Sắp xếp theo thời gian sửa đổi (
stat().st_mtime) trước khi đánh số. khô_chạy=Truechỉ in ra sẽ đổi gì, không động vào đĩa — mặc định an toàn.- Xử lý xung đột: tên đích đã tồn tại thì không được ghi đè.
- Có tuỳ chọn đệ quy vào thư mục con.
Gợi ý
Dùng p.rename(dich) hoặc p.replace(dich) — khác nhau: rename
lỗi nếu đích tồn tại trên Windows, replace luôn ghi đè. Ở đây bạn muốn
không ghi đè nên kiểm tra dich.exists() trước.
Bẫy tinh vi: đổi a.jpg → b.jpg rồi b.jpg → c.jpg có thể giẫm lên nhau.
Cách chắc chắn: đổi qua tên tạm ở lượt một, rồi đổi sang tên cuối ở lượt hai.
Bộ kiểm tra hợp lệ bằng regex
Viết các hàm trả về bool, dùng re.compile ở cấp module và re.fullmatch:
la_sdt_vn(s)—0912345678hoặc+84912345678; đầu số hợp lệ 3/5/7/8/9.la_email(s)— kiểm tra thô, hợp lý (đừng cố theo RFC 5322).la_mat_khau_manh(s)— ≥8 ký tự, có chữ hoa, chữ thường, số, ký tự đặc biệt. Dùng lookahead.la_ma_so_thue(s)— 10 chữ số, hoặc 10 chữ số +-+ 3 chữ số.la_ngay_iso(s)—YYYY-MM-DD, tháng 01-12, ngày 01-31.
Viết bảng test cho mỗi hàm: ít nhất 3 giá trị hợp lệ và 3 giá trị không hợp lệ.
Gợi ý
Mật khẩu mạnh là bài tập lookahead kinh điển:
r"(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}" — mỗi
(?=...) kiểm tra một điều kiện mà không tiêu thụ ký tự nào, nên bốn điều
kiện cùng áp lên một chuỗi.
Câu hỏi suy ngẫm cho la_ngay_iso: regex kiểm tra được ngày 31/02 không? Không —
đó là lúc bạn nên dùng datetime.strptime thay vì regex.
Che thông tin nhạy cảm trong tệp nhật ký
Viết che_du_lieu(nguon: Path, dich: Path) -> dict[str, int] đọc tệp log và ghi ra bản
đã che, trả về số lần che theo từng loại:
- Email →
a***@example.com(giữ ký tự đầu và tên miền) - Số điện thoại →
091****678(giữ 3 đầu, 3 cuối) - Số thẻ 16 chữ số →
****-****-****-1234 "password": "..."trong JSON →"password": "[ẨN]"
Dùng re.sub với hàm thay thế cho các trường hợp cần giữ một phần.
Gợi ý
Hàm thay thế nhận một re.Match và trả về chuỗi:
re.sub(MAU_EMAIL, lambda m: m.group(1)[0] + "***@" + m.group(2), s).
Xử lý theo thứ tự từ mẫu cụ thể nhất đến chung nhất — nếu che số điện thoại
trước, mẫu số thẻ 16 chữ số có thể không còn khớp. Đếm số lần che bằng
re.subn (trả về tuple (chuoi_moi, so_lan)).
Quiz
Chế độ "w" và "a" khác nhau thế nào? Chuyện gì xảy ra với tệp đã tồn tại?
Xem đáp án
"w" xoá sạch nội dung tệp ngay khoảnh khắc open() chạy —
không phải lúc bạn gọi write(). Kể cả chương trình sập ngay dòng sau, dữ liệu cũ đã mất.
Con trỏ đặt ở đầu tệp.
"a" giữ nguyên nội dung cũ và đặt con trỏ ở cuối tệp; mọi lần
write() đều nối thêm vào cuối, kể cả sau khi bạn gọi seek(0).
Cả hai đều tạo tệp mới nếu chưa tồn tại. Muốn được bảo vệ khỏi ghi đè, dùng "x" —
nó ném FileExistsError nếu tệp đã có.
Vì sao phải dùng with open(...) thay vì open() rồi close()?
Xem đáp án
Vì with đảm bảo close() chạy kể cả khi có exception giữa
chừng. Với cách thủ công, một lỗi trước dòng close() sẽ khiến tệp không bao giờ được
đóng. Ba hậu quả: (1) dữ liệu còn trong bộ đệm không được xả xuống đĩa → mất dữ liệu; (2) cạn file
descriptor → OSError: Too many open files; (3) trên Windows tệp bị khoá, không xoá/đổi
tên được.
CPython thường tự đóng nhờ bộ đếm tham chiếu, nhưng đó là chi tiết cài đặt của CPython,
không phải cam kết của ngôn ngữ — PyPy không đảm bảo điều đó. with còn ngắn hơn
try/finally và không thể quên.
Vì sao luôn phải truyền encoding="utf-8"?
Xem đáp án
Không truyền thì Python dùng bảng mã mặc định của hệ thống
(locale.getpreferredencoding()) — thứ khác nhau giữa các máy. macOS và
Linux hiện đại thường là UTF-8, nhưng Windows tiếng Việt có thể là cp1258. Hệ quả: code chạy hoàn hảo
trên máy bạn nhưng ném UnicodeDecodeError trên máy đồng nghiệp, hoặc tệ hơn — chạy được
nhưng ghi ra chữ rác mà không ai phát hiện cho tới khi quá muộn.
Python 3.15 sẽ đổi mặc định thành UTF-8 (PEP 686), nhưng viết tường minh vẫn đúng vì code phải chạy
được trên bản cũ. Tìm chỗ còn thiếu bằng python -X warn_default_encoding.
pathlib.Path hơn os.path ở điểm nào?
Xem đáp án
Bốn điểm: (1) Hướng đối tượng — p.exists(), p.parent,
p.suffix thay vì gọi hàm rời rạc os.path.exists(p).
(2) Toán tử / — Path("a") / "b" / "c" đọc tự nhiên hơn
os.path.join lồng nhau. (3) Đa nền tảng — tự dùng đúng dấu phân cách
cho Windows và Unix. (4) Nối chuỗi thao tác —
Path(__file__).resolve().parent / "data" / "x.csv" viết một hơi, còn với
os.path phải lồng ba lớp hàm.
Ngoài ra Path có sẵn read_text, write_text,
read_bytes, glob, rglob, mkdir(parents=True) —
gộp việc của nhiều module (os, glob, shutil) vào một chỗ.
Vì sao mở tệp CSV phải truyền newline=""?
Xem đáp án
Vì có hai tầng cùng xử lý ký tự xuống dòng: tầng văn bản của open()
(dịch \n ↔ \r\n theo hệ điều hành) và module csv (tự quản lý
ký tự kết thúc bản ghi). Khi cả hai cùng làm việc, trên Windows tệp ghi ra sẽ có \r\r\n
→ mở lên thấy dòng trống xen kẽ giữa mọi dòng dữ liệu.
newline="" tắt tầng dịch của open(), giao toàn quyền cho module
csv. Đây là yêu cầu nêu rõ trong tài liệu Python, áp dụng cho cả đọc và ghi.
Khi đọc, nó còn giúp xử lý đúng ô có chứa ký tự xuống dòng bên trong dấu nháy kép.
re.match, re.search, re.fullmatch khác nhau ra sao?
Xem đáp án
re.search tìm lần khớp đầu tiên ở bất kỳ đâu trong chuỗi — đây là hàm
bạn muốn trong hầu hết trường hợp.
re.match chỉ khớp từ đầu chuỗi, nhưng không bắt buộc khớp hết:
re.match(r"\d+", "123abc") vẫn khớp "123". Tên hàm gây hiểu lầm nhất module.
re.fullmatch bắt buộc khớp toàn bộ chuỗi — đây mới là hàm dùng để
kiểm tra hợp lệ.
re.search(r"\d+", "abc123") # khớp '123'
re.match(r"\d+", "abc123") # None
re.match(r"\d+", "123abc") # khớp '123' ⚠️ vẫn khớp!
re.fullmatch(r"\d+", "123abc") # None ✅ đúng cho validate
.* và .*? khác nhau thế nào? Cho ví dụ hậu quả.
Xem đáp án
.* tham lam — khớp dài nhất có thể rồi mới lùi dần cho tới khi phần
còn lại của mẫu khớp được. .*? lười — khớp ngắn nhất rồi mới nới dần.
html = "<b>đậm</b> và <i>nghiêng</i>"
re.findall(r"<.*>", html) # ['<b>đậm</b> và <i>nghiêng</i>'] ❌ nuốt hết
re.findall(r"<.*?>", html) # ['<b>', '</b>', '<i>', '</i>'] ✅
re.findall(r"<[^>]*>", html) # ['<b>', '</b>', '<i>', '</i>'] ✅ và nhanh hơn
Cách thứ ba — dùng lớp ký tự phủ định [^>]* — thường là tốt nhất: nó diễn đạt ý định
rõ ràng hơn ("mọi ký tự trừ dấu đóng") và không cần quay lui, nên nhanh hơn và tránh được rủi ro
ReDoS.
Khi nào không nên dùng regex?
Xem đáp án
Ba nhóm tình huống:
(1) Khi method chuỗi đủ dùng — "abc" in s,
s.startswith(...), s.replace(...), s.split(",") đều nhanh hơn
và dễ đọc hơn regex tương đương.
(2) Khi dữ liệu có cấu trúc lồng nhau — HTML, XML, JSON, CSV. Regex là ngôn ngữ
chính quy, về mặt lý thuyết không thể phân tích đúng cấu trúc lồng nhau tuỳ ý. Dùng
BeautifulSoup, json, csv, urllib.parse.
(3) Khi mẫu đến từ người dùng — rủi ro ReDoS: mẫu có lượng từ lồng
nhau như (a+)+$ khiến engine thử số tổ hợp tăng theo hàm mũ, treo cả tiến trình. Nếu bắt
buộc, dùng thư viện regex có tham số timeout.
Quy tắc thực dụng: nếu regex của bạn dài quá một dòng, hãy viết lại bằng
re.VERBOSE kèm chú thích — hoặc cân nhắc rằng có thể nó không nên là regex.
Tổng kết
Sau chương 7, bạn đã master:
- Chế độ mở tệp:
r/w/a/x+b/+;"w"xoá tệp ngay lúc mở. withlà bắt buộc — đảm bảo đóng tệp kể cả khi có exception.- Đọc tệp:
for dong in flà mặc định, chạy được với tệp lớn hơn RAM. - Ghi tệp:
writekhông tự thêm\n; mẫu ghi nguyên tử qua tệp tạm +replace. - Encoding: luôn
encoding="utf-8"; hiểu BOM vàutf-8-sig; các chế độerrors=. pathlib:/để nối,read_text/write_text,glob/rglob,Path(__file__).resolve().parent.- JSON:
ensure_ascii=Falsecho tiếng Việt;default=cho kiểu tự định nghĩa. - CSV:
DictReader/DictWriter,newline="",utf-8-sigcho Excel, mọi giá trị đọc ra làstr. - Regex: raw string, bảng cú pháp, tham lam vs lười, nhóm có tên, 7 hàm của
re, cờ vàcompile. - Giới hạn của regex: khi nào dùng method chuỗi hoặc parser chuyên dụng; rủi ro ReDoS.
Kết nối
- Chương 3 (Data Structures) —
Counter,defaultdictdùng liên tục khi tổng hợp dữ liệu từ tệp. - Chương 5 (Exceptions) —
FileNotFoundError,PermissionError,UnicodeDecodeError,JSONDecodeError; EAFP tránh TOCTOU khi thao tác tệp. - Chương 6 (Modules) —
pathlib,json,csv,ređều là stdlib;Path(__file__)để định vị tài nguyên trong package. - Chương 9 (Iterators & Generators) — đối tượng tệp là iterator; viết generator đọc tệp lười để dựng pipeline xử lý dữ liệu.
- Chương 10 (Context Managers) — cơ chế
__enter__/__exit__đứng sauwith; tự viết context manager cho tài nguyên của bạn. - Chương 11 (Async Python) — I/O tệp là thao tác chặn; dùng
asyncio.to_threadhoặcaiofilestrong code async. - Chương 14 (Dự án) — ghép tất cả: đọc CSV, gọi API, ghi JSON, sinh báo cáo.
- JavaScript Chương 8 — đối chiếu
fs/promises: Node bắt buộc bất đồng bộ, Python mặc định đồng bộ. - CLI pillar —
grep,sed,awkdùng chung ngôn ngữ regex (phương ngữ POSIX/PCRE, khác Python vài chi tiết nhỏ).