Đồ án nhỏ: báo cáo doanh thu theo tháng
Bạn sẽ học được
- Ghép toàn bộ chương 5 vào một chương trình hoàn chỉnh.
- Đọc CSV, parse ngày, nhóm theo tháng, tổng hợp doanh thu.
- Xử lý dòng lỗi mà không làm dừng cả báo cáo.
- Xuất kết quả dạng bảng và JSON.
- Tự kiểm tra bằng bộ câu hỏi cuối bài.
Đề bài
Cho file sales.csv ghi các đơn hàng:
ngay,mat_hang,so_luong,don_gia
2026-01-05,Áo thun,2,150000
2026-01-12,Quần jean,1,450000
2026-02-03,Áo thun,3,150000
2026-02-14,Áo khoác,2,350000
2026-02-20,Quần jean,1,190000
2026-02-21,Mũ,,90000Viết chương trình:
- Đọc từng dòng, bỏ qua dòng lỗi và đếm lại.
- Tính doanh thu từng dòng =
so_luong × don_gia. - Nhóm doanh thu theo tháng (
YYYY-MM). - In bảng tổng hợp và tổng cộng.
- Xuất dict doanh thu theo tháng ra JSON.
Kết quả mong đợi:
Số dòng hợp lệ: 5 | dòng lỗi: 1
2026-01 750,000
2026-02 1,340,000
Tổng 2,090,000
{"2026-01": 750000, "2026-02": 1340000}Dòng cuối (Mũ) thiếu so_luong nên int("") lỗi — đây là dòng bị bỏ qua.
Bước 1 — Đọc và kiểm tra từng dòng
Điểm quyết định chất lượng báo cáo: một dòng hỏng không được làm hỏng cả file. Đọc từng dòng, parse ngay khi đọc, và gom ngày + tháng vào bản ghi:
import csv
import io
from datetime import datetime
def doc_ban_ghi(reader):
rows, loi = [], 0
for row in reader:
try:
ngay = datetime.strptime(row["ngay"], "%Y-%m-%d").date()
so_luong = int(row["so_luong"])
don_gia = int(row["don_gia"])
except (ValueError, KeyError, TypeError):
loi += 1
continue
rows.append({
"ngay": ngay,
"thang": ngay.strftime("%Y-%m"),
"mat_hang": row["mat_hang"],
"doanh_thu": so_luong * don_gia,
})
return rows, loiVì sao bắt cả ba loại:
ValueError: ô rỗng hoặc không phải số (int("")).KeyError: file thiếu cột so với dự kiến.TypeError: hiếm hơn, nhưng giữ lại để hàm không vỡ nếu đầu vào khác kiểu.
continue sau khi đếm lỗi là mấu chốt: báo cáo đi tiếp, còn số dòng lỗi được trả về để người dùng biết dữ liệu không sạch.
Trong file thật:
with open("sales.csv", newline="", encoding="utf-8") as f:
rows, loi = doc_ban_ghi(csv.DictReader(f))Với file xuất từ Excel, đổi encoding="utf-8" thành "utf-8-sig".
Bước 2 — Nhóm theo tháng
rows đã có sẵn trường thang, nên chỉ cần cộng dồn vào dict. Dùng get(..., 0) + là mẫu gom nhóm cơ bản:
theo_thang = {}
for r in rows:
theo_thang[r["thang"]] = theo_thang.get(r["thang"], 0) + r["doanh_thu"]
tong = sum(theo_thang.values())Dict giữ thứ tự chèn, nên tháng xuất hiện trước in trước — với dữ liệu đã sắp theo ngày thì báo cáo tự nhiên đúng thứ tự. Nếu nguồn lộn xộn, sắp lại:
theo_thang = dict(sorted(theo_thang.items()))Nếu chỉ cần một dòng cho mỗi tháng, có thể thay toàn bộ vòng lặp bằng defaultdict(int) hoặc Counter:
from collections import defaultdict
theo_thang = defaultdict(int)
for r in rows:
theo_thang[r["thang"]] += r["doanh_thu"]Bước 3 — In bảng
Canh cột bằng format spec trong f-string: < canh trái cho nhãn, > canh phải cho số, , thêm dấu phân cách hàng nghìn:
for thang, tien in theo_thang.items():
print(f"{thang:<10}{tien:>10,}")
print(f"{'Tổng':<10}{tong:>10,}")2026-01 750,000
2026-02 1,340,000
Tổng 2,090,000Kiểm tra bằng tay để chắc số đúng:
| Tháng | Các dòng | Doanh thu |
|---|---|---|
| 2026-01 | 2 × 150.000; 1 × 450.000 | 300.000 + 450.000 = 750.000 |
| 2026-02 | 3 × 150.000; 2 × 350.000; 1 × 190.000 | 450.000 + 700.000 + 190.000 = 1.340.000 |
| Tổng | 2.090.000 |
Nên in cả số dòng đã bỏ qua. Một báo cáo không nói gì về dòng lỗi là báo cáo không đáng tin:
print("Số dòng hợp lệ:", len(rows), "| dòng lỗi:", loi)Số dòng hợp lệ: 5 | dòng lỗi: 1Bước 4 — Xuất JSON
Sau khi đã có bảng trong bộ nhớ, xuất JSON rất ngắn vì theo_thang đã đúng cấu trúc dict → object:
import json
print(json.dumps(theo_thang, ensure_ascii=False)){"2026-01": 750000, "2026-02": 1340000}Ghi ra file:
with open("bao_cao.json", "w", encoding="utf-8") as f:
json.dump(theo_thang, f, ensure_ascii=False, indent=2)Trong đồ án này mọi kiểu đều serialize được (str, int), nên không gặp TypeError: Object of type ... is not JSON serializable. Nhưng nếu bạn thêm trường ngay kiểu date vào JSON, nhớ .isoformat() trước.
Bước 5 — Ghép lại
Toàn bộ chương trình, dùng chuỗi mẫu thay cho file:
import csv
import io
import json
from datetime import datetime
raw = """ngay,mat_hang,so_luong,don_gia
2026-01-05,Áo thun,2,150000
2026-01-12,Quần jean,1,450000
2026-02-03,Áo thun,3,150000
2026-02-14,Áo khoác,2,350000
2026-02-20,Quần jean,1,190000
2026-02-21,Mũ,,90000
"""
def doc_ban_ghi(reader):
rows, loi = [], 0
for row in reader:
try:
ngay = datetime.strptime(row["ngay"], "%Y-%m-%d").date()
so_luong = int(row["so_luong"])
don_gia = int(row["don_gia"])
except (ValueError, KeyError, TypeError):
loi += 1
continue
rows.append({
"ngay": ngay,
"thang": ngay.strftime("%Y-%m"),
"mat_hang": row["mat_hang"],
"doanh_thu": so_luong * don_gia,
})
return rows, loi
rows, loi = doc_ban_ghi(csv.DictReader(io.StringIO(raw)))
print("Số dòng hợp lệ:", len(rows), "| dòng lỗi:", loi)
theo_thang = {}
for r in rows:
theo_thang[r["thang"]] = theo_thang.get(r["thang"], 0) + r["doanh_thu"]
tong = sum(theo_thang.values())
for thang, tien in theo_thang.items():
print(f"{thang:<10}{tien:>10,}")
print(f"{'Tổng':<10}{tong:>10,}")
print(json.dumps(theo_thang, ensure_ascii=False))Chạy đúng cho kết quả ở phần đề bài. Đổi io.StringIO(raw) thành open("sales.csv", newline="", encoding="utf-8") là dùng được với file thật.
Mở rộng
- Thêm cột số đơn mỗi tháng:
so_don[thang] = so_don.get(thang, 0) + 1. - Tính giá trị trung bình mỗi đơn từng tháng.
- Đọc thêm cột
khu_vucvà nhóm hai tầngthang → khu_vuc. - Ghi báo cáo ra CSV bằng
DictWriterthay vì in màn hình. - Kiểm tra
don_giaâm và đếm riêng loại lỗi đó thay vì gộp chung.
Tự kiểm tra
1. Tổng doanh thu tháng 2 là bao nhiêu?
450.000 + 700.000 + 190.000 = 1.340.000.
2. Vì sao dòng Mũ bị bỏ qua?
Cột so_luong rỗng; int("") ném ValueError. except bắt lỗi, loi += 1, continue.
3. Nếu xóa continue thì chuyện gì xảy ra?
Vòng lặp vẫn chạy tiếp (vì except đã bắt lỗi), nhưng rows.append vẫn được gọi với so_luong/don_gia chưa gán — gây UnboundLocalError ở lần lặp đầu tiên bị lỗi.
4. Vì sao datetime.strptime cần "%Y-%m-%d"?
Chuỗi dạng 2026-01-05; nếu dùng %d/%m/%Y sẽ lỗi hoặc đọc sai. Định dạng phải khớp nguồn.
5. Tổng tính từ theo_thang hay từ rows — có khác nhau không?
Không, sum(theo_thang.values()) bằng tổng doanh_thu của mọi dòng hợp lệ. Tính từ theo_thang gọn hơn và tự khớp với bảng đã in.
6. Muốn báo cáo luôn bắt đầu bằng 2026-01 dù tháng đó không có đơn thì làm sao?
Không thể suy ra từ dữ liệu. Duyệt danh sách tháng cố định và dùng theo_thang.get(thang, 0) để in 0 cho tháng thiếu.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
| Cả chương trình dừng ở dòng hỏng | Không có try/except quanh parse | Bắt (ValueError, KeyError, TypeError) |
| Báo cáo thiếu số mà không hay | Nuốt lỗi bằng pass | Đếm loi và in ra |
UnboundLocalError | continue bị bỏ sót | Thêm continue trong except |
| Tháng bị đảo thứ tự | Nguồn lộn xộn | sorted(theo_thang.items()) |
| Số dính liền | Thiếu , trong format spec | f"{tien:,}" |
Tiếng Việt thành \u | ensure_ascii mặc định | ensure_ascii=False |
strptime lỗi | Sai định dạng ngày | Khớp %Y-%m-%d với nguồn |
| Tổng lệch | Cộng trước khi ép kiểu | Ép int rồi mới nhân |
Tóm tắt
- Đọc CSV → parse ngay → bỏ qua dòng lỗi có đếm.
- Nhóm bằng
dict.get(..., 0) +, tổng bằngsum(values()). - In bảng bằng format spec
<,>,,. - Xuất JSON bằng
dumps(..., ensure_ascii=False). - Một đồ án dữ liệu tốt luôn nói rõ bao nhiêu dòng bị bỏ — im lặng là lỗi.
Use ← → to move between lessons