Đọc ghi CSV và Excel trong Python

Kế toán gửi bạn file CSV danh sách khách hàng. Mở bằng Excel thì tiếng Việt thành một mớ ký tự lạ, mã khách hàng 0123 biến thành 123, còn ngày 05/09/2026 nhảy sang tháng năm. Không phải file hỏng — chỉ là vài quy tắc bạn cần biết trước.

Đọc CSV với DictReader

Module csv có sẵn trong Python, không cần cài gì. DictReader lấy dòng đầu làm tên cột và trả về mỗi dòng là một dict.

import csv

with open("khachhang.csv", newline="", encoding="utf-8-sig") as f:
    reader = csv.DictReader(f)
    print(reader.fieldnames)        # ['ma', 'ten', 'tien']
    for dong in reader:
        print(dong["ma"], dong["ten"])

Hai tham số ở dòng open đều quan trọng. newline="" là bắt buộc: module csv tự xử lý ký tự xuống dòng, nếu bạn để Python làm hộ thì file ghi ra sẽ có dòng trống xen kẽ, còn ô chứa nhiều dòng sẽ bị đọc sai. Còn encoding thì cả một mục ở dưới.

Nếu file không có dòng tiêu đề, hoặc bạn chỉ cần dữ liệu thô, csv.reader trả về từng dòng dưới dạng list:

with open("khachhang.csv", newline="", encoding="utf-8-sig") as f:
    for dong in csv.reader(f):
        print(dong)     # ['0123', 'Nguyễn Văn An', '1500000']

Ghi CSV với DictWriter

hang = [
    {"ma": "0123", "ten": "Nguyễn Văn An", "tien": 1500000},
    {"ma": "0456", "ten": "Trần Thị Bình", "tien": 2300000},
]

with open("kh.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.DictWriter(f, fieldnames=["ma", "ten", "tien"])
    writer.writeheader()
    writer.writerows(hang)

Danh sách fieldnames quyết định thứ tự cột. Dict thiếu khóa sẽ báo lỗi trừ khi bạn đặt restval=""; dict thừa khóa thì ném ValueError: dict contains fields not in fieldnames, muốn bỏ qua thì thêm extrasaction="ignore".

writer = csv.DictWriter(f, fieldnames=["ma", "ten"],
                        restval="",              # thiếu khóa -> ô trống
                        extrasaction="ignore")   # thừa khóa -> bỏ qua

utf-8-sig: chìa khóa cho Excel tiếng Việt

Excel trên Windows không đoán được file UTF-8 nếu không có dấu hiệu nhận biết. Dấu hiệu đó là BOM — ba byte vô hình ở đầu file. Encoding utf-8-sig chính là “UTF-8 có BOM”.

# GHI cho Excel đọc được tiếng Việt
open("kh.csv", "w", newline="", encoding="utf-8-sig")

# ĐỌC file do Excel xuất ra
open("kh.csv", newline="", encoding="utf-8-sig")

Đọc bằng utf-8 thường một file có BOM sẽ gây ra lỗi rất khó hiểu: tên cột đầu tiên dính thêm ký tự vô hình.

# Đọc bằng encoding="utf-8" một file có BOM:
print(reader.fieldnames)    # ['ma', 'ten', 'tien']
dong["ma"]                  # KeyError!

Điểm hay là utf-8-sig đọc được cả file có BOM lẫn file không BOM, nên cứ dùng nó khi làm việc với Excel là an toàn cả hai chiều. Nếu file đến từ hệ thống cũ của Việt Nam, đôi khi nó là cp1258 hoặc windows-1252 — lúc đó phải hỏi người gửi chứ không đoán được.

Dấu phân cách không phải lúc nào cũng là dấu phẩy

Excel ở nhiều máy dùng dấu chấm phẩy, vì trong định dạng số của khu vực đó dấu phẩy đang làm nhiệm vụ phân cách hàng nghìn.

with open("kh.csv", newline="", encoding="utf-8-sig") as f:
    for dong in csv.reader(f, delimiter=";"):
        print(dong)

# Tab cũng là một lựa chọn phổ biến, an toàn hơn vì hiếm khi có trong dữ liệu
csv.writer(f, delimiter="\t")

Không biết trước dấu phân cách thì để Sniffer đoán từ vài dòng đầu:

with open("la.csv", newline="", encoding="utf-8-sig") as f:
    mau = f.read(2048)
    f.seek(0)
    dau = csv.Sniffer().sniff(mau).delimiter
    for dong in csv.reader(f, delimiter=dau):
        print(dong)

Việc bọc dấu nháy, module csv lo hết. Ô chứa đúng ký tự phân cách vẫn được ghi và đọc lại chính xác:

csv.writer(f, delimiter=";").writerow(["Bàn; ghế", "1.000"])
# Ghi ra file: "Bàn; ghế";1.000
# Đọc lại:     ['Bàn; ghế', '1.000']

Mọi thứ đọc lên đều là chuỗi

Đây là chỗ vấp thường xuyên nhất. CSV là file văn bản thuần, không lưu kiểu dữ liệu.

dong["tien"]            # '1500000' — chuỗi, không phải số
dong["tien"] + 1        # TypeError
int(dong["tien"]) + 1   # 1500001  — phải tự ép kiểu

with open("kh.csv", newline="", encoding="utf-8-sig") as f:
    tong = sum(int(d["tien"]) for d in csv.DictReader(f))

Dữ liệu thật hay có ô trống hoặc ký tự phân cách hàng nghìn, nên viết một hàm nhỏ để dọn là đáng:

def ve_so(gia_tri, mac_dinh=0):
    s = (gia_tri or "").strip().replace(".", "").replace(",", "")
    return int(s) if s else mac_dinh

Bẫy số 0 ở đầu

Mã khách hàng 0123, số điện thoại 0912345678, mã bưu chính — tất cả đều là chuỗi dù trông giống số. Trong file CSV chúng vẫn nguyên vẹn, và Python đọc lên cũng đủ:

# Nội dung file: 0123,0912345678
# Python đọc lại: {'ma_kh': '0123', 'sdt': '0912345678'}  — không mất gì

Vấn đề xảy ra ở Excel: khi mở file CSV, Excel tự đoán kiểu từng ô và biến 0123 thành số 123. Bọc dấu nháy trong CSV cũng không cứu được, vì đó là quy ước của định dạng CSV chứ không phải lệnh cho Excel. Có hai cách xử lý thật sự:

  1. Trong Excel, dùng Data → From Text/CSV rồi chọn kiểu Text cho cột đó, thay vì nháy đúp mở file.
  2. Xuất thẳng ra file .xlsx và đặt định dạng ô là văn bản — xem mục dưới.

Ở chiều ngược lại, nếu bạn nhận file từ người khác và mã đã mất số 0, hãy xin lại bản gốc chứ đừng cố đoán bằng cách thêm zfill, vì bạn không biết mã gốc dài bao nhiêu.

Bẫy ngày tháng

Chuỗi 05/09/2026 là ngày 5 tháng 9 với người Việt, nhưng là ngày 9 tháng 5 với định dạng Mỹ. Trong Python, hãy nói rõ:

from datetime import datetime, date

d = datetime.strptime("05/09/2026", "%d/%m/%Y").date()
print(d)                    # 2026-09-05

Và khi ghi ra, dùng định dạng ISO YYYY-MM-DD — không nhập nhằng, sắp xếp được bằng chính chuỗi đó:

print(date(2026, 9, 5).isoformat())     # '2026-09-05'

Khi nào cần openpyxl

Module csv đủ dùng cho phần lớn việc. Bạn cần openpyxl (cài bằng pip install openpyxl) khi file thật sự là .xlsx: nhiều sheet, công thức, định dạng ô, màu sắc, độ rộng cột. Đổi lại, file xlsx nặng hơn và đọc chậm hơn CSV nhiều lần.

from openpyxl import Workbook
from datetime import date

wb = Workbook()
ws = wb.active
ws.title = "Khách hàng"

ws.append(["Mã KH", "Tên", "Ngày ký", "Doanh thu"])
ws.append(["0123", "Nguyễn Văn An", date(2026, 9, 5), 1500000])

for o in ws["A"]:
    o.number_format = "@"      # ép cột A là văn bản -> giữ số 0 ở đầu

wb.save("kh.xlsx")

Dòng number_format = "@" chính là cách giải quyết triệt để bẫy số 0: Excel được lệnh coi cả cột là văn bản nên không tự đổi nữa.

Đọc file xlsx

from openpyxl import load_workbook

wb = load_workbook("kh.xlsx")
ws = wb["Khách hàng"]
print(ws.max_row, ws.max_column)        # 2 4

for hang in ws.iter_rows(min_row=2, values_only=True):
    print(hang)
    # ('0123', 'Nguyễn Văn An', datetime.datetime(2026, 9, 5, 0, 0), 1500000)

print(ws["A2"].value)                   # '0123'
print(ws.cell(row=2, column=2).value)   # 'Nguyễn Văn An'

Chú ý values_only=True: không có nó bạn nhận về các đối tượng ô chứ không phải giá trị. Và để ý ngày tháng quay về dưới dạng datetime chứ không phải date — khi xuất sang CSV nó sẽ thành 2026-09-05 00:00:00, nên hãy gọi .date() trước.

Với file lớn, bật chế độ chỉ đọc để không nạp hết vào bộ nhớ:

wb = load_workbook("lon.xlsx", read_only=True, data_only=True)
for hang in wb.active.iter_rows(values_only=True):
    xu_ly(hang)
wb.close()

Tham số data_only=True trả về kết quả của công thức thay vì bản thân công thức — nhưng chỉ khi file đã từng được Excel mở và lưu, vì openpyxl không tự tính công thức.

Vài nguyên tắc nên nhớ

  1. Luôn mở file CSV với newline=""encoding="utf-8-sig".
  2. Mọi giá trị đọc từ CSV đều là chuỗi — tự ép kiểu, và xử lý ô trống.
  3. Mã số có số 0 ở đầu là chuỗi; muốn Excel giữ nguyên thì xuất xlsx với định dạng "@".
  4. Ghi ngày theo chuẩn ISO YYYY-MM-DD, đọc vào bằng strptime với format nói rõ.
  5. Chỉ dùng openpyxl khi thật sự cần định dạng hoặc nhiều sheet; còn lại CSV nhanh và nhẹ hơn.
  6. File lớn thì duyệt từng dòng, đừng nạp cả file vào list.

Phần lớn rắc rối với CSV không nằm ở việc đọc ghi, mà ở chỗ dữ liệu phải đi qua Excel. Nhớ đúng encoding và đừng để Excel tự đoán kiểu — bấy nhiêu đã giải quyết được hầu hết các ca.

Lên đầu trang