1. 概述
当浏览器请求服务器时,如果在服务器上设置了访问日志,就会记录下用户的访问记录。在日志里,通常包含大量的信息,但是这些信息不太容易被利用,这里我们通过对Apache的access.log日志进行分析,来进一步的学习Spark下的程序开发。
2. 假定需求
假设给我们提供一份apache的access.log文件,根据业务需求,我们需要分析得到以下几方面的需求:
1.统计每天的页面访问量
2.统计每种不同的HTTP状态对应的访问数
3.统计不同独立IP的访问量
4.统计不同页面的访问量
3. 准备工作
3.1 日志文件下载
下载指定分析的日志,当然也可以使用自己真实的Apache日志,在tomcat的logs目录中,为了让分析结果更加的直观明显,还是推荐使用下载日志。
Apache access.log日志下载地址:http://labfile.oss.aliyuncs.com/courses/456/access.log
3.2 日志文件格式
下载后的日志文件打开后类似这样:
- Apache 日志内容从左到右依次包括如下内容:
-
远程IP地址
客户端记录
浏览者记录
请求的时间,包括日期,时间,时区
服务器收到的请求,包括请求方法(GET/POST),请求的目标链接地址,HTTP版本号
请求状态代码,表示请求是否成功
发送的字节数
发出请求时所在的URL
客户端的详细信息,操作系统及浏览器等
下面截取一条,对比一下:
4. 实验
4.1 上传日志文件
把下载的日志文件access.log,上传到linux虚拟机的/opt/spark/log目录,当然,在系列一文档中并没有创建log目录,这里需要手动创建一下。
4.2 启动Spark
在系列一中已涉及spark的安装启动,这里简单重提下命令。
启动主节点:
# cd /opt/spark/spark-1.5.1-bin-hadoop2.6/sbin/
# sh start-master.sh
启动从节点:
# sh start-slave.sh spark://xubincc:7077
通过浏览器访问spark,地址为http://ip:8080
4.3 启动spark shell或者pyspark
启动spark shell,准备使用scala方式解决,当然也可以使用pyspark的python,这里不涉及:
# MASTER=spark://xubincc:7077 spark-shell
4.4 引入日志文件
# val logFile = "/opt/spark/log/access.log"
# val logRDD = sc.textFile(logFile)
4.5 Apache日志解析
设计正则表达式对日志文件进行解析,并返回解析后的元组,正则表达式设计如下:
val logPattern = """^(\\S+) (\\S+) (\\S+) \\[([\\w/]+)([\\w:/]+)\s([+\\-]\\d{4})\\] \"(\\S+) (\\S+) (\\S+)\" (\\d{3}) (\\d+)""".r
针对该系列提出的需求场景,我们只需要下面几项数据:
1. 客户端IP地址:正则表达式解析的第1项
2. 请求日期:正则表达式解析的第4项
3. HTTP状态码:正则表达式解析的第8项
4. 请求的URL:正则表达式解析的第10项
为了避免一些杂乱的无法解析的数据干扰,我们使用正则表达式做两件事情,一个是过滤掉无法解析的日志,一个是解析获得需要的数据元组。
这里我们需要实现两个函数:
1. filterWithParse:过滤无法解析的日志记录
2. parseLog:解析日志记录,得到所需信息的元组
具体scala代码如下:
val logPattern = """^(\S+) (\S+) (\S+) \[([\w/]+)([\w:/]+)\s([+\-]\d{4})\] "(\S+) (\S+) (\S+)" (\d{3}) (\d+)""".r
def filterWithParse(s: String) = {
logPattern.findFirstIn(s) match {
case Some(logPattern(_*)) => true
case _ => false
}
}
def parseLog(s: String) = {
val m = logPattern.findAllIn(s)
if(m.hasNext){
val clientIP = m.group(1).toString
val requestDate = m.group(4).toString
val requestURL = m.group(8).toString
val status = m.group(10).toString
(clientIP, requestDate, requestURL, status)
} else {
("null", "null", "null", "null") // 为了便于分析数据,此处用一个内容为“null”的字符串来统计无效值
}
}
执行第一次map操作,获得解析后的(客户端IP,请求日期,请求URL和HTTP状态码)的信息内容
# val logRDDv1 = logRDD.filter(filterWithParse).map(parseLog)
该元组中的元素为后面所有的分析提供基础。
5. 统计结果
5.1 统计每日页面访问量
首先我们统计日志文件中访问的总次数:
# logRDDv1.count
根据日期进行map及reduce操作,获得每日的页面访问数:
# val logRDDv2 = logRDDv1.map(x => (x._2, 1)).reduceByKey(_ + _)
# logRDDv2.sortByKey().saveAsTextFile("/opt/spark/log/resultPV")
使用sortByKey根据日期进行升序排序,并把输出写入到/opt/spark/log/resultPV里。
查看结果如下图所示:
5.2 统计每种不同的HTTP状态对应的访问数
一次map获得(HTTP访问码,1)的元组RDD,然后直接reduce操作:
# val logRDDv6 = logRDDv1.map(x => (x._4, 1)).reduceByKey(_ + _)
# logRDDv6.sortByKey().collect()
列表中存储了日志中出现的HTTP状态码及对应的访问次数,并根据HTTP状态码做了排序,可以看到200是最常见的状态码。
5.3 统计不同独立IP的访问量
与上一节的统计HTTP状态码相同,先map到一个(IP地址,1)的RDD,然后进行reduce后排序输出,需要注意如果使用sortBy函数中需要指明用元组的第二项进行排序,并采用倒序排列:
# val logRDDv7 = logRDDv1.map(x => (x._1, 1)).reduceByKey(_ + _)
# logRDDv7.sortBy(x => x._2, false).collect()
列表中记录了所有访问的IP地址及对应的访问次数,并按照访问次数从多到少进行了排序。
5.4 统计不同页面的访问量
与上一节的统计IP访问量相同,先map到一个(requestURL,1)的RDD,然后进行reduce后排序输出,需要注意如果使用sortBy函数中需要指明用元组的第二项进行排序,并采用倒序排列:
# val logRDDv8 = logRDDv1.map(x => (x._3, 1)).reduceByKey(_ + _)
# logRDDv8.sortBy(x => x._2, false).collect()
列表中记录了所有访问的IP地址及对应的访问次数,并按照访问次数从多到少进行了排序。