4. 数据格式校验与转换:日期格式统一、数字格式校验、字符串清洗
数据格式问题,说白了就是脏数据的「重灾区」。我做了这么多年流处理,见过最离谱的一次——上游系统把日期字段传成了「2023/02/29」,你想想看,2月哪来的29号?还有数字字段里混进字母「O」冒充数字「0」的。这一节,我们就专门来收拾这些格式上的妖魔鬼怪。
核心原则:数据格式校验必须在源头做,越早发现,损失越小。等数据流到下游计算节点再报错,恢复成本至少翻10倍。
4.1 日期格式统一:从「百家争鸣」到「大一统」
日期格式不统一,是实时数仓里最常见的坑。有的系统用 yyyy-MM-dd HH:mm:ss,有的用 yyyy/MM/dd,还有的直接甩一个时间戳过来。我建议,所有日期字段进入 Flink 后,第一时间统一成 yyyy-MM-dd HH:mm:ss 格式,或者直接转成 Timestamp 类型。
下面是我常用的日期清洗 MapFunction:
public class DateCleanMapFunction implements MapFunction<String, String> {
// 定义多种可能的日期格式
private static final DateTimeFormatter[] FORMATTERS = {
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
DateTimeFormatter.ofFormatter("yyyy/MM/dd HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyyMMdd"),
DateTimeFormatter.ISO_LOCAL_DATE_TIME
};
@Override
public String map(String value) throws Exception {
String[] fields = value.split(",");
String rawDate = fields[2]; // 假设日期在第3个字段
// 尝试解析
for (DateTimeFormatter fmt : FORMATTERS) {
try {
LocalDateTime parsed = LocalDateTime.parse(rawDate, fmt);
// 统一输出格式
fields[2] = parsed.format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
);
return String.join(",", fields);
} catch (Exception ignored) {
// 继续尝试下一种格式
}
}
// 所有格式都解析失败,标记为脏数据
throw new RuntimeException("无法解析的日期格式: " + rawDate);
}
}
我的经验:不要只写一种格式就完事。我在项目中遇到过上游系统突然改了日期格式,结果整个作业全挂。多写几个格式兜底,能省去半夜爬起来修作业的痛苦。
4.2 数字格式校验:别让「字母」混进数字里
数字字段的脏数据,常见的有三种:
- 空字符串——字段是空的,但又不是 null
- 非数字字符——比如金额字段里出现了「$」或「,」
- 精度溢出——比如数据库字段是 Decimal(10,2),结果传了个 999999999.999
我一般会写一个通用的数字校验工具:
public class NumberValidator {
public static Double validateAndParse(String raw, String fieldName) {
if (raw == null || raw.trim().isEmpty()) {
throw new IllegalArgumentException(
fieldName + " 字段为空,无法解析为数字"
);
}
// 先清洗常见干扰字符
String cleaned = raw.trim()
.replace(",", "") // 去掉千分位逗号
.replace("$", "") // 去掉货币符号
.replace(" ", ""); // 去掉空格
try {
Double value = Double.parseDouble(cleaned);
// 检查精度:保留两位小数
BigDecimal bd = BigDecimal.valueOf(value);
if (bd.scale() > 2) {
throw new IllegalArgumentException(
fieldName + " 精度超出限制: " + raw
);
}
return value;
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
fieldName + " 包含非数字字符: " + raw
);
}
}
}
避坑指南:我曾经遇到过一个线上事故——某个金额字段传了「1,234.56」,直接 Double.parseDouble 就报错了。后来我才发现,上游系统用了英文逗号做千分位分隔。所以,数字清洗一定要先做 replace(",", "")。
4.3 字符串清洗:trim、replace 与正则
字符串字段的脏数据,说白了就是「不该有的东西太多」。常见的包括:
- 前后空格、制表符、换行符
- 不可见字符(比如 ASCII 码 0-31 的控制字符)
- 全角/半角混用
- 敏感词或特殊符号
我习惯在 MapFunction 里做一套「字符串三连」:
public class StringCleanMapFunction implements MapFunction<String, String> {
@Override
public String map(String value) throws Exception {
// 第一步:trim 掉前后空白
String cleaned = value.trim();
// 第二步:替换不可见字符
cleaned = cleaned.replaceAll("[\\x00-\\x1f]", "");
// 第三步:全角转半角(中文场景常见)
cleaned = fullWidthToHalfWidth(cleaned);
// 第四步:去除重复空格
cleaned = cleaned.replaceAll("\\s+", " ");
return cleaned;
}
private String fullWidthToHalfWidth(String input) {
StringBuilder sb = new StringBuilder(input);
for (int i = 0; i < sb.length(); i++) {
char c = sb.charAt(i);
if (c >= '!' && c <= '~') {
sb.setCharAt(i, (char) (c - 0xFEE0));
}
}
return sb.toString();
}
}
小技巧:字符串清洗的顺序很重要。先 trim,再替换不可见字符,最后做全角转半角。顺序搞反了,可能会把有效字符也误删掉。
4.4 使用 MapFunction 进行格式转换:实战整合
上面我们分别讲了日期、数字、字符串的清洗。但在实际项目中,这些操作往往是组合在一起的。我通常会写一个统一的 DataCleanMapFunction,把所有的格式校验和转换逻辑封装进去。
下面是一个完整的实战示例:
public class DataCleanMapFunction
implements MapFunction<String, CleanedRecord> {
@Override
public CleanedRecord map(String line) throws Exception {
String[] fields = line.split(",", -1); // 保留空字段
// 字段索引:0=用户ID, 1=姓名, 2=日期, 3=金额, 4=手机号
// 1. 字符串清洗:姓名
String name = fields[1].trim()
.replaceAll("[\\x00-\\x1f]", "")
.replaceAll("\\s+", " ");
// 2. 日期统一
String dateStr = fields[2].trim();
LocalDateTime date = parseDate(dateStr);
String formattedDate = date.format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
);
// 3. 数字校验:金额
Double amount = NumberValidator.validateAndParse(fields[3], "金额");
// 4. 手机号格式校验(简单示例)
String phone = fields[4].trim();
if (!phone.matches("^1[3-9]\\d{9}$")) {
throw new RuntimeException("手机号格式错误: " + phone);
}
return new CleanedRecord(
fields[0], name, formattedDate, amount, phone
);
}
private LocalDateTime parseDate(String raw) {
// 多种格式尝试解析(代码略,参考4.1节)
}
}
关键点:MapFunction 是 Flink 中最轻量级的算子,适合做逐条数据的格式转换。但要注意——不要在 MapFunction 里做需要上下文状态的操作,比如计数、去重。那些事留给 RichMapFunction 或 KeyedProcessFunction。
4.5 本章知识体系总览
下面这张图,帮你把数据格式校验与转换的完整流程串起来:
4.6 避坑总结:我踩过的三个坑
| 坑位 | 问题描述 | 解决方案 |
|---|---|---|
| 日期格式硬编码 | 只写了一种日期格式,上游一改就挂 | 用格式数组兜底,尝试多种解析 |
| 数字千分位逗号 | 没做 replace(",", ""),Double.parseDouble 报错 | 先清洗干扰字符,再解析数字 |
| 不可见字符漏处理 | 字符串里藏了控制字符,下游 join 不上 | 用正则 [\\x00-\\x1f] 全部干掉 |
最后提醒一句:数据格式校验不是「一次性」的工作。业务系统随时可能改格式、加字段、换编码。我建议把格式校验逻辑做成可配置的,用配置文件或参数表来管理,别写死在代码里。这样下次上游改格式,你改个配置就能搞定,不用重新上线作业。