在我之前的文章中,我介绍了以模块化、可维护和可序列化的方式对 Spark 应用程序进行编程的设计模式。这次我演示了一种处理格式错误的日期/时间数据的解决方案,以及如何为格式错误的数据设置默认值。
当我从事大数据项目时,我的任务是从不同来源(Kafka、Hadoop 分布式文件系统、Apache Hive、Postgres、Oracle)加载不同格式(JSON、orc 等)的数据,然后转换数据,然后将数据保存到相同或不同的来源。最简单的任务是从单个数据源 (Postgres) 加载数据,然后将数据保存到另一个源 (Hive),无需任何转换。
即使在这个最简单的情况下,也有很多格式错误的数据!特别是,格式错误的日期/时间数据让我们的团队花费了大量时间来处理。此外,通常有空值,有时还有空数据数组。因此,有必要采用紧凑且通用的解决方案来处理此类不规则行为。
这篇文章的组织如下:
- 第 1 节描述了如何处理格式错误的日期/时间数据,
- 第 2 节描述了一种简单的装饰器模式,用于为格式错误的数据分配默认值。
可以在此处找到完全可用的代码。我们走吧。
I. 格式错误的日期/时间数据。
就我而言,数据是从 Kafka 或 JSON 文件以 JSON 格式接收的。Json 格式的数据建立在两种结构之上:键值对的集合(对象)和值的有序列表(数组)。值可以是对象、数组、字符串、十进制数、布尔值或空值。因此,一段日期/时间数据通常以字符串形式出现。
Java 没有内置的 Date 类,但有一个java.time
包可以处理日期和时间。该包包含LocalDate
、LocalTime
、LocalDateTime
、ZonedDateTime
等类来存储日期/时间数据和解析日期/时间字符串。此外,还有解析器,例如SimpleDateFormat 和DateTimeFormatter。这些解析器接受模式字符串(如“dd.MM.yyyy HH:mm”)和输入字符串(如“20.10.2020 12:30”),以从输入字符串中返回 DateTime 或 Date 对象。然后这些对象可以充当 Hibernate 中的字段@Entity
。看起来很简单,对吧?
抱歉不行。在许多大数据项目中,日期/时间数据有许多不同的格式。此外,我遇到了一些例子,当日期/时间数据字符串包含不同语言的子字符串时!因此,可能无法使用单一模式甚至单一解析工具来解析此类日期字符串。
为了解决这个问题,让我们回顾一下责任链设计模式。该模式由一个Handler
接口和ConcreteHandler
一个实现组成。一个 ConcreteHandler 引用另一个 ConcreteHandler;所有的 ConcreteHandlers 形成一个链表。最后一个 ConcreteHandler 引用 null。
在我们的例子中,这种模式的实现如下。我们的Handler
接口被称为IChainDT
:
公共 接口 IChainDT {
接口 IChainDT {
LocalDate getDateTime (字符串 输入);
LocalDate getDateTime (字符串 输入);
日期 解析日期时间(字符串 输入);
日期 解析日期时间(字符串 输入);
无效 setNextChain ( IChainDT 元素);
无效 setNextChain ( IChainDT 元素);
}
这里parseDateTime
方法解析日期/时间字符串,getDateTime
将 Date 对象转换为更方便的 LocalDate 对象,并且setNextChain
方法设置到另一个解析器的链接。添加转换器以演示解析器如何在返回日期之前使输出日期“更漂亮”。
SimpleDateTimeParser
类实现IChainDT
接口:
公共 类 SimpleDateTimeParser 实现 IChainDT {
类 SimpleDateTimeParser 实现 IChainDT {
字符串 短日期时间模式;
字符串 短日期时间模式;
IChainDT dateTimeParser = null ;
IChainDT dateTimeParser = null ;
日期 默认时间 = 新 日期( 0L );
日期 默认时间 = 新 日期( 0L );
公共 SimpleDateTimeParser(字符串 模式){
公共 SimpleDateTimeParser(字符串 模式){
shortDateTimePattern = 模式;
shortDateTimePattern = 模式;
}
}
公共 SimpleDateTimeParser(字符串 模式,IChainDT nextValidator){
公共 SimpleDateTimeParser(字符串 模式,IChainDT nextValidator){
这(模式);
这(模式);
这个。dateTimeParser = nextValidator ;
这个。dateTimeParser = nextValidator ;
}
}
公共 LocalDate getDateTime(字符串 json){
公共 LocalDate getDateTime(字符串 json){
日期 结果 = parseDateTime ( json );
日期 结果 = parseDateTime ( json );
返回 结果。即时()。atZone ( ZoneId . systemDefault ())。toLocalDate ());
返回 结果。即时()。atZone ( ZoneId . systemDefault ())。toLocalDate ());
}
}
公共 无效 setNextChain(ICainDT 验证器){
公共 无效 setNextChain(ICainDT 验证器){
这个。dateTimeParser = 验证器;
这个。dateTimeParser = 验证器;
}
}
公共 日期 parseDateTime(字符串 输入){
公共 日期 parseDateTime(字符串 输入){
DateFormat simpleDateFormatter = new SimpleDateFormat ( shortDateTimePattern );
DateFormat simpleDateFormatter = new SimpleDateFormat ( shortDateTimePattern );
试试{
试试{
返回 simpleDateFormatter。解析(输入);
返回 simpleDateFormatter。解析(输入);
}捕捉(异常 e){
}捕捉(异常 e){
if ( this.dateTimeParser ! = null )返回这个。_ 日期时间解析器。解析日期时间(输入);
if ( this.dateTimeParser ! = null )返回这个。_ 日期时间解析器。解析日期时间(输入);
否则 返回 默认时间;
否则 返回 默认时间;
}
}
}
}
}
这IChainDT dateTimeParser
是对另一个解析器的引用, String shortDateTimePattern
是一个日期/时间模式字符串。另一个解析器引用可以通过双参数构造函数或通过 setter 设置setNextChain
。
注意该parseDateTime
方法是如何工作的。首先,该方法创建一个 SimpleDateFormat
具有特定模式的实例;我们需要将实例作为SimpleDateTimeParser
可序列化的局部变量(这篇文章解释了 Spark 如何序列化任务)。如果 simpleDateFormatter(具有指定模式)无法解析输入字符串,格式化程序会抛出异常。
异常在 catch 块中被捕获。如果dateTimeParser
链中有下一个,则dateTimeParser.parseDateTime(input)
调用下一个。如果当前解析器是链中的最后一个,则返回最后一个解析器的默认值;该值可能为空。
最后,让我们看看这个解析器叫什么。
<span style="color:#555555">@测试</span>
公共 无效 解析器测试(){
公共 无效 解析器测试(){
字符串 pattern1 = "yyyy-MM-dd" ;
字符串 pattern1 = "yyyy-MM-dd" ;
字符串 pattern2 = "yyyy.MM.dd" ;
字符串 pattern2 = "yyyy.MM.dd" ;
IChainDT 验证器 1 = new SimpleDateTimeParser ( pattern1 );
IChainDT 验证器 1 = new SimpleDateTimeParser ( pattern1 );
IChainDT 验证器2 = new SimpleDateTimeParser ( pattern2 );
IChainDT 验证器2 = new SimpleDateTimeParser ( pattern2 );
验证器1。setNextChain (验证器2 );
验证器1。setNextChain (验证器2 );
字符串 testString = "2020-10-19" ;
字符串 testString = "2020-10-19" ;
LocalDate 结果 = 验证器1 。获取日期时间(测试字符串);
LocalDate 结果 = 验证器1 。获取日期时间(测试字符串);
assertEquals (结果.getYear (), 2020 ) ;
assertEquals (结果.getYear (), 2020 ) ;
testString = "2020.10.19" ;
testString = "2020.10.19" ;
结果 = 验证器1 。获取日期时间(测试字符串);
结果 = 验证器1 。获取日期时间(测试字符串);
assertEquals (结果.getYear (), 2020 ) ;
assertEquals (结果.getYear (), 2020 ) ;
testString = "2020 年 10 月 19 日" ;
testString = "2020 年 10 月 19 日" ;
结果 = 验证器1 。获取日期时间(测试字符串);
结果 = 验证器1 。获取日期时间(测试字符串);
assertEquals(结果。getYear(),1969 );
assertEquals(结果。getYear(),1969 );
}
首先,我们为每个模式字符串创建解析器。接下来,我们链接解析器。最后,我们在日期/时间字符串上调用链中的第一个解析器。如果没有解析器成功解析字符串,则返回链中最后一个解析器的默认LocalDate
值(new Date(0L)
在这种情况下)。
这个解析器也可以通过一个抽象类来实现。在这种情况下,我们定义一个抽象类AChainDT
而不是接口 IChainDT:
公共 抽象 类 AChainDT {
抽象 类 AChainDT {
公共 AChainDT(字符串 shortDateTimePattern){
公共 AChainDT(字符串 shortDateTimePattern){
这个。短日期时间模式 = 短日期时间模式;
这个。短日期时间模式 = 短日期时间模式;
}
}
protected AChainDT nextParser = null ;
protected AChainDT nextParser = null ;
受保护的 字符串 shortDateTimePattern;
受保护的 字符串 shortDateTimePattern;
受保护的 日期 defaultTime = new Date ( 0L );
受保护的 日期 defaultTime = new Date ( 0L );
公共 无效 setNextParser ( AChainDT nextParser ) {
公共 无效 setNextParser ( AChainDT nextParser ) {
这个。下一个解析器 = 下一个解析器;
这个。下一个解析器 = 下一个解析器;
}
}
公共 LocalDate getDateTime(字符串 输入){
公共 LocalDate getDateTime(字符串 输入){
日期 结果 = parseDateTime (输入);
日期 结果 = parseDateTime (输入);
本地 日期本地日期 = 结果。即时()。atZone ( ZoneId . systemDefault ())。toLocalDate ();
本地 日期本地日期 = 结果。即时()。atZone ( ZoneId . systemDefault ())。toLocalDate ();
返回 本地日期;
返回 本地日期;
}
}
公共 抽象 日期 解析日期时间(字符串 输入);
公共 抽象 日期 解析日期时间(字符串 输入);
}
在这里,抽象类包含所有解析器的公共部分——另一个解析器、模式字符串和日期到 LocalDate 转换器。ConcreteHandler 现在看起来更简洁:
公共 类 SimpleDateTimeParserA 扩展 AChainDT {
类 SimpleDateTimeParserA 扩展 AChainDT {
公共 SimpleDateTimeParserA (字符串 shortDateTimePattern ) {
公共 SimpleDateTimeParserA (字符串 shortDateTimePattern ) {
超级(短日期时间模式);
超级(短日期时间模式);
}
}
@覆盖
@覆盖
公共 日期 parseDateTime(字符串 输入){
公共 日期 parseDateTime(字符串 输入){
DateFormat simpleDateFormatter = new SimpleDateFormat ( shortDateTimePattern );
DateFormat simpleDateFormatter = new SimpleDateFormat ( shortDateTimePattern );
试试{
试试{
日期 结果 = simpleDateFormatter。解析(输入);
日期 结果 = simpleDateFormatter。解析(输入);
返回 结果;
返回 结果;
}捕捉(异常 e){
捕捉(异常 e){
if ( nextParser != null )返回 nextParser。解析日期时间(输入);
if ( nextParser != null )返回 nextParser。解析日期时间(输入);
否则 返回 默认时间;
否则 返回 默认时间;
}
}
}
}
同样,我们创建一个SimpleDateFormatter
实例作为解析器可序列化的局部变量。这个解析器像以前一样运行,除了我们IChainDT
用AChainDT
和SimpleDateTimeParser
替换SimpleDateTmeParserA
。有关详细信息,请参阅代码。
二、默认值装饰器。
正如我在介绍中提到的,许多 null 和空数组作为 JSON 字符串中的值出现。此外,有时当数据从一个数据库传输到另一个数据库时,需要转换数据类型,例如 Integer 到 BigDecimal。在所有这些情况下,都需要捕获和处理 NullPointerExceptions、ArrayIndexOutOfBondsExceptions 和其他异常。
一个常见的场景是当有一个功能接口作为 RDD 转换或操作的回调时。让我们装饰这样一个接口来捕获和处理异常。
进口 组织。阿帕奇_ 火花。接口。爪哇_ 功能。功能;
组织。阿帕奇_ 火花。接口。爪哇_ 功能。功能;
公共 接口 IExceptionDecoratorSpark {
接口 IExceptionDecoratorSpark {
静态 <输入,输出> 函数<输入,输出> 过程(函数<输入,输出> 乐趣,输出 定义){
静态 <输入,输出> 函数<输入,输出> 过程(函数<输入,输出> 乐趣,输出 定义){
返回 新 函数<输入,输出>(){
返回 新 函数<输入,输出>(){
@覆盖
@覆盖
公共 输出 调用(输入 o){
公共 输出 调用(输入 o){
试试{
试试{
返回(输出)乐趣。呼叫( o );
返回(输出)乐趣。呼叫( o );
}捕捉(NullPointerException e){
}捕捉(NullPointerException e){
返回 空值;
返回 空值;
}捕捉(异常 e){
}捕捉(异常 e){
返回 定义;
返回 定义;
}
}
}
}
};
};
}
}
}
这里fun
是一个实现Function
接口的输入函数。这个函数的输入是一个Input
类型对象,输出是一个Output
类型对象。方法返回的接口process
覆盖了一个call
方法;在call
方法内部fun
被调用。如果有异常,它们会在 catch 块中被捕获,并def
返回 null 或提供的默认值。正如在 Java 中一样,应该首先处理更具体的异常。
这个装饰器的调用方式如下:
<span style="color:#555555">@测试</span>
public void basicProcessorSparkTest ()抛出 异常{
public void basicProcessorSparkTest ()抛出 异常{
双 定义= 10000.0 ;
双 定义= 10000.0 ;
双倍 应该 = 0.5 ;
双倍 应该 = 0.5 ;
函数<整数,双精度> fun = ( x ) -> 1.0 / x ;
函数<整数,双精度> fun = ( x ) -> 1.0 / x ;
函数<整数,双精度> outFun = IExceptionDecoratorSpark。过程(乐趣,定义);
函数<整数,双精度> outFun = IExceptionDecoratorSpark。过程(乐趣,定义);
双倍 结果 = outFun。调用(2);
双倍 结果 = outFun。调用(2);
assertEquals (结果, shouldBe );
assertEquals (结果, shouldBe );
}
在这种情况下,fun
返回其输入的倒数。在这种情况下,乐趣经常起作用。
另一方面,这里是抛出和处理异常的示例;结果返回提供的默认值:
@测试
@测试
公共 无效 异常处理器SparkTest ()抛出 异常{
公共 无效 异常处理器SparkTest ()抛出 异常{
整数 def = 10 ;
整数 def = 10 ;
双倍 应该 = 0.5 ;
双倍 应该 = 0.5 ;
整数[]输入 = 新 整数[ 0 ];
整数[]输入 = 新 整数[ 0 ];
函数<整数[],整数> fun = ( x ) -> x [ 1 ];
函数<整数[],整数> fun = ( x ) -> x [ 1 ];
函数<整数[],整数> outFun = IExceptionDecoratorSpark。过程(乐趣,定义);
函数<整数[],整数> outFun = IExceptionDecoratorSpark。过程(乐趣,定义);
整数 结果 = outFun。调用(输入);
整数 结果 = outFun。调用(输入);
assertEquals (结果, def );
assertEquals (结果, def );
}
fun
返回整数输入数组的第二个元素。如果这样的元素不存在,则返回提供的默认值。
请注意,org.apache.spark.api.java.function.Function
接口与java.util.function.Function
. 前者必须实现一个call
方法;以前的界面也是Serializable
。后者必须实现一个 apply
不一定可序列化的方法。java.util.function.Function
如果我们将输入函数类型和call
方法替换为方法,则所提出的方法也适用于接口apply
。有关详细信息,请参阅代码。
结论
在这篇文章中,我演示了如何处理格式错误的日期/时间数据以及如何创建默认值装饰器的可能方法。日期/时间处理器可以处理无法通过单个模板字符串解析的日期/时间字符串。装饰器为抛出的不同异常返回不同的默认值。希望这些技巧对你有所帮助。