Hoạt động tệp tin trong Python

Hoạt động tệp tin trong Python

Bài viết này giải thích về hoạt động tệp tin trong Python.

Với mục tiêu "an toàn", "hiệu quả" và "dễ đọc", hướng dẫn này giải thích các khái niệm từng bước, từ cơ bản đến kỹ thuật thực tiễn.

YouTube Video

Hoạt động tệp tin trong Python

Thao tác tệp là một kỹ năng cơ bản thiết yếu, từ các đoạn mã nhỏ cho tới các ứng dụng quy mô lớn.

Mở và đóng tệp

Trước tiên, hãy xem một số ví dụ về đọc và ghi tệp văn bản. Sử dụng câu lệnh with (context manager) đảm bảo tệp được đóng đúng cách.

Đoạn mã sau đây mở một tệp văn bản, đọc nội dung và xử lý từng dòng một.

1# Open and read a text file safely using a context manager.
2with open("example.txt", "r", encoding="utf-8") as f:
3    # Read all lines lazily (as an iterator) and process each line.
4    for line in f:
5        print(line.rstrip())
  • Chỉ định rõ ràng encoding="utf-8" giúp giảm các vấn đề phụ thuộc vào hệ điều hành.
  • Ngay cả với tệp lớn, for line in f vẫn sử dụng bộ nhớ hiệu quả.

Ghi dữ liệu (ghi đè và nối thêm)

Hãy chú ý tới chế độ khi ghi vào tệp. w là để ghi đè, a là để nối thêm. Cũng sử dụng câu lệnh with khi ghi dữ liệu.

Đoạn mã dưới đây biểu diễn các ví dụ cơ bản về ghi đè và nối thêm dữ liệu.

1# Write (overwrite) to a file.
2with open("output.txt", "w", encoding="utf-8") as f:
3    f.write("First line\n")
4    f.write("Second line\n")
5
6# Append to the same file.
7with open("output.txt", "a", encoding="utf-8") as f:
8    f.write("Appended line\n")
  • Đoạn mã này ghi văn bản vào output.txt rồi nối thêm dữ liệu vào cùng tệp đó. Chế độ "w" sẽ ghi đè tệp, còn chế độ "a" sẽ nối thêm một dòng mới vào cuối nội dung hiện có trong tệp.
  • Nếu cần xả bộ đệm, hãy gọi flush(), nhưng thông thường nội dung sẽ được xả tự động khi kết thúc khối with.
  • Nếu nhiều tiến trình hoặc luồng có thể ghi vào cùng tệp đồng thời, bạn cần xem xét kiểm soát truy cập độc quyền, như khóa tệp.

Đọc và ghi dữ liệu nhị phân

Ảnh và các tệp nén nên được xử lý ở chế độ nhị phân (rb hoặc wb). Khác với chế độ văn bản, encoding sẽ bị bỏ qua.

Đoạn mã dưới đây đọc một tệp nhị phân và sao chép nó sang tệp khác ở chế độ nhị phân.

1# Read and write binary files.
2with open("image.png", "rb") as src:
3    data = src.read()
4
5with open("copy.png", "wb") as dst:
6    dst.write(data)
  • Khi xử lý tệp nhị phân lớn, tránh đọc toàn bộ tệp cùng lúc với read(); nên đọc và ghi theo từng phần nhỏ (chunk) để tiết kiệm bộ nhớ hơn.

Ví dụ xử lý tệp lớn theo từng phần nhỏ (chunk)

Với các tệp quá lớn không đủ bộ nhớ để tải toàn bộ, hãy đọc và ghi từng phần cố định kích thước. Do đây là các thao tác I/O, điều chỉnh kích thước bộ đệm sẽ hữu ích.

Đoạn mã bên dưới sao chép tệp thành từng phần 64KB. Cách này giúp xử lý nhanh đồng thời tiết kiệm bộ nhớ.

1# Copy a large file in chunks to avoid using too much memory.
2CHUNK_SIZE = 64 * 1024  # 64 KB
3
4with open("large_source.bin", "rb") as src,      open("large_dest.bin", "wb") as dst:
5    while True:
6        chunk = src.read(CHUNK_SIZE)
7        if not chunk:
8            break
9        dst.write(chunk)
  • Bạn có thể điều chỉnh kích thước mỗi phần (chunk) theo đặc thù của mạng hoặc ổ đĩa. Đối với ổ SSD, kích thước chunk lớn hơn một chút thường hiệu quả hơn.

Ví dụ sử dụng tham số buffering

Bằng cách chỉ định tham số buffering trong hàm open(), bạn có thể kiểm soát kích thước bộ đệm được Python sử dụng bên trong. Điều này giúp bạn tối ưu hóa hiệu quả nhập/xuất hơn nữa.

 1# Explicitly set the internal buffer size to 128 KB for faster I/O.
 2CHUNK_SIZE = 64 * 1024
 3BUFFER_SIZE = 128 * 1024  # 128 KB internal buffer
 4
 5with open("large_source.bin", "rb", buffering=BUFFER_SIZE) as src,      open("large_dest.bin", "wb", buffering=BUFFER_SIZE) as dst:
 6    while True:
 7        chunk = src.read(CHUNK_SIZE)
 8        if not chunk:
 9            break
10        dst.write(chunk)
  • Nếu bạn đặt giá trị của buffering là 0, các thao tác nhập/xuất sẽ không sử dụng bộ đệm; 1 sẽ bật bộ đệm theo dòng, còn các giá trị từ 2 trở lên sẽ sử dụng bộ đệm với kích thước byte đã chỉ định.
  • Thông thường, giá trị mặc định là đủ vì hệ điều hành xử lý bộ nhớ đệm rất hiệu quả, nhưng điều chỉnh tham số này có thể hữu ích với các tệp rất lớn hoặc thiết bị đặc biệt.

Thao tác tệp hiện đại với Pathlib

Module thư viện chuẩn pathlib giúp xử lý đường dẫn trực quan hơn. So với dạng đường dẫn chuỗi, nó cải thiện sự dễ đọc và an toàn.

 1from pathlib import Path
 2
 3path = Path("data") / "notes.txt"
 4
 5# Ensure parent directory exists.
 6path.parent.mkdir(parents=True, exist_ok=True)
 7
 8# Write and read using pathlib.
 9path.write_text("Example content\n", encoding="utf-8")
10content = path.read_text(encoding="utf-8")
11print(content)
  • Đoạn mã này minh họa cách tạo thư mục với pathlib, ghi dữ liệu vào tệp văn bản và đọc nội dung của nó. Với đối tượng Path, bạn có thể xử lý đường dẫn một cách trực quan và an toàn.
  • Path cung cấp nhiều API tiện lợi như iterdir()glob(). Bạn có thể viết mã mà không phải quan tâm đến ký tự phân tách đường dẫn trên các hệ điều hành khác nhau.

Tệp và thư mục tạm thời (tempfile)

Tệp tạm thời có thể được tạo một cách an toàn với tempfile. Cách này tránh được các vấn đề tranh chấp bảo mật và trùng tên tệp.

Đoạn mã dưới đây là ví dụ minh họa việc tạo dữ liệu tạm bằng một tệp tạm thời.

 1import tempfile
 2
 3# Create a temporary file that is deleted on close.
 4with tempfile.NamedTemporaryFile(
 5    mode="w+",
 6    encoding="utf-8",
 7    delete=True
 8) as tmp:
 9    tmp.write("temporary data\n")
10    tmp.seek(0)
11    print(tmp.read())
12
13# tmp is deleted here
  • Đoạn mã này tạo một tệp tạm, ghi và đọc dữ liệu, sau đó tự động xóa khi kết thúc khối with. Với tempfile.NamedTemporaryFile, bạn có thể xử lý tệp tạm thời an toàn và không xung đột. Do đã chỉ định delete=True, tệp sẽ được xóa tự động.
  • Trên Windows, bạn có thể không thể mở tệp từ một handle khác ngay lập tức, nên bạn có thể đặt delete=False và tự quản lý việc xóa.

shutil: Các thao tác cấp cao để sao chép, di chuyển và xóa tệp

Sao chép, di chuyển và xóa đệ quy các tệp và thư mục rất dễ dàng với shutil.

 1import shutil
 2
 3# Copy a file preserving metadata.
 4shutil.copy2("source.txt", "dest.txt")
 5
 6# Move a file or directory.
 7shutil.move("old_location", "new_location")
 8
 9# Remove an entire directory tree (use with caution).
10shutil.rmtree("some_directory")
  • shutil.copy2 cũng sao chép cả siêu dữ liệu (ví dụ thời gian chỉnh sửa). move sẽ tự động di chuyển tệp ngay cả khi không thể dùng rename.
  • rmtree là thao tác nguy hiểm, hãy luôn kiểm tra và sao lưu trước khi xóa.

Siêu dữ liệu tệp (os.stat) và xử lý quyền truy cập

Kích thước tệp, thời gian sửa đổi và quyền truy cập có thể đọc và thay đổi bằng osstat.

Đoạn mã dưới đây lấy thông tin tệp cơ bản với os.stat và thay đổi quyền với os.chmod.

 1import os
 2import stat
 3from datetime import datetime
 4
 5st = os.stat("example.txt")
 6print("size:", st.st_size)
 7print("modified:", datetime.fromtimestamp(st.st_mtime))
 8
 9# Make file read-only for owner.
10os.chmod("example.txt", stat.S_IREAD)
  • Quyền truy cập hoạt động khác nhau trên hệ thống POSIX và Windows. Nếu cần tính tương thích đa nền tảng, hãy dùng API cấp cao hoặc xử lý bổ sung theo điều kiện.

Khóa tệp (kiểm soát độc quyền) — Khác biệt giữa Unix và Windows

Cần kiểm soát độc quyền khi nhiều tiến trình cùng truy cập một tệp. UNIX sử dụng fcntl, còn Windows dùng msvcrt.

Đoạn mã dưới đây sử dụng fcntl.flock trên hệ thống UNIX để lấy khóa độc quyền khi ghi dữ liệu.

 1# Unix-style file locking example
 2import fcntl
 3
 4with open("output.txt", "a+", encoding="utf-8") as f:
 5    fcntl.flock(f, fcntl.LOCK_EX)
 6    try:
 7        f.write("Safe write\n")
 8        f.flush()
 9    finally:
10        fcntl.flock(f, fcntl.LOCK_UN)
  • Đoạn mã này sử dụng fcntl.flock trên hệ thống giống UNIX để ghi an toàn và ngăn chặn ghi đồng thời vào tệp. Luôn luôn giải phóng khóa sau khi xử lý để cho phép tiến trình khác truy cập.
  • Trên Windows, hãy dùng msvcrt.locking(). Để sử dụng ở mức cao hơn, hãy cân nhắc các thư viện ngoài như portalocker hoặc filelock.

Mô hình ghi tệp nguyên tử

Để tránh lỗi dữ liệu khi cập nhật tệp, hãy ghi ra tệp tạm rồi dùng os.replace thay thế khi thành công.

Ghi vào tệp tạm rồi thay thế sẽ tránh bị lỗi dữ liệu nếu gặp sự cố.

 1import os
 2from pathlib import Path
 3import tempfile
 4
 5def atomic_write(path: Path, data: str, encoding="utf-8"):
 6    # Write to a temp file in the same directory and atomically replace.
 7    dirpath = path.parent
 8    with tempfile.NamedTemporaryFile(
 9        mode="w",
10        encoding=encoding,
11        dir=str(dirpath),
12        delete=False
13    ) as tmp:
14        tmp.write(data)
15        tmp.flush()
16        tempname = tmp.name
17
18    # Atomic replacement (on most OSes)
19    os.replace(tempname, str(path))
20
21# Usage
22atomic_write(Path("config.json"), '{"key":"value"}\n')

os.replace thực hiện thao tác thay thế nguyên tử trong cùng một hệ thống tệp. Lưu ý rằng tính nguyên tử không được đảm bảo nếu khác phân vùng/ổ đĩa.

Truy cập nhanh với mmap (cho dữ liệu quy mô lớn)

Để truy cập ngẫu nhiên các tệp lớn, mmap sẽ cải thiện hiệu suất I/O. Chủ yếu áp dụng cho thao tác nhị phân.

Đoạn mã sau sẽ ánh xạ tệp vào bộ nhớ và đọc hoặc ghi từng khoảng byte cụ thể. Hãy cẩn thận khi thay đổi kích thước tệp.

1import mmap
2
3with open("data.bin", "r+b") as f:
4    mm = mmap.mmap(f.fileno(), 0)  # map entire file
5    try:
6        print(mm[:20])  # first 20 bytes
7        mm[0:4] = b"\x00\x01\x02\x03"  # modify bytes
8    finally:
9        mm.close()
  • Đoạn mã này ánh xạ một tệp nhị phân lên bộ nhớ với mmap và thực hiện các thao tác đọc/ghi trực tiếp theo byte. Ánh xạ bộ nhớ giúp truy cập ngẫu nhiên cực nhanh với các tập dữ liệu lớn.
  • mmap rất hiệu quả nhưng nếu dùng sai có thể gây bất nhất dữ liệu. Gọi flush() để đồng bộ khi cần thiết.

CSV / JSON / Pickle: Đọc và ghi theo định dạng

Các định dạng dữ liệu riêng có module hỗ trợ tương ứng. Sử dụng csv cho tệp CSV, json cho JSON, và pickle để lưu trữ đối tượng Python.

Đoạn mã sau là ví dụ cơ bản cho việc đọc và ghi CSV, JSON, và dùng Pickle. Vì Pickle có thể thực thi mã tùy ý, hãy tránh tải dữ liệu từ nguồn không tin cậy.

 1import csv
 2import json
 3import pickle
 4
 5# CSV write
 6with open("rows.csv", "w", encoding="utf-8", newline="") as f:
 7    writer = csv.writer(f)
 8    writer.writerow(["name", "age"])
 9    writer.writerow(["Alice", 30])
10
11# JSON write
12data = {"items": [1, 2, 3]}
13with open("data.json", "w", encoding="utf-8") as f:
14    json.dump(data, f, ensure_ascii=False, indent=2)
15
16# Pickle write (only for trusted environments)
17obj = {"key": "value"}
18with open("obj.pkl", "wb") as f:
19    pickle.dump(obj, f)
  • Nên chỉ định newline="" cho CSV để tránh thêm dòng trắng dư thừa trên Windows. Với ensure_ascii=False, JSON giữ nguyên ký tự UTF-8 để dễ đọc.

Đọc và ghi trực tiếp tệp nén (gzip / bz2 / zipfile)

Xử lý trực tiếp tệp gzip và zip giúp tiết kiệm dung lượng ổ đĩa. Thư viện chuẩn đã bao gồm các module hỗ trợ tương ứng.

Đoạn mã dưới đây là ví dụ đơn giản về đọc và ghi tệp gzip dạng văn bản.

1import gzip
2
3# Write gzipped text.
4with gzip.open("text.gz", "wt", encoding="utf-8") as f:
5    f.write("Compressed content\n")
6
7# Read gzipped text.
8with gzip.open("text.gz", "rt", encoding="utf-8") as f:
9    print(f.read())
  • Có một sự đánh đổi giữa tỷ lệ nén và tốc độ tùy thuộc vào mức độ nén và định dạng.

Các biện pháp bảo mật và phòng chống lỗ hổng

Những điểm sau đây có thể được xem xét để bảo đảm an ninh và phòng chống lỗ hổng.

  • Không bao giờ sử dụng dữ liệu đầu vào không tin cậy làm tên tệp hoặc đường dẫn.
  • Chỉ dùng Pickle với nguồn dữ liệu đáng tin cậy.
  • Giảm thiểu quyền thực thi, chỉ cấp quyền tối thiểu cho các tiến trình thao tác với tệp.
  • Luôn sử dụng tệp tạm thời với tempfile, không lưu tệp thường vào thư mục công cộng.

Nếu sử dụng dữ liệu đầu vào của người dùng cho đường dẫn tệp, cần chuẩn hóa và xác thực. Ví dụ, dùng Path.resolve() và kiểm tra thư mục cha.

 1from pathlib import Path
 2
 3def safe_path(base_dir: Path, user_path: str) -> Path:
 4    candidate = (base_dir / user_path).resolve()
 5    if base_dir.resolve() not in candidate.parents and base_dir.resolve() != candidate:
 6        raise ValueError("Invalid path")
 7    return candidate
 8
 9# Usage
10base = Path("/srv/app/data")
11
12# Raises an exception: attempted path traversal outside `base`
13safe = safe_path(base, '../etc/passwd')
  • Đặc biệt cẩn thận khi dùng đầu vào bên ngoài làm đường dẫn tệp trong ứng dụng web hoặc API công cộng.

Tóm tắt các mẫu sử dụng phổ biến

  • Luôn sử dụng câu lệnh with (tự động đóng tệp).
  • Luôn chỉ định rõ encoding cho dữ liệu văn bản.
  • Đọc và ghi tệp lớn từng phần nhỏ (chunk).
  • Triển khai khóa tệp cho các tài nguyên dùng chung.
  • Đối với các cập nhật quan trọng, hãy sử dụng mô hình nguyên tử: 'Ghi vào tệp tạm → os.replace'.
  • Luôn xác nhận và tạo bản sao lưu trước khi thực hiện các thao tác nguy hiểm (như xóa hoặc ghi đè).
  • Chuẩn hóa và xác minh khi sử dụng đầu vào bên ngoài làm đường dẫn tệp tin.

Tóm tắt

Đối với thao tác tệp tin, điều quan trọng là sử dụng các kỹ thuật an toàn và đáng tin cậy như sử dụng câu lệnh with, chỉ định rõ encoding và ghi dữ liệu theo phương pháp nguyên tử. Đối với xử lý quy mô lớn hoặc truy cập song song, cần triển khai hệ thống khóa tệp và quản lý nhật ký để tránh hỏng dữ liệu và xung đột. Cân bằng hiệu suất và an toàn là chìa khóa cho các thao tác tệp tin đáng tin cậy.

Bạn có thể làm theo bài viết trên bằng cách sử dụng Visual Studio Code trên kênh YouTube của chúng tôi. Vui lòng ghé thăm kênh YouTube.

YouTube Video