一、說在前面的話
上一篇,樓主介紹了使用flume集群來模擬網站產生的日志數據收集到hdfs。但我們所采集的日志數據是不規則的,同時也包含了許多無用的日志。當需要分析一些核心指標來滿足系統業務決策的時候,對日志的數據清洗在所難免,樓主本篇將介紹如何使用mapreduce程序對日志數據進行清洗,將清洗后的結構化數據存儲到hive,並進行相關指標的提取。
先明白幾個概念:
1)PV(Page View)。頁面瀏覽量即為PV,是指所有用戶瀏覽頁面的總和,一個獨立用戶每打開一個頁面就被記錄1 次。計算方式為:記錄計數
2)注冊用戶數。對注冊頁面訪問的次數。計算方式:對訪問member.php?mod=register的url,計數
3)IP數。一天之內,訪問網站的不同獨立IP 個數加和。其中同一IP無論訪問了幾個頁面,獨立IP 數均為1。這是我們最熟悉的一個概念,無論同一個IP上有多少台主機,或者其他用戶,從某種程度上來說,獨立IP的多少,是衡量網站推廣活動好壞最直接的數據。計算方式:對不同ip,計數
4)跳出率。只瀏覽了一個頁面便離開了網站的訪問次數占總的訪問次數的百分比,即只瀏覽了一個頁面的訪問次數 / 全部的訪問次數匯總。跳出率是非常重要的訪客黏性指標,它顯示了訪客對網站的興趣程度。跳出率越低說明流量質量越好,訪客對網站的內容越感興趣,這些訪客越可能是網站的有效用戶、忠實用戶。該指標也可以衡量網絡營銷的效果,指出有多少訪客被網絡營銷吸引到宣傳產品頁或網站上之后,又流失掉了,可以說就是煮熟的鴨子飛了。比如,網站在某媒體上打廣告推廣,分析從這個推廣來源進入的訪客指標,其跳出率可以反映出選擇這個媒體是否合適,廣告語的撰寫是否優秀,以及網站入口頁的設計是否用戶體驗良好。
計算方式:(1)統計一天內只出現一條記錄的ip,稱為跳出數
(2)跳出數/PV
本次樓主只做以上幾項簡單指標的分析,各個網站的作用領域不一樣,所涉及的分析指標也有很大差別,各位同學可以根據自己的需求盡情拓展。廢話不多說,上干貨。
二、環境准備
1)hadoop集群。樓主用的6個節點的hadoop2.7.3集群,各位同學可以根據自己的實際情況進行搭建,但至少需要1台偽分布式的。(參考http://www.cnblogs.com/qq503665965/p/6790580.html)
2)hive。用於對各項核心指標進行分析(安裝樓主不再介紹了)
3)mysql。存儲分析后的數據指標。
4)sqoop。從hive到mysql的數據導入。
三、數據清洗
我們先看看從flume收集到hdfs中的源日志數據格式:
1 27.19.74.143 - - [30/4/2017:17:38:20 +0800] "GET /static/image/common/faq.gif HTTP/1.1" 200 1127 2 211.97.15.179 - - [30/4/2017:17:38:22 +0800] "GET /home.php?mod=misc&ac=sendmail&rand=1369906181 HTTP/1.1" 200 -
上面包含條個靜態資源日志和一條正常鏈接日志(樓主這里不做靜態資源日志的分析),需要將以 /static 開頭的日志文件過濾掉;時間格式需要轉換為時間戳;去掉IP與時間之間的無用符號;過濾掉請求方式;“/”分隔符、http協議、請求狀態及當次流量。效果如下:
1 211.97.15.179 20170430173820 home.php?mod=misc&ac=sendmail&rand=1369906181
先寫個日志解析類,測試是否能解析成功,我們再寫mapreduce程序:
1 package mapreduce; 2 3 import java.text.ParseException; 4 import java.text.SimpleDateFormat; 5 import java.util.Date; 6 import java.util.Locale; 7 8 9 public class LogParser { 10 public static final SimpleDateFormat FORMAT = new SimpleDateFormat("d/MM/yyyy:HH:mm:ss", Locale.ENGLISH); 11 public static final SimpleDateFormat dateformat1=new SimpleDateFormat("yyyyMMddHHmmss"); 12 public static void main(String[] args) throws ParseException { 13 final String S1 = "27.19.74.143 - - [30/04/2017:17:38:20 +0800] \"GET /static/image/common/faq.gif HTTP/1.1\" 200 1127"; 14 LogParser parser = new LogParser(); 15 final String[] array = parser.parse(S1); 16 System.out.println("源數據: "+S1); 17 System.out.format("清洗結果數據: ip=%s, time=%s, url=%s, status=%s, traffic=%s", array[0], array[1], array[2], array[3], array[4]); 18 } 19 /** 20 * 解析英文時間字符串 21 * @param string 22 * @return 23 * @throws ParseException 24 */ 25 private Date parseDateFormat(String string){ 26 Date parse = null; 27 try { 28 parse = FORMAT.parse(string); 29 } catch (ParseException e) { 30 e.printStackTrace(); 31 } 32 return parse; 33 } 34 /** 35 * 解析日志的行記錄 36 * @param line 37 * @return 數組含有5個元素,分別是ip、時間、url、狀態、流量 38 */ 39 public String[] parse(String line){ 40 String ip = parseIP(line); 41 String time = parseTime(line); 42 String url = parseURL(line); 43 String status = parseStatus(line); 44 String traffic = parseTraffic(line); 45 46 return new String[]{ip, time ,url, status, traffic}; 47 } 48 49 private String parseTraffic(String line) { 50 final String trim = line.substring(line.lastIndexOf("\"")+1).trim(); 51 String traffic = trim.split(" ")[1]; 52 return traffic; 53 } 54 private String parseStatus(String line) { 55 final String trim = line.substring(line.lastIndexOf("\"")+1).trim(); 56 String status = trim.split(" ")[0]; 57 return status; 58 } 59 private String parseURL(String line) { 60 final int first = line.indexOf("\""); 61 final int last = line.lastIndexOf("\""); 62 String url = line.substring(first+1, last); 63 return url; 64 } 65 private String parseTime(String line) { 66 final int first = line.indexOf("["); 67 final int last = line.indexOf("+0800]"); 68 String time = line.substring(first+1,last).trim(); 69 Date date = parseDateFormat(time); 70 return dateformat1.format(date); 71 } 72 private String parseIP(String line) { 73 String ip = line.split("- -")[0].trim(); 74 return ip; 75 } 76 }
輸出結果:
1 源數據: 27.19.74.143 - - [30/04/2017:17:38:20 +0800] "GET /static/image/common/faq.gif HTTP/1.1" 200 1127 2 清洗結果數據: ip=27.19.74.143, time=20170430173820, url=GET /static/image/common/faq.gif HTTP/1.1, status=200, traffic=1127
再看mapreduce業務邏輯,在map中,我們需要拿出ip、time、url這三個屬性的值,同時過濾掉靜態資源日志。map的k1用默認的LongWritable就OK,v1不用說Text,k2、v2與k1、v1類型對應就行:
1 static class MyMapper extends Mapper<LongWritable, Text, LongWritable, Text>{ 2 LogParser logParser = new LogParser(); 3 Text v2 = new Text(); 4 @Override 5 protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, LongWritable, Text>.Context context) 6 throws IOException, InterruptedException { 7 final String[] parsed = logParser.parse(value.toString()); 8 9 //過濾掉靜態信息 10 if(parsed[2].startsWith("GET /static/") || parsed[2].startsWith("GET /uc_server")){ 11 return; 12 } 13 //過掉開頭的特定格式字符串 14 if(parsed[2].startsWith("GET /")){ 15 parsed[2] = parsed[2].substring("GET /".length()); 16 } 17 else if(parsed[2].startsWith("POST /")){ 18 parsed[2] = parsed[2].substring("POST /".length()); 19 } 20 //過濾結尾的特定格式字符串 21 if(parsed[2].endsWith(" HTTP/1.1")){ 22 parsed[2] = parsed[2].substring(0, parsed[2].length()-" HTTP/1.1".length()); 23 } 24 v2.set(parsed[0]+"\t"+parsed[1]+"\t"+parsed[2]); 25 context.write(key, v2); 26 }
reduce相對來說就比較簡單了,我們只需再講map的輸出寫到一個文件中就OK:
1 static class MyReducer extends Reducer<LongWritable, Text, Text, NullWritable>{ 2 @Override 3 protected void reduce(LongWritable arg0, Iterable<Text> arg1, 4 Reducer<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException { 5 for (Text v2 : arg1) { 6 context.write(v2, NullWritable.get()); 7 } 8 } 9 }
最后,組裝JOB:
1 public static void main(String[] args) throws IllegalArgumentException, IOException, ClassNotFoundException, InterruptedException { 2 Job job = Job.getInstance(new Configuration()); 3 job.setJarByClass(LogParser.class); 4 job.setMapperClass(MyMapper.class); 5 job.setMapOutputKeyClass(LongWritable.class); 6 job.setMapOutputValueClass(Text.class); 7 FileInputFormat.setInputPaths(job, new Path("/logs/20170430.log")); 8 job.setReducerClass(MyReducer.class); 9 job.setOutputKeyClass(Text.class); 10 job.setOutputValueClass(NullWritable.class); 11 FileOutputFormat.setOutputPath(job, new Path("/20170430")); 12 job.waitForCompletion(true); 13 }
mapreduce完成后就是運行job了:
1)打包,mapreduce程序為loger.jar
2)上傳jar包。運行loger.jar hadoop jar loger.jar
運行結果:
hdfs多了20170430目錄:
我們下載下來看看清洗后的數據是否符合要求:
日志數據的清洗到此就完成了,接下來我們要在此之上使用hive提取核心指標數據。
四、核心指標分析
1)構建一個外部分區表,sql腳本如下:
1 CREATE EXTERNAL TABLE sitelog(ip string, atime string, url string) PARTITIONED BY (logdate string) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LOCATION '/20170430';
2)增加分區,sql腳本如下:
ALTER TABLE sitelog ADD PARTITION(logdate='20170430') LOCATION '/sitelog_cleaned/20170430';
3)統計每日PV,sql腳本如下:
1 CREATE TABLE sitelog_pv_20170430 AS SELECT COUNT(1) AS PV FROM sitelog WHERE logdate='20170430';
4)統計每日注冊用戶數,sql腳本如下:
1 CREATE TABLE sitelog_reguser_20170430 AS SELECT COUNT(1) AS REGUSER FROM sitelog WHERE logdate=20170430' AND INSTR(url,'member.php?mod=register')>0;
5)統計每日獨立IP,sql腳本如下:
1 CREATE TABLE site_ip_20170430 AS SELECT COUNT(DISTINCT ip) AS IP FROM sitelog WHERE logdate='20170430';
6)統計每日跳出的用戶數,sql腳本如下:
CREATE TABLE sitelog_jumper_20170430 AS SELECT COUNT(1) AS jumper FROM (SELECT COUNT(ip) AS times FROM sitelog WHERE logdate='20170430' GROUP BY ip HAVING times=1) e;
7)把每天統計的數據放入一張表中,sql腳本如下:
1 CREATE TABLE sitelog_20170430 AS SELECT '20170430', a.pv, b.reguser, c.ip, d.jumper FROM sitelog_pv_20170430 a JOIN sitelog_reguser_20170430 b ON 1=1 JOIN sitelog_ip_20170430 c ON 1=1 JOIN sitelog_jumper_20170430 d ON 1=1 ;
8)使用sqoop把數據導出到mysql中:
sqoop export --connect jdbc:mysql://hadoop02:3306/sitelog --username root --password root --table sitelog-result --fields-terminated-by '\001' --export-dir '/user/hive/warehouse/sitelog_20170430'
結果如下:
2017年4月30日日志分析結果:PV數為:169857;當日注冊用戶數:28;獨立IP數:10411;跳出數:3749.
到此,一個簡單的網站日志分析樓主就介紹完了,后面可視化的展示樓主就不寫了,比較簡單。相關代碼地址:https://github.com/LJunChina/hadoop