用 Python 读 Excel:日期变 datetime、公式读成 None、空行多出一半
三份记账 Excel,两年多的数据,想把它们并成一份 CSV。本来以为读单元格、拼一行、写文件,十几行代码的事。结果五个坑挨个踩了一遍,每一个都不报错,只是给你错的数据。
用的 openpyxl,装了就一个包。
坑一:日期读出来是 datetime,不是你想的字符串
单元格里明明写的是 2026-11-01,读出来是这样:
repr(ws["A2"].value) → datetime.datetime(2026, 11, 1, 0, 0) type(ws["A2"].value).__name__ → 'datetime'
它不是字符串,也不是 date,是 datetime。直接写进 CSV 变成 2026-11-01 00:00:00,等你想按天分组的时候发现全带时间尾巴。
from datetime import datetime, date
v = ws["A2"].value
if isinstance(v, (datetime, date)):
v = v.strftime("%Y-%m-%d")
坑二:data_only=True 读出来是 None
表里有一列合计是公式 =SUM(C2:C4)。想拿算好的值,用了 load_workbook(path, data_only=True),结果拿到 None。
原因是 openpyxl 自己不计算公式。data_only=True 只是去读 Excel 上次保存时缓存的结果。这个文件如果是脚本刚生成的、从没被 Excel 打开保存过,缓存就是空的,只能给你 None。
两种处理:用 Excel 手动打开保存一次;或者干脆别读公式列,自己算。
坑三:合并单元格只有左上角有值
B4:B5 合并写了「合并测试」,读出来是:
B4 → '合并测试' B5 → None
遍历行的时候 B5 那行就少一个字段。如果合并的是「月份」这种关键列,按月份统计会直接漏掉一半数据。可以先展开再读:
for rng in ws.merged_cells.ranges:
top = ws.cell(rng.min_row, rng.min_col).value
for r in range(rng.min_row, rng.max_row + 1):
for c in range(rng.min_col, rng.max_col + 1):
ws.cell(r, c).value = top
坑四:给空单元格设个样式,max_row 就翻倍
这个最阴。我在 A12 设了加粗,一个字都没写。存盘再读:
max_row = 12 ← 实际数据只到第 7 行 iter_rows 行数 = 12 最后三行 = [(None, None, None, None), (None, None, None, None), (None, None, None, None)]
7 行数据读出 12 行,多出 5 行全 None。因为 max_row 统计的是「有内容或有格式」的最大行,一个字体设置也算数。写到 CSV 就是一堆空行。
别拿 max_row 当行数,过滤一遍:
rows = [r for r in ws.iter_rows(values_only=True)
if any(c is not None and str(c).strip() != "" for c in r)]
坑五:CSV 不带 BOM,Excel 打开全乱码
Python 默认 encoding="utf-8" 写出来的中文 CSV,双击用 Excel 打开是乱码。看一眼文件头就明白:
utf-8 → b'.\xe6\x97\xa5\xe6\x9c\x9f,' utf-8-sig → b'\xef\xbb\xbf.\xe6\x97\xa5\xe6\x9c\x9f,'
后者多出 EF BB BF 三个字节,就是 BOM,Excel 认这个。
with open("out.csv", "w", newline="", encoding="utf-8-sig") as f:
newline="" 也别省,不然 Windows 上每行会多一个空行。
合成一段能跑的代码
把上面五个坑都堵上,从读 Excel 到写 CSV 完整一段:
import openpyxl, csv
from datetime import datetime, date
wb = openpyxl.load_workbook("bill.xlsx", data_only=False)
ws = wb.active
# 展开合并单元格
for rng in ws.merged_cells.ranges:
top = ws.cell(rng.min_row, rng.min_col).value
for r in range(rng.min_row, rng.max_row + 1):
for c in range(rng.min_col, rng.max_col + 1):
ws.cell(r, c).value = top
def clean(v):
if isinstance(v, (datetime, date)):
return v.strftime("%Y-%m-%d")
return v
with open("bill.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
for row in ws.iter_rows(values_only=True):
if not any(c is not None and str(c).strip() != "" for c in row):
continue
w.writerow([clean(c) for c in row])
十几份表、二十多万行,这套跑下来没再出问题。真正花时间的不是写代码,是发现「读出来的东西和你眼睛看到的不是一回事」。这五个坑没有一个会抛异常,全都安安静静给你错的数据,直到你拿去做统计才发现对不上。
- 点赞
- 收藏
- 关注作者
评论(0)