1.1 項(xiàng)目來源
本次實(shí)踐的目的就在于通過對該技術(shù)論壇網(wǎng)站的tomcat access log日志進(jìn)行分析废境,計(jì)算該論壇的一些關(guān)鍵指標(biāo)没讲,供運(yùn)營者進(jìn)行決策時(shí)參考盐固。
PS:開發(fā)該系統(tǒng)的目的是為了獲取一些業(yè)務(wù)相關(guān)的指標(biāo)看疗,這些指標(biāo)在第三方工具中無法獲得的唆阿;
1.2 數(shù)據(jù)情況
該論壇數(shù)據(jù)有兩部分:
(1)歷史數(shù)據(jù)約56GB燥筷,統(tǒng)計(jì)到2012-05-29。這也說明院崇,在2012-05-29之前荆责,日志文件都在一個(gè)文件里邊,采用了追加寫入的方式亚脆。
(2)自2013-05-30起,每天生成一個(gè)數(shù)據(jù)文件盲泛,約150MB左右濒持。這也說明,從2013-05-30之后寺滚,日志文件不再是在一個(gè)文件里邊柑营。
圖2展示了該日志數(shù)據(jù)的記錄格式,其中每行記錄有5部分組成:訪問者IP村视、訪問時(shí)間官套、訪問資源、訪問狀態(tài)(HTTP狀態(tài)碼)蚁孔、本次訪問流量奶赔。
圖2 日志記錄數(shù)據(jù)格式
二、關(guān)鍵指標(biāo)KPI
2.1 瀏覽量PV
(1)定義:頁面瀏覽量即為PV(Page View)杠氢,是指所有用戶瀏覽頁面的總和站刑,一個(gè)獨(dú)立用戶每打開一個(gè)頁面就被記錄1 次。
(2)分析:網(wǎng)站總瀏覽量鼻百,可以考核用戶對于網(wǎng)站的興趣绞旅,就像收視率對于電視劇一樣。
計(jì)算公式:記錄計(jì)數(shù)温艇,從日志中獲取訪問次數(shù)因悲。
2.2 注冊用戶數(shù)
該論壇的用戶注冊頁面為member.php,而當(dāng)用戶點(diǎn)擊注冊時(shí)請求的又是member.php?mod=register的url勺爱。
計(jì)算公式:對訪問member.php?mod=register的url晃琳,計(jì)數(shù)。
2.3 IP數(shù)
(1)定義:一天之內(nèi)琐鲁,訪問網(wǎng)站的不同獨(dú)立 IP 個(gè)數(shù)加和蝎土。其中同一IP無論訪問了幾個(gè)頁面,獨(dú)立IP 數(shù)均為1绣否。
(2)分析:這是我們最熟悉的一個(gè)概念誊涯,無論同一個(gè)IP上有多少電腦,或者其他用戶蒜撮,從某種程度上來說暴构,獨(dú)立IP的多少跪呈,是衡量網(wǎng)站推廣活動好壞最直接的數(shù)據(jù)。
計(jì)算公式:對不同的訪問者ip取逾,計(jì)數(shù)
2.4 跳出率
(1)定義:只瀏覽了一個(gè)頁面便離開了網(wǎng)站的訪問次數(shù)占總的訪問次數(shù)的百分比耗绿,即只瀏覽了一個(gè)頁面的訪問次數(shù) / 全部的訪問次數(shù)匯總。
(2)分析:跳出率是非常重要的訪客黏性指標(biāo)砾隅,它顯示了訪客對網(wǎng)站的興趣程度:跳出率越低說明流量質(zhì)量越好误阻,訪客對網(wǎng)站的內(nèi)容越感興趣,這些訪客越可能是網(wǎng)站的有效用戶晴埂、忠實(shí)用戶究反。
PS:該指標(biāo)也可以衡量網(wǎng)絡(luò)營銷的效果,指出有多少訪客被網(wǎng)絡(luò)營銷吸引到宣傳產(chǎn)品頁或網(wǎng)站上之后儒洛,又流失掉了精耐,可以說就是煮熟的鴨子飛了。比如琅锻,網(wǎng)站在某媒體上打廣告推廣卦停,分析從這個(gè)推廣來源進(jìn)入的訪客指標(biāo),其跳出率可以反映出選擇這個(gè)媒體是否合適恼蓬,廣告語的撰寫是否優(yōu)秀惊完,以及網(wǎng)站入口頁的設(shè)計(jì)是否用戶體驗(yàn)良好。
計(jì)算公式:①統(tǒng)計(jì)一天內(nèi)只出現(xiàn)一條記錄的ip处硬,稱為跳出數(shù)专执;②跳出數(shù)/PV;
處理步驟
1 數(shù)據(jù)清洗
使用MapReduce對HDFS中的原始數(shù)據(jù)進(jìn)行清洗郁油,以便后續(xù)進(jìn)行統(tǒng)計(jì)分析本股;
2 統(tǒng)計(jì)分析
使用Hive對清洗后的數(shù)據(jù)進(jìn)行統(tǒng)計(jì)分析;
數(shù)據(jù)清洗
一桐腌、數(shù)據(jù)情況分析
1.1 數(shù)據(jù)情況回顧
該論壇數(shù)據(jù)有兩部分:
(1)歷史數(shù)據(jù)約56GB拄显,統(tǒng)計(jì)到2012-05-29。這也說明案站,在2012-05-29之前躬审,日志文件都在一個(gè)文件里邊,采用了追加寫入的方式蟆盐。
(2)自2013-05-30起承边,每天生成一個(gè)數(shù)據(jù)文件,約150MB左右石挂。這也說明博助,從2013-05-30之后,日志文件不再是在一個(gè)文件里邊痹愚。
圖1展示了該日志數(shù)據(jù)的記錄格式富岳,其中每行記錄有5部分組成:訪問者IP蛔糯、訪問時(shí)間、訪問資源窖式、訪問狀態(tài)(HTTP狀態(tài)碼)蚁飒、本次訪問流量。
圖1 日志記錄數(shù)據(jù)格式
本次使用數(shù)據(jù)來自于兩個(gè)2013年的日志文件萝喘,分別為access_2013_05_30.log與access_2013_05_31.log淮逻,下載地址為:http://pan.baidu.com/s/1pJE7XR9
1.2 要清理的數(shù)據(jù)
(1)根據(jù)前一篇的關(guān)鍵指標(biāo)的分析,我們所要統(tǒng)計(jì)分析的均不涉及到訪問狀態(tài)(HTTP狀態(tài)碼)以及本次訪問的流量阁簸,于是我們首先可以將這兩項(xiàng)記錄清理掉爬早;
(2)根據(jù)日志記錄的數(shù)據(jù)格式,我們需要將日期格式轉(zhuǎn)換為平常所見的普通格式如20150426這種强窖,于是我們可以寫一個(gè)類將日志記錄的日期進(jìn)行轉(zhuǎn)換;
(3)由于靜態(tài)資源的訪問請求對我們的數(shù)據(jù)分析沒有意義削祈,于是我們可以將"GET /staticsource/"開頭的訪問記錄過濾掉翅溺,又因?yàn)镚ET和POST字符串對我們也沒有意義,因此也可以將其省略掉髓抑;
二咙崎、數(shù)據(jù)清洗過程
2.1 定期上傳日志至HDFS
首先,把日志數(shù)據(jù)上傳到HDFS中進(jìn)行處理吨拍,可以分為以下幾種情況:
(1)如果是日志服務(wù)器數(shù)據(jù)較小褪猛、壓力較小,可以直接使用shell命令把數(shù)據(jù)上傳到HDFS中羹饰;
(2)如果日志服務(wù)器非常多伊滋、數(shù)據(jù)量大,使用flume進(jìn)行數(shù)據(jù)處理队秩;
這里我們的實(shí)驗(yàn)數(shù)據(jù)文件較小笑旺,因此直接采用第一種Shell命令方式。
清洗之前
27.19.74.143 - - [30/May/2013:17:38:20 +0800] "GET /static/image/common/faq.gif HTTP/1.1" 200 1127
110.52.250.126 - - [30/May/2013:17:38:20 +0800] "GET /data/cache/style_1_widthauto.css?y7a HTTP/1.1" 200 1292
27.19.74.143 - - [30/May/2013:17:38:20 +0800] "GET /static/image/common/hot_1.gif HTTP/1.1" 200 680
27.19.74.143 - - [30/May/2013:17:38:20 +0800] "GET /static/image/common/hot_2.gif HTTP/1.1" 200 682
清洗之后
110.52.250.126 20130530173820 data/cache/style_1_widthauto.css?y7a
110.52.250.126 20130530173820 source/plugin/wsh_wx/img/wsh_zk.css
110.52.250.126 20130530173820 data/cache/style_1_forum_index.css?y7a
110.52.250.126 20130530173820 source/plugin/wsh_wx/img/wx_jqr.gif
27.19.74.143 20130530173820 data/attachment/common/c8/common_2_verify_icon.png
27.19.74.143 20130530173820 data/cache/common_smilies_var.js?y7a
2.2 編寫MapReduce程序清理日志
package com.neuedu;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class LogCleanJob {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration conf = new Configuration();
if (args.length != 2) {
System.err.println("Usage:Merge and duplicate removal <in> <out>");
System.exit(2);
}
Job job = Job.getInstance(conf, "LogCleanJob");
job.setJarByClass(LogCleanJob.class);
job.setMapperClass(MyMapper.class);
job.setReducerClass(MyReducer.class);
job.setMapOutputKeyClass(LongWritable.class);
job.setMapOutputValueClass(Text.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
static class MyMapper extends
Mapper<LongWritable, Text, LongWritable, Text> {
LogParser logParser = new LogParser();
Text outputValue = new Text();
protected void map(
LongWritable key,
Text value,
Context context)
throws java.io.IOException, InterruptedException {
final String[] parsed = logParser.parse(value.toString());
// step1.過濾掉靜態(tài)資源訪問請求
if (parsed[2].startsWith("GET /static/")
|| parsed[2].startsWith("GET /uc_server")) {
return;
}
// step2.過濾掉開頭的指定字符串
if (parsed[2].startsWith("GET /")) {
parsed[2] = parsed[2].substring("GET /".length());
} else if (parsed[2].startsWith("POST /")) {
parsed[2] = parsed[2].substring("POST /".length());
}
// step3.過濾掉結(jié)尾的特定字符串
if (parsed[2].endsWith(" HTTP/1.1")) {
parsed[2] = parsed[2].substring(0, parsed[2].length()
- " HTTP/1.1".length());
}
// step4.只寫入前三個(gè)記錄類型項(xiàng)
outputValue.set(parsed[0] + "\t" + parsed[1] + "\t" + parsed[2]);
context.write(key, outputValue);
}
}
static class MyReducer extends
Reducer<LongWritable, Text, Text, NullWritable> {
protected void reduce(
LongWritable k2,
Iterable<Text> values,
Context context)
throws java.io.IOException, InterruptedException {
context.write(values.iterator().next(), NullWritable.get());
}
}
/*
* 日志解析類
*/
static class LogParser {
public static final SimpleDateFormat FORMAT = new SimpleDateFormat(
"d/MMM/yyyy:HH:mm:ss", Locale.ENGLISH);
public static final SimpleDateFormat dateformat1 = new SimpleDateFormat(
"yyyyMMddHHmmss");
public static void main(String[] args) throws ParseException {
final String S1 = "27.19.74.143 - - [30/May/2013:17:38:20 +0800] \"GET /static/image/common/faq.gif HTTP/1.1\" 200 1127";
LogParser parser = new LogParser();
final String[] array = parser.parse(S1);
System.out.println("樣例數(shù)據(jù): " + S1);
System.out.format(
"解析結(jié)果: ip=%s, time=%s, url=%s, status=%s, traffic=%s",
array[0], array[1], array[2], array[3], array[4]);
}
/**
* 解析英文時(shí)間字符串
*
* @param string
* @return
* @throws ParseException
*/
private Date parseDateFormat(String string) {
Date parse = null;
try {
parse = FORMAT.parse(string);
} catch (ParseException e) {
e.printStackTrace();
}
return parse;
}
/**
* 解析日志的行記錄
*
* @param line
* @return 數(shù)組含有5個(gè)元素馍资,分別是ip筒主、時(shí)間、url鸟蟹、狀態(tài)乌妙、流量
*/
public String[] parse(String line) {
String ip = parseIP(line);
String time = parseTime(line);
String url = parseURL(line);
String status = parseStatus(line);
String traffic = parseTraffic(line);
return new String[] { ip, time, url, status, traffic };
}
private String parseTraffic(String line) {
final String trim = line.substring(line.lastIndexOf("\"") + 1)
.trim();
String traffic = trim.split(" ")[1];
return traffic;
}
private String parseStatus(String line) {
final String trim = line.substring(line.lastIndexOf("\"") + 1)
.trim();
String status = trim.split(" ")[0];
return status;
}
private String parseURL(String line) {
final int first = line.indexOf("\"");
final int last = line.lastIndexOf("\"");
String url = line.substring(first + 1, last);
return url;
}
private String parseTime(String line) {
final int first = line.indexOf("[");
final int last = line.indexOf("+0800]");
String time = line.substring(first + 1, last).trim();
Date date = parseDateFormat(time);
return dateformat1.format(date);
}
private String parseIP(String line) {
String ip = line.split("- -")[0].trim();
return ip;
}
}
}
一、借助Hive進(jìn)行統(tǒng)計(jì)
1.1 準(zhǔn)備工作:建立分區(qū)表
為了能夠借助Hive進(jìn)行統(tǒng)計(jì)分析建钥,首先我們需要將清洗后的數(shù)據(jù)存入Hive中藤韵,那么我們需要先建立一張表。這里我們選擇分區(qū)表熊经,以日期作為分區(qū)的指標(biāo)荠察,建表語句如下:(這里關(guān)鍵之處就在于確定映射的HDFS位置置蜀,我這里是/project/techbbs/cleaned即清洗后的數(shù)據(jù)存放的位置)
hive> dfs -mkdir -p /project/techbbs/cleaned
hive>CREATE EXTERNAL TABLE techbbs(ip string, atime string, url string) PARTITIONED BY (logdate string) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LOCATION '/project/techbbs/cleaned';
建立了分區(qū)表之后,就需要增加一個(gè)分區(qū)悉盆,增加分區(qū)的語句如下:(這里主要針對20150425這一天的日志進(jìn)行分區(qū))
hive>ALTER TABLE techbbs ADD PARTITION(logdate='2015_04_25') LOCATION '/project/techbbs/cleaned/2015_04_25';
hive> load data local inpath '/root/cleaned' into table techbbs3 partition(logdate='2015_04_25');
1.2 使用HQL統(tǒng)計(jì)關(guān)鍵指標(biāo)
(1)關(guān)鍵指標(biāo)之一:PV量
頁面瀏覽量即為PV(Page View)盯荤,是指所有用戶瀏覽頁面的總和,一個(gè)獨(dú)立用戶每打開一個(gè)頁面就被記錄1 次焕盟。這里秋秤,我們只需要統(tǒng)計(jì)日志中的記錄個(gè)數(shù)即可,HQL代碼如下:
hive>CREATE TABLE techbbs_pv_2015_04_25 AS SELECT COUNT(1) AS PV FROM techbbs WHERE logdate='2015_04_25';
(2)關(guān)鍵指標(biāo)之二:注冊用戶數(shù)
該論壇的用戶注冊頁面為member.php脚翘,而當(dāng)用戶點(diǎn)擊注冊時(shí)請求的又是member.php?mod=register的url灼卢。因此,這里我們只需要統(tǒng)計(jì)出日志中訪問的URL是member.php?mod=register的即可来农,HQL代碼如下:
hive>CREATE TABLE techbbs_reguser_2015_04_25 AS SELECT COUNT(1) AS REGUSER FROM techbbs WHERE logdate='2015_04_25' AND INSTR(url,'member.php?mod=register')>0;
(3)關(guān)鍵指標(biāo)之三:獨(dú)立IP數(shù)
一天之內(nèi)鞋真,訪問網(wǎng)站的不同獨(dú)立 IP 個(gè)數(shù)加和。其中同一IP無論訪問了幾個(gè)頁面沃于,獨(dú)立IP 數(shù)均為1涩咖。因此,這里我們只需要統(tǒng)計(jì)日志中處理的獨(dú)立IP數(shù)即可繁莹,在SQL中我們可以通過DISTINCT關(guān)鍵字檩互,在HQL中也是通過這個(gè)關(guān)鍵字:
hive>CREATE TABLE techbbs_ip_2015_04_25 AS SELECT COUNT(DISTINCT ip) AS IP FROM techbbs WHERE logdate='2015_04_25';
(4)關(guān)鍵指標(biāo)之四:跳出用戶數(shù)
只瀏覽了一個(gè)頁面便離開了網(wǎng)站的訪問次數(shù),即只瀏覽了一個(gè)頁面便不再訪問的訪問次數(shù)咨演。這里闸昨,我們可以通過用戶的IP進(jìn)行分組,如果分組后的記錄數(shù)只有一條薄风,那么即為跳出用戶饵较。將這些用戶的數(shù)量相加,就得出了跳出用戶數(shù)遭赂,HQL代碼如下:
hive>select count(*) from (select ip,count(ip) as num from techbbs group by ip) as tmpTable where tmpTable.num = 1;
PS:跳出率是指只瀏覽了一個(gè)頁面便離開了網(wǎng)站的訪問次數(shù)占總的訪問次數(shù)的百分比告抄,即只瀏覽了一個(gè)頁面的訪問次數(shù) / 全部的訪問次數(shù)匯總。這里嵌牺,我們可以將這里得出的跳出用戶數(shù)/PV數(shù)即可得到跳出率打洼。