4. Spark 数据读取:读取 CSV、JSON、Parquet、JDBC 数据源

在催收业务场景中,数据源格式多样:历史还款记录常以 CSV 存储,客户画像数据多为 JSON 格式,经过 ETL 后的宽表通常以 Parquet 列式存储,而核心账务数据则存放在关系型数据库中。本章将系统讲解 Spark 读取这四种主流数据源的方法,并结合催收业务实例进行演示。

4.1 数据源读取概览

Spark 通过统一的 DataFrameReader 接口读取外部数据,核心 API 为 spark.read.format("...").option("...", "...").load("...")。下表对比了四种数据源的关键特性:

数据源 格式标识 适用场景 压缩支持 Schema 推断
CSV csv 催收员每日外呼记录、还款流水导出 gzip, bzip2, snappy 默认开启,可手动指定
JSON json 客户画像、催收策略配置、API 响应日志 gzip, bzip2 自动推断嵌套结构
Parquet parquet 催收宽表、特征工程中间表、数仓分层存储 内置 snappy, gzip, lzo 自带 Schema,无需推断
JDBC jdbc 催收系统核心库、贷后管理数据库 不适用 通过 JDBC 元数据获取
选型建议: 在催收批处理作业中,推荐使用 Parquet 作为中间存储格式,其列式存储和谓词下推特性可大幅提升查询性能;JDBC 适用于实时性要求较高的核心数据读取,但需注意连接池配置和分区策略。

4.2 读取 CSV:催收外呼记录

催收外呼记录通常以 CSV 格式导出,包含字段:案件编号、客户姓名、联系电话、外呼时间、通话时长、催收员编号、催收结果等。以下代码演示如何读取并处理这类数据:

// 读取催收外呼记录 CSV 文件
val callRecords = spark.read
  .format("csv")
  .option("header", "true")                // 首行为列名
  .option("inferSchema", "true")           // 自动推断数据类型
  .option("delimiter", ",")                // 分隔符,默认逗号
  .option("dateFormat", "yyyy-MM-dd HH:mm:ss") // 日期格式
  .option("mode", "PERMISSIVE")            // 容错模式:将损坏行放入 _corrupt_record
  .load("/data/collection/call_records_2024.csv")

// 查看 Schema 和前 5 行
callRecords.printSchema()
callRecords.show(5, truncate = false)
催收业务要点: 外呼记录中常存在空值(如未接通时通话时长为空)和异常值(如通话时长为负数)。建议在读取时使用 PERMISSIVE 模式,并在后续清洗步骤中处理 _corrupt_record 列。

4.3 读取 JSON:客户画像与策略配置

客户画像数据通常以嵌套 JSON 格式存储,包含基本信息、联系人信息、历史行为标签等。催收策略配置也常以 JSON 格式下发。Spark 读取 JSON 时自动处理嵌套结构:

// 读取客户画像 JSON 数据
val customerProfile = spark.read
  .format("json")
  .option("multiLine", "true")             // 支持多行 JSON 对象
  .option("mode", "FAILFAST")              // 遇到格式错误立即失败
  .load("/data/collection/customer_profile/")

// 展开嵌套字段
customerProfile
  .select(
    $"customer_id",
    $"basic_info.name".alias("customer_name"),
    $"basic_info.age",
    $"contact.phone",
    $"tags"                                 // 数组类型
  )
  .show(5, truncate = false)
注意: 催收策略 JSON 文件可能包含复杂的条件嵌套(如 if-then-else 结构),读取后建议使用 from_json 函数配合自定义 Schema 进行解析,避免 Schema 推断偏差导致数据丢失。

4.4 读取 Parquet:催收宽表与特征数据

Parquet 是催收数据仓库中最常用的存储格式。经过 ETL 加工后的催收宽表(包含客户基本信息、历史还款行为、催收评分等数百个特征)通常以 Parquet 格式存储。Parquet 自带 Schema 且支持谓词下推,读取效率极高:

// 读取催收宽表 Parquet 文件
val collectionWideTable = spark.read
  .format("parquet")
  .option("mergeSchema", "true")           // 合并多个文件的 Schema
  .load("/data/warehouse/collection_wide_table/")

// 利用谓词下推:只读取逾期天数大于 90 天的记录
val overdue90 = collectionWideTable
  .filter($"overdue_days" > 90)
  .select("case_id", "customer_name", "overdue_days", "collection_score")

overdue90.explain("cost")                  // 查看执行计划,确认下推生效
性能优化: 催收宽表通常包含数百列,建议在读取时使用 select 指定所需列,减少 I/O 开销。Parquet 的列式存储特性使得只读取部分列时性能提升显著。

4.5 读取 JDBC:催收核心数据库

催收系统的核心数据(如案件分配表、还款计划表、催收结果表)通常存储在关系型数据库中。Spark 通过 JDBC 读取时,需要配置连接信息、分区策略和谓词下推:

// 读取 MySQL 催收案件表
val jdbcDF = spark.read
  .format("jdbc")
  .option("url", "jdbc:mysql://192.168.1.100:3306/collection_db")
  .option("dbtable", "collection_case")
  .option("user", "spark_user")
  .option("password", "encrypted_password")
  .option("driver", "com.mysql.cj.jdbc.Driver")
  // 分区策略:按案件 ID 范围分片,提升并行度
  .option("partitionColumn", "case_id")
  .option("lowerBound", "1")
  .option("upperBound", "10000000")
  .option("numPartitions", "10")
  // 连接池与 fetch 优化
  .option("fetchSize", "10000")
  .option("connectionPool", "HikariCP")
  .load()

// 使用谓词下推:只读取分配给指定催收团队的案件
val teamCases = jdbcDF
  .filter("team_id = 'T001' AND status = 'ACTIVE'")
  .select("case_id", "customer_name", "assign_date", "priority_level")

teamCases.show(10)
安全与性能提醒: 数据库密码建议使用 Spark 安全配置或密钥管理服务,避免明文硬编码。分区列应选择分布均匀的数值列(如自增 ID),避免数据倾斜导致部分任务过慢。

4.6 四种数据源读取流程对比

下图展示了 Spark 读取四种数据源时的核心流程与关键配置项:

Spark 四种数据源读取流程对比 Spark DataFrameReader CSV 读取 JSON 读取 Parquet 读取 JDBC 读取 关键配置 • header: true/false • inferSchema: true/false • delimiter: 分隔符 • mode: PERMISSIVE • dateFormat: 日期格式 关键配置 • multiLine: true/false • mode: FAILFAST • allowComments: true • 自定义 Schema • from_json 解析嵌套 关键配置 • mergeSchema: true/false • 谓词下推自动生效 • 列剪枝自动生效 • 压缩格式配置 • 分区发现 关键配置 • url / dbtable • partitionColumn 分区 • fetchSize 批量拉取 • 连接池配置 • 谓词下推 催收业务场景映射 CSV → 外呼记录 / 还款流水 JSON → 客户画像 / 策略配置 Parquet → 催收宽表 / 特征数据 JDBC → 核心案件 / 还款计划

4.7 催收业务数据读取实战案例

以下是一个完整的催收数据读取案例,整合了四种数据源,构建催收分析的基础数据集:

// 1. 读取 CSV:催收员每日外呼统计
val dailyCalls = spark.read
  .option("header", "true")
  .option("inferSchema", "true")
  .csv("/data/collection/daily_calls/")

// 2. 读取 JSON:客户风险画像
val riskProfile = spark.read
  .option("multiLine", "true")
  .json("/data/collection/risk_profile/")

// 3. 读取 Parquet:催收特征宽表
val features = spark.read
  .parquet("/data/warehouse/collection_features/")

// 4. 读取 JDBC:案件分配信息
val caseAssign = spark.read
  .format("jdbc")
  .option("url", "jdbc:postgresql://db-host:5432/collection")
  .option("dbtable", "case_assignment")
  .option("user", "etl_user")
  .option("password", "secure_pass")
  .option("partitionColumn", "assignment_id")
  .option("lowerBound", "1")
  .option("upperBound", "5000000")
  .option("numPartitions", "8")
  .load()

// 5. 数据关联:构建催收分析宽表
val analysisDF = dailyCalls
  .join(riskProfile, Seq("customer_id"), "left")
  .join(features, Seq("case_id"), "left")
  .join(caseAssign, Seq("case_id"), "left")

analysisDF.createOrReplaceTempView("collection_analysis")

// 6. 业务分析:统计各催收团队的高风险案件处理情况
spark.sql("""
  SELECT
    team_id,
    COUNT(DISTINCT case_id) AS total_cases,
    SUM(CASE WHEN risk_level = 'HIGH' THEN 1 ELSE 0 END) AS high_risk_cases,
    AVG(call_duration) AS avg_call_duration,
    SUM(CASE WHEN result = 'CONTACTED' THEN 1 ELSE 0 END) AS contacted_count
  FROM collection_analysis
  WHERE overdue_days > 30
  GROUP BY team_id
  ORDER BY high_risk_cases DESC
""").show(10)
实战要点: 在催收数据读取阶段,建议统一使用 spark.read 的链式调用风格,保持代码可读性。对于 JDBC 数据源,务必配置合理的分区策略,避免单节点压力过大。Parquet 和 CSV 文件建议按日期分区存储,便于增量读取和回溯。

4.8 常见问题与最佳实践

问题场景 原因分析 解决方案
CSV 读取时列数不匹配 数据中包含逗号、引号未转义 使用 option("quote", "\"") 和 option("escape", "\\")
JSON 解析失败 文件包含多行 JSON 但未开启 multiLine 设置 option("multiLine", "true")
Parquet Schema 冲突 不同分区的文件 Schema 不一致 设置 option("mergeSchema", "true")
JDBC 读取过慢 未配置分区列或 fetchSize 过小 设置 partitionColumn 和 fetchSize 为 10000 以上
数据倾斜导致 OOM 分区列分布不均匀 使用 where 条件预过滤或自定义分区策略
催收数据治理建议: 建议在催收数据湖中建立统一的元数据管理,记录每个数据源的 Schema 版本、分区策略和读取配置。这有助于在数据源变更时快速定位问题,保障催收作业的稳定性。