查看更多Apache Pig的教程请点击这里。
在大数据处理领域,JSON格式的数据非常常见,然而用Apache Pig读取JSON并正确取出其中的字段我觉得并不算方便(在某些情况下很容易写错),所以总结一下几个常见的JSON loader/UDF的用法。
假设有数据文件 1.txt,内容是一行JSON(为了简单,这里以一行为例):
{"items":[{"id":"111","name":"aaa","extra":{"k":"ttt","v":"uuu"}},{"id":"222","name":"bbb","extra":{"k":"rrr","v":"sss"}}]}
为了便于观看,这里把它格式化如下(注意,Pig是按行读取数据,这里格式化只是为了写这篇文章便于展示,实际存储在文件里仍然是一行):
{
"items": [
{
"id": "111",
"name": "aaa",
"extra": {
"k": "ttt",
"v": "uuu"
}
},
{
"id": "222",
"name": "bbb",
"extra": {
"k": "rrr",
"v": "sss"
}
}
]
}
我们的目的是正确解析这个JSON。
➤ 来自于Elephant Bird的 com.twitter.elephantbird.pig.piggybank.JsonStringToMap()
其实它不是一个JSON loader而是一个UDF,它的作用是把JSON字符串转成一个map。由于JSON结构中都是key:value形式的数据,所以把JSON字符串转成map之后就可以取出指定字段的内容。
这个UDF对于只有“一层”的JSON非常好用,例如(格式化后)长下面这个样子的JSON:
{
"id": "111",
"name": "aaa"
}
此时,可以像下面这样取到字段 id 及 name 的值:
DEFINE Json2Map com.twitter.elephantbird.pig.piggybank.JsonStringToMap();
A = LOAD '1.txt' AS (jsonStr: chararray);
B = FOREACH A GENERATE Json2Map(jsonStr) AS jsonObj;
C = FOREACH B GENERATE (chararray) jsonObj#'id' AS id, (chararray) jsonObj#'name' AS name;
DUMP C;
在这里,JSON被转成了一个map对象,用 #'id' 的方式可以取出名为 id 的key对应的value。
可见这个UDF在某些场合下用起来很方便,但对本文一开始举例的那种嵌套有多层的JSON就不好处理了。
文章来源:https://www.codelast.com/
➤ Pig内置的 JsonLoader
对付本文开头的那种JSON,我们可以使用Pig内置的 JsonLoader 来加载数据,使用它唯一的坏处就是需要定义JSON的结构:
A = LOAD '1.txt' USING JsonLoader('items: {(id:chararray, name:chararray, extra:(k:chararray, v:chararray))}');
B = FOREACH A GENERATE FLATTEN(items);
C = FOREACH B GENERATE id, name, FLATTEN(extra) AS (k, v);
D = FOREACH C GENERATE id, name, k, v;
DUMP D;
我们仔细看一下本文开头的JSON,它的 items 是一个数组,对应到Pig里就是一个bag(bag里的每个元素是一个tuple),因此用JsonLoader定义JSON的结构时,items 是 items: {()} 这样一种结构。
而JSON中的 extra 字段对应的并不是一个数组,所以它对应到Pig里就是一个tuple,因此在JsonLoader中定义结构时 extra 是 extra:(k:chararray, v:chararray) 这样的形式。
文章来源:https://www.codelast.com/
注意:JsonLoader看似好用,然而这里有一个巨坑——JsonLoader中定义的字段名和JSON字符串中的字段名并不对应,它只跟顺序相关!
例如,你代码这样写:
A = LOAD '1.txt' USING JsonLoader('items: {(id:chararray, invalidField:chararray, extra:(k:chararray, v:chararray))}');
B = FOREACH A GENERATE FLATTEN(items);
C = FOREACH B GENERATE id, invalidField, FLATTEN(extra) AS (k, v);
D = FOREACH C GENERATE id, invalidField, k, v;
DUMP D;
竟然能正确输出:
(111,aaa,ttt,uuu)(222,bbb,rrr,sss)
在上面的代码中,invalidField 是一个在JSON字符串中根本不存在的字段名,然而它却能被JsonLoader正确识别,那是因为JsonLoader是按字段的顺序来识别的,JSON字符串中的第2个字段 name,被JsonLoader映射到了 invalidField 这个字段上。
也正是因为这个原因,如果你的JSON字符串的各个字段顺序是不恒定的,就不能用这种方法来读取!
并且,在JsonLoader中定义JSON的结构时要非常小心,如果不小心写错了顺序,那么肯定会得到错误的值,例如,你这样写:
A = LOAD '1.txt' USING JsonLoader('items: {(name:chararray, id:chararray, extra:(k:chararray, v:chararray))}');
你以为JsonLoader会自动把JSON字符串中的 id 字段映射到你定义的第二个字段(id:chararray)上吗?根本不会!它会把它映射到 name:chararray 上,因为在JSON字符串中,id 字段是排在第1位的字段,而在JsonLoader中,name:chararray 也是排第1位的字段,因此它俩就映射上了。
所以,如果你有JSON数据的控制权,我建议在生成JSON数据的时候,固定生成字段的顺序。例如,如果是使用 Jackson的ObjectMapper生成JSON,那么可以通过 @JsonPropertyOrder 标注来控制JSON字段的顺序。
另外,如果你定义的结构中漏了一个字段,很可能会造成解析不成功,导致程序逻辑不能按设想的执行!
最后,这仅仅是一个嵌套层数非常少的JSON,如果层数非常多的话,会导致你代码很难写,这个时候还是用JAVA写Map-Reduce job去处理吧。
文章来源:https://www.codelast.com/
➤➤ 版权声明 ➤➤
转载需注明出处:codelast.com
感谢关注我的微信公众号(微信扫一扫):
以及我的微信视频号: