在用pandas处理Excel报表时,日期列读出来变成44562.0、44601.5这种浮点数,是不少开发都遇到过的坑。Excel单元格明明显示的是2025-03-15,read_excel之后却成了数字,这通常不是pandas的bug,而是Excel底层存储日期的方式导致的。
Excel内部用浮点数表示日期时间:整数部分是从1900年1月1日起累计的天数,小数部分是当天的时间比例。例如44562就代表从基准日往后数44562天,对应2025年3月15日。由于Excel存在著名的1900年闰年bug,实际基准日应为1899年12月30日。单元格里看到的日期只是格式化后的显示效果,底层值始终是浮点数。当pandas读取时没有拿到格式信息,就会直接暴露这些原始数字。
一、用parse_dates参数指定日期列
最直接的方法是在read_excel时告诉pandas哪些列是日期:
- import pandas as pd
- df = pd.read_excel(
- 'sales_report.xlsx',
- parse_dates=['order_date', 'delivery_date']
- )
复制代码
如果列名不确定,也可以用列索引:
- df = pd.read_excel(
- 'sales_report.xlsx',
- parse_dates=[2, 5] # 第3列和第6列是日期
- )
复制代码
这个方法要求Excel里这些列本身是日期格式。如果用户把日期列格式改成“常规”或“文本”,parse_dates就不会生效,读出来仍然是浮点数。比如有人对日期列执行了“清除格式”,格式信息丢失后,pandas只能得到底层数字。
二、浮点数手动转日期
当parse_dates失效,或数据已经读成浮点数时,可以基于Excel日期基准手动转换:
- from datetime import datetime, timedelta
- def excel_float_to_date(f):
- # Excel基准日期是1899-12-30(因为1900闰年bug)
- base = datetime(1899, 12, 30)
- return base + timedelta(days=f)
- print(excel_float_to_date(44562)) # 2025-03-15 00:00:00
- print(excel_float_to_date(44601.5)) # 2025-03-15 12:00:00(实际是39.5天后)
复制代码
批量转换整列时,要注意列中可能混有浮点数和正常字符串,还可能存在空单元格(NaN)。NaN本身是float类型,所以要先排除空值再做类型判断:
- df['order_date'] = df['order_date'].apply(
- lambda x: datetime(1899, 12, 30) + timedelta(days=x)
- if pd.notna(x) and isinstance(x, (int, float))
- else x
- )
复制代码
三、更换Excel读取引擎
pandas读Excel支持xlrd和openpyxl两种引擎。xlrd 2.0之后只支持xls文件,openpyxl只支持xlsx文件。两者对日期格式的识别能力不同。
对于xlsx文件,指定openpyxl引擎往往能更好地保留日期类型:
- df = pd.read_excel('report.xlsx', engine='openpyxl')
复制代码
但openpyxl读大文件较慢,碰到“yyyy年mm月dd日”这类自定义日期格式时,也可能识别失败。对于xls文件,xlrd是唯一选择,但它也更容易出现日期变浮点数的问题。可以先借助xlrd检测哪些列是日期类型,再把列索引传给parse_dates:
- import xlrd
- book = xlrd.open_workbook('old_report.xls')
- sheet = book.sheet_by_index(0)
- date_cols = []
- for col_idx in range(sheet.ncols):
- for row_idx in range(min(5, sheet.nrows)):
- cell = sheet.cell(row_idx, col_idx)
- if cell.ctype == xlrd.XL_CELL_DATE:
- date_cols.append(col_idx)
- break
- print('日期列索引:', date_cols)
- df = pd.read_excel('old_report.xls', parse_dates=date_cols)
复制代码
xlrd.XL_CELL_DATE的值是3,表示单元格类型为日期。这种方法比盲猜parse_dates列名更靠谱。
四、容易踩的边角坑
1. 同一列日期格式不一致。比如前100行是日期格式,后面有人手敲了“3月15日”文本,pandas读出的列类型会变成mixed,parse_dates直接失效。处理方式是在Excel里统一格式,或者读出来后按类型分段处理。
2. 时区问题。Excel日期没有时区概念,转换后的datetime也是naive。跨时区比较时,需要手动设置时区:
- df['order_date'] = pd.to_datetime(df['order_date']).dt.tz_localize('Asia/Shanghai')
复制代码
3. 时间精度丢失。像44601.5这种小数带半天时间,如果只用.dt.date取日期,时间信息就没了。需要精确到小时时,必须保留小数部分。
4. 不要直接用pd.to_datetime处理Excel浮点数。pd.to_datetime默认把浮点数当Unix时间戳(从1970年开始),与Excel基准完全不同,会差出一百多年:
- # 错误做法:44562被解释为从1970年起的44562秒
- pd.to_datetime(44562, unit='s') # 约等于1970-01-02
- # 正确做法:先转成Excel基准datetime
- from datetime import datetime, timedelta
- base = datetime(1899, 12, 30)
- df['date'] = df['date'].apply(lambda x: base + timedelta(days=x))
复制代码
五、一套稳妥的最终方案
综合下来,可以设计一个容错性较强的处理流程:xlsx文件先用openpyxl配合parse_dates读取,检查日期列是否解析成功;失败时手动转换;xls文件先用xlrd检测日期列索引;最后统一用pd.to_datetime做一轮格式清洗,把残留的字符串日期都转掉。
- import pandas as pd
- from datetime import datetime, timedelta
- def excel_float_to_date(f):
- base = datetime(1899, 12, 30)
- return base + timedelta(days=float(f))
- def clean_date_column(col):
- """统一处理日期列中的混合类型"""
- result = []
- for val in col:
- if pd.isna(val):
- result.append(pd.NaT)
- elif isinstance(val, (int, float)):
- result.append(excel_float_to_date(val))
- else:
- result.append(pd.to_datetime(val, errors='coerce'))
- return pd.Series(result)
- df = pd.read_excel('sales_report.xlsx', engine='openpyxl', parse_dates=['order_date'])
- if df['order_date'].dtype != 'datetime64[ns]':
- df['order_date'] = clean_date_column(df['order_date'])
复制代码
这套流程在实际处理几十个报表时没有再出现日期问题。多写几行代码,但能省下后续反复清洗数据的时间。
六、总结
Excel把格式和值分开存储,跨工具读取时格式信息一旦丢失,pandas就只能返回原始浮点数。这不是代码逻辑错误,而是源文件格式不可靠。遇到日期列全是浮点数,parse_dates不行就手动转换,转换时注意NaN和混合类型,基本都能解决。如果自己生成Excel,从一开始就把日期列设为明确的日期格式,避免使用“常规”或“文本”,这样后续任何人读取都不会再踩坑。 |