Bỏ qua đến nội dung chính
Học Python
Logo Python bên cạnh file Excel báo cáo năm 2025 có bảng doanh thu 4 kênh theo tháng và biểu đồ cột chồng do script Python tạo ra

Python cho Excel: tự động hóa báo cáo bán hàng

AIDev TeamTham vấn chuyên môn: Nguyễn Việt Chung

Đăng 12 phút đọc1 lượt xem

Mục lục · 16

Python Excel là cách dùng Python đọc, gộp và định dạng file Excel thay cho thao tác tay: pandas tính, openpyxl tô màu và vẽ biểu đồ. Bài gộp 12 file bán hàng của một cửa hàng mẫu thành báo cáo có định dạng, kèm số đo thời gian và hai lỗi gặp khi chạy.

Dùng Python cho Excel nghĩa là để một đoạn code mở các file Excel, gộp lại, tính tổng và ghi ra một file báo cáo đã định dạng sẵn. Với 12 file bán hàng của một năm, đoạn code trong bài này chạy xong trong 0,47 giây trên máy Mac M4 Pro của mình. Tháng sau có file mới, bạn chạy lại đúng lệnh đó là có báo cáo mới.

Mình làm thử trên dữ liệu cửa hàng "Tiệm Mây" của khóa Phân tích dữ liệu với Python. Đây là dữ liệu mô phỏng, tải công khai ở lab.aidev.edu.vn/k2. Mình tách 3.778 đơn hàng năm 2025 thành 12 file .xlsx, mỗi tháng một file, giống file mà phần mềm bán hàng xuất ra hằng tháng.

Mấy điều cần nắm trước khi đọc tiếp:

  • Bài dùng hai library (thư viện code viết sẵn) của Python: pandas đọc, gộp và tính; openpyxl lo phần định dạng ô, độ rộng cột và biểu đồ trong file Excel.

  • Báo cáo năm 2025 chỉ nên cộng đơn "Hoàn tất". Nếu cộng cả đơn hủy và đổi trả, doanh thu bị đội thêm 1,55 tỷ đồng.

  • Đọc file .xlsx là bước chậm nhất. Với 377.800 dòng, bước này mất khoảng 15 giây, còn đọc cùng dữ liệu đó từ CSV chỉ mất 0,1 giây.

  • Không cần cài Excel để chạy code. Mình chỉ mở Excel để xem kết quả.

Python Excel làm được những gì?

Người ta hay tìm Python Excel vì ba việc: gộp nhiều file thành một bảng, tính lại cùng một báo cáo mỗi tuần hoặc mỗi tháng, và xuất kết quả ra file có định dạng để gửi sếp. Mỗi việc trong bảng dưới đây có một library làm hợp nhất:

Việc

Library

Ghi chú

Đọc, gộp, lọc, tính tổng

pandas 3.1.0

read_excel, concat, pivot_table

Định dạng ô, cột, biểu đồ

openpyxl 3.1.5

sửa được file đã có; pandas cũng gọi nó để đọc .xlsx

Ghi file mới nhiều định dạng

XlsxWriter 3.3.0

chỉ ghi file mới, không mở được file có sẵn

Điều khiển Excel đang mở

xlwings 0.37.8

cần cài Excel trên máy

Viết Python ngay trong ô Excel

Python in Excel (=PY)

tính năng của Microsoft 365, code chạy trên đám mây Microsoft

Bài này dùng pandas và openpyxl vì hai library này đủ cho việc gộp file và làm báo cáo. Phiên bản trong bảng là bản mới nhất trên PyPI ngày 23/10/2026.

Python in Excel chạy theo cách khác hẳn bốn library trên. Theo trang hướng dẫn của Microsoft, code chạy trong một container (môi trường chạy tách riêng) trên đám mây của Microsoft, chỉ đọc được dữ liệu trong sheet hoặc dữ liệu nạp qua Power Query (công cụ nhập dữ liệu của Excel). Các hàm pandas.read_csv và pandas.read_excel không dùng được trong đó. Vì vậy, muốn gộp 12 file trong một thư mục thì bạn vẫn cần Python chạy trên máy.

Cần cài gì trước khi bắt đầu?

Bạn cần Python 3 và hai package (gói cài bằng lệnh pip) là pandas và openpyxl. Mình tạo một virtual environment (môi trường Python riêng của thư mục làm việc) để các package cài vào không đụng tới Python của hệ thống:

bash
python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install pandas openpyxl

Bạn phải cài openpyxl riêng vì lệnh cài pandas không kéo theo openpyxl, dù hàm pd.read_excel của pandas cần openpyxl để đọc file .xlsx. Bài Python là gì có ảnh chụp lỗi ImportError khi quên bước này. Nếu lệnh pip install pandas báo lỗi, xem bài cài pandas và sửa lỗi No module named pandas. Muốn hiểu kỹ hơn virtual environment ở đoạn lệnh trên, bài môi trường ảo Python (venv) chạy đủ vòng tạo, kích hoạt, cài package và xóa trên Windows lẫn Ubuntu.

Gộp 12 file Excel thành một bảng bằng pandas

Thư mục ban_hang/ có 12 file từ ban_hang_2025_01.xlsx đến ban_hang_2025_12.xlsx. Mỗi file có một sheet với các cột Mã đơn, Ngày đặt, Mã KH, Mã SP, Danh mục, Số lượng, Đơn giá, Kênh, Trạng thái và Doanh thu. Tháng 1 có 574 đơn, gấp đôi các tháng khác, vì Tết Ất Tỵ rơi vào ngày 29/01/2025.

Phần đầu của bao_cao.py đọc cả 12 file rồi nối thành một DataFrame (kiểu bảng dữ liệu của pandas, giống một sheet):

python
import time
from pathlib import Path
import pandas as pd
from openpyxl import load_workbook
from openpyxl.chart import BarChart, Reference
from openpyxl.styles import Font, PatternFill

t0 = time.perf_counter()

cac_file = sorted(Path("ban_hang").glob("ban_hang_2025_*.xlsx"))
df = pd.concat([pd.read_excel(f) for f in cac_file], ignore_index=True)
t1 = time.perf_counter()
print(f"Đọc {len(cac_file)} file, {len(df)} dòng: {t1 - t0:.2f} giây")

Path.glob lấy danh sách file theo mẫu tên, sorted xếp theo thứ tự tháng. pd.concat nối 12 bảng thành một. Tham số ignore_index=True đánh số lại các dòng từ 0 đến 3.777. Nếu thiếu tham số này, bảng gộp có 12 dòng mang số 0, 12 dòng mang số 1 và cứ thế tiếp. pandas tự nhận cột Ngày đặt là kiểu ngày tháng, nên bước sau lấy được tháng mà không cần đổi kiểu. Nếu chưa quen DataFrame và Series, bài pandas là gì giải thích hai khái niệm này qua 10 dòng code đầu tiên.

Mình để mẫu tên là ban_hang_2025_*.xlsx thay vì *.xlsx vì đã gặp lỗi khi một file tháng đang mở trong Excel. Mục "Lỗi khi một file tháng đang mở trong Excel" phía sau kể lại chuyện này.

Vì sao phải lọc trạng thái đơn trước khi cộng doanh thu?

File bán hàng thường chứa cả đơn đã hủy và đơn đổi trả. Trong 3.778 đơn năm 2025 có 3.255 đơn Hoàn tất, 334 đơn Hủy và 189 đơn Đổi trả. Cộng cả cột Doanh thu thì ra 10,05 tỷ đồng. Chỉ tính đơn Hoàn tất thì còn 8,50 tỷ đồng. Chênh lệch là 1,55 tỷ đồng, tức báo cáo đội lên khoảng 18%.

Phần thứ hai của script lọc đơn trước rồi mới tính:

python
xong = df[df["Trạng thái"] == "Hoàn tất"].copy()
xong["Tháng"] = xong["Ngày đặt"].dt.strftime("%m/%Y")
theo_thang = xong.pivot_table(index="Tháng", columns="Kênh", values="Doanh thu",
                              aggfunc="sum", fill_value=0, margins=True, margins_name="Tổng")
san_pham = pd.read_csv("goc/san_pham.csv")[["ma_sp", "ten_sp"]]
top_sp = (xong.groupby("Mã SP")["Doanh thu"].sum().nlargest(10).reset_index()
          .merge(san_pham, left_on="Mã SP", right_on="ma_sp")[["Mã SP", "ten_sp", "Doanh thu"]]
          .rename(columns={"ten_sp": "Tên sản phẩm"}))
t2 = time.perf_counter()
print(f"Tính bảng tổng hợp: {t2 - t1:.2f} giây")

pivot_table làm đúng việc của PivotTable trong Excel: tháng nằm ở dòng, kênh bán nằm ở cột, ô giữa là tổng doanh thu. margins=True thêm dòng và cột "Tổng". Bảng thứ hai lấy 10 sản phẩm có doanh thu cao nhất rồi ghép tên sản phẩm từ file san_pham.csv, cũng là file của bộ dữ liệu Tiệm Mây.

Định dạng báo cáo Excel bằng openpyxl thế nào?

to_excel của pandas chỉ ghi dữ liệu, không định dạng: số không có dấu ngăn nghìn, cột hẹp, dòng tiêu đề không tô màu. Phần cuối của script ghi file rồi mở lại bằng openpyxl để sửa những chỗ đó:

python
with pd.ExcelWriter("bao_cao_2025.xlsx") as w:
    theo_thang.to_excel(w, sheet_name="Theo tháng")
    top_sp.to_excel(w, sheet_name="Top sản phẩm", index=False)

wb = load_workbook("bao_cao_2025.xlsx")
for ws in wb.worksheets:
    for o in ws[1]:
        o.font = Font(bold=True, color="FFFFFF")
        o.fill = PatternFill("solid", fgColor="0056D2")
    for cot in ws.iter_cols(min_row=2):
        for o in cot:
            if isinstance(o.value, (int, float)):
                o.number_format = "#,##0"
    for cot in ws.columns:
        ws.column_dimensions[cot[0].column_letter].width = 16
    ws.freeze_panes = "A2"
ws = wb["Theo tháng"]
ws.column_dimensions["A"].width = 10
bd = BarChart()
bd.title = "Doanh thu đơn hoàn tất theo tháng"
bd.type, bd.grouping, bd.overlap = "col", "stacked", 100
bd.add_data(Reference(ws, min_col=2, max_col=5, min_row=1, max_row=13), titles_from_data=True)
bd.set_categories(Reference(ws, min_col=1, min_row=2, max_row=13))
bd.width, bd.height = 19, 9
bd.x_axis.delete = bd.y_axis.delete = False  # openpyxl 3.1 mặc định ẩn hai trục
bd.y_axis.number_format = '#,##0,," tr"'
bd.legend.position = "b"
bd.legend.overlay = False
ws.add_chart(bd, "A16")
wb["Top sản phẩm"].column_dimensions["B"].width = 30
wb.save("bao_cao_2025.xlsx")
t3 = time.perf_counter()
print(f"Ghi và định dạng: {t3 - t2:.2f} giây")
print(f"Tổng: {t3 - t0:.2f} giây")
print(theo_thang.loc["Tổng"].map("{:,.0f}".format).to_string())

Mã định dạng #,##0 là mã của chính Excel. Máy đặt kiểu số Việt Nam sẽ hiện dấu chấm ngăn nghìn, máy kiểu Mỹ hiện dấu phẩy. Mã #,##0,," tr" trên trục biểu đồ chia số cho một triệu rồi thêm chữ "tr", nên trục hiện 200 tr, 400 tr thay vì 200.000.000, 400.000.000.

Khi chạy script trong Terminal, cả ba bước xong trong 0,20 giây:

Terminal chạy python bao_cao.py: đọc 12 file, 3778 dòng mất 0,18 giây, tính bảng tổng hợp 0,01 giây, ghi và định dạng 0,01 giây, tổng 0,20 giây; doanh thu cả năm App 2.067.304.000, Facebook 1.557.242.000, Sàn TMĐT 1.994.548.000, Website 2.883.311.000, tổng 8.502.405.000

Mở bao_cao_2025.xlsx bằng Excel for Mac 16.114.1, sheet "Theo tháng" trông như sau:

Sheet Theo tháng của bao_cao_2025.xlsx mở trong Excel for Mac: dòng tiêu đề nền xanh, doanh thu 12 tháng theo 4 kênh có dấu chấm ngăn nghìn, dòng Tổng 8.502.405.000 và biểu đồ cột chồng doanh thu theo tháng, tháng 01/2025 cao nhất với 1.208.286.000

Biểu đồ cho thấy tháng 1 vượt hẳn các tháng còn lại với 1,21 tỷ đồng, vì có Tết. Website là kênh mang về nhiều tiền nhất năm, 2,88 tỷ đồng. Sheet "Top sản phẩm" có 10 dòng, đứng đầu là đồng hồ thông minh bản thường với 460,5 triệu đồng:

Sheet Top sản phẩm trong Excel: 10 sản phẩm doanh thu cao nhất năm 2025, đứng đầu SP0055 Đồng hồ thông minh bản thường 460.517.000, thứ mười SP0056 Bàn phím cơ Pro 295.365.000

Lần chạy đầu, biểu đồ của mình không có trục: phía dưới không có tên tháng, bên trái không có mốc tiền, còn chú thích thì đè lên cột của tháng 11 và tháng 12.

Biểu đồ trong Excel khi chưa sửa: các cột chồng không có nhãn trục tháng và trục tiền, chú thích Website, Sàn TMĐT, Facebook, App nằm đè lên cột

Với openpyxl 3.1.5, biểu đồ tạo mới để trống thuộc tính delete của hai trục (giá trị None), và Excel for Mac 16.114.1 hiểu là ẩn cả hai trục. Dòng bd.x_axis.delete = bd.y_axis.delete = False bật lại hai trục. Hai dòng legend.position = "b" và legend.overlay = False đưa chú thích xuống dưới, không cho đè lên cột.

Chạy mất bao lâu khi file to lên?

Con số 0,20 giây ở trên chỉ tính thời gian bên trong script. Tính cả lúc khởi động Python và nạp pandas, lệnh python bao_cao.py mất 0,47 giây, trung vị của ba lần chạy. Để xem script chạy ra sao khi file lớn hơn, mình chép lặp các dòng trong mỗi file tháng cho số dòng tăng gấp 10, 30 và 100 lần, rồi chạy lại đúng script đó:

Biểu đồ thời gian chạy bao_cao.py theo số dòng: 3.778 dòng 0,47 giây, 37.780 dòng 1,8 giây, 113.340 dòng 4,71 giây, 377.800 dòng 15,81 giây

Tổng số dòng

Đọc 12 file .xlsx

Cả lệnh (trung vị 3 lần)

3.778

0,18 giây

0,47 giây

37.780

1,48 giây

1,80 giây

113.340

4,36 giây

4,71 giây

377.800

15,23 giây

15,81 giây

Thời gian tăng gần như tỉ lệ thuận với số dòng, và gần hết thời gian nằm ở bước đọc file. File .xlsx thực chất là một file nén chứa XML, nên openpyxl phải giải nén rồi mới đọc được từng ô. Cùng 377.800 dòng đó lưu dưới dạng CSV thì pandas đọc nhanh hơn khoảng 150 lần:

Terminal chạy python do_csv.py: đọc 377.800 dòng từ 12 file xlsx mất 15,01, 15,02 và 15,20 giây; từ 12 file csv mất 0,11, 0,10 và 0,10 giây

Nếu phần mềm bán hàng của bạn xuất được cả CSV, hãy chọn CSV cho phần dữ liệu thô. Khi đọc CSV, cột ngày chưa được đổi sang kiểu ngày tháng, nên bạn thêm pd.to_datetime trước khi lấy tháng. Excel chỉ chứa được 1.048.576 dòng trong một sheet theo giới hạn chính thức của Microsoft. Dữ liệu lớn hơn mức đó thì bạn chỉ nên ghi bảng tổng hợp ra Excel, còn dữ liệu thô để ở CSV.

Lỗi khi một file tháng đang mở trong Excel

Lúc thử viết gọn mẫu tên thành glob("*.xlsx"), mình đang mở file tháng 3 trong Excel để xem. Script dừng ngay:

text
['~$ban_hang_2025_03.xlsx']
ValueError: Excel file format cannot be determined, you must specify an engine manually.
Terminal chạy gop_file.py báo ValueError: Excel file format cannot be determined, you must specify an engine manually, do glob lấy cả file khóa ~$ban_hang_2025_03.xlsx

Khi bạn mở một file, Excel tạo thêm một file khóa cùng thư mục, tên bắt đầu bằng ~$ (ở đây là ~$ban_hang_2025_03.xlsx). Mẫu *.xlsx bắt luôn file này. pandas đọc thử, không nhận ra định dạng nên báo lỗi. Cách sửa là dùng mẫu tên cụ thể như ban_hang_2025_*.xlsx, hoặc bỏ các file có tên bắt đầu bằng ~$:

python
cac_file = [f for f in sorted(Path("ban_hang").glob("*.xlsx")) if not f.name.startswith("~$")]

Khi nào không nên dùng Python cho Excel?

  • Báo cáo chỉ làm một lần, dữ liệu vài trăm dòng: PivotTable trong Excel nhanh hơn việc ngồi viết script.

  • Người nhận cần sửa công thức trong file. Script này ghi ra số đã tính sẵn, file báo cáo không còn công thức nào để người khác lần theo.

  • Máy công ty không cho cài Python. Khi đó Power Query trong Excel for Windows gộp được cả thư mục file mà không cần code. Bài Excel, Power BI hay Python cho phân tích dữ liệu so ba công cụ trên cùng một câu hỏi với 6 file đơn hàng mẫu, trong đó Power BI gộp cả thư mục bằng Get data > Folder.

Ngược lại, nếu tháng nào bạn cũng lặp lại cùng một chuỗi thao tác trên nhiều file, một script Python Excel viết một lần sẽ đỡ cho bạn nhiều công nhất: các tháng sau chỉ cần chạy lại.

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

Python có thay được Excel không?

Không hẳn. Python lo phần lặp lại: gộp file, lọc, tính, xuất báo cáo. Excel vẫn là nơi người nhận mở file, xem và ghi chú. Trong bài này, Excel chỉ dùng để mở file kết quả, còn mọi phép tính đều chạy trong pandas.

Có cần cài Microsoft Excel để chạy code không?

Không. pandas và openpyxl đọc, ghi thẳng file .xlsx, không cần Excel trên máy. Chỉ xlwings mới cần Excel vì library này điều khiển cửa sổ Excel đang mở.

File .xls cũ có đọc được không?

pd.read_excel đọc được file .xls kiểu cũ qua package xlrd, theo tài liệu của pandas. openpyxl chỉ làm việc với .xlsx. Mình chưa thử file .xls trong bài này.

Python in Excel có thay được cách làm trong bài không?

Chưa thay được cho việc gộp file. Python in Excel chỉ đọc dữ liệu đã nằm trong sheet hoặc nạp qua Power Query, code không truy cập được file trên máy. Theo trang hỗ trợ của Microsoft, tính năng này có trong Microsoft 365, với Excel for Mac từ bản 16.96 ở kênh Current Channel cho gói Enterprise và Business.

Đọc tiếp

  • Học Python từ số 0: lộ trình cho người chưa biết code, nên đọc trước nếu bạn chưa tự viết được script như trong bài.

  • Data Analyst cần học gì: nếu việc làm báo cáo khiến bạn muốn đi tiếp sang nghề dữ liệu.

Gộp file, lọc, pivot_table và merge trong bài này là các kỹ thuật được dạy kỹ trong ebook Phân tích dữ liệu với Python. Sách viết cho pandas 3, có 89 bài tập tự chấm và dùng chính bộ dữ liệu Tiệm Mây ở trên. Nếu muốn học theo một đề cương có project được chấm điểm, khóa Phân tích dữ liệu với Python là chặng thứ hai của lộ trình AI Engineer từ số 0.

Code chạy ngày 23/10/2026 trên Mac M4 Pro, macOS 27.0.1, Python 3.14.7, pandas 3.1.0, openpyxl 3.1.5; file kết quả mở bằng Excel for Mac 16.114.1. Dữ liệu Tiệm Mây là dữ liệu mô phỏng của AIDev (giấy phép MIT).

Tham vấn chuyên môn

Nguyễn Việt Chung

Nhà sáng lập AIDev và VietPlatform · Thạc sĩ AI MSE

  • Tốt nghiệp thủ khoa xuất sắc Thạc sĩ AI MSE.
  • Đồng sáng lập, CTO E-University, SVOnline: số hóa 10+ đại học, 40.000+ sinh viên.
  • Xây hệ thống AI cho doanh nghiệp tại VietPlatform.
Biên soạn: AIDev Team · Rà soát · Quy trình biên tập