资讯详情

网站日志分析:从56GB日志到五个运营指标

📅 2026/10/15 4:12:39 | 华诺云谱 👁 阅读
网站日志分析:从56GB日志到五个运营指标
简介《大数据综合案例-网站日志分析》是面向大数据学习者与数据分析人员的项目实战文档以国内技术学习论坛的Apache Common日志为对象完整讲解网站日志离线分析的过程。文档从项目背景出发说明历史日志数据约56GB、每日新增约150MB的文件情况并分解访问者IP、访问时间、访问资源、访问状态、访问流量组成的日志格式指导读者掌握从数据采集、清洗、统计到结果导出的全链路实现方法。资源为单份docx文档压缩包约1013KB结构清晰、内容完整。已有1402人学习或下载。文中详细给出PV浏览量、注册用户数、独立IP数、跳出率、板块热度排行榜等KPI的定义与计算公式并覆盖Shell上传日志至HDFS、MapReduce数据清洗、Hive统计分析、Sqoop导出至MySQL、HBase存储明细等关键开发环节同时包含表结构设计思路适合需要将大数据组件落地到真实日志分析场景的读者作为参考。1. 网站日志分析从 56GB 日志到五个运营指标网站日志分析这个场景很多人第一反应是上 Elasticsearch 或者接在线 BI但这套资源给的是一条完全不同的离线路线56GB 历史日志加上每天 150MB 增量日志用 HDFS 存、MapReduce 洗、Hive 算、Sqoop 导出 MySQL最后交给运营做决策参考。值钱的地方在于它算的是 PV、注册用户数、独立 IP、跳出率、板块热度这一类第三方统计工具拿不到的自定义指标而且是全链路可复现的。适合刚接触 Hadoop 生态、想用一套真实日志把离线分析流程走通的人也适合正在做离线数仓项目、需要参考清洗和指标口径的从业者。2. 数据形态与指标口径五个 KPI 的计算起点2.1 日志五字段追加式历史文件与天级文件这份数据来自某培训机构主办的技术学习论坛原始日志分两个阶段2012-05-29 之前约 56GB 全部追加在一个文件里之后每天生成一个约 150MB 的独立文件。两种形态直接影响后续上传和分区策略所以拿到任何日志数据先看它是一张大表还是按天分片这决定了你建 Hive 分区表的方式。日志每行固定 5 个字段解析起来非常规整字段含义示例IP访问者 IP192.168.1.2时间访问时间含时区30/May/2013:00:00:02 0800资源请求方法 URL HTTP 版本GET /forum.php HTTP/1.1状态码HTTP 状态200 / 404流量本次响应字节数532注意时间字段不是常见的 yyyy-MM-dd HH:mm:ss而是d/MMM/yyyy:HH:mm:ss格式月份是英文缩写。后面清洗阶段必须写解析类做转换这是个核心动作。2.2 五个 KPI 口径与公式这套流程最终要输出的是五个运营指标每个指标的 SQL 逻辑和口径必须先定清楚指标口径计算公式PV页面浏览量统计日志记录数可细分到栏目注册用户数点击注册页统计 URL 含member.php?modregister的记录独立 IP去重 IP 数对 ip 字段 COUNT(DISTINCT)跳出用户数只打开一个页面的 IP 数按 IP 分组后记录数为 1 的组求和跳出率跳出用户数 / PV跳出用户数除以 PV跳出率这个指标我在实际项目里见过不少误解。它是只浏览一个页面就离开的访问次数占总访问次数的比例用于衡量流量质量和广告投放效果。文档里有个说法很贴切跳出率高说明访客被引流到页面后又流失了相当于煮熟的鸭子飞了。它的 SQL 实现要用子查询先按 IP 分组再筛COUNT(ip)1的组不能直接用 DISTINCT 去重这是后面最容易翻车的地方。技术链路方面文档明确用的是 Linux Shell、HDFS、MapReduce、HBase、Hive、Sqoop。整套流程从上到下是定时脚本上传日志到 HDFS → MapReduce 清洗 → Hive 统计分析 → Sqoop 导出 MySQL → 指标查询走 MySQL、明细查询走 HBase。HBase 部分在文档里只给了表结构设计实操重点是前面这一条主线。3. 上传与调度Shell 脚本驱动 HDFS 日志入库3.1 三种上传方式怎么选日志服务器规模不同上传到 HDFS 的方案完全不同。文档给了三个档位我实际做项目时也是按这个逻辑判断的场景方案理由数据量小、服务器压力低Shell 直接 put简单直接一条命令搞定数据量较大、上传压力高NFS 共享目录日志先落 NFS再从其他节点拉取日志服务器多、数据量大Flume 采集分布式采集支持断点续传作为实验场景数据文件是百 MB 级别直接走第一种方案。但要注意一点脚本里的 put 命令是覆盖写还是追加取决于你的业务。日志场景一般按天生成独立文件天然避免冲突。3.2 techbbs_core.sh 脚本拆解这是整个流程的入口脚本把上传、清洗、统计、导出串在一起。先看核心的上传部分#!/bin/sh # step1. 取昨天日期作为日志文件名的一部分 yesterday$(date --date1 days ago %Y_%m_%d) # step2. 上传昨天生成的日志到 HDFS 指定目录 hadoop fs -put /usr/local/files/apache_logs/access_${yesterday}.log /project/techbbs/datadate --date1 days ago %Y_%m_%d是获取昨天的日期格式为2015_04_26刚好匹配日志文件命名规则access_2015_04_26.log。hadoop fs -put把本地文件原样复制到 HDFS不做任何处理这一步只是解决存储问题。目标目录/project/techbbs/data后续会作为 MapReduce 的输入路径。脚本里把上传和清洗写在一起时要注意清洗依赖上传完成所以两条命令在同一脚本内按顺序执行不要拆成两个 crontab 任务否则可能出现清洗任务跑的时候文件还没传完的情况。3.3 crontab 配置与测试脚本写好后交给 crontab 调度每天凌晨 1 点自动执行crontab -e # 每天 1:00 执行技术论坛日志处理脚本 * 1 * * * /usr/local/files/apache_logs/techbbs_core.shcrontab 五段格式分别是分、时、日、月、周* 1 * * *表示每天凌晨 1 点整执行。这里有个测试技巧日志文件是 2013 年的直接跑脚本匹配不到昨天的文件名所以需要先把两个日志文件重命名为当天和前一天对应的名字比如access_2015_04_25.log和access_2015_04_26.log再执行脚本验证。验证结果用crontab -l查看定时任务是否生效同时看 HDFS 上/project/techbbs/data目录下是否多出对应文件。我一般还会加一步手动执行一次脚本确认退出码是 0 再等 crontab 自动跑避免把环境问题留到凌晨才发现。4. 数据清洗MapReduce 日志解析与过滤的四个关键点4.1 LogParser 解析类时间格式转换是核心原始日志进 HDFS 后不能直接统计原因有三个状态码和流量字段用不上时间格式是英文月份缩写不适合直接做分区静态资源访问记录会污染 PV。清洗阶段用 MapReduce 完成第一步是写解析类把每行拆成五个字段。static class LogParser { // 原始时间格式30/May/2013:00:00:02 0800 public static final SimpleDateFormat FORMAT new SimpleDateFormat( d/MMM/yyyy:HH:mm:ss, Locale.ENGLISH); // 目标时间格式20130426120000 public static final SimpleDateFormat dateformat1 new SimpleDateFormat( yyyyMMddHHmmss); 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 parseIP(String line) { return line.split(- -)[0].trim(); } private String parseTime(String line) { int first line.indexOf([); int last line.indexOf(0800]); String time line.substring(first 1, last).trim(); Date date FORMAT.parse(time); return dateformat1.format(date); } private String parseURL(String line) { int first line.indexOf(\); int last line.lastIndexOf(\); return line.substring(first 1, last); } private String parseStatus(String line) { String trim line.substring(line.lastIndexOf(\) 1).trim(); return trim.split( )[0]; } private String parseTraffic(String line) { String trim line.substring(line.lastIndexOf(\) 1).trim(); return trim.split( )[1]; } }这段代码最需要注意的是SimpleDateFormat必须传Locale.ENGLISH。日志里的月份是May、Apr这类英文缩写默认 Locale 环境下解析MMM会直接抛 ParseException。我在本地复现文档里的清洗逻辑时第一次就踩了这个坑后来凡是解析英文月份一律显式指定 Locale。parseURL用两个引号截取中间内容得到的是类似GET /forum.php HTTP/1.1的完整请求串。parseStatus和parseTraffic都从最后一个引号后面取值再按空格拆分前一个是状态码后一个是流量。解析完成后状态码和流量这两个字段后续直接丢弃这也是清洗的意义之一。4.2 Mapper 过滤逻辑静态资源与协议前缀解析类拿到五个字段后Mapper 要做的过滤动作很关键它直接决定统计结果干不干净。static class MyMapper extends MapperLongWritable, Text, LongWritable, Text { LogParser logParser new LogParser(); Text outputValue new Text(); protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { final String[] parsed logParser.parse(value.toString()); // step1. 过滤静态资源和用户中心请求 if (parsed[2].startsWith(GET /static/) || parsed[2].startsWith(GET /uc_server)) { return; } // step2. 去掉 GET / 或 POST / 前缀 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. 去掉结尾的 HTTP/1.1 协议标记 if (parsed[2].endsWith( HTTP/1.1)) { parsed[2] parsed[2].substring(0, parsed[2].length() - HTTP/1.1.length()); } // step4. 只输出 ip、时间、url 三个字段 outputValue.set(parsed[0] \t parsed[1] \t parsed[2]); context.write(key, outputValue); } }三个判断缺一不可。GET /static/是静态资源前缀GET /uc_server是论坛用户中心这两类请求对统计 PV 和跳出率都没意义。step2 和 step3 是在做 URL 规整把GET /forum.php HTTP/1.1简化成forum.php这样后面统计板块热度时 URL 才是干净的。这里有个容易漏的点过滤条件只判断了GET开头如果日志里出现POST /static/这类请求会被放过去我通常会把静态资源的判断改成对 URL 主体做 contains 判断而不只依赖方法前缀。4.3 Reducer 与 Job 提交Reducer 这个场景不需要聚合只需要把 Mapper 输出的记录原样写出相当于确认清洗后的数据落盘。真正决定输出形态的是 Job 配置Job job new Job(conf, logClean); job.setJarByClass(LogCleanJob.class); job.setMapperClass(MyMapper.class); job.setReducerClass(MyReducer.class); job.setOutputKeyClass(LongWritable.class); job.setOutputValueClass(Text.class); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1);注意这份代码是 Hadoop 1.x 时代的 APIJob的构造方式和新版Job.getInstance(conf)略有不同但 Mapper/Reducer 的继承逻辑是通用的。我一般会在提交前检查输入路径是否存在输出路径是否已存在因为 Hadoop 不允许输出目录已存在重复跑同一个日期会导致任务直接失败。清洗完成后HDFS 上的/project/techbbs/cleaned/2015_04_26目录下生成part-m-00000文件每行三列IP、时间、URL用制表符分隔。这个目录就是下一步 Hive 分区表的数据源。5. 统计分析避坑与常见问题五条翻车记录5.1 分区表建好了查不到数现象建了外部表并执行 ADD PARTITIONSELECT COUNT(1)返回 0但 HDFS 上明明有数据文件。原因外部表的 LOCATION 指向了/project/techbbs/cleanedADD PARTITION 时 LOCATION 写成了/project/techbbs/cleaned而不是/project/techbbs/cleaned/2015_04_26Hive 在分区目录下找不到任何文件。解决ADD PARTITION 的 LOCATION 必须精确到日期子目录也就是LOCATION /project/techbbs/cleaned/2015_04_26。另外确认 HDFS 上该目录存在且文件不是_SUCCESS这种空标记。建完分区后用hive -e SHOW PARTITIONS techbbs验证分区是否挂上。5.2 跳出率子查询 HAVING 引用别名报错现象执行跳出用户数 HQL 时报错提示HAVING里找不到times字段或者结果明显偏大。原因GROUP BY ip HAVING times1中times是 SELECT 里的别名部分 Hive 版本不允许在 HAVING 子句引用别名。解决把 HAVING 条件改成GROUP BY ip HAVING COUNT(ip)1不依赖别名。同时注意跳出数是按 IP 分组后只出现一次的组数量不是直接COUNT(DISTINCT ip)这两个结果差很远。统计口径错了跳出率会高得离谱。5.3 时间解析崩溃ParseException现象MapReduce 清洗任务跑了一半抛java.text.ParseException进程失败退出。原因SimpleDateFormat(d/MMM/yyyy:HH:mm:ss)没有指定Locale.ENGLISH系统默认 Locale 解析不了英文月份缩写另外日志里如果出现格式异常的行比如时间字段为空或带了多余空格也会抛异常。解决构造 FORMAT 时强制传Locale.ENGLISHparseTime里对indexOf返回 -1 的情况做防御直接跳过该行。我一般会加一个计数器记录解析失败的行数任务结束后看计数是否异常避免坏数据悄悄蒸发。5.4 Sqoop 导出字段分隔符错位现象MySQL 里pv列的值全变成 0或者整列数据串位日期列变成乱码。原因Hive 用 CTAS 生成的汇总表底层文件默认分隔符是\001ASCII 码 1而 Sqoop export 命令里没有指定--fields-terminated-by或者写成了\t导入时字段对不上。解决Sqoop export 时指定--fields-terminated-by \001并且确认 MySQL 表字段顺序和 Hive 汇总表一致。这条我在项目里踩过看起来是值不对查半天发现是分隔符问题。sqoop export \ --connect jdbc:mysql://hadoop-master:3306/techbbs \ --username root \ --password admin \ --table techbbs_logs_stat \ --fields-terminated-by \001 \ --export-dir /hive/techbbs_2015_04_265.5 crontab 定时任务到点不执行现象脚本手动执行一切正常但 crontab 到点没跑查/var/log/cron也没报错。原因crontab 环境是精简的 shell 环境PATH 里没有 Java 和 Hadoop 的路径脚本里用的hadoop、hive命令直接找不到默认 shell 可能也不是 bash。解决在脚本开头显式声明环境变量或者所有命令使用绝对路径#!/bin/sh export JAVA_HOME/usr/local/java export HADOOP_HOME/usr/local/hadoop export PATH$HADOOP_HOME/bin:$JAVA_HOME/bin:$PATH我现在的习惯是在脚本第一行加上环境变量声明再用crontab -l确认任务格式最后手动改系统时间或临时把调度时间设为当前时间后一分钟跑通一次再恢复。6. 进阶验证给日志清洗链路加一道体检清洗和统计跑通只是第一步真正让人放心的是每次任务跑完都有一个客观标准确认数据没被洗坏。我习惯在每个日期目录跑完后追加一个检查脚本把它写进 techbbs_core.sh 末尾当天清洗完直接自检#!/bin/bash yesterday$1 raw/project/techbbs/data/access_${yesterday}.log cleaned/project/techbbs/cleaned/${yesterday} # 1. 清洗前后行数对比过滤比例异常时报警 raw_lines$(hadoop fs -cat ${raw} | wc -l) clean_lines$(hadoop fs -cat ${cleaned}/part-* | wc -l) echo raw${raw_lines} clean${clean_lines} ratio$(echo scale2;${clean_lines}/${raw_lines} | bc) # 2. 清洗后每行必须是 3 个字段 bad_fields$(hadoop fs -cat ${cleaned}/part-* | awk -F\t NF!3{count} END{print count0}) echo bad_fields${bad_fields} # 3. 时间字段必须是 14 位数字 bad_time$(hadoop fs -cat ${cleaned}/part-* | awk -F\t $2 !~ /^[0-9]{14}$/{count} END{print count0}) echo bad_time${bad_time} # 4. 残留静态资源请求数正常应为 0 bad_static$(hadoop fs -cat ${cleaned}/part-* | grep -c static || true) echo bad_static${bad_static}这个脚本做的事情不复杂但每一行都有实际意义。行数对比能看出过滤比例是否在合理区间如果某天清洗后的行数比原始还多说明解析逻辑出了问题或者有重复数据。字段数校验是防止 Mapper 输出格式变了导致 Hive 读不出来。时间格式校验能提前发现解析类有没有放过异常行。残留静态资源检查是成本最低的回归测试一旦过滤器被误改这里立刻暴露。触发方式是手动跑一次bash check_clean.sh 2015_04_26。所有检查项都通过后再执行后半段的 Hive 统计和 Sqoop 导出。从那以后我每次改完清洗逻辑都强制走一遍这个体检脚本确认四项指标全部正常才继续往下走。这套习惯帮我拦截过好几次因为调整过滤规则带来的数据质量事故希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑