你打开Jupyter,敲下df = pd.read_excel('销售数据.xlsx'),看着前五行输出像模像样,心里暗想:“就这?”可接下来按月份汇总时,日期列变成了“2023-01-01 00:00:00”,销售金额列里藏着几个文本型数字,总数差了几千。你翻遍数据,发现罪魁祸首是个空白单元格,它在那儿笑你。Excel是给人眼设计的,Python是给逻辑设计的,两者之间有一道布满暗坑的沼泽地。这篇文章,我们一起趟一遍。
数字不是数字,而是“伪装者”
很多人在Excel里存长数字,比如身份证、订单号、卡号。Excel为了省地方,会自动转成科学计数法:1.23E+17。你用pandas读取,它默认按数值类型处理,于是精度丢了,尾号全变0。你说那好,我指定dtype=str读,结果读出来的是'123456789012345600'或者'1.23E+17',因为你没明白:Excel保存数据时已经牺牲了精度。你以为是字符串,它其实是浮点数,你以为是浮点数,它变成了科学计数法。
更烦的是“文本型数字”。在Excel界面里,单元格左上角有一个绿色小三角,意思是“这里的数字是文本格式”。pandas读完,这一列是object dtype,你df['金额'].sum()会把所有数字拼接成一个大字符串。你需要pd.to_numeric(df['金额'], errors='coerce'),把非法值置为NaN再处理。Excel的单元格格式只是化妆,数据在底层已经定型,格式挡不住pandas的一意孤行。
日期:比前任还难猜的格式
read_excel处理日期,行为像天气一样不可预测。有时返回Timestamp对象,有时返回字符串,有时返回一串五位数——那是Excel内部的日期序列号,比如44828代表2023年9月1日。你打印出来,明明是一个整数,却要掰着手指头从1900年1月1日往后算。日期在Excel眼里只是一个数字,在pandas眼里是一套时区系统,在你眼里是一切。
如果你用parse_dates参数指定日期列,它还算靠谱。一旦列里混了“2023/09/01”和“2023年9月1日”两种写法,read_excel可能直接罢工,把整列读成字符串。你转头用pd.to_datetime强制解析,它又会把“2023年”解析成2023-01-01,因为年后面没有月日,默认补成1月1日。唯一稳妥的办法是写自己的日期解析函数,而不是指望Excel自觉。
空值:一个比薛定谔的猫还复杂的现象
Excel里的“空”在Python眼里至少有四种形态:真正的空单元格(NaN)、空字符串('')、None、字符串“NA”。pandas的read_excel默认把空单元格和“NA”字符串都转成NaN,看起来好像很友好。但当你fillna('')后,原本是空字符串的单元格根本不会被填充,因为它们不是NaN。你再一数,还有几行写着“#N/A”和“-”,你没处理到,最后统计结果仍然缺着。空值不是一个值,而是一堆值,每种空都等着你给个说法。
更抓狂的是,pandas把“NA”当作缺失值,但你的业务数据里真的存在城市代码“NA”。你想保留它,于是设置keep_default_na=False。结果,所有真正的空单元格又变成空字符串而不是NaN,你的isna().sum()统计彻底失效。pandas的缺失值判断是一套默认协议,但不吻合你的业务逻辑。要收拾这种局面,最好老老实实先astype(str),再逐项判断各种各样的空值形态。
合并单元格:数据界的连环诈骗
合并单元格是Excel给人类视觉的馈赠,却是数据分析师的毒药。pandas读取时,被合并的区域只有左上角有值,其余全是NaN。比如一个表格中“华东区”合并了三行,读进来后只有第一行的“华东区”有值,下面两行显示NaN。你按区域分组,出现了一堆“空组”。合并单元格是给眼睛看的,不是给代码用的。
你可能会用fillna(method='ffill')来向下填充,好像解决了。但如果合并单元格不是垂直排列,而是水平合并,你填充方向就错了。更麻烦的是,当你把处理好的数据写回Excel,想恢复合并单元格,pandas没有原生支持,你得手动用openpyxl的merge_cells一个个处理。用Python处理Excel,最贵的不是代码,而是你反复试错的时间。
公式与缓存:你看到的是结果,代码读到的是公式
遇到公式是家常便饭。在Excel里,你看到B10单元格显示100,因为公式=SUM(B2:B9)算好了。用openpyxl直接读B10.value,可能返回字符串'=SUM(B2:B9)'。如果你不检查类型,把这个字符串丢进报告里,老板会以为是乱码。虽然pandas的read_excel底层会用openpyxl缓存的数值,但缓存存在的前提是:这个文件在Excel里被打开并保存过。公式是Excel的生命,却是Python的陷阱。
如果文件是从在线Excel导出的,或者由某些库生成的,缓存值压根儿不存在。你读到的可能是None。你心想,None就当成0吧。结果本月的“合计”一栏在你输出时变成了0,在Excel里却写着大大的1000。不要相信read_excel读到的值,除非你确认公式已经被Excel计算过。实在不行,用data_only=True碰碰运气,同时做好后备方案。
性能:循环一千行,你就以为自己在写爬虫
曾经有位同事,用openpyxl对一万行数据逐行读取、逐行计算、逐行写入,整个脚本跑了二十分钟。他说Python太慢。其实不是Python慢,是他用错了工具。pandas的Vectorized操作跑同量级数据只需几十毫秒。逐行操作pandas,就像开着跑车在胡同里倒车入库。
性能的另一个大坑是Excel的“垃圾格式”。你的工作表明明只有几行数据,但某人按了Ctrl+End,把格式刷到了第1048576行。pandas读取时会尝试让每一行都有对应的索引,于是那个孤零零的格式占用量被解释成上百万个NaN,内存瞬间爆炸。解决办法是读取前先检查Excel的“最后一格”,或者指定nrows和usecols,把无辜的空白区域挡在门外。Excel的空白不是真空,是格式的暴风雪,随时拖垮你的内存。
索引的阴谋:0和1的世界与1和0的世界
你df.drop(index=2)删掉一行,然后df.loc[3]想取原来的第四行,取到了。下一次你又删了几行,索引变得稀疏,你df['金额'][df['金额']>100]之后,再想reset_index(),却忘了加drop=True,结果原索引变成一列“index”,写进Excel后和你的目标列对不上。索引是pandas的翅膀,也是你的深渊。
更隐蔽的是,当数据里有重复的索引值,df.loc[2]会返回一个DataFrame而不报错,你以为是一个Series,做后续操作时全乱套。你反过来用iloc,没问题,但行号跟Excel行号之间永远差着1,因为Excel第一行是标题。你总得在两者之间跳来跳去,一不留神就把“第2行”当成索引2。不要用Excel的坐标思维去理解pandas的索引,它们是两个宇宙。
文件格式的隐形陷阱:xlsx、xls、还有CSV
写pd.read_excel('数据.xls'),报错“Missing optional dependency 'xlrd'”。你乖乖pip install xlrd,结果装的是2.0.1版本,只支持.xls,不支持.xlsx。于是你又去查手册,发现pandas对.xlsx默认用openpyxl,对.xls只能用xlrd,而老版本xlrd是同时支持的。版本兼容的坑,比你想的要深。格式的坑,在于你永远猜不到对方手上是什么文件。
CSV也一样。Excel保存CSV时,默认编码是本地代码页(中文系统就是gbk),你用utf-8读必然乱码。而且CSV没有样式、没有公式、没有多Sheet,一旦原始Excel里有两个工作表,你转成CSV就只能保存当前页,信息悄悄丢了。别把CSV当成Excel的廉价替代品,它本质上是Excel的阉割版。
写回Excel的格式地狱
pandas的to_excel写出的文件,样式基本是素颜:没有加粗表头、没有颜色、没有调整列宽,公式也全变成了静态数值。更重要的是,如果你用Excel函数和数据验证,pandas写回后会全部剥离。你辛辛苦苦建好的模板,经过一个to_excel就面目全非。对pandas来说,样式不重要,但对你来说,样式可能是老板的全部KPI。
要保留格式,你得用openpyxl加载原文件,然后手动把DataFrame逐格写入指定位置,再手动设置字体、边框、列宽。这一套操作下来,代码量轻松超过两百行,而你还得处理“原文件中有多个Sheet”和“Sheet名有空格”的情况。所谓“用Python处理Excel”,到最后你会发现自己其实在写一份Excel操作说明书。
编码与字符的玄学
读取CSV时,你一拍脑袋用encoding='utf-8',看到一堆“锟斤拷”就知道完了。换成gbk,可能又因为某个特殊字符报UnicodeDecodeError。你试着用errors='ignore',结果字符被吞掉,数据悄无声息地丢了。编码问题往往在你觉得“这也要讲?”时出现。
Excel单元格里还藏有隐形字符:\u200b(零宽空格)、\ufeff(BOM)、不间断空格\xa0。它们不显示,但你的字符串匹配永远失败。最搞笑的是,Excel的TRIM函数也去不掉这些鬼东西,Python的strip()同样无能为力。你得祭出正则表达式,才能还单元格一片干净。Excel的单元格是一个杂物间,Python要当保洁员。
重复列名和同名陷阱
Excel表头里有两个“金额”,一个叫“金额”,另一个叫“金额 ”(后面多了空格)。pandas读入后,会把你拆开,一个叫“金额”,另一个可能被自动改成“金额.1”。也有时会完全重复,你df['金额']返回一列,但你可能忘了那一列是哪个。列名在Excel里可以重复,但在pandas里必须唯一,这是两个世界的基本法律。
另外,如果表头有换行符或者特殊符号,pandas会一并保留,你在select时写错了,报KeyError。你检查了一遍,发现明明看起来一样,就是选不出来,因为多了个不可见字符。用df.columns打印一下,也许能发现端倪,但很多人在这一步之前已经放弃了。不要相信用肉眼看到的列名,要用编码和清单验证。
收尾:讲点方法论
踩了这么多坑,你可能会说:那我干脆不用Python了。其实不是。这些问题背后有一个共同的规律:你是在用Excel的经验去操作一个编程工具,而编程工具的默认假设与Excel完全不同。Excel把数据、格式、公式、显示方式捆在一起,而Python希望你把它们拆开。
如果你想少踩坑,处理任何Excel前,先备份原文件;读进来后先别急着用,用info()和head()观察每一列的真实类型;写回文件前,明确你到底要保留什么,是数值,还是样式,还是公式。在你打开read_excel之前,先问自己三个问题:数据是什么类型、缺失值是怎么表示的、公式要怎么处理。想清楚这些,再动手也不迟。在这些问题得到答案之前,你写的代码都只是在给Excel擦屁股。要的话,就让Python成为你的利器,而不是另一台制造垃圾的机器。