夜雨聆风学习资料网

ARTICLE · 1149457

pandas 读 Excel 的 5 个默认行为

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 行才是真表头:

XX项目供应商报价表
制表日期:2026-10-08
供应商物料单价数量
A公司
螺丝
0.5
1000
B公司
螺母
0.3
2000
C公司
垫片
0.1
5000
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。


作者简介:码上工坊,探索用编程为己赋能,定期分享编程知识和项目实战经验。持续学习、适应变化、记录点滴、复盘反思、成长进步。

重要提示:本文主要是记录自己的学习与实践过程,所提内容或者观点仅代表个人意见,只是我以为的,不代表完全正确,欢迎交流讨论。

相关学习资料