Chuyển đến nội dung chính

Hướng dẫn toàn diện về sử dụng pathlib trong Python để thao tác hệ thống tệp

Tìm hiểu ưu điểm của pathlib trong Python so với mô-đun os bằng cách khám phá đối tượng đường dẫn, các thành phần đường dẫn và các thao tác đường dẫn phổ biến khi tương tác với hệ thống tệp.
· 9 phút đọc

Khám phá cùng AI

Cho đến gần đây, thao tác với hệ thống tệp trong Python vốn nổi tiếng là khó khăn. Các lập trình viên thường gặp rắc rối với đường dẫn tệp không chính xác, dễ lỗi vì cần những chuỗi dài làm đầu vào. Ngoài ra, họ cũng báo cáo rằng mã của mình hay hỏng do không nhất quán giữa các hệ điều hành khác nhau.

May mắn thay, trong Python phiên bản 3.4, các nhà phát triển đã đưa mô-đun pathlib vào thư viện chuẩn. pathlib cung cấp một giải pháp thanh lịch để xử lý đường dẫn hệ thống tệp bằng cách tiếp cận hướng đối tượng vốn được chờ đợi từ lâu, đồng thời đảm bảo hành vi độc lập với nền tảng.

Hướng dẫn toàn diện này sẽ dạy bạn các tính năng của mô-đun pathlib để hỗ trợ tương tác hàng ngày của bạn với hệ thống tệp. Với pathlib, bạn sẽ có quy trình làm việc hiệu quả và truy xuất dữ liệu dễ dàng. pathlib đã phát triển đáng kể qua nhiều năm và chúng tôi luôn cập nhật để bạn không phải bận tâm. Hãy bắt đầu thôi.

Mô-đun os của Python so với pathlib

Trước Python 3.4, cách truyền thống hơn để xử lý đường dẫn tệp là sử dụng mô-đun os. Dù từng rất hiệu quả, mô-đun os nay đã bộc lộ dấu hiệu lạc hậu.

Chúng ta có thể cho thấy giá trị độc đáo của pathlib bằng cách xét một tác vụ phổ biến trong khoa học dữ liệu: cách tìm tất cả các tệp png trong một thư mục cho trước và toàn bộ các thư mục con của nó.

Nếu dùng mô-đun os, ta có thể viết đoạn mã sau:

import os
dir_path = "/home/user/documents"
files = [
os.path.join(dir_path, f)
for f in os.listdir(dir_path)
if os.path.isfile(os.path.join(dir_path, f)) and f.endswith(".png")
]

Mặc dù đoạn mã này giải quyết ngay nhiệm vụ tìm tệp png, nó bộc lộ một số nhược điểm lớn của mô-đun os. Thứ nhất, mã dài và gần như khó đọc, thật đáng tiếc vì đây là thao tác khá đơn giản. Thứ hai, mã giả định người đọc hiểu về list comprehension, điều không nên coi là hiển nhiên. Thứ ba, mã dùng thao tác trên chuỗi, vốn dễ sai sót. Và ngoài tất cả điều đó, mã cũng không gọn gàng. 

Nếu thay vào đó, chúng ta dùng mô-đun pathlib, mã sẽ đơn giản hơn nhiều. Như đã đề cập, pathlib cung cấp cách tiếp cận hướng đối tượng để xử lý đường dẫn hệ thống tệp. Hãy xem:

from pathlib import Path

# Create a path object
dir_path = Path(dir_path)

# Find all text files inside a directory
files = list(dir_path.glob("*.png"))

Lập trình hướng đối tượng tổ chức mã quanh các đối tượng và sự tương tác của chúng, giúp mã mô-đun hóa, tái sử dụng và dễ bảo trì hơn. Nếu bạn chưa quen với lập trình hướng đối tượng, đáng để học qua khóa học Lập trình Python hướng đối tượng của chúng tôi. 

Làm việc với đối tượng Path trong Python

Thư viện pathlib xoay quanh các đối tượng Path, đại diện cho đường dẫn hệ thống tệp theo cách có cấu trúc và độc lập nền tảng.

Trước đó trong hướng dẫn, chúng ta đã đưa lớp Path từ mô-đun pathlib vào không gian tên hiện tại bằng dòng mã sau:

from pathlib import Path

Sau khi gọi lớp Path từ pathlib, ta có thể tạo đối tượng Path theo nhiều cách, bao gồm từ chuỗi, từ các đối tượng Path khác, từ thư mục làm việc hiện tại, và từ thư mục home.

Hãy xem lần lượt từng cách.

Tạo đối tượng đường dẫn từ chuỗi

Ta có thể tạo một đối tượng Path bằng cách truyền một chuỗi biểu diễn đường dẫn hệ thống tệp cho một biến. Điều này chuyển đổi chuỗi biểu diễn đường dẫn tệp thành một đối tượng Path.

file_path_str = "data/union_data.csv"

data_path = Path(file_path_str)

Tạo đối tượng đường dẫn từ đối tượng đường dẫn khác

Các đối tượng Path hiện có có thể đóng vai trò là khối xây dựng để tạo đường dẫn mới. 

Ta làm điều này bằng cách kết hợp đường dẫn gốc, thư mục dữ liệu và tên tệp thành một đường dẫn tệp duy nhất. Cần nhớ dùng dấu gạch chéo xuôi khi thích hợp để mở rộng các đối tượng Path.

base_path = Path("/home/user")
data_dir = Path("data")

# Combining multiple paths
file_path = base_path / data_dir / "prices.csv"  
print(file_path)
'/home/user/data/prices.csv'

Tạo đối tượng đường dẫn từ thư mục làm việc hiện tại

Ở đây ta gán thư mục làm việc hiện tại cho biến cwd bằng phương thức Path.cwd(). Sau đó có thể truy xuất đường dẫn của thư mục làm việc hiện tại nơi tập lệnh đang chạy. 

cwd = Path.cwd()

print(cwd)
'/home/bexgboost/articles/2024/4_april/8_pathlib'

Tạo đối tượng đường dẫn từ thư mục home

Ta có thể dựng một đường dẫn bằng cách kết hợp thư mục home với các thư mục con bổ sung. Ở đây ta đang kết hợp thư mục home với các thư mục con "downloads" và "projects." 

home = Path.home()

home / "downloads" / "projects"
PosixPath('/home/bexgboost/downloads/projects')

Lưu ý quan trọng: Bản thân lớp Path không thực hiện bất kỳ thao tác hệ thống tệp nào như kiểm tra hợp lệ đường dẫn, tạo thư mục hay tệp. Nó được thiết kế để biểu diễn và thao tác đường dẫn. Để thực sự tương tác với hệ thống tệp (kiểm tra tồn tại, đọc/ghi tệp), ta sẽ phải dùng các phương thức chuyên biệt của đối tượng Path và trong một số trường hợp nâng cao, cần đến mô-đun os.

Làm việc với các thành phần đường dẫn trong Python

Thuộc tính đường dẫn tệp là các đặc tính và thành phần khác nhau của một đường dẫn tệp giúp nhận diện và quản lý tệp, thư mục trong hệ thống tệp. Cũng như địa chỉ thực có nhiều phần như số nhà, thành phố, quốc gia và mã bưu chính, một đường dẫn hệ thống tệp có thể được tách thành các thành phần nhỏ hơn. pathlib cho phép chúng ta truy cập và thao tác các thành phần này bằng thuộc tính đường dẫn thông qua cú pháp chấm.

Làm việc với thư mục gốc (root)

Root là thư mục cao nhất trong hệ thống tệp. Trên hệ thống kiểu Unix, nó được biểu diễn bằng dấu gạch chéo xuôi (/). Trên Windows, thường là một ký tự ổ đĩa theo sau bởi dấu hai chấm, như C:.

image_file = home / "downloads" / "midjourney.png"

image_file.root
'/'

Làm việc với thư mục cha (parent)

Thư mục cha chứa tệp hoặc thư mục hiện tại. Nó ở một cấp cao hơn so với thư mục hoặc tệp hiện tại. 

image_file.parent
PosixPath('/home/bexgboost/downloads')

Làm việc với tên tệp

Thuộc tính này trả về toàn bộ tên tệp, bao gồm phần mở rộng, dưới dạng chuỗi.

image_file.name
'midjourney.png'

Làm việc với phần mở rộng tệp (suffix)

Thuộc tính suffix trả về phần mở rộng tệp, gồm cả dấu chấm, dưới dạng chuỗi (hoặc chuỗi rỗng nếu không có phần mở rộng).

image_file.suffix
'.png'

Làm việc với phần thân tên tệp (stem)

Stem trả về tên tệp không bao gồm phần mở rộng. Làm việc với stem hữu ích khi chuyển đổi tệp sang định dạng khác.

image_file.stem
'midjourney'

Lưu ý: Trên máy Mac, đường dẫn tệp phân biệt chữ hoa chữ thường, vì vậy /Users/username/Documents và /users/username/documents sẽ khác nhau. 

Thuộc tính parts của pathlib

Ta có thể dùng thuộc tính .parts để tách một đối tượng Path thành các thành phần của nó.

image_file.parts
('/', 'home', 'bexgboost', 'downloads', 'midjourney.png')

Thuộc tính parents của pathlib

Thuộc tính parents, trả về một generator, biến các thành phần này thành các đối tượng Path. 

list(image_file.parents)
[PosixPath('/home/bexgboost/downloads'),
PosixPath('/home/bexgboost'),
PosixPath('/home'),
PosixPath('/')]

Các thao tác đường dẫn phổ biến với pathlib

Các đối tượng Path có nhiều phương thức cho phép bạn tương tác hiệu quả với thư mục và nội dung của chúng. Hãy xem cách thực hiện một số thao tác phổ biến nhất.

Liệt kê thư mục

Phương thức iterdir() cho phép bạn lặp qua tất cả tệp và thư mục con trong một thư mục. Nó đặc biệt hữu ích để xử lý tất cả tệp trong một thư mục hoặc thực hiện thao tác trên từng mục.

cwd = Path.cwd()
 for entry in cwd.iterdir():
# Process the entry here
...
# print(entry)

Vì iterdir() trả về một iterator, các mục sẽ được lấy theo nhu cầu khi bạn đi qua vòng lặp.

Phương thức is_dir()

Phương thức is_dir() trả về True nếu đường dẫn trỏ đến một thư mục, False nếu không.

for entry in cwd.iterdir():
   if entry.is_dir():
      print(entry.name)
.ipynb_checkpoints
data
images

Phương thức is_file()

Phương thức .is_file() trả về True nếu đường dẫn trỏ đến một tệp, False nếu không.

for entry in cwd.iterdir():
   if entry.is_file():
      print(entry.suffix)
.ipynb
.txt

Phương thức exists()

Vì các đối tượng Path chỉ biểu diễn đường dẫn, đôi khi bạn cần kiểm tra xem đường dẫn có tồn tại hay không bằng phương thức .exists():

Phương thức .exists() kiểm tra một đường dẫn có tồn tại hay không. Điều này hữu ích vì các đối tượng Path có thể biểu diễn tệp và thư mục có thể có hoặc không thực sự hiện diện trong hệ thống tệp.

image_file.exists()
False

Tạo và xóa đường dẫn

pathlib cũng cung cấp các chức năng để tạo và xóa tệp, thư mục. Hãy xem cách thực hiện.

Phương thức mkdir()

Phương thức mkdir() tạo một thư mục mới tại đường dẫn chỉ định. Theo mặc định, nó tạo thư mục trong thư mục làm việc hiện tại.

from pathlib import Path

data_dir = Path("new_data_dir")

# Create the directory 'new_data_dir' in the current working directory
data_dir.mkdir()

Phương thức mkdir(parents=True)

Phương thức mkdir(parents=True) đặc biệt hữu ích khi bạn muốn tạo một cấu trúc thư mục mà một số thư mục cha có thể chưa tồn tại. Thiết lập parents=True đảm bảo tất cả thư mục cha cần thiết sẽ được tạo trong quá trình này.

sub_dir = Path("data/nested/subdirectory")

# Create 'data/nested/subdirectory', even if 'data' or 'nested' don't exist
sub_dir.mkdir(parents=True)

Hãy nhớ rằng mkdir() sẽ phát sinh ngoại lệ nếu một thư mục cùng tên đã tồn tại.

Path('data').mkdir()
FileExistsError: [Errno 17] File exists: 'data'

Phương thức unlink()

Phương thức unlink() xóa vĩnh viễn một tệp được biểu diễn bởi đối tượng Path. Nên kiểm tra xem tệp có tồn tại trước khi chạy phương thức này để tránh lỗi.

to_delete = Path("data/prices.csv")

if to_delete.exists():
   to_delete.unlink()
   print(f"Successfully deleted {to_delete.name}")
Successfully deleted prices.csv

Phương thức rmdir()

Phương thức rmdir() xóa một thư mục trống. Hãy nhớ rằng rmdir() chỉ hoạt động để xóa thư mục trống. Cách dễ nhất để xóa một thư mục không trống là dùng thư viện shutil hoặc dùng terminal.

empty_dir = Path("new_data_dir")

empty_dir.rmdir()

Lưu ý: Hãy thận trọng khi dùng unlink() hoặc rmdir() vì kết quả là vĩnh viễn.

Thao tác đường dẫn nâng cao

Hãy chuyển sang một số khái niệm thao tác đường dẫn nâng cao và cách áp dụng chúng trong thực tế bằng pathlib.

Đường dẫn tương đối so với tuyệt đối

Chúng ta sẽ bắt đầu bằng cách hiểu sự khác nhau giữa đường dẫn tuyệt đối và tương đối, vì chúng thường xuyên xuất hiện.

Đường dẫn tương đối

Đường dẫn tương đối chỉ vị trí của tệp hoặc thư mục so với thư mục hiện tại, vì vậy mới gọi là tương đối. Chúng ngắn gọn và linh hoạt trong dự án của bạn nhưng có thể gây nhầm lẫn nếu bạn đổi thư mục làm việc.

Ví dụ, tôi có một thư mục images trong thư mục làm việc hiện tại, trong đó có tệp midjourney.png.

image = Path("images/midjourney.png")

image
PosixPath('images/midjourney.png')

Đoạn mã trên hoạt động hiện tại, nhưng nếu tôi di chuyển notebook đang dùng sang vị trí khác, đoạn mã sẽ hỏng vì thư mục images không di chuyển cùng notebook.

Đường dẫn tuyệt đối

Đường dẫn tuyệt đối chỉ vị trí đầy đủ của một tệp hoặc thư mục tính từ root của hệ thống tệp. Chúng độc lập với thư mục hiện tại và cung cấp điểm tham chiếu rõ ràng cho bất kỳ người dùng nào ở bất kỳ đâu trên hệ thống.

image_absolute = Path("/home/bexgboost/articles/2024/4_april/8_pathlib/images/midjourney.png")

image_absolute
PosixPath('/home/bexgboost/articles/2024/4_april/8_pathlib/images/midjourney.png')

Như bạn thấy, đường dẫn tuyệt đối có thể khá dài, đặc biệt trong các dự án phức tạp có cấu trúc cây lồng nhau. Vì lý do này, đa số mọi người thích dùng đường dẫn tương đối vì ngắn hơn.

Phương thức resolve

pathlib cung cấp các phương thức để chuyển đường dẫn tương đối thành tuyệt đối với phương thức resolve().

relative_image = Path("images/midjourney.png")

absolute_image = relative_image.resolve()

absolute_image
PosixPath('/home/bexgboost/articles/2024/4_april/8_pathlib/images/midjourney.png')

Ta cũng có thể làm theo chiều ngược lại: Nếu có đường dẫn tuyệt đối, ta có thể chuyển nó thành đường dẫn tương đối dựa trên một thư mục tham chiếu.

relative_path = Path.cwd()

absolute_image.relative_to(relative_path)
PosixPath('images/midjourney.png')

Globbing

Để minh họa globbing, ta có thể quay lại ví dụ ở đầu bài, nơi ta viết mã để tìm tất cả các tệp png trong một thư mục nhất định.

files = list(dir_path.glob("*.png"))

pathlib dùng mô-đun tích hợp .glob() để tìm kiếm hiệu quả các tệp khớp với một mẫu cụ thể trong bất kỳ thư mục nào. Mô-đun này rất hữu ích khi xử lý các tệp có tên hoặc phần mở rộng tương tự nhau.

Phương thức glob hoạt động bằng cách nhận vào một chuỗi mẫu chứa ký tự đại diện và trả về một đối tượng generator cung cấp các đối tượng Path phù hợp theo nhu cầu:

  • *: Khớp với không hoặc nhiều ký tự.

  • ?: Khớp với bất kỳ một ký tự đơn nào.

  • []: Khớp với một dải ký tự nằm trong ngoặc vuông (ví dụ, [a-z] khớp với bất kỳ chữ thường nào).

Để minh họa, hãy thử tìm tất cả notebook Jupyter trong thư mục articles của tôi.

articles_dir = Path.home() / "articles"

# Find all scripts
notebooks = articles_dir.glob("*.ipynb")

# Print how many found
print(len(list(notebooks)))
0

Phương thức .glob() không tìm thấy notebook nào, điều này thoạt nhìn có vẻ lạ vì tôi đã viết hơn 150 bài. Lý do là .glob() chỉ tìm trong thư mục được chỉ định, không bao gồm các thư mục con.

Ta có thể giải quyết bằng cách tìm kiếm đệ quy, khi đó ta cần dùng phương thức rglob() với cú pháp tương tự:

notebooks = articles_dir.rglob("*.ipynb")

print(len(list(notebooks)))
357

Lần này, mã của chúng ta đã tìm thấy toàn bộ 357 tệp.

Làm việc với tệp

Như đã thấy, các đối tượng Path chỉ biểu diễn tệp chứ không thao tác trực tiếp trên chúng. Tuy vậy, chúng có một số phương thức cho các thao tác tệp phổ biến. Ta sẽ xem cách dùng trong phần này.

Đọc tệp

Đọc nội dung tệp là thao tác cơ bản trong nhiều ứng dụng Python. pathlib cung cấp các phương thức rút gọn tiện lợi để đọc tệp dưới dạng văn bản hoặc bytes thô.

Phương thức read_text() cho phép chúng ta đọc nội dung của một tệp văn bản và đóng tệp.

file = Path("file.txt")

print(file.read_text())
This is sample text.

Với tệp nhị phân, ta có thể dùng phương thức read_bytes().

image = Path("images/midjourney.png")

image.read_bytes()[:10]
b'\x89PNG\r\n\x1a\n\x00\x00'

Hãy nhớ, khi dùng các phương thức read_*, việc xử lý lỗi là quan trọng:

nonexistent_file = Path("gibberish.txt")

try:
   contents = nonexistent_file.read_text()
except FileNotFoundError:
   print("No such thing.")
No such thing.

Ghi tệp

Ghi tệp cũng dễ như đọc tệp. Để ghi tệp, ta có phương thức write_text().

file = Path("file.txt")

file.write_text("This is new text.")
17
file.read_text()
'This is new text.'

Như thấy, phương thức write_text() sẽ ghi đè văn bản. Mặc dù không có chế độ nối thêm (append) cho write_text(), ta có thể dùng read_text() và write_text() cùng nhau để nối thêm văn bản vào cuối tệp.

old_text = file.read_text() + "\n"
final_text = "This is the final text."

# Combine old and new texts and write them back
file.write_text(old_text + final_text)

print(file.read_text())
This is new text.

This is the final text.

write_bytes() hoạt động tương tự. Để minh họa, trước tiên hãy nhân bản ảnh midjourney.png với tên mới.

original_image = Path("images/midjourney.png")

new_image = original_image.with_stem("duplicated_midjourney")

new_image
PosixPath('images/duplicated_midjourney.png')

Phương thức with_stem() trả về đường dẫn tệp với tên tệp khác (mặc dù phần mở rộng giữ nguyên). Điều này cho phép ta đọc ảnh gốc và ghi nội dung của nó vào một ảnh mới. 

new_image.write_bytes(original_image.read_bytes())
1979612

Đổi tên và di chuyển tệp

Ngoài hàm with_stem() để đổi stem của tên tệp, pathlib cung cấp phương thức rename() để đổi tên hoàn chỉnh hơn. 

file = Path("file.txt")

target_path = Path("new_file.txt")

file.rename(target_path)
PosixPath('new_file.txt')

rename() nhận một đường dẫn đích, có thể là chuỗi hoặc đối tượng đường dẫn khác.

Để di chuyển tệp, bạn có thể dùng hàm replace(), hàm này cũng nhận đường dẫn đích:

# Define the file to be moved
source_file = Path("new_file.txt")

# Define the location to put the file
destination = Path("data/new/location")

# Create the directories if they don't exist
destination.mkdir(parents=True)

# Move the file
source_file.replace(destination / source_file.name)
PosixPath('data/new/location/new_file.txt')

Tạo tệp trống

pathlib cho phép chúng ta tạo tệp trống bằng phương thức touch:

# Define new file path
new_dataset = Path("data/new.csv")

new_dataset.exists()
False
new_dataset.touch()

new_dataset.exists()
True

Phương thức touch ban đầu dùng để cập nhật thời gian sửa đổi của tệp, nên cũng có thể dùng với tệp đã tồn tại.

original_image.touch()

Khi bạn cần đặt trước một tên tệp để dùng sau nhưng chưa có nội dung để ghi ngay, ta có thể dùng touch để tạo một tệp trống. Phương thức này lấy cảm hứng từ lệnh touch trong Unix.

Quyền và thông tin hệ thống tệp

Mục cuối cùng, chúng ta sẽ học cách truy cập đặc tính tệp bằng phương thức .stat(). Nếu bạn quen với os, bạn sẽ nhận thấy phương thức mới này có đầu ra giống os.stat().

image_stats = original_image.stat()

image_stats
os.stat_result(st_mode=33188, st_ino=1950175, st_dev=2080, st_nlink=1, st_uid=1000, st_gid=1000, st_size=1979612, st_atime=1714664562, st_mtime=1714664562, st_ctime=1714664562)

Ta cũng có thể truy xuất kích thước tệp bằng cú pháp chấm.

image_size = image_stats.st_size

# File size in megabytes
image_size / (1024**2)
1.8879051208496094

Kết luận

Việc giới thiệu mô-đun pathlib trong Python 3.4 đã đơn giản hóa đáng kể thao tác với hệ thống tệp cho các nhà phát triển. Bằng cách cung cấp cách tiếp cận hướng đối tượng để xử lý đường dẫn tệp, pathlib mang đến một phương thức có cấu trúc và dễ hiểu để biểu diễn đường dẫn hệ thống tệp. pathlib cũng độc lập với nền tảng, nghĩa là pathlib xử lý dấu phân tách đường dẫn nhất quán trên các hệ điều hành khác nhau để mã của chúng ta không bị hỏng khi chạy trên máy mới. Cuối cùng, như đã thấy, pathlib cung cấp một tập hợp lớn các phương thức ngắn gọn và giàu biểu đạt cho các thao tác hệ thống tệp phổ biến. 

Hãy nhớ rằng, pathlib chỉ là một trong nhiều thư viện tích hợp mạnh mẽ của Python. Bằng cách tham gia Lộ trình Nghề nghiệp Data Scientist với Python, Lộ trình Kỹ năng Lập trình Python, và Khóa Nhập môn Python cho Khoa học Dữ liệu của chúng tôi, bạn sẽ làm chủ nhiều thư viện tích hợp để trở thành một lập trình viên Python vững vàng. 

Cảm ơn bạn đã đọc!


Bexruz (Bex) Tuychiev

Tác giả

Bexruz (Bex) Tuychiev

Tôi là người sáng tạo nội dung về khoa học dữ liệu với hơn 2 năm kinh nghiệm và là một trong những tài khoản có lượng người theo dõi lớn nhất trên Medium. Tôi thích viết các bài chuyên sâu về AI và ML với chút giọng điệu mỉa mai, vì bạn cũng phải làm gì đó để chúng bớt khô khan. Tôi đã xuất bản hơn 130 bài viết và một khóa học trên DataCamp, cùng với một khóa nữa đang được thực hiện. Nội dung của tôi đã được hơn 5 triệu lượt xem, trong đó 20 nghìn người đã trở thành người theo dõi trên cả Medium và LinkedIn. 

Chủ đề

Liên quan