一、处理json格式的hdfs日志
处理json格式的日志,常规方法是使用sc.textFile(hdfs_path)
读数据,然后json.loads()
将没一行json串解析成字典,但是解析json串时会产生pyton-spark的数据交互,这种交互的成本很大。spark2.0以上的版本支持spark.sql.read.json
方法直接读取json格式,但是分布式文件系统的json日志往往是很不规整的,json串的key各有不同,甚至相同的key,值的属性也不一样(1000, “1000”),脏的数据会导致程序报错。
from pyspark.sql import SparkSession
from pyspark.sql.window import Window
from pyspark.sql import functions as F
from pprint import pprint
spark = SparkSession.builder.getOrCreate()
pprint(spark.sparkContext.getConf().getAll())
date = '17-12-14'
root_path = 'hdfs://hadoop-ha/user/st/new/'
hdfs_path = 'hdfs://hadoop-ha/api/%s/**/*.gz' % date
d = spark.sparkContext.textFile(hdfs_path)
df = spark.createDataFrame(d, 'string')
折中的办法是,sc.textFile
将数据以rdd读入,不解析,直接将json串转化成dataframe的一列。不设置列名通常默认为value, 如上代码, 结果展示如下。
+--------------------+
| value|
+--------------------+
|{"drag_count": 0,...|
|{"device_id": "d8...|
|{"request_time": ...|
|{"request_time": ...|
|{"request_time": ...|
|{"request_time": ...|
|{"request_time": ...|
|{"request_time": ...|
|{"request_time": ...|
|{"drag_count": 0,...|
|{"salt": "PT.n", ...|
|{"request_time": ...|
|{"request_time": ...|
|{"request_time": ...|
|{"request_time": ...|
|{"request_time": ...|
|{"device_id": "4e...|
|{"request_time": ...|
|{"request_time": ...|
|{"request_time": ...|
+--------------------+
only showing top 20 rows
二、spark.sql.function原生函数的使用
使用spark原生函数不会产生spark-python的数据交互。函数经过优化,能满足大部分数据处理要求。其中对于处理json字串的get_json_object
可以从属性是json的列中取出给定的key值。具体用法如下,从value中取出captcha_id, type, client_type
等列。json串中没有给定key的用null补充。
**注意:**嵌套的json格式,可用get_json_object('value', '$.insight.browserLanguage')
取出想要的browserLanguage字段。
df = df.select('value',
F.get_json_object('value', '$.captcha_id').alias('captcha_id'),
F.get_json_object('value', "$.type").alias('type'),
F.get_json_object('value', '$.client_type').alias('client_type'),
F.get_json_object('value', '$.extra_parameter').alias('extra_parameter'),
F.get_json_object('value', '$.challenge').alias('challenge'),
F.get_json_object('value', '$.passtime').alias('passtime'),
F.get_json_object('value', '$.random_parameter').alias('random_parameter'),
F.get_json_object('value', '$.ip').alias('ip'))
F.get_json_object('value',
'$.insight.browserLanguage').alias('browserLanguage').show()
部分列的结果如下
+--------------------+--------+--------------------+---------------+---------------+
| captcha_id|is_robot| extra_parameter| ip|browserLanguage|
+--------------------+--------+--------------------+---------------+---------------+
|7c56e28e905f0c9f3...| false| null| 182.107.171.31| zh-CN|
|2f5e33b3c37769a84...| false|{"e":{"st":"TypeE...|171.117.120.102| zh-CN|
|ffcd1599b3cf9a4cd...| false|{"e":{"st":"TypeE...|113.195.145.173| zh-CN|
|7ce498b2b35b1f4ba...| true| null| 92.42.8.1| zh-CN|
|566e50cd4783b181c...| false| null| 113.129.239.38| zh-CN|
|e0ebfb63e6ee7cf7e...| false|{"ts":15131807856...| 217.209.224.60| sv-SE|
|e0ebfb63e6ee7cf7e...| false|{"ts":15131809173...| 80.188.248.1| cs-CZ|
|f3da43872204b2aa7...| false|{"ts":15131807886...| 218.31.53.74| zh-CN|
|7c56e28e905f0c9f3...| false|{"ts":15131807932...| 49.80.83.38| zh-CN|
|942640dbfd390721d...| false|{"ts":15131807858...| 176.112.71.202| ru|
|942640dbfd390721d...| false|{"e":{"na":"TypeE...| 93.77.88.20| ru-RU|
|7c56e28e905f0c9f3...| false|{"e":{"na":"TypeE...| 114.83.72.122| zh-CN|
|2f5e33b3c37769a84...| false|{"e":{"na":"TypeE...| 175.18.33.90| zh-CN|
|e2b6156afcd010528...| false|{"e":{"na":"TypeE...| 115.50.190.36| zh-CN|
|2f5e33b3c37769a84...| false|{"e":{"na":"TypeE...| 36.228.232.239| zh-TW|
|79fbb5ed49aecd03a...| false|{"e":{"ts":"Type"...|202.107.235.102| zh-CN|
|0262326656f5c0426...| false|{"e":{"ts":"Type"...| 120.84.10.137| zh-CN|
|354d9d4cf14d2cf61...| false|{"e":{"ts":"Type"...|114.226.122.173| zh-CN|
|e0ebfb63e6ee7cf7e...| false|{"e":{"ts":"Type"...| 99.203.5.155| en-US|
|7ce498b2b35b1f4ba...| false|{"e":{"ts":"Type"...| 106.61.20.253| zh-CN|
+--------------------+--------+--------------------+---------------+---------------+
only showing top 20 rows
其他spark内置函数如:sum
, collect_list
,count
, collect_set
, countDistinct
等。详情请查询spark.sql.function
源码。
三、窗口函数
我们通常使用groupby
的时候想保留原有的列,但这在现有sql数据库查询中不能实现(最新的版本可能有)。窗口函数能帮我们达到这样的要求。
下面,我们想统计每个captcha_id出现的次数,并且我们还想保留is_robot列。如果不使用窗口函数,代码如下:
key = 'captcha_id'
dd1 = df.select('is_robot',
F.get_json_object('value', '$.%s' % key).alias(key)
). \
dropna(subset=[key]). \
groupby(key).count().cache()
groupby
结果:
+--------------------+--------------+
| captcha_id|captcha_id_cnt|
+--------------------+--------------+
|05e7077f462b6523b...| 21950|
|1c3f91720d7a7f604...| 829|
|226ed230f305b2f3a...| 10|
|25f06d5dabe2d6e9e...| 23|
|2ebd463b285527c3c...| 126|
|383dc849256f9aee3...| 261|
|38d66226567502a5b...| 17107|
|3a5bb578740870c98...| 12|
|482ac913535ce51db...| 383|
|6f51163b6c7c1f992...| 17|
|721b245d5f3ce5074...| 8|
|857f1cc63d82c9865...| 13|
|999db8e10d6ff2d49...| 14|
|9f857f36da444bc44...| 385|
|a54b9a641ea7851a2...| 1|
|c4a8c2848cde61b2e...| 12|
|c89a27ba59dfd5b94...| 3|
|d8ba661bf6713fb7c...| 35|
|e37813b5915b66861...| 303|
|e7760e57a18d33453...| 5|
+--------------------+--------------+
only showing top 20 rows
聚合后结果中并没有保留is_robot列,而使用窗口函数
key = 'captcha_id'
w = Window().partitionBy(key)
dd1 = df.select('is_robot',
F.get_json_object('value', '$.%s' % key).alias(key)
). \
dropna(subset=[key]). \
withColumn('%s_cnt' % key, F.count(key).over(w)).cache()
结果如下:
+--------+--------------------------------+--------------+
|is_robot|captcha_id |captcha_id_cnt|
+--------+--------------------------------+--------------+
|false |006129a83fab1b5d22bd5e29142a6c22|2 |
|false |006129a83fab1b5d22bd5e29142a6c22|2 |
|false |025a98cc521ac1dcd04188e0f476ce50|2 |
|false |025a98cc521ac1dcd04188e0f476ce50|2 |
|false |033f3f9bf8edd87e4e24eedbcc82ab7e|2 |
|false |033f3f9bf8edd87e4e24eedbcc82ab7e|2 |
|false |044830f05a9be3df7cacf9a7dab40545|2 |
|false |044830f05a9be3df7cacf9a7dab40545|2 |
|false |0a6245de5a490d86aa4f99df9be9f0ef|2 |
|false |0a6245de5a490d86aa4f99df9be9f0ef|2 |
|false |0b58d8fd7f1c16366dfa6a33e1eaa7b7|2 |
|false |0b58d8fd7f1c16366dfa6a33e1eaa7b7|2 |
|false |0b9a7d096047da61c666308fdf4c38d9|2 |
|true |0b9a7d096047da61c666308fdf4c38d9|2 |
|false |0c332e47bd1a24698f2cd3b3d705b7d3|2 |
|false |0c332e47bd1a24698f2cd3b3d705b7d3|2 |
|false |0c414d0aa3f7f062d7a64675702f4d67|2 |
|false |0c414d0aa3f7f062d7a64675702f4d67|2 |
|false |0c52c51518126029f726e2db7bc5d270|2 |
|false |0c52c51518126029f726e2db7bc5d270|2 |
+--------+--------------------------------+--------------+
only showing top 20 rows
四、透视表
透视表将长格式转化成宽格式(将一列或多列的值变成列名)。
pivot_df = dd1.groupBy(key). \
pivot("is_robot", [True, False]).count(). \
na.fill({"True": 0, "False": 0}).show()
结果,将is_robot这一列中的两个值true, false变成列名,其中每一列表示个数,如下:
+--------------------+----+-----+
| captcha_id|true|false|
+--------------------+----+-----+
|1c3f91720d7a7f604...| 0| 118|
|25f06d5dabe2d6e9e...| 0| 3|
|38d66226567502a5b...| 3| 2698|
|482ac913535ce51db...| 0| 3|
|857f1cc63d82c9865...| 0| 1|
|9f857f36da444bc44...| 0| 8|
|e37813b5915b66861...| 0| 27|
|f65a04a92afd93b2b...| 0| 23|
|162c5103ed2526d1d...| 0| 3906|
|2eebc5b805cf6a2d5...| 0| 14|
|36919cf9b5065b216...| 0| 69|
|42c9f3cd76ae1dd85...| 0| 97|
|5d1c5adc14739af22...| 0| 36|
|7b2078c619e679f9f...| 0| 52|
|7b60ad0dd03c22813...| 11| 8004|
|858b9ebf1412700d3...| 0| 9|
|8c8be929eeddf30e0...| 0| 41|
|cc420f2055d0244ad...| 0| 12|
|d9de4e84eb3baba99...| 0| 12|
|dd8557354686547d5...| 0| 10|
+--------------------+----+-----+
only showing top 20 rows