ARTICLE · 1149457
pandas 读 Excel 的 5 个默认行为
一个 Excel 文件,用 pandas 读进来,结果经常跟表格里写的不一样。更麻烦的是它不报错,前导零没了、空行少了、文本被换成布尔值,read_excel 一行跑完,返回一个看着正常的 DataFrame。
问题出在默认值上。read_excel 有十几个参数,不写就是用默认值,单看都合理,合起来就是一套替你做主的决定。下面这 5 个最容易跟表格里看到的样子对不上。
先准备测试文件,后面每段代码都基于它。一个工作簿,9 张表:
import datetimeimport openpyxlimport pandas as pdwb = openpyxl.Workbook()sheets = {"报价单": [["XX项目供应商报价表"], ["制表日期:2026-10-08"], ["供应商", "物料", "单价", "数量"], ["A公司", "螺丝", 0.5, 1000], ["B公司", "螺母", 0.3, 2000], ["C公司", "垫片", 0.1, 5000]],"工号": [["工号", "姓名"], ["0001234", "张三"], ["0001235", "李四"]],"混类型": [["编号", "金额"], ["001", 100], ["002", 200], ["003", "待定"]],"状态": [["名称", "状态"]] + [[c, v] for c, v in [("a", "TRUE"), ("b", "FALSE"), ("c", "NA"), ("d", "null"), ("e", "None")]],"空行": [["甲", 1], [None, 2], [None, None], ["乙", 4]],"日期": [["真日期", "假日期"], [datetime.date(2026, 10, 8), "2026-10-08"], [datetime.date(2026, 10, 9), "2026-10-09"]],"待定日期": [["日期", "值"], ["2026-10-08", 1], ["待定", 2], ["2026-10-09", 3]],"汇总": [["项目", "值"], ["合计", 1]],"明细": [["项目", "值"], ["明细1", 2]],}for name, rows in sheets.items(): ws = wb.active if name == "报价单"else wb.create_sheet(name) ws.title = namefor r in rows: ws.append(r)wb.save("demo.xlsx")header 默认把第一行当列名
header 默认 0,第 0 行直接变成列名。"报价单"这张表第 1 行是总标题,第 2 行是制表日期,第 3 行才是真表头:
| 供应商 | 物料 | 单价 | 数量 |
df = pd.read_excel("demo.xlsx", sheet_name="报价单")print(df.head(3)) XX项目供应商报价表 Unnamed: 1 Unnamed: 2 Unnamed: 30 制表日期:2026-10-08 NaN NaN NaN1 供应商 物料 单价 数量2 A公司 螺丝 0.5 1000列名成了 ['XX项目供应商报价表', 'Unnamed: 1', 'Unnamed: 2', 'Unnamed: 3'],真表头降级成一行数据。那几个 Unnamed: 1 的意思是:pandas 按位置读到一列但没列名,给你编了个占位名。
代价是上游把标题从 1 行改成 2 行,下游列名全部错位,没有警告。
知道表头在第 3 行,header 写 2 就对了,它收从 0 起的行号:
df = pd.read_excel("demo.xlsx", sheet_name="报价单", header=2)print(df.head(3)) 供应商 物料 单价 数量0 A公司 螺丝 0.5 10001 B公司 螺母 0.3 20002 C公司 垫片 0.1 5000容易记错的是第 3 行写 2 不是 3,写成 header=3 首行数据会被当成列名。
dtype 默认交给 pandas 猜
dtype 默认 None,全部自动推断。第一条,纯数字字符串会被转成数字,工号列写的是 "0001234":
df = pd.read_excel("demo.xlsx", sheet_name="工号")print(repr(df["工号"].iloc[0]))np.int64(1234)前导零在拿到数据之前就没了,读出来已是 1234,得在读取时写 dtype={"工号": str}:
df = pd.read_excel("demo.xlsx", sheet_name="工号", dtype={"工号": str})print(repr(df["工号"].iloc[0])) # '0001234'第二条,一个文本单元格能让整列退回 object。金额列本是 100、200,混进一个"待定":
df = pd.read_excel("demo.xlsx", sheet_name="混类型")print(df.dtypes.to_dict()){'编号': dtype('int64'), '金额': dtype('O')}整列变成 object。这时候去求和会怎样?实测是直接抛错:
df["金额"].sum()TypeError: unsupported operand type(s) for +: 'int' and 'str'sum、min、max、mean 全都会抛错,因为列里既有 int 又有 str,运算没有定义。但报错不是最坏的情况,如果这一列一个数字都没有、全是文本,它不报错,而是把字符串拼起来一路传下去:
pd.DataFrame({"金额": ["待定", "待定"]})["金额"].sum() # '待定待定'修的时候先定位,errors="coerce" 转不动的会变 NaN,非空但变 NaN 的就是问题单元格:
raw = df["金额"]bad = pd.to_numeric(raw, errors="coerce").isna() & raw.notna()print(df[bad]) 编号 金额2 3 待定怎么补要根据业务决定。"待定"和"0 元"是两回事,我倾向先变 NaN 让缺失显式存在,别补 0:
raw = pd.to_numeric(raw, errors="coerce") # 非数字 → NaNraw.fillna(0) # 非数字 → 0也可以读取时拦住,用 converters:
df = pd.read_excel("demo.xlsx", sheet_name="混类型", converters={"金额": lambda v: pd.to_numeric(v, errors="coerce")})df["金额"].dtype # dtype('float64')dtype={"金额": float} 做不到同样的事,实测会报 ValueError。区别是 dtype 强制转换,转不动就放弃;converters 逐值处理,允许你把非法值安排成 NaN。
换成 converters 后 sum() 能算出 300.0。但需要警惕那些不出错的操作。回到没处理过的原始列上看:count() 返回 3,把"待定"也算成了一行有效数据;unique() 返回 [100, 200, '待定'],看着像正常值。这类静默偏差只能靠读取时先看 dtypes 拦下来。
第三条,某些文本会被识别成布尔值。"状态"列写的是 "TRUE"、"FALSE":
df = pd.read_excel("demo.xlsx", sheet_name="状态")print(df) 名称 状态0 a True1 b False2 c NaN3 d NaN4 e NaN"TRUE"、"FALSE" 变成 True、False,"NA"、"null"、"None" 全变 NaN。后面几个是 na_filter=True 干的,它拿一张 NA 词表对照,命中的全当缺失值。实测有 19 个词:
['', '#N/A', '#N/A N/A', '#NA', '-1.#IND', '-1.#QNAN', '-NaN', '-nan', '1.#IND', '1.#QNAN', '<NA>', 'N/A', 'NA', 'NULL', 'NaN', 'None', 'n/a', 'nan', 'null']要是表里有一列叫"区域",值是 NA(North America 的缩写),它会被换成缺失值,看着没动,实际少了。
要解决,可以设置 dtype=str 能让它原样保留:
df = pd.read_excel("demo.xlsx", sheet_name="状态", dtype=str)print(df["状态"].tolist())# ['TRUE', 'FALSE', nan, nan, nan]注意 dtype=str 只关住布尔识别,关不了 NA,"NA"、"null"、"None" 依然是 NaN,得另外关:
df = pd.read_excel("demo.xlsx", sheet_name="状态", dtype=str, keep_default_na=False)print(df["状态"].tolist())# ['TRUE', 'FALSE', 'NA', 'null', 'None']两个参数都要设置才能解决这个问题。
read_excel 里还有 true_values、false_values,看着像是用来关掉布尔识别的,其实默认 None,作用是额外指定哪些值算布尔。实测设成不相关值 "TRUE" 照样被转。
dtype=str 保住的是读出来的写法,不是来源。实测同一列里:Excel 真布尔 True 读出来 'True',文本 "TRUE" 读出来 'TRUE'、文本 "true" 读出来 'true',写法不同还能认。但文本 "True" 读出来也是 'True',跟真布尔一模一样,光看值分不出来。要判断原始类型只能用 openpyxl,它返回的是 bool 和 str。
整行空的行会自己消失
上一节那张 NA 词表管的是"单元格里写了什么",还有一种是整行都没值,这行的下场是自己消失。
df = pd.read_excel("demo.xlsx", sheet_name="空行")print(df)print(df.shape) 甲 10 NaN 2.01 NaN NaN2 乙 4.0(3, 2)原表 4 行读进来 3 行,没有提示。第 0 行只有"甲"是空、另一个单元格有值,被留下了;被删的那行两个字都空。
这里还藏着一个变化:数值列的 1 变成 2.0,因为 NaN 是浮点,int64 放不下,只能把整列升级成 float64。
想救回那行,第一反应是关 na_filter。实测没用:
df = pd.read_excel("demo.xlsx", sheet_name="空行", na_filter=False)print(df.shape, df.dtypes.to_dict())# (3, 2) {'甲': <StringDtype(na_value=nan)>, 1: dtype('O')}行数还是 3,代价还大:数值列从 float64 掉成 object,2 变字符串 '2'。na_filter 和 keep_default_na 都只管词表匹配,跟"整行空"是两套逻辑。
拿 openpyxl 直接读同一个文件,4 行都在,说明这行是进到 pandas 之后才丢的。再试 header=None:
df = pd.read_excel("demo.xlsx", sheet_name="空行", header=None)print(df)print(df.shape) 0 10 甲 1.01 NaN 2.02 NaN NaN3 乙 4.0(4, 2)4 行全回来了。代价是列名变成 0、1,因为 header 的工作也交给你了,得自己补 names=["材料", "数量"]。
另外一个实测出来的边界:丢的其实只是末尾的整行空。同样一张表,空行夹在中间能留住:
空行在中间 ['h1', 'h2'] + 3 行数据 -> shape (3, 2),空行还在空行在末尾 ['h1', 'h2'] + 3 行数据 -> shape (2, 2),直接没了末尾空行多半是 Excel 里多敲出来的,pandas 帮你清了,这个行为不算坏事。中间那个才是要注意的,它会被当成一行正常数据留下来。
parse_dates 默认不解析日期
parse_dates 默认 False。关键不是"不解析",而是日期的类型由 Excel 文件本身决定,不由 pandas 决定。同一张表里放两个看着一样的日期,一个真日期格式,一个文本:
df = pd.read_excel("demo.xlsx", sheet_name="日期")print(df.dtypes.to_dict())print(repr(df["真日期"].iloc[0]), "|", repr(df["假日期"].iloc[0])){'真日期': dtype('<M8[us]'), '假日期': <StringDtype(na_value=nan)>}Timestamp('2026-10-08 00:00:00') | '2026-10-08'这两列在 Excel 里长得一样,在 pandas 里一个是时间类型、一个是字符串类型。字符串那列上 df["假日期"].dt.year 会直接抛 AttributeError: Can only use .dt accessor with datetimelike values,不是返回空值,也不会自动转。
要统一得指名道姓写 parse_dates=["假日期"]:
df = pd.read_excel("demo.xlsx", sheet_name="日期", parse_dates=["假日期"])print(df.dtypes.to_dict())print(df["假日期"].dt.year.tolist())# {'真日期': dtype('<M8[us]'), '假日期': dtype('<M8[us]')}# [2026, 2026]parse_dates 收列名列表,也可以给列序号 [1]。这里容易踩的是写成 parse_dates=True,以为能让 pandas 自动找出所有日期列。实测没这个效果:
df = pd.read_excel("demo.xlsx", sheet_name="日期", parse_dates=True)print(df.dtypes.to_dict())# {'真日期': dtype('<M8[us]'), '假日期': <StringDtype(na_value=nan)>}查了文档,True 的语义是"试着解析索引",不是解析所有列。这个跟 read_csv 不一样,两边参数重名、行为不同。
需要格外注意的是:parse_dates 遇到解析不了的值会整列放弃,我们往日期列里混一个"待定",然后测试一下效果:
df = pd.read_excel("demo.xlsx", sheet_name="待定日期", parse_dates=["日期"])print(df["日期"].dtype)整列返回字符串,不报错也不提示,只有 dtype 变了。这种情况就得等读完后,自己手动转,让坏值显式变成 NaT:
df["日期"] = pd.to_datetime(df["日期"], errors="coerce")# [Timestamp('2026-10-08 00:00:00'), NaT, Timestamp('2026-10-09 00:00:00')]格式不标准(比如 08/10/2026 这种)使用同样的方法解决,文档里建议就是 pd.to_datetime 事后处理。
sheet_name 默认是 0,只读第一个工作表
sheet_name 默认是 0,读第一个工作表。本次测试文件demo.xlsx 中有 9 张表,read_excel 只返回了第一个,其余的都被静默忽略了:
df = pd.read_excel("demo.xlsx")print(list(df.columns), df.shape)要拿全部需要在读取时设置 sheet_name=None参数,其返回一个字典,然后再另行处理:
dfs = pd.read_excel("demo.xlsx", sheet_name=None)print(type(dfs).__name__, list(dfs.keys()))# dict ['报价单', '工号', '混类型', '状态', '空行', '日期', '待定日期', '汇总', '明细']结尾
这 5 个默认值不是设计失误,都是合理默认,问题是"默认"意味着你把决定权交出去了。采用默认值,pandas 猜的依据是"大多数 Excel 长什么样",不一定适配你的表格。
不改参数不是省事,是把 5 个决定交给了不认识你数据的库。把默认值写全,读一张不规整的表大概是这样:
df = pd.read_excel("demo.xlsx", sheet_name="报价单", header=2, dtype={"数量": "Int64"}, keep_default_na=True)print(df.dtypes.to_dict()){'供应商': <StringDtype(na_value=nan)>, '物料': <StringDtype(na_value=nan)>, '单价': dtype('float64'), '数量': Int64Dtype()}数量列指定成 Int64 后,空值就不再强制把整列变成 float。
下次在读取 Excel时,先别急着跑聚合,下看一眼 dtypes。
作者简介:码上工坊,探索用编程为己赋能,定期分享编程知识和项目实战经验。持续学习、适应变化、记录点滴、复盘反思、成长进步。
重要提示:本文主要是记录自己的学习与实践过程,所提内容或者观点仅代表个人意见,只是我以为的,不代表完全正确,欢迎交流讨论。