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

URL: https://skillaria.top/vi/guides/python/json-python

Để 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 Việt là cp1258. Một script chạy tốt trên máy chủ có thể hiển thị «Ä‚n uá»‘ng» thay vì «Ăn uống» 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="cp1258")   # tệp được lưu bằng cp1258
```

Mặt trái: CSV cũ từ Excel hoặc script trên Windows được lưu bằng cp1258 sẽ bị đọc như UTF-8 và gây `UnicodeDecodeError`. Cách xử lý: đọc với `encoding="cp1258"` 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àm | Tá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

| Python | JSON |
| --- | --- |

| `dict` | object `{}` |

| `list`, `tuple` | mảng `[]` |

| `str` | chuỗi |

| `int`, `float` | số |

| `True` / `False` | `true` / `false` |

| `None` | `null` |

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 với cài đặt vùng tiếng Việt 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õ (`cp1258`, `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.

## FAQ

**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.


---
Source: Skillaria.Top (https://skillaria.top)
