Python 编程教程:读取 CSV 文件并完成数据清洗

AI教程13小时前发布 jdkjadmin
0 00
Python 编程教程:读取 CSV 文件并完成数据清洗

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 设置和日志。忽略错误会带来静默丢数,只有在业务明确允许时才应跳过,并记录数量和原因。

十、生产使用时的检查清单

  1. 确认文件来源、编码、分隔符、是否含表头,以及字段的业务含义。
  2. 对文件大小、列名、必需字段和记录列数做基本校验。
  3. 先检查样例值与缺失统计,再决定类型、缺失标记和日期格式。
  4. 区分“可自动修正”的值与“需要人工确认”的值,并保留原始值或问题记录。
  5. 明确去重键、空值处理和异常行策略,避免无意丢失合法数据。
  6. 对大文件评估内存与跨块操作需求;对电子表格使用场景评估公式注入风险。
  7. 输出后重新读取或抽样检查,并记录输入文件、处理规则、输出数量和异常数量。

CSV 清洗的核心不是堆叠参数,而是把数据约定变成可验证的规则:明确哪些值有效、哪些值允许缺失、转换失败后如何处置,以及哪些记录可以去重。标准库适合轻量、逐行处理;pandas 适合表格型转换和分析。无论选择哪种工具,都应保留异常证据、验证清洗结果,并避免用静默跳过或宽松推断掩盖源数据问题。

© 版权声明

相关文章