Module pathlib thay thế cách làm việc với đường dẫn bằng chuỗi. Nó có từ Python 3.4 nhưng nhiều người vẫn quen dùng os.path — chuyển sang pathlib làm code sạch hơn hẳn.
So sánh
import os.path
duong_dan = os.path.join("du_lieu", "2026", "bao_cao.csv")
ten = os.path.basename(duong_dan)
thu_muc = os.path.dirname(duong_dan)
duoi = os.path.splitext(duong_dan)[1]
if os.path.exists(duong_dan): ...
from pathlib import Path
duong_dan = Path("du_lieu") / "2026" / "bao_cao.csv"
ten = duong_dan.name
thu_muc = duong_dan.parent
duoi = duong_dan.suffix
if duong_dan.exists(): ...
Toán tử / tự dùng đúng dấu phân cách của hệ điều hành, nên code chạy được cả trên Windows lẫn Linux mà không phải nghĩ tới.
Các thuộc tính
p = Path("/var/www/du_lieu/bao_cao.2026.csv")
p.name # 'bao_cao.2026.csv'
p.stem # 'bao_cao.2026'
p.suffix # '.csv'
p.suffixes # ['.2026', '.csv']
p.parent # Path('/var/www/du_lieu')
p.parents[1] # Path('/var/www')
p.parts # ('/', 'var', 'www', 'du_lieu', 'bao_cao.2026.csv')
p.is_absolute()
Đổi tên và phần mở rộng
p.with_suffix(".json") # bao_cao.2026.json
p.with_name("moi.csv") # /var/www/du_lieu/moi.csv
p.with_stem("bao_cao_moi") # Python 3.9+
Đọc ghi nhanh
noi_dung = p.read_text(encoding="utf-8")
du_lieu = p.read_bytes()
p.write_text("nội dung", encoding="utf-8")
p.write_bytes(du_lieu)
# Vẫn dùng được với with khi cần kiểm soát chi tiết
with p.open(encoding="utf-8") as f:
for dong in f:
...
Với tệp nhỏ, read_text gọn hơn nhiều so với mở rồi đọc rồi đóng. Nhớ luôn chỉ rõ encoding.
Duyệt thư mục
thu_muc = Path("du_lieu")
list(thu_muc.iterdir()) # mọi thứ trong thư mục
list(thu_muc.glob("*.csv")) # tệp csv ở cấp một
list(thu_muc.rglob("*.py")) # đệ quy mọi cấp
list(thu_muc.glob("**/2026/*.csv")) # mẫu phức tạp
# Chỉ lấy tệp, bỏ thư mục
tep = [p for p in thu_muc.rglob("*") if p.is_file()]
# Sắp xếp theo thời gian sửa
moi_nhat = sorted(thu_muc.glob("*.log"),
key=lambda p: p.stat().st_mtime, reverse=True)[:5]
glob trả về generator nên duyệt được thư mục rất lớn mà không tốn bộ nhớ.
Tạo và xóa
Path("ket_qua/2026/thang_09").mkdir(parents=True, exist_ok=True)
Path("tam.txt").unlink(missing_ok=True) # không lỗi nếu tệp không tồn tại
Path("thu_muc_rong").rmdir() # chỉ xóa được thư mục rỗng
import shutil
shutil.rmtree("thu_muc_co_noi_dung")
Hai tham số parents=True và exist_ok=True nên thành thói quen — chúng loại bỏ nhu cầu kiểm tra trước khi tạo.
Thông tin tệp
from datetime import datetime
t = p.stat()
print(f"{t.st_size:,} byte")
print(datetime.fromtimestamp(t.st_mtime))
p.exists()
p.is_file()
p.is_dir()
p.is_symlink()
Đường dẫn hữu ích
Path.cwd() # thư mục hiện tại
Path.home() # thư mục người dùng
Path(__file__).resolve().parent # thư mục chứa script đang chạy
Dòng cuối rất quan trọng. Nó cho phép tìm tệp dữ liệu đi kèm mà không phụ thuộc vào việc người dùng chạy script từ thư mục nào:
GOC = Path(__file__).resolve().parent
CAU_HINH = GOC / "cau_hinh.yml"
DU_LIEU = GOC / "du_lieu"
Chuẩn hóa đường dẫn
p.resolve() # thành đường dẫn tuyệt đối, gỡ symlink và ..
p.absolute() # tuyệt đối nhưng không chuẩn hóa
p.expanduser() # đổi ~ thành thư mục người dùng
Path("~/du_lieu/a.txt").expanduser().resolve()
Kiểm tra an toàn cho đường dẫn từ người dùng
def an_toan(goc: Path, ten_nguoi_dung_dat: str) -> Path:
goc = goc.resolve()
dich = (goc / ten_nguoi_dung_dat).resolve()
if not dich.is_relative_to(goc): # Python 3.9+
raise ValueError("Đường dẫn không hợp lệ")
return dich
an_toan(Path("uploads"), "../../etc/passwd") # ValueError
Đây là lớp bảo vệ bắt buộc khi ghép đường dẫn từ dữ liệu người dùng. Không có nó, chuỗi ../ cho phép đọc ghi bất kỳ đâu trên máy chủ.
Sao chép và di chuyển
import shutil
shutil.copy2(nguon, dich) # giữ cả thời gian sửa
shutil.copytree(nguon, dich, dirs_exist_ok=True)
shutil.move(nguon, dich)
# Di chuyển an toàn trong cùng ổ đĩa
import os
os.replace(tam, dich) # nguyên tử, ghi đè nếu đã tồn tại
Tệp và thư mục tạm
import tempfile
with tempfile.TemporaryDirectory() as tm:
tam = Path(tm)
(tam / "lam_viec.txt").write_text("dữ liệu")
xu_ly(tam)
# thư mục tự xóa
with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as f:
duong_dan_tam = Path(f.name)
Vài mẫu hữu ích
# Tổng dung lượng thư mục
tong = sum(p.stat().st_size for p in Path("du_lieu").rglob("*") if p.is_file())
print(f"{tong / 1024**2:.1f} MB")
# Nhóm tệp theo phần mở rộng
from collections import defaultdict
nhom = defaultdict(list)
for p in Path(".").rglob("*"):
if p.is_file():
nhom[p.suffix or "(không đuôi)"].append(p)
# Dọn tệp cũ
import time
nguong = time.time() - 30 * 86400
for p in Path("log").glob("*.log"):
if p.stat().st_mtime < nguong:
p.unlink()
Khi nào vẫn cần chuỗi
Một số thư viện cũ chỉ nhận chuỗi. Chuyển đổi rất đơn giản:
str(p) # thành chuỗi
Path(chuoi) # thành Path
Nhưng phần lớn thư viện hiện đại nhận trực tiếp đối tượng Path, kể cả open, pandas.read_csv và json.load.