4、数据读取与写入:读取 CSV、JSON、Parquet、ORC 等格式,写入模式与分区
数据读写的功底,说白了就是 Spark SQL 的「基本功」。你想想看,不管多牛的分析,数据进不来、出不去,全白搭。我个人习惯,拿到一个新数据集,第一件事就是搞清楚它的格式和分区策略。今天咱们就把 CSV、JSON、Parquet、ORC 这四种主流格式,连同写入模式和分区,一次性讲透。
4.1 读取 CSV:最常用,坑也最多
CSV 是数据工程师的「家常便饭」。但我在项目中遇到过太多次因为 CSV 格式不规范导致的血案。比如字段里藏着逗号、引号没转义、文件头缺失……嗯,这里要注意。
基本读取方式很简单:
df = spark.read \
.option("header", "true") \
.option("inferSchema", "true") \
.csv("hdfs://path/to/file.csv")
但别急着用 inferSchema。为什么?因为它会额外扫描一遍数据,大文件时慢得你想哭。我建议你手动指定 schema:
from pyspark.sql.types import StructType, StructField, StringType, IntegerType
schema = StructType([
StructField("name", StringType(), True),
StructField("age", IntegerType(), True),
StructField("city", StringType(), True)
])
df = spark.read \
.option("header", "true") \
.schema(schema) \
.csv("hdfs://path/to/file.csv")
4.2 读取 JSON:灵活但别滥用
JSON 格式灵活,嵌套结构天然适合半结构化数据。但灵活的另一面是「混乱」。同一个字段,这条记录是字符串,下条记录是数组,Spark 会直接报错。
读取 JSON 的标准写法:
df = spark.read \
.option("multiLine", "true") \
.json("hdfs://path/to/file.json")
这里有个小细节:multiLine 参数。如果你的 JSON 文件是每行一个 JSON 对象(即 JSON Lines 格式),就别开这个选项,否则性能会下降。如果是标准的 JSON 数组格式(整个文件一个对象),那就必须开。
4.3 读取 Parquet:Spark 的「亲儿子」
Parquet 是列式存储格式,压缩率高、查询快。说白了,Spark SQL 对 Parquet 的支持是最好的。我几乎所有的生产环境数据都存成 Parquet。
读取 Parquet 最简单:
df = spark.read.parquet("hdfs://path/to/file.parquet")
不需要指定 schema,因为 Parquet 文件自带元数据。而且 Spark 会自动利用列式存储的优势,只读取你需要的列。比如你只查两列,它不会把其他列的数据也加载进来。
df.filter(col("age") > 30) 时,Spark 会在读取文件时就过滤掉不符合条件的数据行,而不是全量读进来再过滤。这在处理 TB 级数据时,性能差距是数量级的。
4.4 读取 ORC:Hive 生态的「老搭档」
ORC 和 Parquet 很像,也是列式存储。如果你在 Hive 生态里工作,ORC 可能是更自然的选择。我记得有一次帮客户迁移数据仓库,他们所有 Hive 表都是 ORC 格式,用 Spark 读起来毫无压力。
df = spark.read.orc("hdfs://path/to/file.orc")
ORC 的压缩率通常比 Parquet 更高,但查询性能略逊一筹。怎么选?我的建议是:如果你主要用 Spark,选 Parquet;如果你主要用 Hive,选 ORC。别在两个格式之间反复横跳,维护成本太高。
4.5 写入模式:覆盖、追加、忽略、报错
写入数据时,最怕的就是「写错了」。Spark 提供了四种写入模式,你想想看,选错了会出大事。
| 模式 | 含义 | 使用场景 |
|---|---|---|
overwrite |
覆盖已有数据 | 全量刷新,比如每日全量快照 |
append |
追加到已有数据 | 增量写入,比如日志流 |
ignore |
如果目标路径已存在,直接跳过 | 幂等写入,防止重复跑任务 |
error(默认) |
如果目标路径已存在,直接报错 | 防止误覆盖,开发环境常用 |
写入代码示例:
df.write \
.mode("overwrite") \
.parquet("hdfs://path/to/output")
overwrite 模式写数据,结果路径写错了,把上一层的整个目录给覆盖了。从那以后,我养成了一个习惯:写数据前,先用 spark._jvm.org.apache.hadoop.fs.Path 检查目标路径是否存在,确认无误再执行写入。
4.6 分区写入:让查询快 10 倍的关键
分区是数据仓库的「灵魂」。你想想看,一张 100TB 的表,如果不分区,每次查询都要全表扫描,谁也扛不住。
Spark 支持按列分区写入:
df.write \
.partitionBy("year", "month") \
.mode("append") \
.parquet("hdfs://path/to/output")
这样写出来的目录结构是:
output/
year=2024/
month=01/
part-00001.parquet
part-00002.parquet
month=02/
part-00001.parquet
year=2025/
month=01/
part-00001.parquet
查询时,如果你只查 2025 年 1 月的数据,Spark 只会读取 year=2025/month=01/ 目录下的文件,其他目录根本不会碰。这就是分区裁剪(Partition Pruning)。
4.7 动态分区与静态分区
Spark 支持两种分区写入方式:
- 静态分区:你在代码里写死分区值,比如
.partitionBy("year", "month"),然后数据里必须有这两列。 - 动态分区:Spark 根据数据内容自动决定数据写到哪个分区。需要开启配置:
spark.conf.set("spark.sql.sources.partitionOverwriteMode", "dynamic")
df.write \
.mode("overwrite") \
.partitionBy("year", "month") \
.parquet("hdfs://path/to/output")
动态分区的好处是灵活,但坏处是容易产生大量小文件。我记得有一次,一个同事用动态分区写数据,结果生成了几万个分区目录,每个目录里只有几 KB 数据。后续读取时,光打开文件列表就花了 10 分钟。
coalesce() 或 repartition() 合并小文件。
4.8 总结:选对格式,用对模式
好了,咱们把数据读写的核心点都过了一遍。总结一下我的个人经验:
- CSV:适合小数据量、临时分析。生产环境尽量别用。
- JSON:适合半结构化数据、日志流。注意用 JSON Lines 格式。
- Parquet:Spark 生态的首选。列式存储、谓词下推、压缩率高。
- ORC:Hive 生态的首选。压缩率更高,但查询性能略低。
- 写入模式:
append用于增量,overwrite用于全量,ignore用于幂等。 - 分区:按基数适中的列分区,避免小文件问题。
数据读写看似基础,但往往是整个数据 pipeline 的瓶颈。你想想看,如果读数据慢了 10 分钟,写数据又慢了 10 分钟,一天跑一次的任务,一个月就多浪费 10 个小时。把这些细节抠好,才是资深工程师该有的样子。