☰
Python 数据处理入门(三)读取数据:CSV 与 Excel 的正确打开方式
2026/10/15 19:16:11 网站建设 项目流程

先确认示例数据在手边:

import os import mkdata # 第 1 篇里的 mkdata.py ​ if not os.path.exists("data/sales.csv"): mkdata.main() # 没生成过就先跑一次 print("示例数据就绪:", os.path.exists("data/sales.csv"))

这段会检查第 1 篇生成的data/目录在不在,不在就自动补一次;已经有数据时只会打印一行示例数据就绪: True,不影响后面的结果。

写在前面

本系列已更新到第 3 篇。

上一篇我们把 Series 和 DataFrame 摸清了,这一篇解决"数据从哪来"。真实场景里,数据基本只有两个来源:系统导出的 CSV,和同事发来的 Excel。而新手最常卡住的地方不是分析,是文件根本读不进来——路径写错、编码不对、表头位置不对、Excel 有多个 sheet。

这篇用第 1 篇生成的几个文件把这些问题过一遍:data/sales.csv(销售明细)、data/sales.xlsx(两个 sheet)、data/employees.csv(员工信息)、data/students.csv(学生成绩)。

一、读 CSV 第一步:编码和 BOM

先看数据本身,再谈代码。我们用二进制方式读一下文件的前三个字节。

import os import runpy import pandas as pd ​ # 兜底:data/ 目录由第 1 篇的 mkdata.py 生成,没有就先生成一次 if not os.path.exists("data/sales.csv"): runpy.run_path("mkdata.py", run_name="__main__") ​ # 看一眼前 3 个字节:带 BOM 的 UTF-8 文件以 EF BB BF 开头 with open("data/sales.csv", "rb") as f: print("文件开头三个字节:", f.read(3)) ​ df = pd.read_csv("data/sales.csv", encoding="utf-8-sig") print("列名:", df.columns.tolist()) print(df.head(3).to_string(index=False)) print("形状:", df.shape)
文件开头三个字节: b'\xef\xbb\xbf' 列名: ['订单日期', '城市', '产品', '数量', '单价', '销售员', '金额'] 订单日期 城市 产品 数量 单价 销售员 金额 2024-01-05 杭州 A-笔记本 3 49.9 小张 149.7 2024-01-06 上海 B-鼠标 5 29.9 小李 149.5 2024-01-06 上海 B-鼠标 5 29.9 小李 149.5 形状: (16, 7)

EF BB BF就是 BOM,Excel 另存为 CSV 时常常会加上它,作用是告诉程序"我是 UTF-8"。这个字符肉眼看不见,但它可能被算进第一个列名里——表现就是列名打印出来看着是"订单日期",可df["订单日期"]偏偏报KeyError。写encoding="utf-8-sig"就等于告诉 Pandas"开头有 BOM 就跳过",零成本保险,本系列统一这么写。

另外说明一下开头那几行:os和runpy是兜底用的——如果data/目录还没生成,它会自动帮你跑一次mkdata.py;已经有数据时它什么都不打印,不会影响后面的结果。每段代码都自带依赖,才是最省心的复制粘贴体验。

二、路径:相对路径、绝对路径和那个 r

在读文件这件事上,路径写错的概率比编码还高。先记住一句:相对路径是相对于"你运行 Python 时所在的目录",不是脚本所在的目录。

import os import pandas as pd ​ # 三种写法都能找到同一个文件(Windows 上正斜杠也认) print("data/sales.csv →", os.path.exists("data/sales.csv")) print("./data/sales.csv →", os.path.exists("./data/sales.csv")) print(r"data\sales.csv →", os.path.exists(r"data\sales.csv")) print("拼路径推荐 os.path.join:", os.path.join("data", "sales.csv")) print("----") # 用带 r 的路径真的读进来一次 df = pd.read_csv(r"data\sales.csv", encoding="utf-8-sig") print("读进来的形状:", df.shape) print("----") print("不写 r 的字符串会被转义,这是新手最常踩的坑:") print("data\new_sales.csv") print("加了 r 才是原样:", r"data\new_sales.csv")
data/sales.csv → True ./data/sales.csv → True data\sales.csv → True 拼路径推荐 os.path.join: data\sales.csv ---- 读进来的形状: (16, 7) ---- 不写 r 的字符串会被转义,这是新手最常踩的坑: data ew_sales.csv 加了 r 才是原样: data\new_sales.csv

"data\new_sales.csv"里的\n被 Python 当成了换行符,路径直接碎成两行——Windows 路径里的\t、\n、\f都是转义字符。所以从资源管理器复制来的绝对路径,前面一定要加r:

<你的项目目录>\data\sales.csv

也就是写成pd.read_csv(r"<你的项目目录>\data\sales.csv", encoding="utf-8-sig")(<你的项目目录>换成你电脑上的真实路径)。绝对路径虽然清楚,但换台电脑就失效,所以更推荐把脚本和data/放同一层,用相对路径data/sales.csv。在 PyCharm 里点运行、工作目录默认是项目根目录,这样写最稳。

三、read_csv 的常用参数

read_csv有几十个参数,真正天天用到的就这几个:

import pandas as pd ​ # ① nrows:先读前几行探路,几十万行的文件也能秒回 print("① nrows=3:", pd.read_csv("data/sales.csv", encoding="utf-8-sig", nrows=3).shape) ​ # ② usecols:只读要用的列,列多的表能省一半内存 part = pd.read_csv("data/sales.csv", encoding="utf-8-sig", usecols=["城市", "产品", "数量"]) print("② usecols:", part.columns.tolist()) print(part.head(3).to_string(index=False)) ​ # ③ dtype:指定列的类型。学号、手机号、邮编一律按字符串读,否则前导 0 直接丢 stu = pd.read_csv("data/students.csv", encoding="utf-8-sig", dtype={"学号": "str"}) print("③ dtype 学号列:", stu["学号"].dtype) print("③ dtype 语文列:", stu["语文"].dtype) ​ # ④ header=None:第一行不当表头(有些导出文件开头就是数据) noheader = pd.read_csv("data/sales.csv", encoding="utf-8-sig", header=None) print("④ header=None:", noheader.shape, noheader.columns.tolist()) ​ # ⑤ skiprows=1:跳过开头几行(导出文件前面常有多余的标题行) skip = pd.read_csv("data/sales.csv", encoding="utf-8-sig", skiprows=1) print("⑤ skiprows=1 之后列名变成:", skip.columns.tolist())
① nrows=3: (3, 7) ② usecols: ['城市', '产品', '数量'] 城市 产品 数量 杭州 A-笔记本 3 上海 B-鼠标 5 上海 B-鼠标 5 ③ dtype 学号列: object ③ dtype 语文列: float64 ④ header=None: (17, 7) [0, 1, 2, 3, 4, 5, 6] ⑤ skiprows=1 之后列名变成: ['2024-01-05', '杭州', 'A-笔记本', '3', '49.9', '小张', '149.7']

第 ⑤ 个最值得琢磨:skiprows=1跳过的是表头那一行,于是第一行数据被迫当了列名,列名变成了['2024-01-05', '杭州', ...]。所以真实文件前面有多余行时,正确姿势是skiprows=2, header=0(跳过 2 行废话,第 3 行当表头),或者干脆header=None, names=["订单日期", "城市", ...]自己命名。

再换一个文件,看看类型还会怎么坑人:

import pandas as pd ​ # 只关心这三列,用 usecols 拿进来就够了 emp = pd.read_csv("data/employees.csv", encoding="utf-8-sig", usecols=["姓名", "部门", "月薪"]) print("月薪的类型:", emp["月薪"].dtype) print(emp.to_string(index=False)) print("----") print("转成可空整数:", emp["月薪"].astype("Int64").tolist())
月薪的类型: float64 姓名 部门 月薪 张伟 技术部 12000.0 王芳 市场部 9500.0 李娜 技术部 15000.0 刘洋 人事部 8000.0 陈静 市场部 NaN 杨波 技术部 18000.0 赵敏 人事部 8500.0 周涛 市场部 10500.0 ---- 转成可空整数: [12000, 9500, 15000, 8000, <NA>, 18000, 8500, 10500]

原始数据里月薪明明是 12000、9500 这样的整数,读进来却全成了12000.0,类型也变成了float64——因为一列里只要有一个缺失值,整列就会被提升成浮点数。想保留"整数 + 空值"的样子,可以转成 Pandas 的可空整数类型Int64(I 要大写),空值显示为<NA>而不是小数。至于空值该删还是该填,是第 6 篇的主题。

四、分隔符不只是逗号:sep

CSV 的全称是"逗号分隔",但现实里分号、制表符、竖线都有人用。先用我们自己的数据造一个分号文件,再读回来。

import pandas as pd ​ df = pd.read_csv("data/sales.csv", encoding="utf-8-sig") # 模拟别的系统导出的"分号分隔"文件 df.to_csv("data/sales_semicolon.csv", index=False, sep=";", encoding="utf-8-sig") ​ semi = pd.read_csv("data/sales_semicolon.csv", encoding="utf-8-sig", sep=";") print("用 sep=';' 读回来:", semi.shape) print(semi.head(3).to_string(index=False)) print("----") # 故意不写 sep,看看会发生什么 wrong = pd.read_csv("data/sales_semicolon.csv", encoding="utf-8-sig") print("不写 sep 时的列名:", wrong.columns.tolist())
用 sep=';' 读回来: (16, 7) 订单日期 城市 产品 数量 单价 销售员 金额 2024-01-05 杭州 A-笔记本 3 49.9 小张 149.7 2024-01-06 上海 B-鼠标 5 29.9 小李 149.5 2024-01-06 上海 B-鼠标 5 29.9 小李 149.5 ---- 不写 sep 时的列名: ['订单日期;城市;产品;数量;单价;销售员;金额']

不写sep时,整行被当成了一列,列名变成一长串订单日期;城市;产品;...——数据其实进来了,但每一列都取不出来,这是"文件读到了却没读对"的典型症状。看到列名长得离谱,先检查分隔符。制表符文件同理,写sep="\t"。

五、读 Excel:sheet_name 的三种写法

Excel 比 CSV 多一层结构:一个文件里可能有多个 sheet。sheet_name决定了你拿到的是表还是字典。

import pandas as pd ​ # ① 读单个 sheet:给名字(也可以给序号 0) one = pd.read_excel("data/sales.xlsx", sheet_name="明细", engine="openpyxl") print("① 单个 sheet:", one.shape) ​ # ② 读多个 sheet:给一个列表,返回字典 two = pd.read_excel("data/sales.xlsx", sheet_name=["明细", "按城市汇总"], engine="openpyxl") print("② 多个 sheet:", list(two.keys()), type(two).__name__) ​ # ③ 全部读进来:sheet_name=None sheets = pd.read_excel("data/sales.xlsx", sheet_name=None, engine="openpyxl") for name, sub in sheets.items(): print(f"③ {name}:{sub.shape}") print(sheets["按城市汇总"].to_string(index=False)) ​ # ④ usecols:Excel 一样支持只取部分列 part = pd.read_excel("data/sales.xlsx", sheet_name="明细", usecols=["城市", "金额"], engine="openpyxl") print("④ usecols:", part.columns.tolist())
① 单个 sheet: (16, 7) ② 多个 sheet: ['明细', '按城市汇总'] dict ③ 明细:(16, 7) ③ 按城市汇总:(5, 4) 城市 订单数 销量 销售额 上海 4 24 1545.0 北京 3 5 227.9 广州 3 16 535.4 杭州 5 19 2805.1 NaN 1 3 149.7 ④ usecols: ['城市', '金额']

规律很好记:sheet_name给字符串或整数 → 返回 DataFrame;给列表或None→ 返回字典,键是 sheet 名,值是对应的 DataFrame,用sheets["按城市汇总"]取就行。另外.xlsx依赖openpyxl,engine="openpyxl"写不写都能跑,写上是让代码更明确。

六、导出:to_csv、to_excel 和多 sheet 写入

分析完总要交东西,index=False是导出时最容易忘的一个参数——不写它,结果里会多出一列 0、1、2 的行号。

import os import pandas as pd ​ sales = pd.read_csv("data/sales.csv", encoding="utf-8-sig") os.makedirs("output", exist_ok=True) ​ # 导出 CSV:index=False 去掉行号,utf-8-sig 让 Excel 双击打开不乱码 sales.to_csv("output/sales_clean.csv", index=False, encoding="utf-8-sig") ​ # 一个 Excel 里写多个 sheet with pd.ExcelWriter("output/report.xlsx", engine="openpyxl") as writer: sales.to_excel(writer, sheet_name="明细", index=False) sales.head(5).to_excel(writer, sheet_name="前5行", index=False) ​ # 读回来验证一下 back = pd.read_excel("output/report.xlsx", sheet_name=None, engine="openpyxl") print("写出来的 sheet:", {name: sub.shape for name, sub in back.items()}) print("output 目录:", sorted(os.listdir("output"))) print(back["前5行"].head(3).to_string(index=False))
写出来的 sheet: {'明细': (16, 7), '前5行': (5, 7)} output 目录: ['report.xlsx', 'sales_clean.csv'] 订单日期 城市 产品 数量 单价 销售员 金额 2024-01-05 杭州 A-笔记本 3 49.9 小张 149.7 2024-01-06 上海 B-鼠标 5 29.9 小李 149.5 2024-01-06 上海 B-鼠标 5 29.9 小李 149.5

多 sheet 必须用with pd.ExcelWriter(...) as writer这种写法,写完自动保存;如果直接连着调两次to_excel("same.xlsx"),第二个 sheet 会把第一个覆盖掉。

七、大文件:chunksize 分块读取

几百 MB 的日志或导出文件一次性读进来,内存会直接爆掉。chunksize让 Pandas 每次只交给你一小块。

import pandas as pd ​ total = 0 # chunksize=5:每块 5 行,处理完就丢,内存里永远只有一小块 for i, chunk in enumerate(pd.read_csv("data/sales.csv", encoding="utf-8-sig", chunksize=5), start=1): print(f"第 {i} 块:{chunk.shape},本块数量合计 {chunk['数量'].sum()}") total += chunk["数量"].sum() print("全表数量合计:", total)
第 1 块:(5, 7),本块数量合计 19 第 2 块:(5, 7),本块数量合计 27 第 3 块:(5, 7),本块数量合计 16 第 4 块:(1, 7),本块数量合计 5 全表数量合计: 67

chunksize返回的不再是 DataFrame,而是一个可以 for 循环的读取器:适合"逐块过滤后累加""逐块写进数据库"这类流式处理。要注意跨块的操作(比如求中位数、去重、分组排名)不能简单地在每块里各算一遍再合并,那种需求还是得用usecols减列、dtype减内存,或者换 Parquet 格式。

常见问题

Q1:报UnicodeDecodeError: 'utf-8' codec can't decode byte ...怎么办?

说明文件根本不是 UTF-8。国内银行、老系统导出的 CSV 多半是 GBK/GB18030,把encoding换成"gbk"试一次,不行再试"gb18030"(GBK 的超集,兼容性更好)。不要用errors="ignore"糊过去,那会静默吃掉读不懂的字符,数据错了你都不知道。

Q2:读 Excel 报ImportError,提示里带着 openpyxl。

缺 Excel 读写引擎,一条命令就好:pip install openpyxl。注意要装进你实际在用的那个 Python 环境(python -m pip install openpyxl最保险)。

Q3:导出时报PermissionError: [Errno 13] Permission denied: 'output/report.xlsx'。

文件正被 Excel 打开着,Windows 会锁住它。关掉 Excel 再跑一次即可。养成"导出文件先关、后覆盖"的习惯,能省很多莫名其妙的报错。

小结

这一篇把"数据进门"这一步拆开了:read_csv用encoding="utf-8-sig"处理 BOM,路径优先用相对路径、Windows 反斜杠记得加r,nrows/usecols/dtype/header/skiprows/sep应对各种不规整的文件;read_excel用sheet_name的三种写法拿单个、多个或全部 sheet;导出时别忘index=False,多 sheet 要用ExcelWriter;文件太大就chunksize分块读。

读进来只是开始,接下来才是真正决定分析质量的一步:先别急着算,把表看清楚。

下一篇我们聊数据探查:拿到一张表先看这 6 个指标——形状、类型、统计量、缺失、重复、分布,写成一套拿到任何新表都能跑一遍的检查清单。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询