3. 数据读取与写入:CSV、Excel、JSON、数据库等常见数据源的读取与写入
数据清洗的第一步是什么?说白了,你得先把数据弄进来。
我见过太多新手,花了一整天写清洗逻辑,结果发现数据读进来就是错的。嗯,这很常见。所以今天咱们就聊聊,怎么把各种常见的数据源——CSV、Excel、JSON、数据库——稳稳当当地读进来,再干干净净地写出去。
核心原则:读入时多花一分钟检查,清洗时少花一小时返工。
3.1 CSV 文件的读取与写入
CSV 是最常见的数据交换格式。我个人习惯用 pandas.read_csv(),但有几个坑你得注意。
3.1.1 基本读取
import pandas as pd
# 最基础的读取
df = pd.read_csv('data.csv')
# 指定编码,避免乱码
df = pd.read_csv('data.csv', encoding='utf-8')
# 如果文件是 GBK 编码(国内很多老系统用)
df = pd.read_csv('data.csv', encoding='gbk')
我的经验:我曾经接手一个项目,CSV 文件里混着 UTF-8 和 GBK 两种编码。读进来全是乱码。后来我加了个 try-except,先试 UTF-8,不行再试 GBK。这招救了我好几次。
3.1.2 处理常见问题
- 分隔符不是逗号:有些系统用分号或制表符。用
sep=';'或sep='\t'指定。 - 表头问题:如果文件没有表头,设置
header=None,再手动指定列名names=['col1', 'col2']。 - 数据类型推断:pandas 会自动推断类型,但有时会出错。比如身份证号被当成数字。用
dtype={'id': str}强制指定。
# 处理没有表头的文件
df = pd.read_csv('data_no_header.csv', header=None, names=['id', 'name', 'age'])
# 强制指定列类型
df = pd.read_csv('data.csv', dtype={'user_id': str, 'amount': float})
3.1.3 写入 CSV
# 基本写入
df.to_csv('output.csv', index=False)
# 指定编码,避免中文乱码
df.to_csv('output.csv', index=False, encoding='utf-8-sig')
注意:写入时默认会带行索引。如果你不想多一列,记得加 index=False。另外,utf-8-sig 编码会在文件开头加 BOM,Excel 打开时不会乱码。这是个小技巧。
3.2 Excel 文件的读取与写入
Excel 文件比 CSV 复杂,因为它可以有多张工作表。我建议用 pandas 配合 openpyxl 或 xlrd 引擎。
3.2.1 读取 Excel
# 读取默认工作表(第一张)
df = pd.read_excel('data.xlsx')
# 读取指定工作表
df = pd.read_excel('data.xlsx', sheet_name='Sheet1')
# 读取所有工作表,返回字典
dfs = pd.read_excel('data.xlsx', sheet_name=None)
# dfs 是一个字典,键是工作表名,值是 DataFrame
3.2.2 处理合并单元格
Excel 里经常有合并单元格。pandas 读进来后,合并的单元格只有第一个有值,其他是 NaN。你需要手动填充。
# 读取时保留合并单元格的样式
df = pd.read_excel('data.xlsx', header=None)
# 向前填充合并单元格的值
df = df.fillna(method='ffill')
避坑指南:我曾经处理过一个财务报表,里面全是合并单元格。读进来后,一半数据是 NaN。我花了半小时才意识到是合并单元格的问题。后来我写了个函数,自动检测并填充合并单元格。嗯,这招很实用。
3.2.3 写入 Excel
# 写入单个工作表
df.to_excel('output.xlsx', sheet_name='清洗结果', index=False)
# 写入多个工作表
with pd.ExcelWriter('output.xlsx') as writer:
df1.to_excel(writer, sheet_name='原始数据', index=False)
df2.to_excel(writer, sheet_name='清洗后', index=False)
3.3 JSON 文件的读取与写入
JSON 在 API 接口和 NoSQL 数据库中很常见。它的结构比 CSV 灵活,但也更复杂。
3.3.1 读取 JSON
# 读取标准 JSON 文件
df = pd.read_json('data.json')
# 读取嵌套 JSON(需要指定路径)
df = pd.read_json('data.json', orient='records')
3.3.2 处理嵌套 JSON
JSON 经常嵌套多层。比如一个用户信息里包含地址对象。你需要用 json_normalize 展开。
from pandas import json_normalize
# 假设 data 是嵌套的 JSON 列表
data = [
{"name": "张三", "address": {"city": "北京", "district": "海淀"}},
{"name": "李四", "address": {"city": "上海", "district": "浦东"}}
]
# 展开嵌套字段
df = json_normalize(data)
# 结果:name, address.city, address.district
核心技巧:嵌套 JSON 展开后,列名会带点号。你可以用 df.columns = df.columns.str.replace('.', '_') 替换,方便后续操作。
3.3.3 写入 JSON
# 写入为 JSON 文件
df.to_json('output.json', orient='records', force_ascii=False)
注意:force_ascii=False 保证中文不被转义成 \u 编码。否则你写出来的 JSON 文件里全是乱码。
3.4 数据库的读取与写入
数据库读取是数据工程师的日常。我主要用 SQLAlchemy 连接各种数据库。
3.4.1 连接数据库
from sqlalchemy import create_engine
# MySQL 连接
engine = create_engine('mysql+pymysql://user:password@host:port/database')
# PostgreSQL 连接
engine = create_engine('postgresql://user:password@host:port/database')
# SQLite 连接(本地文件)
engine = create_engine('sqlite:///data.db')
3.4.2 读取数据
# 读取整张表
df = pd.read_sql_table('table_name', engine)
# 使用 SQL 查询
df = pd.read_sql_query('SELECT * FROM table_name WHERE date > "2024-01-01"', engine)
# 更推荐的方式:分块读取,避免内存溢出
chunks = pd.read_sql_query('SELECT * FROM large_table', engine, chunksize=10000)
for chunk in chunks:
# 处理每一块数据
process(chunk)
我的经验:有一次我要从生产库读取 5000 万行数据。直接读,内存直接爆了。后来我用了 chunksize 分块读取,每 1 万行处理一次,再写入临时表。嗯,这招救了我的服务器。
3.4.3 写入数据
# 写入数据库
df.to_sql('table_name', engine, if_exists='replace', index=False)
# 追加写入
df.to_sql('table_name', engine, if_exists='append', index=False)
# 批量写入(大数据量时推荐)
df.to_sql('table_name', engine, if_exists='append', index=False, chunksize=10000)
注意:if_exists='replace' 会删除原表再创建。如果你只是想追加数据,用 append。另外,写入大数据量时,chunksize 可以避免数据库连接超时。
3.5 知识体系总览
下面这张图,是我自己总结的数据读取与写入的核心逻辑。你想想看,不管数据源是什么,流程其实都一样:连接 -> 读取 -> 处理 -> 写入。
3.6 实战建议
说了这么多,我给你几个实战建议:
- 先检查再处理:读入数据后,先用
df.info()和df.head()看一眼。数据类型对不对?有没有空值? - 统一编码:所有文件统一用 UTF-8 编码。如果遇到乱码,试试 GBK 或 GB2312。
- 分块处理大文件:超过 1GB 的文件,别一次性读入内存。用
chunksize分块处理。 - 备份原始数据:写入前,先备份原始数据。万一写错了,还能恢复。
最后说一句:数据读取看似简单,但细节决定成败。我见过太多因为编码问题、分隔符问题、合并单元格问题导致的数据清洗失败。嗯,多花一分钟检查,少花一小时返工。这句话,你记住就行。