日志分析
数据情况
每行记录有5部分组成:访问者IP、访问时间、访问资源、访问状态(HTTP状态码)、本次访问流量
指标KPI
1.浏览PV
(1)定义:页面浏览量即为PV(Page View),是指所有用户浏览页面的总和,一个独立用户每打开一个页面就被记录1 次。
(2)分析:网站总浏览量,可以考核用户对于网站的兴趣,就像收视率对于电视剧一样。但是对于网站运营者来说,更重要的是,每个栏目下的浏览量。
计算公式:记录计数,从日志中获取访问次数,又可以细分为各个栏目下的访问次数。
用户注册数
注册用户量,指的是某款软件系统截止到某一时刻所拥有的已注册的用户总量。通常所说的用户量,如果没有特别说明,一般指的就是注册用户量
3.IP数
(1)定义:一天之内,访问网站的不同独立 IP 个数加和。其中同一IP无论访问了几个页面,独立IP 数均为1。
(2)分析:这是我们最熟悉的一个概念,无论同一个IP上有多少电脑,或者其他用户,从某种程度上来说,独立IP的多少,是衡量网站推广活动好坏最直接的数据。
计算公式:对不同的访问者ip,计数
跳出率
(1)定义:只浏览了一个页面便离开了网站的访问次数占总的访问次数的百分比,即只浏览了一个页面的访问次数 / 全部的访问次数汇总。
(2)分析:跳出率是非常重要的访客黏性指标,它显示了访客对网站的兴趣程度:跳出率越低说明流量质量越好,访客对网站的内容越感兴趣,这些访客越可能是网站的有效用户、忠实用户。
计算公式:①统计一天内只出现一条记录的ip,称为跳出数;②跳出数/PV;
5.板块热度TopN
查看某个区域、模板的热度情况
开发步骤
技术框架描述
(1)Linux Shell编程
(2)HDFS、MapReduce(Apache或者CDH)
(3)Hive、Sqoop框架(Apache或者CDH)
上传日志文件至HDFS
把日志数据上传到HDFS中进行处理,可以分为以下几种情况:
(1)如果是日志服务器数据较小、压力较小,可以直接使用shell命令把数据上传到HDFS中;
(2)如果是日志服务器数据较大、压力较大,使用NFS在另一台服务器上上传数据;
(3)如果日志服务器非常多、数据量大,使用flume进行数据处理;
数据清洗
使用MapReduce对HDFS中的原始数据进行清洗,以便后续进行统计分析;
统计分析
使用Hive对清洗后的数据进行统计分析
分析结果导入MySQL
使用Sqoop把Hive产生的统计结果导出到mysql中;
表结构设计
数据清洗过程
数据定时上传
首先,把日志数据上传到HDFS中进行处理,可以分为以下几种情况:
(1)如果是日志服务器数据较小、压力较小,可以直接使用shell命令把数据上传到HDFS中;
(2)如果是日志服务器数据较大、压力较大,使用NFS在另一台服务器上上传数据;
(3)如果日志服务器非常多、数据量大,使用flume进行数据处理;
这里我们的实验数据文件较小,因此直接采用第一种Shell命令方式。又因为日志文件时每天产生的,因此需要设置一个定时任务,在第二天的1点钟自动将前一天产生的log文件上传到HDFS的指定目录中。所以,我们通过shell脚本结合crontab创建一个定时任务techbbs_core.sh,内容如下:
#!/bin/sh
#step1.get yesterday format string
yesterday=$(date --date='1 days ago' +%Y_%m_%d)
#step2.upload logs to hdfs
hadoop fs -put /usr/local/files/apache_logs/access_${yesterday}.log /project/techbbs/data
结合crontab设置为每天1点钟自动执行的定期任务:crontab -e,内容如下(其中1代表每天1:00,techbbs_core.sh为要执行的脚本文件):
* 1 * * * techbbs_core.sh
验证方式:通过命令 crontab -l 可以查看已经设置的定时任务
MR代码编写
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* 解析日志
*
* ip
* 时间
* url
* 状态码
* 流量
* */
public class ParserLog {
public static final SimpleDateFormat FORMAT = new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss", Locale.ENGLISH);
public static final SimpleDateFormat FORMATDATE = new SimpleDateFormat("yyyyMMddHHmmss");
/**
*解析ip
* @param line 日志中的一行数据
*27.19.74.143 - - [30/May/2018:17:38:20 +0800] "GET /static/image/common/faq.gif HTTP/1.1" 200 1127
* */
public String parserIp(String line){
String[] ips = line.split("- -");
String ip = ips[0].trim();
return ip;
}
/**
* 解析时间
* */
public String parserTime(String line){
//起始索引
int first = line.indexOf("[");
//结束索引
int last = line.indexOf("+0800");
//获取日志时间
String time = line.substring(first+1,last);
//进行格式化
//1.先将日志时间格式化为时间戳dd/MMM/yyyy:HH:mm:ss
Date parse = null;
try {
parse = FORMAT.parse(time);
} catch (ParseException e) {
e.printStackTrace();
}
//2.将时间戳格式化yyyyMMddHHmmss
return FORMATDATE.format(parse);
}
/**
* 解析url
* */
public String parserUrl(String line){
//获取起始索引
int indexOf = line.indexOf("\"");
//结束索引
int lastIndexOf = line.lastIndexOf("\"");
//截取url
return line.substring(indexOf+1,lastIndexOf);
}
/**
* 解析状态码
* */
public String parserStatus(String line){
String trim = line.substring(line.lastIndexOf("\"") + 1).trim();
return trim.split(" ")[0];
}
/**
* 解析流量
* */
public String parserFlow(String line){
String trim = line.substring(line.lastIndexOf("\"") + 1).trim();
return trim.split(" ")[1];
}
/**
* 组装数组
* ip 时间 url 状态码 流量
* */
public String[] parser (String line){
String ip = parserIp(line);
String time = parserTime(line);
String url = parserUrl(line);
String status = parserStatus(line);
String flow = parserFlow(line);
return new String[]{ip,time,url,status,flow};
}
}
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class LOGMapper extends Mapper<LongWritable, Text, NullWritable,Text> {
ParserLog parserLog = new ParserLog();
Text outlog = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//日志的清洗过程
String line= value.toString();
String[] parser = parserLog.parser(line);
//1.过滤静态的资源请求
if (parser[2].startsWith("GET /static") || parser[2].startsWith("GET /uc_server")){
return;
}
//2.过滤指定的字符串头部
if (parser[2].startsWith("GET /")){
//parser[2] = parser[2].substring(parser[2].indexOf("/")+1);
parser[2] = parser[2].substring(parser[2].indexOf("GET /")+("GET /").length());
}else if (parser[2].startsWith("POST /")){
parser[2] = parser[2].substring(parser[2].indexOf("POST /")+("POST /").length());
}else if (parser[2].startsWith("GET ")){
parser[2] = parser[2].substring(parser[2].indexOf("GET ") + "GET ".length());
}
//3.过滤指定字符串尾部
//if (parser[2].endsWith("HTTP/1.1") || parser[2].endsWith("HTTP/1.0")){
if (parser[2].endsWith("HTTP/1.1")){
parser[2] = parser[2].split(" ")[0];
}else if (parser[2].endsWith("HTTP/1.0")){
parser[2] = parser[2].split(" ")[0];
}
//4.输出清洗后的日志
outlog.set(parser[0] + "\t" + parser[1] + "\t" + parser[2]);
context.write(NullWritable.get(),outlog);
}
}
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class LOGReduce extends Reducer<NullWritable, Text,NullWritable,Text> {
@Override
protected void reduce(NullWritable key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
for (Text t : values){
context.write(NullWritable.get(),t);
}
}
}
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class LOGClean {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
args = new String[]{"",""};
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//配置Job信息
job.setJarByClass(LOGClean.class);
job.setMapperClass(LOGMapper.class);
job.setMapOutputKeyClass(NullWritable.class);
job.setMapOutputValueClass(Text.class);
job.setReducerClass(LOGReduce.class);
job.setOutputKeyClass(NullWritable.class);
job.setMapOutputValueClass(Text.class);
//job.setNumReduceTasks(0);
FileInputFormat.setInputPaths(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
System.exit(job.waitForCompletion(true)?0:1);
}
}
定期清理日志至HDFS
#!/bin/sh
#step1.get yesterday format string
yesterday=$(date --date='1 days ago' +%Y_%m_%d)
#step2.upload logs to hdfs
hadoop fs -put /usr/local/files/apache_logs/access_${yesterday}.log /project/techbbs/data
#step3.clean log data
hadoop jar /usr/local/files/apache_logs/mycleaner.jar /project/techbbs/data/access_${yesterday}.log /project/techbbs/cleaned/${yesterday}
Hive统计分析
建立分区表
hive>CREATE EXTERNAL TABLE techbbs(ip string, atime string, url string) PARTITIONED BY (logdate string) ROW FORMAT DELIMITED FIELDS TERMINATED BY ‘\t’ LOCATION ‘/project1/techbbs/cleaned’;
hive>ALTER TABLE techbbs ADD PARTITION(logdate=‘2019_09_24’) LOCATION ‘/project/techbbs1/cleaned/2018_05_30’;
HQL统计关键指标
关键指标之一:PV量
页面浏览量即为PV(Page View),是指所有用户浏览页面的总和,一个独立用户每打开一个页面就被记录1 次。这里,我们只需要统计日志中的记录个数即可,HQL代码如下
hive>CREATE TABLE techbbs_pv_2019_09_24 AS SELECT COUNT(1) AS PV FROM techbbs WHERE logdate=‘2018_05_30’;
关键指标之二:注册用户数
该论坛的用户注册页面为member.php,而当用户点击注册时请求的又是member.php?mod=register的url。因此,这里我们只需要统计出日志中访问的URL是member.php?mod=register的即可,HQL代码如下:
hive>CREATE TABLE techbbs_reguser_2019_09_24 AS SELECT COUNT(1) AS REGUSER FROM techbbs WHERE logdate=‘2018_05_30’ AND INSTR(url,‘member.php?mod=register’)>0;
关键指标之三:独立IP数
一天之内,访问网站的不同独立 IP 个数加和。其中同一IP无论访问了几个页面,独立IP 数均为1。因此,这里我们只需要统计日志中处理的独立IP数即可,在SQL中我们可以通过DISTINCT关键字,在HQL中也是通过这个关键字:
hive>CREATE TABLE techbbs_ip_2019_09_24 AS SELECT COUNT(DISTINCT ip) AS IP FROM techbbs WHERE logdate=‘2018_05_30’;
关键指标之四:跳出用户数
只浏览了一个页面便离开了网站的访问次数,即只浏览了一个页面便不再访问的访问次数。这里,我们可以通过用户的IP进行分组,如果分组后的记录数只有一条,那么即为跳出用户。将这些用户的数量相加,就得出了跳出用户数,HQL代码如下:
hive>CREATE TABLE techbbs_jumper_2019_09_24 AS SELECT COUNT(1) AS jumper FROM (SELECT COUNT(ip) AS times FROM techbbs WHERE logdate=‘2018_05_30’ GROUP BY ip HAVING times=1) e;
create table techbbs_jumper_2019_09_5 AS select count(1) as jumper from (select count(ip) as times from techbbs where logdate=‘2018_05_30’ group by ip HAVING times=1)e;
所有关键指标放入一张汇总表中以便于通过Sqoop导出到MySQL
为了方便通过Sqoop统一导出到MySQL,这里我们借助一张汇总表将刚刚统计到的结果整合起来,通过表连接结合,HQL代码如下:
hive>CREATE TABLE db_hive.techbbs_2019_09_24 AS SELECT ‘2019_09_24’, a.pv, b.reguser, c.ip, d.jumper FROM db_hive.techbbs_pv_2019_09_24 a JOIN db_hive.techbbs_reguser_2019_09_24 b ON 1=1 JOIN db_hive.techbbs_ip_2019_09_24 c ON 1=1 JOIN db_hive.techbbs_jumper_2019_09_24 d ON 1=1;
create table techbbs_2019_09_5 as select ‘2019_09_5’,a.pv,b.reguser,c.ip,d.jumper from techbbs_pv_2019_09_5 a JOIN techbbs_reguser_2019_09_5 b on 1=1 JOIN techbbs_ip_2019_09_5 c on 1=1 JOIN techbbs_jumper_2019_09_5 d on 1=1;
Sqoop导入MySQL
在MySQL中创建结果汇总表
创建一个新数据库:techbbs
mysql> create database techbbs;
创建一张新数据表:techbbs_logs_stat
mysql> create table techbbs_logs_stat(
logdate varchar(10) primary key,
pv int,
reguser int,
ip int,
jumper int);
导入操作
sqoop export --connect jdbc:mysql://cdh1:3306/techbbs --username root --password 000000 --table techbbs_logs_stat --fields-terminated-by ‘\001’ --export-dir '/user/hive/warehouse/db_hive.db/techbbs_2019_09_24’