如题,磨蹭了好几天总算把这个需求整明白了,写篇笔记整理一下自己的思路,也希望能给大家带来帮助。
第一次看到json日志数据的我内心是崩溃的,但是不用担心,json日志每一条记录的存储都是遵循一定的结构规则,只要你是从生产化的hdfs上获取数据,相信也是这样的。
一上来就直接整代码不是一种良好的学习方式,因此在正式讲解如何将这种日志数据结构化之前,要先理解两种spark中的数据结构:RDD和DataFrame
由上图,明显DataFrame的可读性比RDD好很多,RDD的结构是若干条Row,每一条Row是一条记录,而DataFrame就很接近我们平时能够见到的各种表格了,并且DataFrame还能够输出每个字段的结构与属性信息,这些都包含在它的scheme中。
在性能方面,DataFrame属于RDD的进化版,按照我目前的理解(参考其他人的帖子,还没看过源码,以后争取),性能比RDD更好, 具体好在哪,请读者自行百度,我就不复制粘贴别人的原创文字了。
了解了这些以后,选择dataframe结构作为数据结构化的储存形式,这里先推荐一个帖子,把dataframe的各项操作都说的很细了,当然,首推官方API。
DataFrame官方API:http://spark.apache.org/docs/2.1.0/api/python/pyspark.sql.html#pyspark.sql.DataFrame
推荐帖子:https://blog.csdn.net/sinat_26917383/article/details/80500349
开始结构化部分,结构化方案:
'''
日志文件结构化的步骤
1、读取原始日志
2、抽取字段、修改列名、规范json的取值
3、生成log_in和log_out
4、log_in join log_out
5、输出
'''
- 读取原始日志,这里建议先了解一下json格式的读取和解析,因为上面看到的原始日志,就是妥妥的json格式文件
#读取日志文件
log_data = spark.read.format("json").load("E:/Asiainfo/Hadoop/a01")
#打印读取形成的数据类型
print("type of jsons",type(log_data))
output:
type of jsons <class 'pyspark.sql.dataframe.DataFrame'>
#打印数据的目录树
log_data.printSchema()
output:
root
|-- fields.Call_id: string (nullable = true)
|-- fields.Class_name: string (nullable = true)
|-- fields.Client_ip: string (nullable = true)
|-- fields.Timestamp: string (nullable = true)
|-- fields.Trace_id: string (nullable = true)
|-- fields.in_param: struct (nullable = true)
| |-- ROOT.BODY.ACT_ID: string (nullable = true)
| |-- ROOT.BODY.APP_ID: string (nullable = true)
| |-- ROOT.BODY.APP_NAME: string (nullable = true)
| |-- ROOT.BODY.ATTR_ID: string (nullable = true)
| |-- ROOT.BODY.TRANSPRCID: string (nullable = true)
| |-- ROOT.BODY.TYPE_CODE: string (nullable = true)
| |-- ROOT.BODY.UPDATE_ACCEPT: string (nullable = true)
|-- fields.jcfParam: string (nullable = true)
|-- fields.redundant_field: string (nullable = true)
我的文件目录树非常长,我删掉了一部分,所以你接下来如果看到我提取的字段不在上面的目录树中,不要惊讶,这只是目录树的一部分。
- 抽取字段、修改列名、规范json的取值
这个时候你已经可以看到字段结构化的显示了
#使用select()函数
log_data.select("`fields.Login_no`","`fields.Timestamp`","`fields.Op_code`","`fields.tid`","`fields.jcfParam`","`fields.Trace_id`","`ulmp.inkey`").show(20)
output:
+---------------+--------------------+--------------+-----------+---------------+--------------------+----------+
|fields.Login_no| fields.Timestamp|fields.Op_code| fields.tid|fields.jcfParam| fields.Trace_id|ulmp.inkey|
+---------------+--------------------+--------------+-----------+---------------+--------------------+----------+
| A1BXHD001|2019-09-23 16:54:...| J702| 662425848| pout|11*20190923164551...| a01|
| B0A030339|2019-09-23 16:54:...| | 850454376| pout|11*20190923165437...| a01|
| A8AZYX004|2019-09-23 16:54:...| 5730| -531230181| pout|11*20190923165609...| a01|
| M5BMEG001|2019-09-23 16:55:...| | 1647345168| pin| | a01|
| N0BZFW001|2019-09-23 16:55:...| 1000| 398358364| pin|11*20190923165203...| a01|
| N0BZFW001|2019-09-23 16:55:...| 1000| 398358364| pout|11*20190923165203...| a01|
| ASBDHX002|2019-09-23 16:55:...| 1000| 1765858292| pout|11*20190923164948...| a01|
| M5BMEG001|2019-09-23 16:55:...| 4317|-2140972746| pout|11*20190923165459...| a01|
| M5BMEG001|2019-09-23 16:55:...| 4317| -384410219| pout|11*20190923165459...| a01|
| M5BMEG001|2019-09-23 16:55:...| 4317| -384410219| pout|11*20190923165459...| a01|
| K3BEJ0001|2019-09-23 16:55:...| 1000|-1000205510| pin|11*20190923165310...| a01|
| M5BMEG001|2019-09-23 16:55:...| 4317| 1591331815| pin|11*20190923165459...| a01|
| M5BMEG001|2019-09-23 16:55:...| 4317| 1092476648| pout|11*20190923165459...| a01|
| ASAND0001|2019-09-23 16:55:...| | 778707156| pin|11*20190923144609...| a01|
| N0BZFW001|2019-09-23 16:55:...| 1000| 1331934291| pin|11*20190923165203...| a01|
| M3BBEE502|2019-09-23 16:55:...| 1487|-2086653415| pout|11*20190923164732...| a01|
| N0BZFW001|2019-09-23 16:55:...| 1000| 1480993097| pin|11*20190923165203...| a01|
| A1BXHD001|2019-09-23 16:55:...| | 147034812| pin|11*20190923164551...| a01|
| ASAND0001|2019-09-23 16:55:...| 4317|-1396355690| pout|11*20190923170313...| a01|
| C0B118002|2019-09-23 16:55:...| 8089| -88052277| pout|11*20190923165502...| a01|
+---------------+--------------------+--------------+-----------+---------------+--------------------+----------+
only showing top 20 rows
你已经知道了你要选择哪些字段以后,抽出这些字段,形成一个新的dataframe:
'''
.alias()修改列名的方法
.substring_index()截取某字段中的一部分的方法
'''
log_structured=log_data.select(log_data["`fields.Login_no`"].alias("login_no"),
log_data["`fields.Timestamp`"].alias("time"),
log_data["`fields.Op_code`"].alias("op_code"),
log_data["`fields.SpanName`"].alias("service_name"),
log_data["`ulmp.inkey`"].alias("source"),
log_data["`fields.Trace_id`"].alias("trace_id"),
log_data["`fields.jcfParam`"].alias("param_type"),
log_data["`fields.tid`"].alias("tid"),
#使用substring_index函数抽取log.message的字段
substring_index(substring_index(log_data["`log.message`"],'~!~', -1),'~$~',1).alias("service_json"),
)
这里详细说明一下抽取字段时的一些问题:
【问题一】:如果路径名中带有点”.”的话,如果直接使用点的话,会报错。从报错里看你可能会疑惑,明明里面有为什么报取不出来,比如我这边取列的时候就遇到了这个问题,我折腾了好久才定位到原因;
【解决方案】:在路径名中带有点”.”的情况下,我们要使用反引号”`”将一个完整名字包裹起来,让Spark SQL认为这是一个完整的整体而不是两层路径,就像我上面的代码;
这里有一篇博客把这个问题讲得很仔细,贴出供参考:https://blog.csdn.net/wang_wbq/article/details/79675522
【问题二】:有一个值很长的字段,你只想取其一部分,怎么做呢?
【解决方案】:使用substring_index函数,先用分隔符分开,再取分隔符前后的值,有点类似于Python中的split("")[]函数,官方API展示了这个函数的使用方法:
>>> df = spark.createDataFrame([('a.b.c.d',)], ['s'])
>>> df.select(substring_index(df.s, '.', 2).alias('s')).collect()
[Row(s=u'a.b')]
>>> df.select(substring_index(df.s, '.', -3).alias('s')).collect()
[Row(s=u'b.c.d')]
看一下我们抽取字段后形成的数据
log_structured.show(5)
output
+---------+--------------------+-------+------------+------+--------------------+----------+----------+--------------------+
| login_no| time|op_code|service_name|source| trace_id|param_type| tid| service_json|
+---------+--------------------+-------+------------+------+--------------------+----------+----------+--------------------+
|A1BXHD001|2019-09-23 16:54:...| J702| sICertScan| a01|11*20190923164551...| pout| 662425848|{"ROOT":{"RETURN_...|
|B0A030339|2019-09-23 16:54:...| | sGBM_authen| a01|11*20190923165437...| pout| 850454376|{"ROOT":{"RETURN_...|
|A8AZYX004|2019-09-23 16:54:...| 5730| sDynSvc| a01|11*20190923165609...| pout|-531230181|{"ROOT":{"RETURN_...|
|M5BMEG001|2019-09-23 16:55:...| | sGBM_authen| a01| | pin|1647345168|{"ROOT":{"REGION_...|
|N0BZFW001|2019-09-23 16:55:...| 1000| sPwdNotice| a01|11*20190923165203...| pin| 398358364|{"ROOT":{"REQUEST...|
+---------+--------------------+-------+------------+------+--------------------+----------+----------+--------------------+
only showing top 5 rows
接下来我们需要根据param_type这一列的值将数据分成log_out和log_in,这里使用filter()方法。
'''
filter()过滤取值
withColumnRenamed()对取出的数据修改列名
'''
log_in=log_structured.filter(log_structured.param_type == "pin").withColumnRenamed("time", "time_in").withColumnRenamed("service_json", "json_in")
log_out=log_structured.filter(log_structured.param_type == "pout").withColumnRenamed("time", "time_out").withColumnRenamed("service_json", "json_out")
print("length of RDD:log_in",log_in.count())
print("RDD:log_in",log_in.show(5))
print("length of RDD:log_out",log_out.count())
print("RDD:log_out",log_out.show(5))
output:
length of RDD:log_in 47
+---------+--------------------+-------+---------------+------+--------------------+----------+-----------+--------------------+
| login_no| time_in|op_code| service_name|source| trace_id|param_type| tid| json_in|
+---------+--------------------+-------+---------------+------+--------------------+----------+-----------+--------------------+
|M5BMEG001|2019-09-23 16:55:...| | sGBM_authen| a01| | pin| 1647345168|{"ROOT":{"REGION_...|
|N0BZFW001|2019-09-23 16:55:...| 1000| sPwdNotice| a01|11*20190923165203...| pin| 398358364|{"ROOT":{"REQUEST...|
|K3BEJ0001|2019-09-23 16:55:...| 1000| s1000GetAutNo| a01|11*20190923165310...| pin|-1000205510|{"ROOT":{"REQUEST...|
|M5BMEG001|2019-09-23 16:55:...| 4317|sGOQ_MobInfoQry| a01|11*20190923165459...| pin| 1591331815|{"ROOT":{"COMMON_...|
|ASAND0001|2019-09-23 16:55:...| | sUserBasInfo| a01|11*20190923144609...| pin| 778707156|{"ROOT":{"OPR_INF...|
+---------+--------------------+-------+---------------+------+--------------------+----------+-----------+--------------------+
only showing top 5 rows
RDD:log_in None
length of RDD:log_out 53
+---------+--------------------+-------+------------+------+--------------------+----------+----------+--------------------+
| login_no| time_out|op_code|service_name|source| trace_id|param_type| tid| json_out|
+---------+--------------------+-------+------------+------+--------------------+----------+----------+--------------------+
|A1BXHD001|2019-09-23 16:54:...| J702| sICertScan| a01|11*20190923164551...| pout| 662425848|{"ROOT":{"RETURN_...|
|B0A030339|2019-09-23 16:54:...| | sGBM_authen| a01|11*20190923165437...| pout| 850454376|{"ROOT":{"RETURN_...|
|A8AZYX004|2019-09-23 16:54:...| 5730| sDynSvc| a01|11*20190923165609...| pout|-531230181|{"ROOT":{"RETURN_...|
|N0BZFW001|2019-09-23 16:55:...| 1000| sPwdNotice| a01|11*20190923165203...| pout| 398358364|{"ROOT":{"RETURN_...|
|ASBDHX002|2019-09-23 16:55:...| 1000| sDynSvc| a01|11*20190923164948...| pout|1765858292|{"ROOT":{"RETURN_...|
+---------+--------------------+-------+------------+------+--------------------+----------+----------+--------------------+
only showing top 5 rows
最后将log_in和log_out数据通过tid字段的值join起来
log_join=log_in.join(log_out,log_in.tid == log_out.tid,"left").select(log_in.login_no,log_in.time_in,log_out.time_out,log_in.op_code,
log_in.service_name,log_in.source,log_in.trace_id,log_in.tid,
log_in.json_in,log_out.json_out).orderBy(log_in.time_in)
这样就完成了我们hdfs上的json数据结构化的初步操作,在后面的使用中我们还需要根据自己数据的特点和集群分配的资源进行相应的调优工作。