[toc]
用户需求
- 学生成绩
- 以学生为单位:计算每个学生总的平均分(按年限分)
- 图书馆数据(按借阅为准)
- 以学生为单位:计算每个学生借阅总数 (按年限分)
- 以学生为单位:计算每个学生借阅类型ABCD(按年限分)
- 只统计学生借阅次数,忽略学生还书数据
- 消费数据
- 以学生为单位:消费总额(按年限分)
- 删除学生充值记录
- 门禁数据
- 以学生为单位:9点后进校门次数,11点进校门(按年限分)
不允许使用MySQL等数据库进行数据存储或者计算
系统以及软件要求
系统要求
硬盘:100GB及以上
处理器:Intel/AMD
RAM:4GB以及以上
软件要求
操作系统:Linux
计算软件:Hadoop
运行环境:Java
目录结构及文件结构
目录结构
+ Root
+--- booklist_1
+--- booklist_2
+--- booklist_3
+--- booklist_4
+--- MJXXB.sql
+--- view.sql(更名为v_tsg_jylog.sql)
+--- YKTYHXFSJZL_NEW.sql
+--- xueshengchengji.tsv
文件结构
booklist_x(x为从1开始的自然数序列)
ID, SSH1, TM, ZRZ, CBRQ, BZBM, DLH, DJ1, CBZ, JYCS, SSH2, DJ2 (ID列在booklist文件中通过excel导出功能去除)
$1 类型(取第一位)
$6 索引号
$7 单价
MJXXB.sql
USER_ID, SBBH, SBMC, SBLXBH, SBLX, SKRQ, SKSJ
$1 学号
$6 日期
$7 时间
view.sql(v_tsg_jylog.sql)
XM, JSZH, ZJM, CZLX, TSCCH, TSTXM, CLSJ, SM
$1 姓名
$2 学号
$4 操作类型
$5 索引号
$7 时间
YKTYHXFSJZL_NEW.sql
YKTYHXFSJZL_ID, SOURCETABLE, ID, ECODE, NOTECASE, CUSTOMERID, OUTID, CARDSN, SAVEOPCOUNT, OPCOUNT, CZSJ, YE, CZE, MNGFARE, ACCCODE, DSCRP, CZLX, JYLSH, XH, JYLXBM, JYLXBMMC, XFDD
$11 刷卡时间
$12 余额
$13 充值额
$19 学号
$21 刷卡类型
xueshengchengji.tsv
XH, XZB, DOSZJ, ZYMC, KCMC, CJ, BKCJ, CXCJ, CXBJ
$1 学号
$3 学年
$5 科目
$6 成绩
$7 补考成绩
操作思路
如何清理文件?
windows环境下可以使用WinHex软件等将无用数据全文查找通过替换的方式删除
Linux下使用awk,sed等命令进行处理
我需要怎样的数据结构?
数据结构关系到算法的实现,合适的数据结构将使得程序在计算上省去多余的硬件开销和时间开销,所以数据结构在正式工作开始以前变得尤为关键.那么我们需要怎样的数据结构呢?
Hadoop的数据计算模式实现是通过key->value的形式进行的,所以数据key,value的格式就变得尤为关键.由于是学生数据,所以,学生学号也就成了每个数据必不可少的字段,所以,key一定是学生的学号,value就是表中的有用数据了.所以,基本的数据结构我们可以确定:
key(学号),value(数据字段1,数据字段2,数据字段3,...)
通过awk/sed进行数据清洗,最后就可以分别得到以上的图书馆记录,饭卡消费记录等清洗以后的数据文件.
通过观察文件结构我们发现,除了在图书馆的记录中我们有姓名信息,其他地方都没有姓名信息了.那么我们就需要从图书馆的记录中获取姓名,并通过学号关联到姓名.同时我们所需要的图书类型数据,需要从图书列表中获取,所以,我们需要通过索书号来从图书列表中获取图书的类型.
booklist -> get(bookType, price) -> save(bookIndex)
view.sql -> get(Name) -> save(ID)
将图书类型和姓名id单独保存成文件最后做两表关联就可以得到最后的结果.
有没有更好的办法?
可以看到上一步中我们已经确定了基本的数据格式,通过Hadoop进行最少5次计算就可以分别得到结果了,但是有没有更好的办法呢?我们需要的有两个数据都需要通过外表关联的方法取得,一个是图书的类型,另一个就是学生的姓名.在通过awk/sed进行数据清洗的时候,有没有办法直接将这两个字段关联到数据结果中呢?
需要注意的问题是,无论是hadoop还是linux的awk/sed都是单行操作,这就表示着,要么最后通过Hadoop全表扫描的方式获取到学生的姓名,要么直接通过linux进行全表扫描进行关联.这个时候你可能会想,如果图书类型和学生姓名的key(图书类型的key对应图书的索引号,学生姓名的key对应学生的学号)是在数据库中该多方便,就可以直接通过查找key的方法获取到value.那么有没有办法实现一个数据库通过key去获取value的数据库呢?
还记得你在explorer里进入文件目录的时候么,或者当你要打开hosts而你直接复制粘贴 vi /etc/hosts 或者 c:\window\system32\dirvers\hosts的时候么?通过路径我们可以直接访问文件,那么路径就是你的key,文件内容就是你需要的value.从而我们在处理数据的时候,将学号作为文件名,姓名作为文件内容的形式保存到文件.这样,我们的awk在print学号的时候,就取得了唯一的一个key,再去我们的文件数据库中通过cat去访问这个key,就获得对应学号的学生姓名,即vaule,通过print重定向到文件,就实现了不用全表扫描的信息关联,图书类型也是同理的.
细心的同学可能还会注意到我们的数据格式在加入了姓名以后,为了方便hadoop进行计算,Key就变成了”学号 姓名”的形式,但是value在每个表里的格式确实类似的.既然key相同,有没有办法将最后的数据文件变成单文件呢,而不是每个数据做了清洗以后都为独立的单个文件呢?可以通过添加说明的方式进行.比如门禁信息和刷卡消费
name id value
tom 10000 EG:2008-01-01,07:00:00
tom 10000 MC:200,3,2008-01-02 07:00:00
*EG:entrance Gurad; MC: meal card*
通过value最前面Symbol判断数据来源,hadoop在计算的时候进行选择.就实现了单个文件记录的保存.
数据清洗
Linux平台下的清洗工作
通过awk,sed等linux命令,最后通过print进行重定向进行处理.
Windows平台下的清洗工作
通过使用WinHex等软件进行替换
示例代码
数据清洗脚本
#!/bin/bash
echo "请将图书数据文件另存为txt文件,请去除空行和索引号,并重命名为booklist_x的格式,x为自然数列"
echo "======================================================================================="
echo -n "是否清空缓存文件以及数据结果文件:(1=yes,0=no)"
starttime=`date +'%Y-%m-%d %H:%M:%S'`
read dec
if [ $dec -eq "1" ]; then
if [ -d "./tmp_book" ]; then
rm -rf ./tmp_book
mkdir -m 777 ./tmp_book
fi
if [ -d "./tmp_student" ]; then
rm -rf ./tmp_student
mkdir -m 777 ./tmp_student
fi
if [ -d "./tmp_result" ]; then
rm -rf ./tmp_result
mkdir -m 777 ./tmp_result
fi
elif [ $dec -eq "0" ]; then
if [ ! -d "./tmp_book" ]; then
mkdir -m 777 ./tmp_book
fi
if [ ! -d "./tmp_student" ]; then
mkdir -m 777 ./tmp_student
fi
if [ ! -d "./tmp_result" ]; then
mkdir -m 777 ./tmp_student
fi
else
#计算执行时间
endtime=`date +'%Y-%m-%d %H:%M:%S'`
start_seconds=$(date --date="$starttime" +%s);
end_seconds=$(date --date="$endtime" +%s);
echo "本次运行时间: "$((end_seconds-start_seconds))"s"
exit
fi
echo "===========================生成图书索引数据============================="
# 处理图书索引数据
awk -F '\t' '{print $1","$7 >> "./tmp_book/"$6}' booklist_1
awk -F '\t' '{print $1","$7 >> "./tmp_book/"$6}' booklist_2
awk -F '\t' '{print $1","$7 >> "./tmp_book/"$6}' booklist_3
awk -F '\t' '{print $1","$7 >> "./tmp_book/"$6}' booklist_4
# 处理图书索引数据完毕
echo "=========================图书索引数据生成完成============================="
#
echo "=======================生成数据索引数据并产生借阅结果========================="
# 处理图书馆借阅数据同时获取学生姓名(姓名,学号)
awk -F '(' '{print $2}' v_tsg_jylog.sql | \
awk -F ')' '{print $1}' | \
sed "s/'//g" | \
awk -F ', ' '{ print $1 > "./tmp_student/"$2 ; print $0}' | \
awk -F ', ' '{ system("if [ -f ./tmp_book/"$5" ]; then book=$(cat ./tmp_book/"$5"); echo "$1"\t"$2"\tBB:"$4",$book,"$7" >> ./tmp_result/"$2"; else echo "$1"\t"$2"\tBB:"$4",0,0,"$7" >> ./tmp_result/"$2"; fi") }'
# 图书馆借阅数据处理完成学生姓名获取完成
echo "=====================生成数据索引数据并产生借阅结果完成========================="
#
echo "=================================生成门禁数据================================"
# 处理门禁数据(name,id)
awk -F '(' '{print $2}' MJXXB.sql | \
awk -F ')' '{print $1}' | \
sed "s/'//g" | \
awk -F ', ' '{ system("if [ -f ./tmp_student/"$1" ]; then name=$(cat ./tmp_student/"$1"); echo $name\t"$1"\tEG:"$6","$7" >> ./tmp_result/"$1"; else echo "$1"\t"$1"\tEG:"$6","$7" >> ./tmp_result/"$1"; fi") }'
# 门禁数据处理完成
echo "==============================门禁数据处理完成================================"
#
echo "==============================生成学生成绩数据================================"
# 处理学生成绩数据
awk -F '\t' '{ system("if [ -f ./tmp_student/"$1" ]; then name=$(cat ./tmp_student/"$1"); echo $name\t"$1"\tSS:"$6","$7","$3" >> ./tmp_result/"$1"; else echo "$1"\t"$1"\tSS:"$6","$7","$3" >> ./tmp_result/"$1"; fi") }' xueshengchengji.tsv
# 学生成绩数据处理完成
echo "============================学生成绩数据处理完成=============================="
#
echo "============================生成学生饭卡消费数据================================"
# 处理饭卡消费数据
awk -F '(' '{print $2}' YKTYHXFSJZL_NEW.sql | \
awk -F ')' '{print $1}' | \
sed "s/'//g" | \
awk -F ', ' '{ system("if [ "$19"==NULL ]; then echo NULL\tNULL\tMC:"$12","$13","$21","$11" >> ./tmp_result/0; exit; fi; if [ -f ./tmp_student/"$19" ]; then name=$(cat ./tmp_student/"$19"); echo $name\t"$19"\tMC:"$12","$13","$21","$11" >> ./tmp_result/"$19"; else echo "$19"\t"$19"\tMC:"$12","$13","$21","$11" >> ./tmp_result/"$19"; fi") }'
# 饭卡消费数据处理完成
echo "=========================学生饭卡消费数据处理完成================================"
# 删除冗余数据
rm -rf ./tmp_result/0
rm -rf ./tmp_result/XH
# 计算执行时间
endtime=`date +'%Y-%m-%d %H:%M:%S'`
start_seconds=$(date --date="$starttime" +%s);
end_seconds=$(date --date="$endtime" +%s);
echo "本次运行时间: "$((end_seconds-start_seconds))"s"
脚本编写中出现的问题
- 关于awk内置函数
awk没有办法直接判断文件存不存在,如果直接使用awk内置的条件判断语句来判断文件是否存在,将会导致无法正常判断.如果是用system函数拉起一个shell的话,system将返回0和1而不是shell执行的结果.并且这个返回的结果将会导致awk的非BEGIN action语句发生错误,导致awk获取到system的返回结果,使awk获取的文件流发生重定向到system的结果上而不是获取的文件流上.
所以在执行system()函数的时候,要么将system()放到action的最后部分,要么将虽有的$传送到system拉起的shell中进行操作.
- 关于cat在awk中的使用
实际测试cat在awk中是可以用的,但是在文件不存在的时候,将会导致awk所读取的行信息以及分割信息错误,导致不论在什么命令下,awk直接print $0.