Danh sách bài viết

Bài 17: Đọc / ghi file text với open và context manager

Học cách đọc và ghi file text trong Python: hàm open với các mode r / w / a, context manager with, đọc lazy theo dòng, encoding utf-8, pathlib và các pitfall thường gặp khi xử lý dataset text cho AI / data.

24/05/2026
12 phút đọc
0 lượt xem
1

Mục tiêu bài học

Sau bài này bạn sẽ:

  • Hiểu hàm open(path, mode, encoding) và phân biệt các mode r, w, a, r+.
  • Biết tại sao luôn phải close() file, và dùng with để tự động đóng.
  • Đọc file theo nhiều cách: read(), readline(), readlines(), iterate for line in f.
  • Ghi file bằng write() / writelines(), biết khác biệt với print().
  • Dùng pathlib.Path để xử lý đường dẫn cross-platform.
  • Tránh được pitfall encoding (BOM, cp1252 trên Windows).

Bài chạy trên Python 3.8 trở lên.

2

Vì sao cần đọc / ghi file

Trong công việc AI / data, code không tồn tại độc lập — nó phải đọc dữ liệu vào và ghi kết quả ra. Một số trường hợp điển hình:

  • Input data: dataset .txt (corpus văn bản), .csv (bảng số liệu), .json (records cấu trúc), .jsonl (mỗi dòng một JSON object — format phổ biến cho training data của LLM).
  • Log output: ghi lại loss / accuracy mỗi epoch khi train model, ghi lỗi để debug sau.
  • Lưu kết quả: prediction trên test set, embedding đã tính, danh sách prompt đã sinh.
  • Config: tham số huấn luyện (learning rate, batch size) để tránh hardcode trong code.

Hiểu file I/O là điều kiện cần để làm việc với bất kỳ pipeline dữ liệu thực tế nào. Bài này tập trung vào file text; binary và format chuyên biệt (CSV, JSON, Parquet) sẽ học ở các bài sau.

3

Hàm open và các mode

Built-in open() là cổng vào mọi thao tác file. Signature rút gọn:

open(file, mode="r", encoding=None)
  • file — đường dẫn file (string hoặc Path).
  • mode — kiểu mở file (đọc / ghi / append, text / binary).
  • encoding — encoding cho text file. Luôn nên set encoding="utf-8" với file text.

Các mode hay dùng:

ModeÝ nghĩaFile chưa cóFile đã có
rĐọc (mặc định)Lỗi FileNotFoundErrorĐọc từ đầu
wGhi đèTạo mớiXoá sạch, ghi mới
aAppendTạo mớiGiữ nguyên, ghi nối cuối
r+Đọc + ghiLỗiGiữ nguyên, con trỏ ở đầu
xTạo mới độc quyềnTạo mớiLỗi FileExistsError

Mặc định mode là text mode. Thêm hậu tố b để chuyển sang binary (rb, wb, ab) — sẽ nói ở bước 7.

Ví dụ mở file để đọc:

# Mở file để đọc, encoding utf-8
f = open("data.txt", mode="r", encoding="utf-8")
content = f.read()
f.close()           # Phải đóng thủ công
print(content)

Cảnh báo: mode="w" sẽ xoá sạch file đã tồn tại ngay khi gọi open() — kể cả khi bạn chưa write() dòng nào. Nếu muốn giữ data cũ, dùng a.

4

Vì sao phải close — context manager

Khi gọi open(), Python yêu cầu hệ điều hành cấp một file handle (file descriptor). Đây là tài nguyên có giới hạn — OS chỉ cho mỗi process mở một số lượng file nhất định. Nếu không close() sau khi dùng:

  • File handle bị giữ lại, gây resource leak.
  • Với mode ghi: dữ liệu có thể còn trong buffer, chưa được flush xuống đĩa — đọc file lại sẽ thấy thiếu.
  • Trên Windows, file đang mở thường không xoá / đổi tên được từ tiến trình khác.

Cách thủ công với try / finally (xem bài 16):

f = open("data.txt", "r", encoding="utf-8")
try:
    content = f.read()
    # ... xử lý ...
finally:
    f.close()       # Luôn close, dù có exception hay không

Python cung cấp cú pháp gọn hơn: context manager với từ khoá with. Khối with tự động gọi close() khi ra khỏi block, kể cả khi có exception:

with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()
    # ... xử lý ...
# Tự động close ở đây — không cần gọi f.close()
print(content)

Đây là cách viết được khuyến nghị trong mọi tài liệu Python chính thức. Quy tắc đơn giản: luôn dùng with open(...), đừng dùng open() trần.

Có thể mở nhiều file trong cùng một with:

with open("input.txt", "r", encoding="utf-8") as fin, \
     open("output.txt", "w", encoding="utf-8") as fout:
    for line in fin:
        fout.write(line.upper())
5

Đọc text file

Có 4 cách đọc, mỗi cách hợp với một tình huống khác nhau.

1. f.read() — đọc toàn bộ thành 1 string

with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()
print(type(content), len(content))   # <class 'str'> ...

Nhanh và tiện cho file nhỏ (config, prompt template). Nhưng với file vài GB, read() sẽ load toàn bộ vào RAM — có thể làm tràn bộ nhớ.

2. f.readline() — đọc 1 dòng

with open("data.txt", "r", encoding="utf-8") as f:
    first = f.readline()    # Dòng đầu, kèm '\n' ở cuối
    second = f.readline()   # Dòng thứ hai
print(first.rstrip())       # rstrip để bỏ '\n'

Mỗi lần gọi đọc đúng 1 dòng và đẩy con trỏ tiếp. Khi hết file, readline() trả về string rỗng "".

3. f.readlines() — đọc tất cả thành list

with open("data.txt", "r", encoding="utf-8") as f:
    lines = f.readlines()
print(len(lines))           # Số dòng
print(lines[0].rstrip())    # Mỗi phần tử là 1 dòng (kèm '\n')

Tiện khi cần index theo dòng (lines[10]), nhưng vẫn load toàn bộ vào RAM.

4. Iterate for line in f — lazy, khuyến nghị cho file lớn

# Đọc lazy — chỉ giữ một dòng tại một thời điểm
with open("big.txt", "r", encoding="utf-8") as f:
    for line in f:
        # Mỗi line kèm '\n' ở cuối (trừ dòng cuối có thể không)
        line = line.rstrip("\n")
        # ... xử lý line ...
        print(line)

Đây là pattern memory-efficient: dù file 100 GB cũng chỉ tốn RAM bằng một dòng. Object file trả về từ open() tự nó là một iterator dòng.

Quy tắc thực dụng:

  • File nhỏ (< vài MB), cần toàn bộ nội dung: read().
  • File lớn, xử lý từng dòng: for line in f.
  • Chỉ cần vài dòng đầu: readline() trong loop có điều kiện dừng.
  • Cần random access theo dòng: readlines() — nhưng cân nhắc pandas nếu là data có cấu trúc.
6

Ghi text file

Hai phương thức chính:

f.write(s) — ghi một string. Không tự thêm xuống dòng:

with open("output.txt", "w", encoding="utf-8") as f:
    f.write("Hello")
    f.write("World")
# Nội dung file: HelloWorld   (cùng một dòng)

Muốn xuống dòng phải tự thêm \n:

with open("output.txt", "w", encoding="utf-8") as f:
    f.write("Hello\n")
    f.write("World\n")
# Nội dung:
# Hello
# World

Đây là khác biệt quan trọng so với print() — vốn mặc định thêm \n ở cuối. Nếu muốn dùng cú pháp print() nhưng ghi vào file:

with open("output.txt", "w", encoding="utf-8") as f:
    print("Hello", file=f)
    print("World", file=f)
# Nội dung tương đương write("Hello\n") + write("World\n")

f.writelines(lines) — ghi một iterable string. Tên hơi gây hiểu nhầm: nó không tự thêm \n giữa các phần tử. Cần tự nối:

lines = ["alpha", "beta", "gamma"]

with open("output.txt", "w", encoding="utf-8") as f:
    f.writelines(line + "\n" for line in lines)
# Nội dung:
# alpha
# beta
# gamma

Mode a (append) hữu ích khi ghi log — không xoá data cũ:

import time

# Ghi log mỗi epoch khi train
with open("train.log", "a", encoding="utf-8") as f:
    f.write(f"{time.time():.0f} epoch=1 loss=0.523\n")

# Lần chạy sau, log mới sẽ nối vào cuối file
with open("train.log", "a", encoding="utf-8") as f:
    f.write(f"{time.time():.0f} epoch=2 loss=0.412\n")
7

Mode binary rb / wb

Thêm hậu tố b vào mode để mở ở chế độ binary: rb, wb, ab. Khi đó read() trả về bytes chứ không phải str, và write() yêu cầu bytes:

# Đọc ảnh ra bytes
with open("photo.jpg", "rb") as f:
    raw = f.read()
print(type(raw), len(raw))   # <class 'bytes'> ...

# Copy file (binary-safe)
with open("photo.jpg", "rb") as src, \
     open("photo_copy.jpg", "wb") as dst:
    dst.write(src.read())

Khi nào dùng binary:

  • File ảnh (JPG, PNG), audio (WAV, MP3), video.
  • File model đã serialize (.pt, .pkl, .safetensors).
  • File nén (.zip, .gz).
  • Bất kỳ format nào không phải text thuần.

Quan trọng: ở binary mode không truyền encoding — bytes là bytes, không có khái niệm encoding. Truyền sẽ báo ValueError.

8

Đường dẫn file và pathlib

Relative vs absolute:

  • Absolute path: bắt đầu từ root — /home/user/data/file.txt (Linux / macOS), C:\Users\user\data\file.txt (Windows).
  • Relative path: tính từ thư mục hiện tại của tiến trình — data/file.txt.

Trong project, ưu tiên relative path để code chạy được trên máy khác. Cần biết thư mục hiện tại đang ở đâu khi chạy script:

import os
print(os.getcwd())   # In ra current working directory

Cross-platform: đừng hardcode / hay \. Trên Windows separator là \, Linux / macOS là /. Cách an toàn là dùng pathlib.Path (chuẩn từ Python 3.4):

from pathlib import Path

# Tạo path bằng toán tử /
data_dir = Path("data")
file_path = data_dir / "input.txt"
print(file_path)              # data/input.txt  (hoặc data\input.txt trên Windows)

# Kiểm tra tồn tại
print(file_path.exists())     # True / False
print(file_path.is_file())    # True nếu là file
print(file_path.is_dir())     # True nếu là thư mục

# Đọc / ghi nhanh — không cần open / with
text = Path("data/input.txt").read_text(encoding="utf-8")
Path("data/output.txt").write_text("Hello\nWorld\n", encoding="utf-8")

Các thuộc tính hay dùng của Path:

p = Path("data/sub/file.txt")
print(p.name)        # 'file.txt'    — tên file kèm extension
print(p.stem)        # 'file'        — tên file không extension
print(p.suffix)      # '.txt'        — extension
print(p.parent)      # PosixPath('data/sub') — thư mục cha
print(p.parts)       # ('data', 'sub', 'file.txt')

# Tạo thư mục nếu chưa có
Path("data/output").mkdir(parents=True, exist_ok=True)

Path object hoạt động được với open() trực tiếp — không cần convert sang string:

p = Path("data/input.txt")
with open(p, "r", encoding="utf-8") as f:
    content = f.read()
9

Encoding pitfall — luôn dùng utf-8

Khi không truyền encoding, Python dùng encoding mặc định của hệ thống (locale.getpreferredencoding()). Trên Linux / macOS thường là utf-8, nhưng trên Windows trước Python 3.15 mặc định là cp1252 (Latin-1 mở rộng).

Hệ quả: code viết trên macOS đọc file utf-8 OK, chạy trên Windows sẽ UnicodeDecodeError với ký tự tiếng Việt có dấu hoặc emoji. Cách phòng tránh đơn giản:

# LUÔN truyền encoding cho text file
with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()

Một bẫy khác là BOM (Byte Order Mark) — 3 byte EF BB BF ở đầu file utf-8, do Notepad / Excel trên Windows hay chèn vào. Đọc với encoding="utf-8" sẽ giữ BOM ở đầu string, gây lỗi parse JSON / CSV. Dùng encoding="utf-8-sig" để bỏ BOM tự động:

# File từ Excel / Notepad có BOM
with open("from_excel.csv", "r", encoding="utf-8-sig") as f:
    content = f.read()
# BOM đã được strip, content sạch

Khi không chắc encoding (file cũ, nguồn lạ), có thể dùng thư viện chardet để đoán, hoặc thử lần lượt utf-8utf-8-sigcp1252latin-1. Với errors="replace", các byte không decode được sẽ thay bằng ? thay vì raise exception:

with open("unknown.txt", "r", encoding="utf-8", errors="replace") as f:
    content = f.read()

Khi ghi, encoding="utf-8" là lựa chọn an toàn nhất — đọc lại được trên mọi platform.

10

Use case AI / data

1. Đọc corpus text — dataset cho NLP:

from pathlib import Path

# Đọc toàn bộ corpus thành list các câu (mỗi dòng 1 câu)
sentences = []
with open("corpus.txt", "r", encoding="utf-8") as f:
    for line in f:
        line = line.strip()
        if line:                # Bỏ dòng trống
            sentences.append(line)

print(f"Số câu: {len(sentences)}")
print(f"Câu đầu: {sentences[0]}")

2. Load danh sách prompt:

from pathlib import Path

prompts = Path("prompts.txt").read_text(encoding="utf-8").splitlines()
prompts = [p for p in prompts if p.strip()]
print(f"Loaded {len(prompts)} prompts")

3. Ghi log training — append mode để không mất log cũ:

from datetime import datetime

def log(msg: str, path: str = "train.log") -> None:
    ts = datetime.now().isoformat(timespec="seconds")
    with open(path, "a", encoding="utf-8") as f:
        f.write(f"[{ts}] {msg}\n")

# Sử dụng
log("Start training")
log("epoch=1 loss=0.523 acc=0.81")
log("epoch=2 loss=0.412 acc=0.85")

4. Load config dạng key=value đơn giản:

def load_config(path: str) -> dict:
    config = {}
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):  # bỏ comment, dòng trống
                continue
            key, _, value = line.partition("=")
            config[key.strip()] = value.strip()
    return config

# config.txt:
# learning_rate=0.001
# batch_size=32
# epochs=10

cfg = load_config("config.txt")
print(cfg)   # {'learning_rate': '0.001', 'batch_size': '32', 'epochs': '10'}

Trong project thực tế, format chuyên dụng (JSON, YAML, TOML) tiện hơn. Sẽ học ở các bài sau khi đi vào dependency management và config tool.

11

Bài tập

Bài 1: Đếm số dòng và số từ.

Cho file data.txt bất kỳ (tạo trước bằng tay vài dòng tiếng Việt có dấu). Viết script đọc file, in ra:

  • Tổng số dòng (kể cả dòng trống).
  • Số dòng không trống.
  • Tổng số từ — coi từ là chuỗi ký tự cách nhau bởi whitespace, dùng str.split().

Yêu cầu: dùng with open(...), iterate for line in f (lazy), encoding="utf-8".

Bài 2: Ghi 10 dòng Hello.

Viết script tạo file output.txt, ghi vào đó 10 dòng theo format:

Hello 0
Hello 1
...
Hello 9

Yêu cầu: dùng with open(...) với mode w, một vòng for, không thêm dòng trống thừa ở cuối.

Bài 3 (mở rộng): Filter file.

Cho file input.txt. Viết script đọc input.txt và ghi sang output.txt tất cả các dòng có độ dài (số ký tự sau khi strip) lớn hơn 10. Dùng pathlib.Path cho cả hai file và mở cả hai trong cùng một with.

12

Tóm tắt

  • open(path, mode, encoding) là cổng vào mọi thao tác file. Mode hay dùng: r (đọc), w (ghi đè), a (append).
  • Luôn dùng with open(...) as f: — context manager tự động close(), an toàn cả khi có exception.
  • Đọc file: read() cho file nhỏ, for line in f cho file lớn (lazy, memory-efficient).
  • Ghi file: write() không tự thêm \n — phải tự nối. print(..., file=f) thì có.
  • Binary mode rb / wb cho ảnh, model, file nén — không truyền encoding.
  • Dùng pathlib.Path để xử lý đường dẫn cross-platform; Path.read_text() / write_text() là shortcut tiện.
  • Luôn truyền encoding="utf-8" cho text file để tránh lỗi trên Windows. Với file có BOM, dùng utf-8-sig.
  • File handle là tài nguyên hữu hạn — không close sẽ leak; chưa flush sẽ mất data.

Bài tiếp theo bắt đầu module toán nền tảng — học về vector: danh sách số có hướng, phép cộng, phép nhân vô hướng. Đây là viên gạch đầu tiên của đại số tuyến tính cho ML.