使用Perl hash来分析大量数据

来源:互联网 发布:java html压缩成一行 编辑:程序博客网 时间:2024/04/30 04:19
在perl中,数组和哈希是两种极为重要数据结构,它们有各自不同的用处。其最为重要的区别笔都认为是前者是线性结构,而后者为随机访问结构,在对其中数据元素的访问效率上,后者更具有不可替代的优势。
接公司运营要求,统计天翼手机用户每个月对某一业务系统的访问量,需要大致可以描述如下:
手机用户通过手机访问两台wap服务器和两台客户端(client),当其对业务请求时便会建立权限,系统会形成日志,每一行一条记录:
153481937xx|2010-05-30 00:00:16|YuLong-Coolpad2938|1||0|

公司要求:对每周、月对日志进行汇总,统计出按天、接时段(每天24小时)、按地区(省或直辖市)统计出wap和client用户的访问量和用户数(即使多次访问按统计要求算做一次)。

按地区统计的话,提供一个全国的字典文件(dic.txt),即区号与地区的对照表:
1890109 北京
手机前7位 地区
像由这样的格式组成的文件目前有2.62M(209704行)。

我们假设这里算一个工作周(05-27--06-02)的访问情况为例来展现使用hash数组的强大功能。合并两台wap服务器的日志文件并将其整理成如下格式的行的文件:
awk -F'|' '{print$1"\t"$2}' wap.tmp|head
189815146xx    2010-05-27 00:00:01
189155253xx    2010-05-27 00:00:01

当然号码里可能包括了国家区号的情况,像上面的86。不过没关系,我们会处理的。

经过处理,收集到如下数据:
-bash-3.2$ ls -lh
total 48M
-rw-r--r-- 1 hto root 2.1M Jun  3 10:55 client.txt
-rw-r--r-- 1 hto root 3.1M May 21 20:42 dic.txt
-rw-r--r-- 1 hto root 8.9K Jun  1 13:41 monacestas.pl
-rw-r--r-- 1 hto root 6.4K May 31 17:15 monacestas.pl.v1
-rw-r--r-- 1 hto root  620 May 21 22:32 swap.txt
drwxr-xr-x 2 hto root 4.0K May 26 16:15 temp
-rw-r--r-- 1 hto root 2.7K May 26 20:08 wapaceschartmon.pl
-rw-r--r-- 1 hto root  12K Jun  1 16:04 wap_aces_tab.xls
-rw-r--r-- 1 hto root  43M Jun  3 10:46 wap.txt

数据量还不少,可以看一下wap.txt中有多少行:
-bash-3.2$ more wap.txt |wc -l
1369110

运行之:/opt/ActivePerl-5.10/bin/perl monacestas.pl
经过使用'top'观察其运行了27s用占用了875m的内存。
机器型号:ibm x3650 m2 4G内存 Xeon(R) 2CPU  E5405  @ 2.00GHz
操作系统centos 5.2 x86_64

代码如下:
----------------------------------------------------------------------------------------
#!/usr/bin/perl
use strict;
use Encode;
use warnings;
use Data::Dumper;

###########################################
#读取字典文件并分析成数组
my $dictionary = 'dic.txt';        #声明字典文件
open(DIC, $dictionary) or die "Can't open $dictionary : $!";        #打开字典文件

#声明字典变量
my %dich=();

#读入字典文件,分解处理,以区号为key,地区为vaule
while () {
$dich{$1} = $2 if($_ =~ /^(\d+)\t(\W+)$/);
}
close(DIC);        #关闭字典

#初始化以地区或区号为key的hash组
#-----------------
#地区
my %area=();
#区号
my %areacd=();
#地区计数器
my %ctarea=();
#-----------------

#处理字典文件得到区号与地区对应关系
while ( my ($key,$value) = each(%dich)) {
#print "$key => $value\n";
#对地区值去尾
chomp($value);
#以地区为key,区号为值的hash组
push @{$area{"$value"}},"$key";
#以区号为key,地区为值的hash组
push @{$areacd{"$key"}},"$value";
}

#建立以地区为key的hash组,来统计该地区电话号码出现的次数,初始其值为'0'
#由于字典文件中的对应不完整,所以为了统计不在字典文件定义的区码,在此增
#加一个'其他'键.
%ctarea=map {$_=>0} (keys %area);
$ctarea{'其他'} = '0';

#print "在字典中找到",(keys %area),"省或直辖市\n";
#建立以小时为key的hash组,来统计该时段用户登录数,初始其值为'0'
#my @timeintv=();
#my %timeintv=map {$_=>0} @timeintv;
my %timeintv=();

#建立以天为key的hash组,来统计该天用户登录数,初始其值为'0'
#my @days=(20..26);
#my @days=();
#my %dayintv=map {$_=>0} @days;
my %dayintv=();
###########################################
sub acestat{
my $dst=shift @_;
#打开从WAP登录的用户文件
open(LOGIN,"$dst");

#声明电话号码与时间对应数组
my %telh=();
#声明电话号码与日期对应数组
my %dtel=();
#需要对用户访问量进行统计,这里声明两个hash,分别记录重复的时间段
#和电话号码,但它们的key相同
my %aestels=();
my %aeshour=();
my %aesday=();
#声明循环计数变量
my $count=0;
while () {
if ($_ =~ /^(\d+)\t\d+\-\d+\-(\d+)\s(\d+)\:\d+\:\d+$/) {
$telh{$1} = $3;
$dtel{$1} = $2;
$aestels{$count}=$1;
$aesday{$count}=$2;
$aeshour{$count}=$3;
}
$count++;
}
close(LOGIN);   
#关闭用户登录文件
#处理用户登录信息,有多少用户来自某地区,同一用户在一天不管有多少次访问都只算作一次,所以
#需要对用户手机号码去重,同时对手机号码规范处理(11位,以1[358]开头).
#-------------------------------------------------------
foreach (keys %telh){
#print $areacd{$tline}[0];
if ($_=~ /([1][358]\d{9}|[8][6]\d{11})/) {
if($1=~/^[8][6](\d{11})/){
}
}
#对取得的电话号码取前七位
my $tline=substr($1,0,7);
#    $ctarea{"$areacd{$tline}[0]"}++ if $areacd{$tline};
if($areacd{$tline}){
$ctarea{"$areacd{$tline}[0]"}++;
} else{
$ctarea{'其他'}++;
}

#-------------
#统计各天用户访问量
#对出现如'09'这样的日期进行取'0'运算
my $dday=$dtel{$_};
$dday=~s/^[0](\d)/$1/;
#对该时的用户登录数统计汇总
$dayintv{"$dday"}++;
#-------------
#统计各时段用户访问量
#对出现如'02'这样的时间进行取'0'运算
my $dtime=$telh{$_};
$dtime=~s/^[0](\d)/$1/;
#对该时的用户登录数统计汇总
$timeintv{"$dtime"}++;
}

#---------------------------------------
#打印每天所登录的用户数目
print "每日用户登录数目如下:\n";
foreach (sort{ $a $b } (keys %dayintv)){
print "$_=>$dayintv{$_}\,";
}
print("\n");
print("-" x 36,"\n");

#打印时段所访问的用户数目
print "各时段用户登录数目如下:\n";
foreach (sort{ $a $b } (keys %timeintv)){
print "$_=>$timeintv{$_}\,";
}
print("\n");
print("-" x 36,"\n");

#打印地区所拥有电话数目
print "各地区用户登录数目如下:\n";
while ( my ($key,$value) = each(%ctarea) ) {
print "\'$key\'=>$value\,";
}
print("\n");
print("-" x 72,"\n");

#-------------------------------------------------------
#################################################
#释放计数hash数组以供重用
%ctarea=();
$ctarea{'其他'} = '0';
%timeintv=();
%dayintv=();
#################################################
#处理用户访问信息
foreach (keys %aestels){
my $tel=$aestels{$_};
if ($tel =~ /([1][358]\d{9}|[8][6]\d{11})/) {
if($1=~/^[8][6](\d{11})/){
}
}

my $tline=substr($1, 0, 7);
if($areacd{$tline}){
$ctarea{"$areacd{$tline}[0]"}++;
} else{
$ctarea{'其他'}++;
}
#------------------
#统计各时段用户访问量
#对出现如'09'这样的时间进行取'0'运算
my $tim=$aeshour{$_};
$tim=~s/^[0](\d)/$1/;

#对该时的用户登录数统计汇总
$timeintv{"$tim"}++;
#------------------
#统计各天用户访问量
my $day=$aesday{$_};
$day=~s/^[0](\d)/$1/;
$dayintv{"$day"}++;
}

#-------------------------------------------------------
#打印每天所登录的用户数目
print "每日用户访问数目如下:\n";
foreach (sort{ $a $b } (keys %dayintv)){
print "$_=>$dayintv{$_}\,";
}
print("\n");
print("-" x 36,"\n");

#打印时段所访问的用户数目
print "各时段用户访问数目如下:\n";
foreach (sort{ $a $b } (keys %timeintv)){
print "$_=>$timeintv{$_}\,";
}
print("\n");
print("-" x 36,"\n");

#打印地区所拥有电话数目
print "各地区用户访问数目如下:\n";
while ( my ($key,$value) = each(%ctarea) ) {
print "\'$key\'=>$value\,";
}
print("\n");
print("#" x 72,"\n");
}
###########################################
acestat('wap.txt');
#建立以地区为key的hash组,来统计该地区电话号码出现的次数,初始其值为'0'
#由于字典文件中的对应不完整,所以为了统计不在字典文件定义的区码,在此增
#加一个'其他'键.
%ctarea=map {$_=>0} (keys %area);
$ctarea{'其他'} = '0';

#print "在字典中找到",(keys %area),"省或直辖市\n";
#建立以小时为key的hash组,来统计该时段用户登录数,初始其值为'0'
#my @timeintv=();
#my %timeintv=map {$_=>0} @timeintv;
%timeintv=();

#建立以天为key的hash组,来统计该天用户登录数,初始其值为'0'
#my @days=(20..26);
#my @days=();
#my %dayintv=map {$_=>0} @days;
%dayintv=();
#acestat('client.txt');
#对相关变量进行打印
#print  Dumper %dayintv;
----------------------------------------------------------------------------------------

运行结果如下:
每日用户登录数目如下:
1=>69862,2=>73899,27=>60655,28=>61038,29=>64879,30=>62104,31=>60790,
------------------------------------
各时段用户登录数目如下:
0=>12436,1=>6127,2=>3374,3=>2134,4=>1725,5=>3124,6=>7610,7=>13000,8=>17932,
9=>21487,10=>24579,11=>24912,12=>26023,13=>23559,14=>23544,15=>25177,16=>25551,
17=>25669,18=>26404,19=>27568,20=>28630,21=>30198,22=>30061,23=>22403,
------------------------------------
各地区用户登录数目如下:
'西藏'=>2785,'山西'=>9472,'云南'=>222,'黑龙江'=>5944,'贵州'=>253,'河南'=>8508,
'广东'=>55016,'其他'=>68235,'上海'=>17572,'湖南'=>11921,'重庆'=>8311,'内蒙古'=>642,
'福建'=>19377,'浙江'=>19370,'陕西'=>744,'宁夏'=>216,'江西'=>10222,'天津'=>4887,
'辽宁'=>8303,'四川'=>29068,'甘肃'=>16259,'吉林'=>5795,'北京'=>14013,'海南'=>4606,
'新疆'=>10040,'河北'=>1643,'山东'=>17160,'安徽'=>8289,'湖北'=>21431,'广西'=>6750,
'江苏'=>63306,'青海'=>2867,
------------------------------------------------------------------------
每日用户访问数目如下:
1=>212006,2=>191707,27=>192499,28=>203756,29=>198223,30=>190303,31=>180616,
------------------------------------
各时段用户访问数目如下:
0=>37259,1=>17404,2=>9247,3=>5671,4=>4471,5=>8222,6=>21353,7=>36769,8=>51530,
9=>65555,10=>75346,11=>75930,12=>77972,13=>70124,14=>70461,15=>74212,16=>74764,
17=>75542,18=>78134,19=>88348,20=>91535,21=>98372,22=>93948,23=>66941,
------------------------------------
各地区用户访问数目如下:
'西藏'=>12319,'山西'=>20052,'云南'=>666,'黑龙江'=>11200,'贵州'=>685,'河南'=>24501,
'广东'=>102484,'上海'=>54074,'其他'=>138348,'南'=>45836,'重庆'=>29091,'内蒙古'=>1687,
'福建'=>36304,'浙江'=>72824,'陕西'=>1720,'宁夏'=>869,'天津'=>12531,'江西'=>51912,
'辽宁'=>15870,'四川'=>99376,'甘肃'=>33980,'吉林'=>10501,'北京'=>41643,'海南'=>7852,
'新疆'=>32144,'河北'=>4219,'山东'=>49012,'安徽'=>18637,'湖北'=>65501,'广西'=>12735,
'江苏'=>353202,'青海'=>7335,
##############################################
每日用户登录数目如下:
1=>2200,2=>2822,27=>1321,28=>1495,29=>1509,30=>1517,31=>1578,
------------------------------------
各时段用户登录数目如下:
0=>272,1=>142,2=>68,3=>40,4=>30,5=>41,6=>137,7=>295,8=>458,9=>569,10=>718,11=>709,
12=>636,13=>603,14=>657,15=>741,16=>774,17=>795,18=>855,19=>868,20=>864,21=>845,
22=>768,23=>557,
------------------------------------
各地区用户登录数目如下:
'西藏'=>81,'山西'=>194,'云南'=>182,'黑龙江'=>80,'贵州'=>115,'河南'=>136,'广东'=>910,
'其他'=>1,'上海'=>1072,'湖南'=>372,'重庆'=>364,'内蒙古'=>101,'福建'=>368,'浙江'=>737,
'陕西'=>344,'宁夏'=>411,'江西'=>376,'天津'=>107,'辽宁'=>150,'四川'=>1112,
'甘肃'=>393,'林'=>81,'北京'=>668,'海南'=>66,'新疆'=>244,'河北'=>270,'山东'=>207,
'安徽'=>280,'湖北'=>656,'广西'=>104,'江苏'=>2085,'青海'=>175,
------------------------------------------------------------------------
每日用户访问数目如下:
1=>10307,2=>9971,27=>9017,28=>9987,29=>9476,30=>8538,31=>8901,
------------------------------------
各时段用户访问数目如下:
0=>1689,1=>860,2=>388,3=>221,4=>131,5=>263,6=>845,7=>1614,8=>2519,9=>3308,
10=>3975,11=>4008,12=>3640,13=>3455,14=>3697,15=>4141,16=>3976,17=>4107,18=>4453,
19=>4506,20=>4198,21=>4025,22=>3546,23=>2632,
------------------------------------
各地区用户访问数目如下:
'西藏'=>580,'山西'=>1284,'云南'=>939,'黑龙江'=>345,'贵州'=>647,'河南'=>768,'广东'=>4170,
'上海'=>8178,'其他'=>2,'湖南'=>1967,'重'=>2058,'内蒙古'=>419,'福建'=>1676,'浙江'=>3603,
'陕西'=>1480,'宁夏'=>2097,'江西'=>1717,'天津'=>433,'辽宁'=>528,'四川'=>5887,'甘'=>1979,
'吉林'=>367,'北京'=>3216,'海南'=>338,'新疆'=>933,'河北'=>1408,'山东'=>852,'安徽'=>1092,
'湖北'=>3217,'广西'=>438,'江苏'=>12785,'青海'=>794,
##########################################

上面打印了wap和客户端的访问统计情况。

写后记:
这个脚本占用内存太历害了,在分析一个月的数据时(180M),竟然吃过3.2G的内存空间,得想个办法来降低内存使用率,后经同事指点:看能不能不分析字典文件,直接以省以初始化,而字典里的区号仅做为来匹配日志文件里的号码。

之前采用数组来存放相关数据,在处理几千行时还可以,一旦过万则不堪忍受。使用hash效率会高很多,尤其是对一个手机号码取了区号位后判断其归属地时;如果建立了一以地区为key,该地区区号为数组的hash来保存和查找的话:
my %hasharea = (
'地区1' => [ '1334567', '1533456', '1892345', ],
'地区2' => [ '1334867', '1533459', '1892305', ],
);

在遍历每个地区的同时,在匹配其下的数组,这样计算量会成几十倍的增加,更糟糕的是perl中要匹配元素是否属于某一数组集合时,在5.10之前是没有这个函数操作符的。只能写成如下方式:
my $string = ‘fme’;
my @array = qw/kitty monkey cats fme two_hanword ccm/;
if(grep $_ eq $string, @array){
print “$string is in the array”;
}

又会对数组进行遍历。在5.10之后(包括5.10)
最令人兴奋的改进是全新的智能匹配操作符(smart match operator) 。该操作符实现了一种全新的比较方式,而其具体作用是随操作符接受的输入而有所不同的。举例而言,要看标量 $need 是否存在于数组 @stack 中,只要使用新的 ~~ 操作符:

if ( $need ~~ @stack )

但这样效率与用hash来实现不可同日而语。目前对这个小程序进行了改进,将输出的结果存到excel中;不过这与文章的主题不一致,为了简单起见,这里就只贴出了核心的分析程序。写下来是怕自己忘掉,二是想帮助那些有碰到相似问题的人。
原创粉丝点击