一、问题背景
近期在处理数据的过程之中遇到了两个棘手的问题,由于在线系统的数据范式不固定,导致大量处理逻辑交由离线系统处理。诚然,最本质的原因来源于Amazon提供的API标准数据结构为异构数据,数据源会发生动态变化,数据定义十分复杂,变量数量总计达到数千个,且存在结构化不完整的问题。
具体说来,则是
1、ODPS库中JsonArray和JsonObject无法区分,在每一层展开时存在大量模糊匹配,增加了算法复杂度,让数据处理变成了面向extend_info的编程。
2、数据落库的形式不固定,存在大量的特殊情况。需要进行枚举,并在程序运行报错时单独检测错误原因,以覆盖错误用例。
3、由于Amazon许诺的返回值可能未返回,对于检验告警的DQC的校验规则实现困难,需要遍历所有字段进行全文匹配。
针对上述三类问题,虽然交给离线系统处理都能解决,但解析过程中上下游代码复用程度很高,就离线的视角来看待问题。三类问题显然交给在线的抓取系统更加合适,一步到位解决业务需求。
既然谈到数据处理,我们不妨来看看在线采集系统是如何处理Amazon返回的XML字段的:
@Override protected <T> List<T> parseResult(String result, Class<T> clazz) throws Exception { org.json.JSONObject xmlJSONObj = XML.toJSONObject(result); JSONObject jsonObject = JSONObject.parseObject(xmlJSONObj.toString()); String jsonString = JSON.toJSONString(JSONPath.eval(jsonObject, SELLER_PERFORMANCE_REPORT_PATH)); Object json = new JSONTokener(jsonString).nextValue(); if(json instanceof org.json.JSONObject){ T t = CreditBizUtils.convertJsonResponseToObject(jsonString, clazz, CreditBizConstants.EXTEND_INFO_FIELD); return Lists.newArrayList(t); }else{ JSONArray jsonArray = JSON.parseArray(jsonString); List<T> list = Lists.newArrayListWithCapacity(jsonArray.size()); for (Object obj : jsonArray) { T t = CreditBizUtils.convertJsonResponseToObject(JSON.toJSONString(obj), clazz, CreditBizConstants.EXTEND_INFO_FIELD); list.add(t); } return list; } }
在上述方法的头两行,就确定了xml在转化为Json时采用的是模糊推断机制。我们深入到org.json的源代码中可以发现这一段,下述代码的具体含义则是有重复的Key则自动转化为JSONArray。我们也不难看出,早在在线系统的处理中,作者就是采用尝试机制取处理Object和Array的,到了离线系统分离字段的时候,这样的操作又要来一遍。
二、解决方案:
2.1 解决方案一:修改XML解析源码
优点:改动代码量少。
缺点:需要完全理解解析流程。
下述代码描述了XML在解析到相同的tagName时的处理过程,对于新key,如果不存在,则默认为JsonObj,如果已经存在则将原JsonObj和新的Value一起转化为JsonArray。正是这种模糊推断式的方式而非指定格式,导致上游系统和下游系统都需要推断一遍。
public JSONObject accumulate( String key, Object value ) throws JSONException { testValidity(value); Object object = this.opt(key); if (object == null) { this.put(key, value instanceof JSONArray ? new JSONArray().put(value) : value); } else if (object instanceof JSONArray) { ((JSONArray)object).put(value); } else { this.put(key, new JSONArray().put(object).put(value)); } return this; }
对于源码最小的改动,在于在其JsonObj的基础上再重载一个新的方法,可以对你所要求的key,在第一步就指定为JsonArray.这样,我们只需要在转化开始时,初始化添加必须要转为JsonArray的对象到规则forcearray中即可。
public class JsonAcuumulate extends JSONObject{ public JSONObject accumulate( String key, Object value, ArrayList<String> forcearray ) throws JSONException { testValidity(value); Object object = this.opt(key); //特殊处理情况:指定初始化array if(object == null && forcearray.contains(key)){ this.put(key, new JSONArray().put(value)); } else if (object == null) { this.put(key, value instanceof JSONArray ? new JSONArray().put(value) : value); } else if (object instanceof JSONArray) { ((JSONArray)object).put(value); } else { this.put(key, new JSONArray().put(object).put(value)); } return this; } }
2.2 解决方案二:Antlr4 语法树/词法树解析
优点:对于代码有着全局性的把控。
缺点:测试案例覆盖有待完善。
采用更加复杂的解决方式解决并不是为了炫技。只有更加基础的复杂才能应对复杂。此处从文本的语法树着手,从字符串层层面自定义解析规则,以便应对后续更加复杂的xml、json、csv文件的解析,让解析层的带面变得更加复杂通用。应用层的代码才会相对变得更加简单。
Antlr4(Another Tool for Language Recognition)是一款基于Java开发的开源的语法分析器生成工具,能够根据语法规则文件生成对应的递归向下的语法分析器,广泛应用于DSL构建,语言词法语法解析等领域。在show code前,我们先介绍一组基本概念:
2.2.1 基本概念
基本概念 |
释义 |
语法分析 |
是指约束语言中的各个组成部分之间的关系的规则。 |
词法分析 |
将字符汇聚为单词或者符号的过程。 |
语法分析树 |
树的非叶子节点代表着规则(语法),而叶子结点代表了词法符号。 |
字符流数据解析成为语法树的过程如下所示:
在idle中也有对应的插件antlr4,在安装完成插件的对应功能后,我们可以使用它来检查我们编写的语法规则。对于antlr4的语法规则编写方法小明在此处不做过多的介绍,有兴趣的小伙伴可以参阅《antlr4权威指南》。
2.2.2 语法树的遍历
对于数据处理而言,词组->行为的集合构成了我们的语言类应用程序。在antlr4中,我们在编写完语法和词法的自定义代码之后,可以根据其自动生成的监听器(listener)对特定的规则进入和退出事件做出响应。此外,Antlr自动生成的还包括广为人知的访问者(Visitor)模式,从而在监听器的基础上更近一步,允许程序控制语法分析树的遍历过程。
接下来将展示一段在项目中真实用到的代码,在原始的xml解析的语法树上作如下修改:
1、添加规则:<'lzmmarktag'>' content '<''/'lzmmarktag'> 在所有解析规则前,由于语法树编写的间接左递归特性,使得在符合多个语法规则时,优先匹配第一个,此处我们对于自定义要生成JsonObj的tag单独拎出来处理。
2、定义句法:lzmmarktag : UserDefinedWords; 后者为正则表达式形式,此处我们可以简单进行全文匹配。
element : '<'lzmmarktag'>' content '<''/'lzmmarktag'>' | '<' Name attribute* '>' content '<' '/' Name '>' | '<' Name attribute* '/>' ;
3、定义遍历规则:对于XML-JSON的解析程序逻辑如下:
三、写在最后
上述讨论的两种方式均是来源于小明站在离线ODPS的owner视角给出的技术解决方案。对于文章中一开头提到的放在上游系统解析的过程,小明的觉得大家可以很好的参考这篇文章领域模型vs数据模型。借用文章UP主的原话:在具体落地的时候,我们可以采用COLA的架构思想,使用gateway作为数据对象(Data Object)和领域对象(Entity)之间的转义网关,其中,gateway除了转义的作用,还起到了防腐解耦的作用,解除了业务代码对底层数据(DO、DTO等)的直接依赖,从而提升系统的可维护性。