需求
- 统计每日 PV 和独立 IP
- 统计每种不同的 HTTP 状态对应的访问数
- 统计不同独立 IP 的访问量
- 统计不同页面的访问量
基础知识准备
Apache日志位置
- Windows下是: C:Program FilesApache Software FoundationApache2.2logsaccess.txt
- Linux下是: /var/log/apache2/access.log
Apache日志文件格式
一条Apache记录,从左往右,大概是这个样子:
1.远程主机IP:表明访问网站的是谁
2.空白(E-mail):为了避免用户的邮箱被垃圾邮件骚扰,第二项就用“-”取代了
3.空白(登录名):用于记录浏览者进行身份验证时提供的名字。
4.请求时间:用方括号包围,而且采用“公用日志格式”或者“标准英文格式”。 时间信息最后的“+0800”表示服务器所处时区位于UTC之后的8小时。
5.方法+资源+协议:服务器收到的是一个什么样的请求。该项信息的典型格式是“METHOD RESOURCE PROTOCOL”,即“方法 资源 协议”。
METHOD: GET、POST、HEAD、……
RESOURCE: /、index.html、/default/index.php、……(请求的文件)
PROTOCOL: HTTP+版本号
6.状态代码:请求是否成功,或者遇到了什么样的错误。大多数时候,这项值是200,它表示服务器已经成功地响应浏览器的请求,一切正常。
7.发送字节数:表示发送给客户端的总字节数。它告诉我们传输是否被打断(该数值是否和文件的大小相同)。把日志记录中的这些值加起来就可以得知服务器在一天、一周或者一月内发送了多少数据
8、url以及客户端的详细信息,比如操作系统、浏览器等等
另外,经过查找,也有说法说,第二项和第三项分别是客户端记录和浏览者记录。
以及,第4项请求时间里面的“标准格式”是有:日期、时间、地区三种信息的。
例如这条Apache日志记录
180.76.15.161 - - [06/Dec/2014:06:49:26 +0800] "GET / HTTP/1.1" 200 10604 "-" "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)"
就,非常明显,每一项都是能够对应得上的,哪个位置都是什么。
正则表达式
PV
pv(Page View)单指访问量,每打开一次页面PV计数+1,刷新页面也是。和UV是不一样的
UV是访问数(Unique Visitor)指独立访客访问数,一台电脑终端为一个访客。
实现
下载资料
自己电脑上的Apache日志基本没有什么,就算做实验搭建过web服务器,也是仅限于局域网,数据量比较少。这个实验选取的数据来源是:
http://labfile.oss.aliyuncs.com/courses/456/access.log
可以用wget命令下载到当前目录下。
嗯,一兆多一点的文本文件。
解析记录生成新RDD
这个正则表达式是对除了客户端详细信息以外的前7项内容做解析的,需求里面要每天的ip,所以需要分出来ip和时间。然后,还要求了不同http状态的访问数和页面访问量,所以还需要有HTTP状态码和URL。下面这个正则表达式,可以用来过滤掉不能够解析的日志记录,并解析有需要的信息的日志记录。下面这句scala让正则表达式的字符串调用了r这个方法从而创建了一个Regex对象也就是正则表达式对象
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
}
}
根据我对scala的浅显了解,这个函数起作用的就是Some类,Some类是继承了Option类的一个类,主要就是用来判断这个是不是none的。
能够匹配,或者说能用的记录就是true否则就是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”的字符串来统计无效值
}
}
然后,筛选的传给filter,filter的结果调用map,而map的参数就是parseLog这个分析函数,最后生成一个新的RDD
val logRDDv1 = logRDD.filter(filterWithParse).map(parseLog)
这个logRDDv1就是后面做分析,然后实现最开始的要求的基础。
在新RDD基础上做统计
pv
新RDD里面一共有5122条记录
题目要求是根据日期统计pv数量的,而刚才新生成RDD的时候,日期是放在了RDD的第二列或者说每一条记录的第二项。
然后,我还需要知道的是,在scala的元组里面计数是从1开始的不是0。
利用这些,生成一个新的键值对RDD: logRDDv2,里面的键是日期,每个键对应的值是加和加出来的数量。
然后,按时间排序,保存
每日ip
生成一个,键是日期,值是ip的键值对rdd
val logRDDv3 = logRDDv1.map(x => (x._2, x._1))
调用distinct给这个rDD去重,再给去完重的RDD统计每日独立的ip访问数,因为distinct完成之后,每一行就是说这一天有这个ip访问,然后同一天的行数就是这一天访问服务器的独立ip数。
所以就按值相加生成一个新的键值对RDD喽。
val logRDDv4 = logRDDv3.distinct()
val logRDDv5 = logRDDv4.map(x => (x._1, 1)).reduceByKey(_ + _)
val DIP = logRDDv5.collect()
每种HTTP的状态数
下面就基本是和统计每日独立ip和pv差不多的操作了
比如这个统计每种HTTP的状态数,就是按每一行的http状态的那个属性生成一个键值对rdd然后就按照相同的键相加,就得到了每种http的状态数。和Wordcount那个例子其实区别不大。
val logRDDv6 = logRDDv1.map(x => (x._4, 1)).reduceByKey(_ + _)
val StatusPV = logRDDv6.sortByKey().collect()
不同的独立ip访问量
嗯。还是一样的操作
val logRDDv7 = logRDDv1.map(x => (x._1, 1)).reduceByKey(_ + _)
val IPPV = logRDDv7.sortBy(x => x._2, false).collect()
这里的话,我需要注意一下这个sortBy,因为以前我觉得在scala里面排序一般都是sortByKey,但是其实sortBy就可以做按键值对的“值”来排序。
sortBy函数的第二个参数传入false的意思让它降序排序。其实还有第三个参数,用来决定排序完之后的RDD的分区个数。详见参考资料。
不同页面的访问量
这个如果没有教程的话,我应该会直接像上面那样做完了就拉到了
就这样
val logRDDv8 = logRDDv1.map(x => (x._3, 1)).reduceByKey(_ + _)
val PagePV = logRDDv8.sortBy(x => x._2, false).collect()
但是,就像教程说的:
通过查看 PagePV 我们发现有大量的 js 文件的访问,这不是我们需要的内容,
然后,教程提供了一个很巧妙地去除有问题的页面访问的办法
一看,这不就是上次学的那个停词表么,只不过这个停的不是词,是文件的后缀名
就,属于这个列表的就不要,不是这个列表里面的就统计,然后根据这个生成新的rdd,再用新的rdd做上面统计ip那些操作。
参考资料
Scala字符串中的查找模式
正则表达式手册
菜鸟工具 正则表达式在线测试
class Some
sortByKey和sortBy函数详解