
Python 编程教程:读取 CSV 文件并完成数据清洗
CSV 是常见的表格交换格式,日志导出、业务报表、数据集和系统间的数据传递都经常使用它。它看起来简单,实际却没有统一的列类型、日期格式或缺失值表示方式:同一个字段可能在不同文件中写成空字符串、NA 或 null;文件也可能使用逗号以外的分隔符,或采用与操作系统默认值不同的字符编码。
本文从可复现的示例出发,分别介绍 Python 标准库 csv 和第三方库 pandas 的读取方式,再逐步完成字段规范化、类型转换、缺失值处理、去重和校验。示例会说明清洗规则的边界,避免把“代码能运行”误当成“数据已经正确”。
一、CSV 适合什么场景
CSV 通常适用于结构简单、以行和列表示的数据交换。例如从电子表格导出一份订单明细,或把一张数据表交给另一个系统处理。它是文本格式,便于查看、生成和传输,也不依赖某一种数据库。
CSV 并不保存完整的表格语义。它不会可靠地记录列类型、日期时区、单元格格式、公式与显示结果之间的关系,也没有统一规定如何表示空值。不同程序对引号、换行和编码的处理也可能不同。因此,读取 CSV 时需要显式确认文件的编码、分隔符、表头和字段规则;如果业务要求保留复杂类型或工作簿格式,应考虑更合适的格式,而不是指望 CSV 自动保存这些信息。
二、先准备一个可运行的样例
下面的文件包含空格、缺失年龄、重复记录和不符合日期格式的值。先运行这段代码生成 people.csv,后续示例均以它为输入。写文件时指定 newline="",这是 Python csv 模块推荐的文件打开方式,可以避免部分平台上的换行处理问题。
import csv
rows = [
[" name ", "age", "signup_date", "city"],
[" Alice ", "30", "2024-01-15", "Beijing"],
["Bob", "", "2024-02-01", "Shanghai"],
["Alice", "30", "2024-01-15", "Beijing"],
["Chen", "unknown", "15/03/2024", "Shenzhen"],
]
with open("people.csv", "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerows(rows)
print("已生成 people.csv")
把代码保存为 Python 文件并运行即可生成样例文件。生产环境中,输入路径应指向真实数据文件;不要假定文件一定与当前脚本位于同一目录。需要稳定定位文件时,可以基于脚本位置构造路径,或由命令行参数、配置文件传入。
三、用标准库 csv 逐行读取
如果项目不需要复杂的数据分析,或希望减少第三方依赖,标准库 csv 就足够。DictReader 会使用第一行作为字段名,并把每一行返回为字典。以下示例逐行读取,不会一次性把整个文件载入内存。
import csv
with open("people.csv", "r", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
print("字段:", reader.fieldnames)
for row in reader:
print(row)
输出中的值默认都是字符串;空单元格通常读成空字符串,而不是 Python 的 None。CSV 本身没有“这一列是整数”的元数据,因此需要在应用代码中验证和转换。
DictReader 适合表头规范、字段较少的文件,但要注意几个边界:
- 文件第一行必须确实是表头。如果没有表头,应使用
csv.reader,并自行指定字段名。 - 表头存在空格、重复名称或空字段时,字典形式可能造成字段难以识别,重复名称还可能让值无法按预期区分。读取后应检查并规范化表头;若必须保留重复列,应使用按位置读取的
csv.reader。 - 某一行的列数与表头不一致时,
DictReader会将多出的字段放入restkey对应的项目,缺少的字段会使用restval。这不代表该行有效,仍需要明确检查列数。 - 文件中的字段若包含逗号、双引号或换行,必须遵循 CSV 引号规则。不要用
line.split(",")代替 CSV 解析器,否则合法字段也可能被拆错。
检查表头与行长度
对格式要求严格的文件,可以先用 csv.reader 检查每行字段数,再继续处理。下面示例会在列数异常时报告行号。这里的行号按 CSV 逻辑记录计数;如果字段内含换行,它不一定等于文本编辑器看到的物理行号。
import csv
expected_columns = 4
with open("people.csv", "r", encoding="utf-8", newline="") as f:
reader = csv.reader(f)
header = next(reader, None)
if header is None:
raise ValueError("CSV 文件为空")
if len(header) != expected_columns:
raise ValueError(
f"表头有 {len(header)} 列,预期 {expected_columns} 列"
)
for record_number, row in enumerate(reader, start=2):
if len(row) != expected_columns:
print(
f"第 {record_number} 条记录列数异常:"
f"实际 {len(row)} 列,预期 {expected_columns} 列"
)
四、用 pandas 读取并查看数据
当任务涉及筛选、统计、类型转换或批量清洗时,pandas 会更方便。若尚未安装,可在当前 Python 环境中执行 python -m pip install pandas。安装命令应在项目使用的虚拟环境中运行,避免装到另一个 Python 环境。
先用默认参数读取样例:
import pandas as pd
df = pd.read_csv("people.csv", encoding="utf-8")
print(df.head())
print(df.dtypes)
print(df.isna().sum())
read_csv 会推断列类型,并把一部分常见文本标记识别为缺失值。推断有便利,也有风险:例如一个本应是编号的列若只包含数字,可能被读成数值;某些字符串也可能被 pandas 按默认规则当成缺失值。应根据业务语义检查 df.dtypes、样例值和缺失统计,而不是假定推断一定符合预期。
常用读取参数
filepath_or_buffer:文件路径或可读取的数据源。传入文件对象时,文件对象的编码应由打开方式决定。sep:分隔符,默认为逗号。制表符文件常用sep="\t"。分隔符不是简单的“按字符切分”,解析仍会遵循 CSV 引号规则。header:表头所在行。默认使用第一行作为列名;无表头文件可以用header=None。names:指定列名。无表头文件常与header=None一起使用。列名数量应与实际字段数匹配。usecols:只读取所需列,可减少无关数据处理;它不会替代对文件结构的校验。dtype:指定列类型。对邮编、账户编号等不参与算术的数字样式字段,指定为字符串通常更合适,以免前导零丢失。na_values、keep_default_na:配置哪些内容视为缺失值,以及是否保留 pandas 默认缺失标记。是否将NA、null或其他字符串当作缺失,必须依据数据约定决定。parse_dates:尝试解析日期列。解析成功与否取决于输入格式和 pandas 版本,复杂或混杂格式宜在读取后显式转换并检查结果。on_bad_lines:处理部分格式异常行的策略,常见取值包括"error"、"warn"和"skip"。跳过行可能造成数据丢失,不应为了让程序继续运行就默认忽略问题。chunksize:按块读取并返回迭代器,适合分批处理大文件。它不会自动完成全文件去重或全局统计;跨块操作需要额外设计。
参数的可用性和行为可能随 pandas 版本、解析引擎及文件内容变化。使用前可查看当前环境中的 pd.read_csv 文档与版本;遇到复杂引号或分隔规则时,也要针对真实文件测试。不要把某个参数当成通用的数据修复器。
五、完成一套可检查的清洗流程
清洗应先定义规则,再执行转换。下面示例做几件明确的事:去掉列名和文本字段两端空白;将空字符串转换为缺失值;将年龄解析为数字;将日期解析为日期类型;按指定业务键去重;最后检查哪些记录未通过规则。它不会悄悄把无法识别的值改成看似合理的值。
import pandas as pd
df = pd.read_csv(
"people.csv",
encoding="utf-8",
dtype={"name": "string", "city": "string"},
)
# 清理列名两端空白,并检查列名是否重复
df.columns = df.columns.str.strip()
if not df.columns.is_unique:
raise ValueError("清理空白后出现重复列名")
required = {"name", "age", "signup_date", "city"}
missing_columns = required - set(df.columns)
if missing_columns:
raise ValueError(f"缺少必要列:{sorted(missing_columns)}")
# 清理文本字段两端空白;空字符串统一作为缺失值
for column in ["name", "city", "signup_date"]:
df[column] = df[column].astype("string").str.strip()
df[column] = df[column].replace("", pd.NA)
# 年龄转换:无法解析的内容成为缺失值,之后由校验步骤处理
df["age"] = pd.to_numeric(df["age"], errors="coerce")
# 日期格式明确为 YYYY-MM-DD;其他格式解析失败后成为 NaT
df["signup_date"] = pd.to_datetime(
df["signup_date"],
format="%Y-%m-%d",
errors="coerce",
)
# 删除关键字段缺失的记录
df = df.dropna(subset=["name", "city"])
# 仅当这三个字段在业务上能唯一标识记录时,才这样去重
df = df.drop_duplicates(
subset=["name", "age", "signup_date", "city"],
keep="first",
)
# 检查年龄是否为非负整数
invalid_age = df["age"].notna() & (
(df["age"] < 0) | (df["age"] % 1 != 0)
)
# 检查年龄和日期的解析失败情况
print("年龄缺失或无法解析:", df["age"].isna().sum())
print("日期缺失或无法解析:", df["signup_date"].isna().sum())
print("年龄取值不合法:", invalid_age.sum())
print(df)
需要特别注意,errors="coerce" 会把不能解析的值变成缺失值。它让批量处理更方便,但也会隐藏原始错误文本。因此,如果后续需要追查问题,应在转换前保留原始列,或另存无法解析的行,而不是只保留转换后的结果。
上例中的日期格式只接受 YYYY-MM-DD,样例中的 15/03/2024 会变成 NaT。这不是 pandas 判断该日期“错误”,而是它不符合本例定义的格式。若数据源允许多种格式,应先明确允许的格式和地区约定,再分别解析;像 03/04/2024 这样的值可能表示 3 月 4 日,也可能表示 4 月 3 日,单靠程序无法推断业务意图。
保留问题记录,而不是直接丢弃
在正式业务流程中,遇到无法解析的数据通常应该隔离并报告。下面的写法保留一份原始年龄,用于定位异常值;日期也按示例格式严格转换。问题记录可以写入单独文件,交由上游修正或由业务人员确认。
import pandas as pd
df = pd.read_csv("people.csv", encoding="utf-8", dtype="string")
df["age_raw"] = df["age"]
df["age"] = pd.to_numeric(df["age"], errors="coerce")
df["signup_date_raw"] = df["signup_date"]
df["signup_date"] = pd.to_datetime(
df["signup_date"],
format="%Y-%m-%d",
errors="coerce",
)
bad_age = df["age_raw"].notna() & df["age"].isna()
bad_date = (
df["signup_date_raw"].notna()
& df["signup_date"].isna()
)
issues = df[bad_age | bad_date].copy()
clean = df[~(bad_age | bad_date)].copy()
issues.to_csv("people_issues.csv", index=False, encoding="utf-8")
clean.to_csv("people_clean.csv", index=False, encoding="utf-8")
print(f"清洗后记录数:{len(clean)}")
print(f"待检查记录数:{len(issues)}")
以上逻辑把所有无法转换的非空年龄或日期都放入问题文件。若空值本身也不允许,应额外检查缺失值;若年龄允许为空,则不能仅凭年龄为空就认定记录错误。规则取决于业务要求。
六、编码、分隔符与引号问题
编码
读取中文 CSV 时,常见情况是文件使用 UTF-8,也可能来自旧软件或特定系统的其他编码。编码不匹配可能引发 UnicodeDecodeError,也可能在某些错误处理方式下产生乱码。应优先向数据提供方确认编码,或查看导出系统的配置。不要把“尝试一种编码直到不报错”当作可靠判断:错误解码有时不会报错,却会破坏内容。
只有在文件确实以 UTF-8 BOM 开头、且读取行为受到 BOM 影响时,才考虑使用 encoding="utf-8-sig"。它用于处理 UTF-8 BOM,不是所有乱码问题的通用解决方案。写出文件时也要考虑下游程序要求的编码,而不是一律改变原始文件。
分隔符与引号
有些文件扩展名是 .csv,内容却使用分号或制表符分隔。应查看文件头部或确认导出设置,再传入正确的 sep。如果字段值本身包含分隔符,标准 CSV 会用引号保护字段;因此不能通过拆分文本行来判断实际列数。
标准库读取器可以设置 delimiter 和引号相关选项,pandas 可通过 sep 等参数配置。遇到非标准转义规则时,不要随意调整参数直至“看起来能读”;应准备覆盖逗号、引号、换行和空字段的测试样本,并与生成文件的软件约定一致。
七、大文件如何处理
文件较大时,pandas 默认读取会把数据集中载入内存,实际占用不只等于文件大小:字符串对象和数据结构也需要额外空间。可以使用 chunksize 分块处理。下面示例逐块计算满足条件的记录数:
import pandas as pd
total = 0
for chunk in pd.read_csv(
"large.csv",
encoding="utf-8",
chunksize=100_000,
):
# 每块独立清理;这里示例只统计城市为 Beijing 的记录
if "city" not in chunk.columns:
raise ValueError("缺少 city 列")
city = chunk["city"].astype("string").str.strip()
total += int(city.eq("Beijing").sum())
print("Beijing 记录数:", total)
chunksize 控制每次读取的行数,不保证每块占用固定内存;单行字段特别大时,仍可能产生较高内存开销。若要全文件去重,块内 drop_duplicates 不能识别分散在不同块中的重复记录;若要计算全局中位数、排序或跨块关联,也需要累积状态、外部存储或其他算法。先明确任务是否可分块,再决定实现方式。
八、清洗结果如何保存
pandas 可以将结果写回 CSV:
df.to_csv(
"people_clean.csv",
index=False,
encoding="utf-8",
)
index=False 表示不把 DataFrame 的行索引额外写成一列,通常更适合后续交换。若下游要求特定空值标记、日期格式、分隔符或编码,应明确配置并测试。写出的 CSV 仍然不携带可靠的列类型信息,下一位读取者仍需要知道每列的语义。
面向电子表格时的公式注入风险
CSV 是纯文本,不会执行 Python 代码;但如果生成的文件被电子表格软件打开,某些以 =、+、- 或 @ 开头的单元格可能被解释为公式。若 CSV 包含不可信用户输入,并且将交付给会自动解析公式的表格软件,这会构成公式注入风险。
处理方式取决于使用场景。内部程序间交换的数据不应无条件改写字段;面向电子表格查看的导出,可以制定针对目标软件的转义策略,并测试其实际打开行为。常见做法是在可疑文本前加单引号等前缀,但不同软件和导入方式的表现可能不同,这会改变显示或原始数据,不能宣称对所有程序都有效。更稳妥的原则是区分“机器交换文件”和“供电子表格查看的文件”,并只对后者实施经过验证的保护。
九、常见错误与排查方法
- 报找不到文件。相对路径是相对于当前工作目录,而不一定是脚本所在目录。先打印当前工作目录,检查文件名和目录;不要通过拼接不可信输入构造任意路径。
- 出现乱码或解码异常。确认源文件编码和读取参数。不要仅仅忽略解码错误,否则可能丢字符,造成无法察觉的数据损坏。
- 列数不对或所有内容落在一列。检查分隔符、表头和引号规则。文件扩展名不能证明内容一定以逗号分隔。
- 数字列转换后有缺失值。检查原始值中是否有空格、单位、千位分隔符、文本标记或非法字符。先保留原始字段并列出转换失败的值,再确定清理规则。
- 日期顺序错了。确认日期格式的地区含义,并指定明确格式。不要依赖模糊日期推断处理生产数据。
- 去重后记录数变化异常。确认“重复”的业务定义。整行重复、相同主键、同一用户同一天记录可能是不同规则;不要未经确认就对所有列去重。
- 写出的 CSV 多出一列。检查是否忘记设置
index=False,以及下游是否把索引误读成业务字段。 - 程序跳过坏行但结果不完整。检查
on_bad_lines设置和日志。忽略错误会带来静默丢数,只有在业务明确允许时才应跳过,并记录数量和原因。
十、生产使用时的检查清单
- 确认文件来源、编码、分隔符、是否含表头,以及字段的业务含义。
- 对文件大小、列名、必需字段和记录列数做基本校验。
- 先检查样例值与缺失统计,再决定类型、缺失标记和日期格式。
- 区分“可自动修正”的值与“需要人工确认”的值,并保留原始值或问题记录。
- 明确去重键、空值处理和异常行策略,避免无意丢失合法数据。
- 对大文件评估内存与跨块操作需求;对电子表格使用场景评估公式注入风险。
- 输出后重新读取或抽样检查,并记录输入文件、处理规则、输出数量和异常数量。
CSV 清洗的核心不是堆叠参数,而是把数据约定变成可验证的规则:明确哪些值有效、哪些值允许缺失、转换失败后如何处置,以及哪些记录可以去重。标准库适合轻量、逐行处理;pandas 适合表格型转换和分析。无论选择哪种工具,都应保留异常证据、验证清洗结果,并避免用静默跳过或宽松推断掩盖源数据问题。




