Hướng dẫn

JSON trong Python: đọc và ghi tệp, CSV, pathlib và mã hóa UTF-8

Cập nhật: · Skillaria.Top

Để làm việc với JSON trong Python có sẵn module json: json.load đọc dữ liệu từ tệp đã mở, json.dump ghi dữ liệu vào tệp, còn json.loads và json.dumps làm việc tương tự với chuỗi. Mở tệp bằng with open(path, encoding="utf-8"), xây dựng đường dẫn bằng pathlib.Path, còn bảng dữ liệu đọc bằng module csv.

Làm việc với JSON trong Python không cần thư viện bên ngoài: module json nằm trong thư viện chuẩn và chuyển đổi dict, list thành văn bản và ngược lại. Cùng với nó, gần như luôn cần thêm ba thứ: hàm open kết hợp với with, module pathlib cho đường dẫn, và module csv cho bảng dữ liệu. Dưới đây là tất cả theo thứ tự, có code chạy được và những cạm bẫy hay gặp nhất.

Hàm open trong Python: chế độ và mã hóa

Mọi tệp văn bản đều được mở bằng hàm tích hợp open(path, mode, encoding=...). Hàm trả về một đối tượng tệp có các phương thức read, readline, write và có thể duyệt bằng vòng lặp theo từng dòng.

Chế độTác dụng
"r"Đọc (mặc định). Không có tệp — FileNotFoundError
"w"Ghi. Tệp được tạo mới hoặc bị xóa trắng ngay khi mở
"a"Ghi thêm vào cuối, nội dung cũ được giữ nguyên
"x"Tạo tệp mới; nếu đã tồn tại — FileExistsError
"b"Thêm vào chế độ: làm việc với byte ("rb", "wb")

Vì sao cần with

Tệp phải được đóng: nếu không, dữ liệu có thể chưa được ghi xuống đĩa, và hệ thống vẫn giữ bộ mô tả tệp. Cấu trúc with tự động đóng tệp — kể cả khi trong khối xảy ra lỗi.

with open("ghi_chu.txt", "w", encoding="utf-8") as f:
    f.write("Dòng đầu tiên\n")
    f.write("Dòng thứ hai\n")

with open("ghi_chu.txt", encoding="utf-8") as f:
    for line in f:
        print(line.rstrip("\n"))

Duyệt tệp bằng vòng lặp đọc từng dòng và không nạp toàn bộ vào bộ nhớ, nên có thể xử lý cả những file log hàng gigabyte. Phương thức f.read() trả về toàn bộ nội dung dưới dạng một chuỗi, f.readlines() trả về danh sách các dòng.

Ghi thêm một dòng mà không xóa nội dung cũ

Lỗi phổ biến nhất của người mới là mở tệp ở chế độ "w" để «thêm» một dòng. Chế độ "w" xóa trắng tệp ngay khi mở, trước cả lệnh write đầu tiên. Để thêm bản ghi vào cuối, dùng "a"; để chắc chắn không ghi đè tệp đã có, dùng "x".

with open("log.txt", "a", encoding="utf-8") as f:
    f.write("bản ghi mới\n")

Lưu ý rằng write không tự thêm ký tự xuống dòng — "\n" phải được viết rõ ràng. Cũng có thể dùng print(..., file=f): nó tự thêm dấu xuống dòng cho bạn.

Mã hóa UTF-8: vì sao xuất hiện chữ lỗi

Trước Python 3.15, hàm open() không có tham số encoding sẽ dùng mã hóa của hệ thống. Trên Linux và macOS đó là UTF-8, còn trên Windows với locale tiếng Nga là cp1251. Một script chạy tốt trên máy chủ có thể hiển thị «Р•РґР°» thay vì «Еда» trên laptop của đồng nghiệp, hoặc gây UnicodeDecodeError.

Trong Python 3.15, theo PEP 686, chế độ UTF-8 được bật mặc định: open() không có encoding đọc và ghi UTF-8 trên mọi hệ điều hành. Muốn quay về hành vi cũ, đặt biến môi trường PYTHONUTF8=0 hoặc cờ -X utf8=0; còn muốn yêu cầu rõ ràng mã hóa của hệ thống, dùng encoding="locale".

import sys
print(sys.flags.utf8_mode)   # 1 trong Python 3.15

open("cu.txt", encoding="locale")         # mã hóa của hệ thống
open("excel_cu.csv", encoding="cp1251")   # tệp được lưu bằng cp1251

Mặt trái: CSV cũ từ Excel hoặc script trên Windows được lưu bằng cp1251 sẽ bị đọc như UTF-8 và gây UnicodeDecodeError. Cách xử lý: đọc với encoding="cp1251" rồi ghi lại bằng UTF-8. Thói quen tốt vẫn là luôn ghi rõ encoding — khi đó code hoạt động giống nhau trên mọi phiên bản.

pathlib trong Python: đường dẫn dưới dạng đối tượng

Ghép đường dẫn bằng chuỗi ("data" + "/" + name) rất bất tiện và dễ hỏng trên các hệ điều hành khác nhau. Module pathlib cung cấp class Path: đường dẫn được nối bằng toán tử /, và đối tượng có sẵn các thuộc tính rõ ràng.

from pathlib import Path

data = Path("data")
csv_path = data / "expenses.csv"

print(csv_path.name)    # expenses.csv
print(csv_path.stem)    # expenses
print(csv_path.suffix)  # .csv
print(csv_path.parent)  # data
print(csv_path.exists())

data.mkdir(parents=True, exist_ok=True)
(data / "hello.txt").write_text("Xin chào", encoding="utf-8")
print((data / "hello.txt").read_text(encoding="utf-8"))

for p in data.glob("*.csv"):
    print(p)

Các phương thức read_text và write_text thay cho cặp open + with với tệp nhỏ. Đối tượng Path có thể truyền cho open, json hay csv — tất cả đều chấp nhận. Để có đường dẫn tính từ vị trí của chính script chứ không phải thư mục hiện tại, dùng Path(__file__).parent / "data".

Trong Python 3.14, Path có thêm các phương thức copy() và move(), nên với thao tác sao chép đơn giản không còn cần shutil. Cũng trong phiên bản này, thư viện chuẩn bổ sung module compression.zstd để nén theo chuẩn Zstandard (PEP 784).

JSON trong Python: load, dump, loads, dumps

JSON là định dạng văn bản để trao đổi dữ liệu: các API trả về JSON, còn cấu hình và báo cáo thường được lưu dưới dạng JSON. Module json làm hai việc: serialize (đối tượng Python → văn bản JSON) và deserialize (văn bản → đối tượng). Có bốn hàm, và việc nhầm lẫn giữa chúng là câu hỏi thường gặp.

HàmTác dụng
json.dumps(obj)Đối tượng → chuỗi JSON
json.loads(s)Chuỗi JSON → đối tượng
json.dump(obj, f)Đối tượng → ghi vào tệp đã mở
json.load(f)Đọc JSON từ tệp đã mở

Cách nhớ đơn giản: chữ s ở cuối là viết tắt của string — làm việc với chuỗi; không có s — làm việc với tệp.

Tương ứng giữa các kiểu dữ liệu

PythonJSON
dictobject {}
list, tuplemảng []
strchuỗi
int, floatsố
True / Falsetrue / false
Nonenull

Tuple sau khi đọc ngược lại sẽ thành list; còn set, date và Decimal module không biết cách xử lý và sẽ gây TypeError: Object of type ... is not JSON serializable.

Cách lưu JSON vào tệp

import json
from pathlib import Path

report = {
    "month": "2026-10",
    "total": 15230.5,
    "categories": {"ăn uống": 8200, "đi lại": 2100},
    "tags": ["nhà", "công việc"],
}

path = Path("data/report.json")
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
    json.dump(report, f, ensure_ascii=False, indent=2)

Tham số ensure_ascii=False giữ nguyên tiếng Việt như vốn có. Nếu không có nó, trong tệp sẽ xuất hiện các chuỗi ă — vẫn là JSON hợp lệ, nhưng con người khó đọc. indent=2 tạo định dạng có thụt đầu dòng cho dễ nhìn.

Cách đọc JSON từ tệp

with open("data/report.json", encoding="utf-8") as f:
    data = json.load(f)

print(data["total"])                  # 15230.5
print(data["categories"]["ăn uống"])  # 8200

Kết quả là một dict hoặc list thông thường, làm việc với nó bằng các công cụ quen thuộc. Nếu tệp không phải JSON hoặc bị cắt cụt, sẽ có ngoại lệ json.JSONDecodeError kèm số dòng và vị trí — nên bắt nó khi dữ liệu đến từ bên ngoài.

Phân tích chuỗi JSON và in đẹp

import json

raw = '{"name": "Lan", "skills": ["python", "sql"], "active": true}'
user = json.loads(raw)
print(user["skills"][0])   # python

print(json.dumps(user, ensure_ascii=False, indent=2, sort_keys=True))

sort_keys=True sắp xếp các khóa khi in — tiện để so sánh tệp trong git. Để định dạng nhanh một tệp từ terminal có sẵn tiện ích: python -m json.tool data/report.json.

Phản hồi của một HTTP API cũng là chuỗi JSON. Thư viện requests phân tích nó bằng phương thức response.json(), bên trong vẫn là json.loads.

Điểm cần lưu ý: khóa JSON luôn là chuỗi

d = {1: "tháng 1", 2: "tháng 2"}
back = json.loads(json.dumps(d))
print(back == d)   # False
print(back)        # {'1': 'tháng 1', '2': 'tháng 2'}
# back[1] -> KeyError, phải dùng back["1"]

fixed = {int(k): v for k, v in back.items()}

Trong JSON, khóa của object chỉ có thể là chuỗi, nên các khóa số sau một vòng serialize sẽ thành chuỗi. Hãy chuyển lại kiểu khi đọc.

Kiểu tùy chỉnh: ngày tháng và Decimal

import json
from datetime import date

def default(obj):
    if isinstance(obj, date):
        return obj.isoformat()
    raise TypeError(f"không biết serialize {type(obj).__name__}")

print(json.dumps({"day": date(2026, 10, 1)}, default=default))
# {"day": "2026-10-01"}

Hàm truyền vào tham số default được gọi với mỗi đối tượng không xác định. Với trường hợp phức tạp hơn, có thể kế thừa json.JSONEncoder và ghi đè phương thức default.

Cách thêm phần tử vào mảng JSON

Không thể thêm vào cuối tệp JSON như với log văn bản: phải đọc toàn bộ tệp, thay đổi rồi ghi lại.

path = Path("data/items.json")
items = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
items.append({"id": len(items) + 1, "title": "mục mới"})
path.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")

Nếu bản ghi rất nhiều và liên tục được thêm vào, nên dùng định dạng JSON Lines: mỗi dòng là một đối tượng JSON, và tệp được mở ở chế độ "a".

CSV trong Python: module csv

CSV là bảng dữ liệu dạng văn bản: mỗi dòng là một bản ghi, các giá trị được ngăn cách bằng dấu phẩy. Module csv xử lý đúng dấu nháy kép, dấu phẩy nằm trong giá trị và xuống dòng — điều mà split(",") viết tay không làm được.

import csv

with open("data/expenses.csv", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    total = 0.0
    for row in reader:
        total += float(row["amount"])
        print(row["date"], row["category"], row["amount"])
print("Tổng cộng:", total)

csv.DictReader biến mỗi dòng thành một dict theo tiêu đề của tệp. Tất cả giá trị đều là chuỗi: số và ngày phải chuyển đổi thủ công (float, int, date.fromisoformat). Tệp dùng dấu «;» làm phân cách (Excel phiên bản tiếng Nga hay lưu như vậy) được đọc với tham số delimiter=";".

rows = [
    {"date": "2026-10-01", "category": "ăn uống", "amount": 450},
    {"date": "2026-10-02", "category": "đi lại", "amount": 60},
]
with open("data/out.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["date", "category", "amount"])
    writer.writeheader()
    writer.writerows(rows)

Khi ghi, bắt buộc có newline="": nếu thiếu, trên Windows giữa các dòng sẽ xuất hiện dòng trống. Nếu tệp sẽ được mở bằng Excel, dùng encoding="utf-8-sig" — ký tự BOM giúp Excel nhận ra UTF-8.

Từ CSV sang JSON: ví dụ đầy đủ

import csv
import json
from collections import defaultdict
from pathlib import Path

src = Path("data/expenses.csv")
dst = Path("data/report.json")

by_category = defaultdict(float)
with src.open(newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        by_category[row["category"]] += float(row["amount"])

report = {"total": sum(by_category.values()), "categories": dict(by_category)}
dst.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(dst.read_text(encoding="utf-8"))

Đây là khung của mọi script báo cáo: đầu vào là CSV, gom nhóm trong dict, đầu ra là JSON.

Lỗi thường gặp

  • FileNotFoundError — đường dẫn tương đối được tính từ thư mục chạy lệnh, chứ không phải từ script. Dùng Path(__file__).parent.
  • Tệp bị xóa trắng — mở ở chế độ "w" thay vì "a".
  • UnicodeDecodeError — tệp dùng mã hóa khác; hãy chỉ rõ (cp1251, utf-8-sig).
  • TypeError: Object of type set is not JSON serializable — chuyển thành list hoặc truyền default.
  • KeyError sau khi tải JSON — các khóa số đã thành chuỗi.
  • Tệp của bạn có tên json.py hoặc csv.py — nó che module chuẩn, và import json sẽ nạp chính tệp đó.

Tổng kết

Với JSON trong Python, thư viện chuẩn là đủ: json.load/json.dump cho tệp, loads/dumps cho chuỗi, ensure_ascii=False và indent cho đầu ra dễ đọc. Mở tệp bằng with kèm mã hóa được chỉ rõ, xây dựng đường dẫn bằng pathlib, đọc bảng bằng csv.DictReader. Chủ đề này được thực hành trong khóa học Python: trình theo dõi chi tiêu đọc các khoản chi từ CSV và lưu báo cáo ra JSON.

Câu hỏi thường gặp

json.load khác json.loads ở điểm nào?

json.load đọc JSON từ một đối tượng tệp đã mở, còn json.loads phân tích một chuỗi. Tương tự, json.dump ghi vào tệp, còn json.dumps trả về chuỗi.

Làm sao lưu tiếng Việt trong JSON mà không có chuỗi \u?

Truyền ensure_ascii=False vào json.dump hoặc json.dumps, và mở tệp với encoding="utf-8".

Có cần cài thư viện riêng để làm việc với JSON trong Python không?

Không, module json được tích hợp sẵn trong thư viện chuẩn; chỉ cần import json.

Có thể lưu ngày tháng hoặc set vào JSON không?

Không trực tiếp: cần chuyển đổi — ví dụ date thành chuỗi ISO bằng isoformat, set thành list — hoặc truyền một hàm vào tham số default.

Làm sao đọc một JSON lớn không vừa bộ nhớ?

json.load đọc cả tệp một lần; với dữ liệu lớn, nên lưu theo định dạng JSON Lines và phân tích từng dòng bằng json.loads.

Học chủ đề này qua video

Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Python 3.15: от новичка до профи”.

Đến khóa học