pathlib: làm việc với đường dẫn thay cho os.path

Bạn viết "du_lieu" + "/" + ten_file, chạy ngon trên máy mình, rồi một hôm đường dẫn có sẵn dấu gạch chéo ở cuối và cả chương trình đi tìm file du_lieu//bao_cao.csv. Đường dẫn không phải là chuỗi, và module pathlib trong thư viện chuẩn coi nó đúng như vậy.

Từ chuỗi sang đối tượng Path

Chỉ cần một dòng import. Path nhận vào chuỗi và trả về một đối tượng biết mình là đường dẫn.

from pathlib import Path

goc = Path("du_lieu")
print(goc)                  # du_lieu
print(Path.cwd())           # thư mục hiện tại
print(Path.home())           # thư mục người dùng

Đối tượng này dùng được ở mọi chỗ trước đây bạn đưa chuỗi vào: open(), các hàm trong os, thư viện bên thứ ba. Không cần đổi ngược về chuỗi, và nếu có thư viện cũ đòi chuỗi thì str(duong_dan) là xong.

Toán tử / thay cho os.path.join

Đây là thứ khiến người ta chuyển sang pathlib và không quay lại. Dấu / được dùng để nối các phần của đường dẫn.

tep = Path("du_lieu") / "2026" / "baocao.csv"
print(tep)                  # du_lieu/2026/baocao.csv

# Tương đương với cách cũ, nhưng dễ đọc hơn nhiều
import os
cu = os.path.join("du_lieu", "2026", "baocao.csv")

Python tự lo dấu phân cách cho hệ điều hành đang chạy, tự dọn các dấu gạch chéo thừa. Không còn cảnh nối chuỗi ra hai dấu // liền nhau hay thiếu mất một dấu.

Bóc tách tên file mà không cần cắt chuỗi

Mỗi phần của đường dẫn là một thuộc tính có tên rõ ràng.

tep = Path("du_lieu/2026/baocao.csv")

tep.name        # 'baocao.csv'   — tên đầy đủ
tep.stem        # 'baocao'       — tên không đuôi
tep.suffix      # '.csv'         — phần đuôi
tep.parent      # PosixPath('du_lieu/2026')
tep.parts       # ('du_lieu', '2026', 'baocao.csv')

Và đổi một phần bất kỳ mà giữ nguyên phần còn lại:

tep.with_suffix(".json")    # du_lieu/2026/baocao.json
tep.with_stem("tongket")    # du_lieu/2026/tongket.csv  (Python 3.9 trở lên)

Chú ý trường hợp nhiều đuôi: với goi.tar.gz thì suffix chỉ trả về .gz còn stem trả về goi.tar. Muốn lấy cả hai đuôi, dùng suffixes để nhận ['.tar', '.gz'].

Đọc và ghi file không cần open()

Với những file nhỏ, đọc cả nội dung chỉ tốn một dòng. Nhớ ghi rõ encoding để tiếng Việt không hỏng.

tep = Path("ghichu.txt")

tep.write_text("xin chào\n", encoding="utf-8")
noi_dung = tep.read_text(encoding="utf-8")

# Với file nhị phân
anh = Path("logo.png").read_bytes()
Path("ban_sao.png").write_bytes(anh)

Cả hai hàm tự mở và tự đóng file, không cần khối with. Chúng ghi đè toàn bộ nội dung cũ, nên chỉ dùng cho file cấu hình, file tạm, file nhỏ. File log hàng trăm megabyte vẫn phải mở và duyệt từng dòng như cũ.

Tìm file với glob và rglob

glob tìm trong một thư mục, rglob tìm đệ quy xuống mọi thư mục con.

goc = Path("du_lieu")

# Mọi file .csv ngay trong du_lieu
for tep in goc.glob("*.csv"):
    print(tep.name)

# Mọi file .csv ở mọi độ sâu
for tep in goc.rglob("*.csv"):
    print(tep)

# Chỉ lấy thư mục con
thu_muc = [p for p in goc.iterdir() if p.is_dir()]

Kết quả trả về là generator nên thứ tự không đảm bảo — muốn ổn định thì bọc sorted(). Vì mỗi phần tử đã là một Path, bạn dùng luôn được các thuộc tính của nó:

# Tìm file .log lớn hơn 10 MB
lon = [p for p in Path("/var/log").rglob("*.log")
       if p.stat().st_size > 10 * 1024 * 1024]

# Sắp xếp theo lần sửa gần nhất
moi_nhat = sorted(goc.glob("*.csv"), key=lambda p: p.stat().st_mtime, reverse=True)

Kiểm tra, tạo, đổi tên, xóa

tep = Path("du_lieu/2026/baocao.csv")

tep.exists()        # có tồn tại không
tep.is_file()       # có phải file không
tep.is_dir()        # có phải thư mục không

# Tạo cả cây thư mục, không báo lỗi nếu đã có
tep.parent.mkdir(parents=True, exist_ok=True)

tep.touch()                     # tạo file rỗng
tep.rename(tep.with_stem("baocao_cu"))
tep.unlink(missing_ok=True)     # xóa, không lỗi nếu file không còn

Hai tham số exist_ok=Truemissing_ok=True thay cho mấy dòng if os.path.exists(...) mà ai cũng từng viết.

Một cái bẫy cần biết

Toán tử / có một quy tắc dễ làm bạn bất ngờ: nối với một đường dẫn tuyệt đối thì vế trái bị vứt bỏ hoàn toàn.

print(Path("/var/du_lieu") / "bao_cao.csv")   # /var/du_lieu/bao_cao.csv
print(Path("/var/du_lieu") / "/etc/passwd")   # /etc/passwd  (!)

Nghĩa là nếu tên file đến từ người dùng, pathlib không tự bảo vệ bạn. Cách kiểm tra là ghép xong rồi resolve() và xác nhận kết quả vẫn nằm trong thư mục cho phép:

goc = Path("/var/du_lieu").resolve()
dich = (goc / ten_nguoi_dung_nhap).resolve()

if not dich.is_relative_to(goc):     # Python 3.9 trở lên
    raise ValueError("Đường dẫn nằm ngoài thư mục cho phép")

Vì sao gọn và an toàn hơn ghép chuỗi

Ghép chuỗi đặt toàn bộ trách nhiệm lên bạn: đúng dấu phân cách, không thừa không thiếu, cắt đuôi bằng rfind("."), và mỗi lần lại tự hỏi hàm nào trong os.path làm việc mình cần. Với pathlib, đường dẫn là một đối tượng mang sẵn ý nghĩa, các thao tác có tên nói đúng việc chúng làm, và trình soạn thảo gợi ý được cho bạn. Code ngắn hơn thì ít chỗ để sai hơn — đó mới là phần “an toàn” thật sự.

Vài nguyên tắc nên nhớ

  1. Dùng / để nối, đừng bao giờ cộng chuỗi đường dẫn.
  2. Luôn ghi rõ encoding="utf-8" khi đọc ghi văn bản tiếng Việt.
  3. read_textwrite_text chỉ dành cho file nhỏ; file lớn vẫn duyệt từng dòng.
  4. Đường dẫn đến từ người dùng phải resolve() rồi kiểm tra bằng is_relative_to.
  5. Thư viện cũ đòi chuỗi thì bọc str(), không cần viết lại theo os.path.

Không có gì trong os.path mà pathlib không làm được, nhưng có rất nhiều thứ pathlib làm trong một dòng còn os.path cần ba dòng. Với code mới, cứ bắt đầu bằng from pathlib import Path.

Lên đầu trang