关于 Apache Spark 中大数据处理的某些方面,第 3 部分:如何处理格式错误的数据?

在我之前的文章中,我介绍了以模块化、可维护和可序列化的方式对 Spark 应用程序进行编程的设计模式。这次我演示了一种处理格式错误的日期/时间数据的解决方案,以及如何为格式错误的数据设置默认值。

当我从事大数据项目时,我的任务是从不同来源(Kafka、Hadoop 分布式文件系统、Apache Hive、Postgres、Oracle)加载不同格式(JSON、orc 等)的数据,然后转换数据,然后将数据保存到相同或不同的来源。最简单的任务是从单个数据源 (Postgres) 加载数据,然后将数据保存到另一个源 (Hive),无需任何转换。 

即使在这个最简单的情况下,也有很多格式错误的数据!特别是,格式错误的日期/时间数据让我们的团队花费了大量时间来处理。此外,通常有空值,有时还有空数据数组。因此,有必要采用紧凑且通用的解决方案来处理此类不规则行为。

这篇文章的组织如下:

  • 第 1 节描述了如何处理格式错误的日期/时间数据,
  • 第 2 节描述了一种简单的装饰器模式,用于为格式错误的数据分配默认值。

可以在此处找到完全可用的代码。我们走吧。

I. 格式错误的日期/时间数据。

就我而言,数据是从 Kafka 或 JSON 文件以 JSON 格式接收的。Json 格式的数据建立在两种结构之上:键值对的集合(对象)和值的有序列表(数组)。值可以是对象、数组、字符串、十进制数、布尔值或空值。因此,一段日期/时间数据通常以字符串形式出现。

Java 没有内置的 Date 类,但有一个java.time包可以处理日期和时间。该包包含LocalDateLocalTimeLocalDateTimeZonedDateTime等类来存储日期/时间数据和解析日期/时间字符串。此外,还有解析器,例如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

爪哇
1
公共 接口 IChainDT { 接口 IChainDT {
2
    LocalDate  getDateTime (字符串 输入);LocalDate  getDateTime (字符串 输入);
3
    日期 解析日期时间(字符串 输入);日期 解析日期时间(字符串 输入);
4
    无效 setNextChain ( IChainDT 元素);无效 setNextChain ( IChainDT 元素);
5
}

这里parseDateTime方法解析日期/时间字符串,getDateTime  将 Date 对象转换为更方便的 LocalDate 对象,并且setNextChain方法设置到另一个解析器的链接。添加转换器以演示解析器如何在返回日期之前使输出日期“更漂亮”。 

SimpleDateTimeParser类实现IChainDT接口:

爪哇
1
公共  SimpleDateTimeParser 实现 IChainDT {  SimpleDateTimeParser 实现 IChainDT {
2
     字符串 短日期时间模式;    字符串 短日期时间模式;   
3
        IChainDT  dateTimeParser  =  null ;    IChainDT  dateTimeParser  =  null ;
4
        日期 默认时间 =  日期( 0L );    日期 默认时间 =  日期( 0L );
5
        公共 SimpleDateTimeParser(字符串 模式){    公共 SimpleDateTimeParser(字符串 模式){
6
            shortDateTimePattern  = 模式;                shortDateTimePattern  = 模式;        
7
        }    }
8
        公共 SimpleDateTimeParser(字符串 模式,IChainDT  nextValidator){    公共 SimpleDateTimeParser(字符串 模式,IChainDT  nextValidator){
9
            (模式);        (模式);
10
            这个。dateTimeParser  =  nextValidator ;        这个。dateTimeParser  =  nextValidator ;
11
        }    }
12
        公共 LocalDate  getDateTime(字符串 json){    公共 LocalDate  getDateTime(字符串 json){
13
            日期 结果 = parseDateTime ( json );        日期 结果 = parseDateTime ( json );
14
            返回 结果。即时()。atZone ( ZoneId . systemDefault ())。toLocalDate ());        返回 结果。即时()。atZone ( ZoneId . systemDefault ())。toLocalDate ());
15
                }            }
16
        公共 无效 setNextChain(ICainDT 验证器){    公共 无效 setNextChain(ICainDT 验证器){
17
            这个。dateTimeParser  = 验证器;        这个。dateTimeParser  = 验证器;
18
        }    }
19
        公共 日期 parseDateTime(字符串 输入){    公共 日期 parseDateTime(字符串 输入){
20
            DateFormat  simpleDateFormatter = new  SimpleDateFormat ( shortDateTimePattern );        DateFormat  simpleDateFormatter = new  SimpleDateFormat ( shortDateTimePattern );
21
            试试{        试试{
22
                返回 simpleDateFormatter。解析(输入);                          返回 simpleDateFormatter。解析(输入);              
23
            }捕捉(异常 e){        }捕捉(异常 e){
24
                if ( this.dateTimeParser ! = null )返回这个_ 日期时间解析器。解析日期时间(输入);               if ( this.dateTimeParser ! = null )返回这个_ 日期时间解析器。解析日期时间(输入);   
25
                否则 返回 默认时间;            否则 返回 默认时间;
26
            }        }
27
        }    }
28
}

IChainDT dateTimeParser是对另一个解析器的引用,  String shortDateTimePattern是一个日期/时间模式字符串。另一个解析器引用可以通过双参数构造函数或通过 setter 设置setNextChain

注意该parseDateTime方法是如何工作的。首先,该方法创建一个  SimpleDateFormat具有特定模式的实例;我们需要将实例作为SimpleDateTimeParser  可序列化的局部变量(这篇文章解释了 Spark 如何序列化任务)。如果 simpleDateFormatter(具有指定模式)无法解析输入字符串,格式化程序会抛出异常。 

异常在 catch 块中被捕获。如果dateTimeParser链中有下一个,则dateTimeParser.parseDateTime(input)调用下一个。如果当前解析器是链中的最后一个,则返回最后一个解析器的默认值;该值可能为空。

最后,让我们看看这个解析器叫什么。

爪哇
1
<span style="color:#555555">@测试</span>
2
  公共 无效 解析器测试(){公共 无效 解析器测试(){
3
      字符串 pattern1  =  "yyyy-MM-dd" ;字符串 pattern1  =  "yyyy-MM-dd" ;
4
      字符串 pattern2  =  "yyyy.MM.dd" ;字符串 pattern2  =  "yyyy.MM.dd" ;
5
    
6
      IChainDT 验证器 1 =  new  SimpleDateTimeParser ( pattern1 );  IChainDT 验证器 1 =  new  SimpleDateTimeParser ( pattern1 );  
7
      IChainDT 验证器2  =  new  SimpleDateTimeParser ( pattern2 );IChainDT 验证器2  =  new  SimpleDateTimeParser ( pattern2 );
8
      验证器1。setNextChain (验证器2 );验证器1。setNextChain (验证器2 );
9
​​​
10
      字符串 testString  =  "2020-10-19" ;字符串 testString  =  "2020-10-19" ;
11
      LocalDate 结果 = 验证器1 。获取日期时间(测试字符串);     LocalDate 结果 = 验证器1 。获取日期时间(测试字符串);     
12
      assertEquals (结果.getYear (), 2020 ) ;assertEquals (结果.getYear (), 2020 ) ;
13
      
14
      testString  =  "2020.10.19" ;testString  =  "2020.10.19" ;
15
      结果 = 验证器1 。获取日期时间(测试字符串);结果 = 验证器1 。获取日期时间(测试字符串);
16
      assertEquals (结果.getYear (), 2020 ) ;assertEquals (结果.getYear (), 2020 ) ;
17
      
18
      testString = "2020 年 10 月 19 日" ;testString = "2020 年 10 月 19 日" ;
19
      结果 = 验证器1 。获取日期时间(测试字符串);结果 = 验证器1 。获取日期时间(测试字符串);
20
      assertEquals(结果。getYear(),1969 assertEquals(结果。getYear(),1969 
21
  }

首先,我们为每个模式字符串创建解析器。接下来,我们链接解析器。最后,我们在日期/时间字符串上调用链中的第一个解析器。如果没有解析器成功解析字符串,则返回链中最后一个解析器的默认LocalDate值(new Date(0L)在这种情况下)。 

这个解析器也可以通过一个抽象类来实现。在这种情况下,我们定义一个抽象类AChainDT而不是接口 IChainDT:

爪哇
1
公共 抽象  AChainDT {     抽象  AChainDT {    
2
    公共 AChainDT(字符串 shortDateTimePattern){         公共 AChainDT(字符串 shortDateTimePattern){         
3
        这个。短日期时间模式 = 短日期时间模式;           这个。短日期时间模式 = 短日期时间模式;       
4
    }}
5
    protected  AChainDT  nextParser = null ;protected  AChainDT  nextParser = null ;
6
    受保护的 字符串 shortDateTimePattern;    受保护的 字符串 shortDateTimePattern;    
7
    受保护的 日期 defaultTime  =  new  Date ( 0L );  受保护的 日期 defaultTime  =  new  Date ( 0L );  
8
    公共 无效 setNextParser ( AChainDT  nextParser ) {公共 无效 setNextParser ( AChainDT  nextParser ) {
9
        这个。下一个解析器 = 下一个解析器;    这个。下一个解析器 = 下一个解析器;
10
    }   }   
11
     公共 LocalDate  getDateTime(字符串 输入){ 公共 LocalDate  getDateTime(字符串 输入){
12
         日期 结果 = parseDateTime (输入);     日期 结果 = parseDateTime (输入);
13
            本地 日期本地日期 = 结果。即时()。atZone ( ZoneId . systemDefault ())。toLocalDate ();        本地 日期本地日期 = 结果。即时()。atZone ( ZoneId . systemDefault ())。toLocalDate ();
14
                返回 本地日期;            返回 本地日期;
15
    }}
16
     公共 抽象 日期 解析日期时间(字符串 输入); 公共 抽象 日期 解析日期时间(字符串 输入);
17
}

在这里,抽象类包含所有解析器的公共部分——另一个解析器、模式字符串和日期到 LocalDate 转换器。ConcreteHandler 现在看起来更简洁:

爪哇
1
公共  SimpleDateTimeParserA 扩展 AChainDT {      SimpleDateTimeParserA 扩展 AChainDT {    
2
    公共 SimpleDateTimeParserA (字符串 shortDateTimePattern ) {公共 SimpleDateTimeParserA (字符串 shortDateTimePattern ) {
3
        超级(短日期时间模式);            超级(短日期时间模式);        
4
    }}
5
    @覆盖@覆盖
6
    公共 日期 parseDateTime(字符串 输入){公共 日期 parseDateTime(字符串 输入){
7
        DateFormat  simpleDateFormatter = new  SimpleDateFormat ( shortDateTimePattern );    DateFormat  simpleDateFormatter = new  SimpleDateFormat ( shortDateTimePattern );
8
        试试{试试{
9
            日期 结果 =  simpleDateFormatter。解析(输入);    日期 结果 =  simpleDateFormatter。解析(输入);
10
​​​
11
            返回 结果;返回 结果;
12
            }捕捉(异常 e){捕捉(异常 e){
13
                if ( nextParser  !=  null )返回 nextParser。解析日期时间(输入);if ( nextParser  !=  null )返回 nextParser。解析日期时间(输入);
14
                否则 返回 默认时间;否则 返回 默认时间;
15
            }
16
    }}
17
}

同样,我们创建一个SimpleDateFormatter实例作为解析器可序列化的局部变量。这个解析器像以前一样运行,除了我们IChainDTAChainDTSimpleDateTimeParser替换SimpleDateTmeParserA。有关详细信息,请参阅代码

二、默认值装饰器。

正如我在介绍中提到的,许多 null 和空数组作为 JSON 字符串中的值出现。此外,有时当数据从一个数据库传输到另一个数据库时,需要转换数据类型,例如 Integer 到 BigDecimal。在所有这些情况下,都需要捕获和处理 NullPointerExceptions、ArrayIndexOutOfBondsExceptions 和其他异常。

一个常见的场景是当有一个功能接口作为 RDD 转换或操作的回调时。让我们装饰这样一个接口来捕获和处理异常。 

爪哇
1
进口 组织。阿帕奇_ 火花。接口。爪哇_ 功能。功能; 组织。阿帕奇_ 火花。接口。爪哇_ 功能。功能;
2
​​​
3
公共 接口 IExceptionDecoratorSpark { 接口 IExceptionDecoratorSpark {
4
     静态 <输入,输出> 函数<输入,输出> 过程(函数<输入,输出> 乐趣,输出 定义){ 静态 <输入,输出> 函数<输入,输出> 过程(函数<输入,输出> 乐趣,输出 定义){
5
            返回  函数<输入,输出>(){        返回  函数<输入,输出>(){
6
                @覆盖            @覆盖
7
                公共 输出 调用(输入 o){            公共 输出 调用(输入 o){
8
                    试试{                试试{
9
                        返回(输出)乐趣。呼叫( o );                    返回(输出)乐趣。呼叫( o );
10
                    }捕捉(NullPointerException  e){                }捕捉(NullPointerException  e){
11
                        返回 空值                    返回 空值
12
                    }捕捉(异常 e){                }捕捉(异常 e){
13
                        返回 定义;                    返回 定义;
14
                    }                }
15
                }            }
16
            };        };
17
        }    }
18
}

这里fun是一个实现Function接口的输入函数。这个函数的输入是一个Input类型对象,输出是一个Output类型对象。方法返回的接口process覆盖了一个call方法;在call方法内部fun被调用。如果有异常,它们会在 catch 块中被捕获,并def返回 null 或提供的默认值。正如在 Java 中一样,应该首先处理更具体的异常。

这个装饰器的调用方式如下: 

爪哇
1
<span style="color:#555555">@测试</span>
2
    public  void  basicProcessorSparkTest ()抛出 异常{public  void  basicProcessorSparkTest ()抛出 异常{
3
         定义= 10000.0  ;  定义= 10000.0  ; 
4
        双倍 应该 =  0.5 ;双倍 应该 =  0.5 ;
5
        函数<整数双精度>  fun  = ( x ) ->  1.0  /  x ;函数<整数双精度>  fun  = ( x ) ->  1.0  /  x ;
6
        函数<整数双精度>  outFun  =  IExceptionDecoratorSpark。过程(乐趣,定义);函数<整数双精度>  outFun  =  IExceptionDecoratorSpark。过程(乐趣,定义);
7
        双倍 结果 =  outFun。调用(2);双倍 结果 =  outFun。调用(2);
8
        assertEquals (结果, shouldBe );assertEquals (结果, shouldBe );
9
​​​
10
    }

在这种情况下,fun返回其输入的倒数。在这种情况下,乐趣经常起作用。 

另一方面,这里是抛出和处理异常的示例;结果返回提供的默认值: 

爪哇
1
 @测试@测试
2
    公共 无效 异常处理器SparkTest ()抛出 异常{公共 无效 异常处理器SparkTest ()抛出 异常{
3
        整数 def  =  10 ;整数 def  =  10 ;
4
        双倍 应该 =  0.5 ;双倍 应该 =  0.5 ;
5
        整数[]输入 =  整数[ 0 ];整数[]输入 =  整数[ 0 ];
6
        函数<整数[],整数>  fun  = ( x ) ->  x [ 1 ];函数<整数[],整数>  fun  = ( x ) ->  x [ 1 ];
7
        函数<整数[],整数>  outFun  =  IExceptionDecoratorSpark。过程(乐趣,定义);函数<整数[],整数>  outFun  =  IExceptionDecoratorSpark。过程(乐趣,定义);
8
        整数 结果 =  outFun。调用(输入);整数 结果 =  outFun。调用(输入);
9
        assertEquals (结果, def );assertEquals (结果, def );
10
    }

fun返回整数输入数组的第二个元素。如果这样的元素不存在,则返回提供的默认值。 

请注意,org.apache.spark.api.java.function.Function接口与java.util.function.Function. 前者必须实现一个call方法;以前的界面也是Serializable。后者必须实现一个 apply不一定可序列化的方法。java.util.function.Function如果我们将输入函数类型和call方法替换为方法,则所提出的方法也适用于接口apply。有关详细信息,请参阅代码。  

结论

在这篇文章中,我演示了如何处理格式错误的日期/时间数据以及如何创建默认值装饰器的可能方法。日期/时间处理器可以处理无法通过单个模板字符串解析的日期/时间字符串。装饰器为抛出的不同异常返回不同的默认值。希望这些技巧对你有所帮助。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值