项目所用数据集:https://download.csdn.net/download/qq_21583077/11005273
数据集偏大,大约有5个G,考虑到机器配置等因素,在本案例中,我们只截取数据集的前10000条进行统计分析。
数据格式:
183.162.52.7 - - [10/Nov/2016:00:01:02 +0800] "POST /api3/getadv HTTP/1.1" 200 813 "www.imooc.com" "-" cid=0×tamp=1478707261865&uid=2871142&marking=androidbanner&secrect=a6e8e14701ffe9f6063934780d9e2e6d&token=f51e97d1cb1a9caac669ea8acc162b96 "mukewang/5.0.0 (Android 5.1.1; Xiaomi Redmi 3 Build/LMY47V),Network 2G/3G" "-" 10.100.134.244:80 200 0.027 0.027
一条数据之间是以空格来分割没一个字段,以上面的一条数据为例:
第一个字段:183.162.52.7是访问者的IP;
第二个字段、第三个字段为空,用“-”代表;
第四、五个字段:访问时间,[10/Nov/2016:00:01:02 +0800]
第六个字段:HTTP协议,“POST”
第7个字段:/api3/getadv,URL地址
第8个字段:HTTP/1.1,HTTP版本号
第9个字段:200,状态码
第10个字段:813,访问所耗费的流量
第11个字段:"www.imooc.com",慕课网主站地址
...............后面的就不重要了,所以不一一介绍了(因为我也不知道了,嘿嘿)
我们本次课程需要的在原始访问信息中提取出:访问时间,访问URL,访问的IP,耗费的流量等几个字段。
import org.apache.spark.sql.SparkSession
/**
* @author YuZhansheng
* @desc 第一步清洗:抽取出我们所需要的指定列的数据
* @create 2019-03-08 9:44
*/
object SparkStatFormatJob {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().appName("SparkStatFormatJob").master("local[3]").getOrCreate()
//textFile:从HDFS、本地文件系统(在所有节点上可用)或任何节点读取文本文件;支持hadoop的文件系统URI,并将其作为字符串的RDD返回。
val access = spark.sparkContext.textFile("file:/root/DataSet/10000_access.log")
//提取出IP,时间,url,流量,最后再把清洗后的数据写入到本地
access.map(line => {
//根据空格进行分割
val splits = line.split(" ")
//获取IP
val ip = splits(0)
//原始日志的第四个和第五个字段拼接起来就是完整的访问时间:[10/Nov/2016:00:01:02 +0800]
//[10/Nov/2016:00:01:02 +0800] ==> yyyy-MM-dd HH:mm:ss,使用DateUtils工具类转换
val time = splits(3) + " " + splits(4)
//获取url,并把引号都给替换成空
val url = splits(11).replace("\"","")
//获取访问耗费的流量
val traffic = splits(9)
DateUtils.parse(time) + "\t" + url + "\t" + traffic + "\t" + ip
}).saveAsTextFile("file:/root/DataSet/output/")
spark.stop()
}
}
import java.util.{Date, Locale}
import org.apache.commons.lang3.time.FastDateFormat
/**
* @author YuZhansheng
* @desc 日期时间解析工具类
* 注意:SimpleDateFormat是线程不安全的,因此在这里我们要使用FastDateFormat.getInstance
* @create 2019-02-26 15:22
*/
object DateUtils {
//输入的日期格式
val YYYYMMDDHHMM_TIME_FORMAT = FastDateFormat.getInstance("dd/MMM/yyyy:HH:mm:ss Z",Locale.ENGLISH)
//目标日期格式
val TARGE_FORMAT = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss")
//获取时间:yyyy-MM-dd HH:mm:ss
def parse(time:String) = {
TARGE_FORMAT.format(new Date(getTime(time)))
}
//获取输入日志时间:返回long类型
//time : [10/Nov/2016:00:01:02 +0800]
def getTime(time:String) = {
try{
YYYYMMDDHHMM_TIME_FORMAT.parse(time.substring(time.indexOf("[") + 1,time.lastIndexOf("]"))).getTime
}catch {
case e : Exception => {
0l
}
}
}
}
清洗过后,最后输出到本地的日志信息如下:
2016-11-10 00:01:53 http://www.imooc.com/video/2314 54 117.181.194.121
2016-11-10 00:01:53 http://www.imooc.com/code/4336 67 120.198.231.150
2016-11-10 00:01:53 - 0 10.100.0.1
2016-11-10 00:01:53 - 0 218.26.76.143
2016-11-10 00:01:53 - 0 10.100.0.1
2016-11-10 00:01:53 http://www.imooc.com/ 2340 120.197.199.74
一般日志处理方式,我们是需要进行分区的,按照日志中的访问时间进行相应的分区,比如按天分区、小时分区等。
经过上一步的日志清洗,现在得到的日志信息是:访问时间、访问URL、耗费的流量、访问IP地址信息;现在我们进行进一步解析,得到更详细的信息:URL、cmsType(video/article)、cmsId(编号)、流量、IP、城市信息、访问时间等。
上一步我们把输出文件给存放到:saveAsTextFile("file:/root/DataSet/output/")这个路径了,里面会有两个文件块,part00000和part00001,我们把两个文件里的内容提取出来,整合成一个文件,命名为access.log,下面的操作就以access.log这个文件为基础来进行解析和清洗。
import org.apache.spark.sql.Row
import org.apache.spark.sql.types.{LongType, StringType, StructField, StructType}
/**
* @author YuZhansheng
* @desc 访问日志转换工具类
* @create 2019-03-08 14:57
*/
object AccessConvertUtil {
//定义的输出的字段
val struct = StructType(
Array(
StructField("url",StringType),
StructField("cmsType",StringType),
StructField("cmsId",LongType),
StructField("traffic",LongType),
StructField("ip",StringType),
StructField("city",StringType),
StructField("time",StringType),
StructField("day",StringType)
)
)
//根据输入的每一行记录信息转换成输出样式
def parseLog(log:String) = {
try{
val splits = log.split("\t")
val url = splits(1)
val traffic = splits(2).toLong
val ip = splits(3)
val domain = "http://www.imooc.com/"
val cms = url.substring(url.indexOf(domain) + domain.length).split("/")
var cmsType = ""
var cmsId = 0l
if (cms.length > 1){
cmsType = cms(0)
cmsId = cms(1).toLong
}
val city = ""
val time = splits(0)
val day = time.substring(0,10).replace("-","")
//这个row里面的字段要和struct中的字段对应上
Row(url,cmsType,cmsId,traffic,ip,city,time,day)
}catch {
case e : Exception => Row(0)
}
}
}
package com.xidian.spark.log
import org.apache.spark.sql.SparkSession
/**
* @author YuZhansheng
* @desc 使用Spark完成数据解析操作
* @create 2019-03-08 14:27
*/
object SparkStatCleanJob {
def main(args: Array[String]): Unit = {
//Spark主程序入口,无需多说
val spark = SparkSession.builder().appName("SparkStatCleanJob").master("local[2]").getOrCreate()
//读取本地文件,创建RDD
val accessRDD = spark.sparkContext.textFile("file:/root/DataSet/access.log")
//将RDD转换为DataFrame
val accessDF = spark.createDataFrame(accessRDD.map(x => AccessConvertUtil.parseLog(x)),AccessConvertUtil.struct)
accessDF.show()
accessDF.printSchema()
spark.stop()
}
}
运行时候打印出如下说明解析成功了:
+--------------------+-------+-----+-------+---------------+----+-------------------+--------+
| url|cmsType|cmsId|traffic| ip|city| time| day|
+--------------------+-------+-----+-------+---------------+----+-------------------+--------+
|http://www.imooc....| video| 4500| 304| 218.75.35.226| |2017-05-11 14:09:14|20170511|
|http://www.imooc....| video|14623| 69| 202.96.134.133| |2017-05-11 15:25:05|20170511|
|http://www.imooc....|article|17894| 115| 202.96.134.133| |2017-05-11 07:50:01|20170511|
|http://www.imooc....|article|17896| 804| 218.75.35.226| |2017-05-11 02:46:43|20170511|
|http://www.imooc....|article|17893| 893|222.129.235.182| |2017-05-11 09:30:25|20170511|
|http://www.imooc....|article|17891| 407| 218.75.35.226| |2017-05-11 08:07:35|20170511|
|http://www.imooc....|article|17897| 78| 202.96.134.133| |2017-05-11 19:08:13|20170511|
|http://www.imooc....|article|17894| 658|222.129.235.182| |2017-05-11 04:18:47|20170511|
|http://www.imooc....|article|17893| 161| 58.32.19.255| |2017-05-11 01:25:21|20170511|
|http://www.imooc....|article|17895| 701| 218.22.9.56| |2017-05-11 13:37:22|20170511|
|http://www.imooc....|article|17892| 986| 218.75.35.226| |2017-05-11 05:53:47|20170511|
|http://www.imooc....| video|14540| 987| 58.32.19.255| |2017-05-11 18:44:56|20170511|
|http://www.imooc....|article|17892| 610| 218.75.35.226| |2017-05-11 17:48:51|20170511|
|http://www.imooc....|article|17893| 0| 218.22.9.56| |2017-05-11 16:20:03|20170511|
|http://www.imooc....|article|17891| 262| 58.32.19.255| |2017-05-11 00:38:01|20170511|
|http://www.imooc....| video| 4600| 465| 218.75.35.226| |2017-05-11 17:38:16|20170511|
|http://www.imooc....| video| 4600| 833|222.129.235.182| |2017-05-11 07:11:36|20170511|
|http://www.imooc....|article|17895| 320|222.129.235.182| |2017-05-11 19:25:04|20170511|
|http://www.imooc....|article|17898| 460| 202.96.134.133| |2017-05-11 15:14:28|20170511|
|http://www.imooc....|article|17899| 389|222.129.235.182| |2017-05-11 02:43:15|20170511|
+--------------------+-------+-----+-------+---------------+----+-------------------+--------+
only showing top 20 rows
root
|-- url: string (nullable = true)
|-- cmsType: string (nullable = true)
|-- cmsId: long (nullable = true)
|-- traffic: long (nullable = true)
|-- ip: string (nullable = true)
|-- city: string (nullable = true)
|-- time: string (nullable = true)
|-- day: string (nullable = true)
根据IP解析地址需要借助GitHub上的一个开源项目:https://github.com/wzhe06/ipdatabase
利用二叉树实现IP查询,首先将10进制IPV4地址转化为二进制构建二叉树,利用二叉树搜索进行搜索,查询时间复杂度log2n,比传统IP库n的查询速度高出一个量级。数据源采用2015年广告协会制定的IP地址标准数据库,中国互联网广告行业统一采用的标准IP库。
首先需要先克隆或者下载这个项目,然后使用maven命令编译这个项目,安装到maven仓库之后,就可以项目中的pom文件中添加groupId、artifactId、version来使用这个jar包了。
依赖:
com.ggstar
ipdatabase
1.0-SNAPSHOT
除此之外,还需要将ipdatabase 这个项目中的两个文件:ipDatabase.csv、ipRegion.xlsx给复制到spark项目的resources目录下,不然会报错。
写一个IPUtiles测试类:
import com.ggstar.util.ip.IpHelper
/**
* @author YuZhansheng
* @desc IP地址解析工具类
* @create 2019-03-11 10:23
*/
object IPUtiles {
def getCity(ip:String) = {
IpHelper.findRegionByIp(ip)
}
def main(args: Array[String]): Unit = {
println(getCity("39.105.104.164"))
}
}
输出:北京市;说明这个工具类可以使用。
修改AccessConvertUtil访问日志转换工具类程序,解析出城市,将city给填充上去: val city = "" ==> val city = IpUtils.getCity(ip)
再次运行,城市已经被填充上去了,如下:
+--------------------+-------+-----+-------+---------------+----+-------------------+--------+
| url|cmsType|cmsId|traffic| ip|city| time| day|
+--------------------+-------+-----+-------+---------------+----+-------------------+--------+
|http://www.imooc....| video| 4500| 304| 218.75.35.226| 浙江省|2017-05-11 14:09:14|20170511|
|http://www.imooc....| video|14623| 69| 202.96.134.133| 广东省|2017-05-11 15:25:05|20170511|
|http://www.imooc....|article|17894| 115| 202.96.134.133| 广东省|2017-05-11 07:50:01|20170511|
|http://www.imooc....|article|17896| 804| 218.75.35.226| 浙江省|2017-05-11 02:46:43|20170511|
//accessDF.printSchema()
//accessDF.show()
//将清理后的数据保存到指定目录
accessDF.write.format("parquet").partitionBy("day").save("file:/root/DataSet/clean")
生成如下文件:
[root@hadoop0 clean]# ls
day=20170511 _SUCCESS
[root@hadoop0 clean]# cd day\=20170511/
[root@hadoop0 day=20170511]# ls
part-00000-ba848fef-ee72-4151-90c7-6aa2644a3eef.c000.snappy.parquet part-00002-ba848fef-ee72-4151-90c7-6aa2644a3eef.c000.snappy.parquet
part-00001-ba848fef-ee72-4151-90c7-6aa2644a3eef.c000.snappy.parquet
[root@hadoop0 day=20170511]# ls -lh
总用量 9.5M
-rw-r--r-- 1 root root 4.5M 3月 11 11:11 part-00000-ba848fef-ee72-4151-90c7-6aa2644a3eef.c000.snappy.parquet
-rw-r--r-- 1 root root 4.5M 3月 11 11:11 part-00001-ba848fef-ee72-4151-90c7-6aa2644a3eef.c000.snappy.parquet
-rw-r--r-- 1 root root 626K 3月 11 11:11 part-00002-ba848fef-ee72-4151-90c7-6aa2644a3eef.c000.snappy.parquet
生成了三个文件,感觉有点多,能不能指定生成的文件块数呢?当然可以啦!
//accessDF.printSchema()
//accessDF.show()
//将清理后的数据保存到指定目录
accessDF.coalesce(1).write.format("parquet").mode("overwrite").partitionBy("day").save("file:/root/DataSet/clean")
coalesce(1):指定生成的文件块数
mode("SaveMode.Overwrite"):保存模式,覆盖掉原有的clean文件
这次运行程序,会生成如下的文件结构:
[root@hadoop0 DataSet]# cd clean/
[root@hadoop0 clean]# ls
day=20170511 _SUCCESS
[root@hadoop0 clean]# cd day\=20170511/
[root@hadoop0 day=20170511]# ls
part-00000-bb0e3fd6-dab9-4ece-a01f-00d0b3b002af.c000.snappy.parquet