perl利用递归提取正常的车次信息
2014-02-09 20:12:50 阿炯

本站赞助商链接,请多关照。 手头现有一些车次及其站点的信息表,其结构大致如下:
| ceci | shifazhan | zhongdianzhan | fazhan | dazhan | type | facheshijian | dazhanshijian | yongshi | piaojian
| 车次 | 始发站 | 终点站 | 发站 | 到站 | 类型 | 发车时间 | 到站时间 | 用时 | 票价 |

现有这样一种问题,发站与到站并不一定是相邻的,它们之间差一个或几个站甚至更多个站,与之相邻的站只能从'yongshi'字段来判断(它们之间的用时是最小的)。这里需要提取出车次的正常顺序,按从发站一个接一个到终点站找出来,并记录下来。

程序主要使用了递归的方式来对某一车次的所有站信息进行处理,只要不是最后一个站,就继续查找其下一个站点信息。其中还涉及了在正则中匹配中文以及对中文编码的处理,写下来的目的是为防止后面忘记,同时也为那些碰到同样问题的人提供一些帮肋。

判断逻辑:对某一车次的某一站来说,用时最少的站即是它的下站,我们需要告诉程序具体的车次、始发站、终点站。

将附件中的文本文件导入到数据库(test)表(train2)中,结构语句如下:
CREATE TABLE `train2` (
  `ceci` varchar(32) NOT NULL DEFAULT '',
  `shifazhan` varchar(32) NOT NULL DEFAULT '',
  `zhongdianzhan` varchar(32) NOT NULL DEFAULT '',
  `fazhan` varchar(32) NOT NULL DEFAULT '',
  `dazhan` varchar(32) NOT NULL DEFAULT '',
  `type` varchar(32) NOT NULL DEFAULT '',
  `facheshijian` varchar(32) NOT NULL DEFAULT '',
  `dazhanshijian` varchar(32) NOT NULL DEFAULT '',
  `yongshi` varchar(32) NOT NULL DEFAULT '',
  `piaojian` varchar(256) NOT NULL DEFAULT ''
) ENGINE=MyISAM DEFAULT CHARSET=gbk;

将文件中的数据导入
mysql> LOAD DATA INFILE '/home/smgadmin/train.info.gbk.txt' INTO TABLE train2 FIELDS TERMINATED BY '\t';
Query OK, 374973 rows affected (2.53 sec)
Records: 374973  Deleted: 0  Skipped: 0  Warnings: 0

将车次、始发、终点站的信息导出为字典
select ceci,shifazhan,zhongdianzhan from train2 group by ceci into outfile '/home/smgadmin/ceci.dic' FIELDS TERMINATED BY '\t';

将程序处理的结果保存到表(train4)中,基结构如下:
CREATE TABLE `train4` (
  `ceci` varchar(32) NOT NULL DEFAULT '',
  `shifazhan` varchar(32) NOT NULL DEFAULT '',
  `zhongdianzhan` varchar(32) NOT NULL DEFAULT '',
  `zhanhao` int(3) NOT NULL DEFAULT '1',
  `benzhan` varchar(32) NOT NULL DEFAULT '',
  `yongshi` varchar(32) NOT NULL DEFAULT '',
  `facheshijian` varchar(32) NOT NULL DEFAULT '',
  `dazhanshijian` varchar(32) NOT NULL DEFAULT '',
  `type` varchar(32) NOT NULL DEFAULT ''
) ENGINE=MyISAM DEFAULT CHARSET=utf8

至此,准备工作完成,下面是程序代码,在perl 5.18下编写测试,系统至少要5.10以上的版本即可。

注意:附件中的文件编码是gbk的,导入表(train2)的编码也应是gbk,结果表(train4)的编码是utf8,请注意调整mysql会话时的编码(mysql>set names gbk|utf8)和连接到服务器的ssh工具的字符集,以免出现乱码的情况。另外需要将脚本中连接到数据库的信息(主机、用户名、密码修改为您所处环境的正确信息);代码中将插入表的操作已经注释。

代码:
use feature qw(:5.18);
use DBI;
use Encode;
use Data::Dumper;
use Getopt::Long;
#将输入与输出的编码转为utf8
binmode(STDOUT, ":encoding(utf8)");
binmode(STDIN, ":encoding(utf8)");
#定义车次,始发、终点站
my ($ceci,$startstion,$endstion)=('1051','天津','齐齐哈尔');

#定义到mysql连接信息及属性
my $db='test';
my $host='127.0.0.1',
my $user='root';
my $password='';
my %mydbattr=(
 RaiseError=>1,
 AutoCommit=>1,
 mysql_enable_utf8=>1,
 RaiseError=>1
);

#连接到mysql的句柄
my $dbh=DBI->connect("DBI:mysql:database=$db;host=$host",$user,$password,\%mydbattr);

#将传入的分钟转换为'x时y分'这样的格式
sub mintohm{
 $tmin=shift;
 my ($permin,$rc)=(60);
 $rc='0时'.min.'分' if($tmin<=$permin);
 my $mh=int($tmin/$permin);
 my $mm=($tmin%$permin);
 #将一位的分钟前置'0',补为两位
 $mm='0'.$mm if((length($mm) == 1) and $mm);
 $rc=$mh.'时'.$mm.'分';
 Encode::_utf8_off($rc);
 $rc=decode("utf8",$rc);
 return $rc;
}

#用于存放某一车次信息的hash变量
my $h;

#打开车次的字典文件,每一行为三列,分别是车次、始发站、终点站
open(DIC,"<ceci2.dic");

#对每一趟车次的信息进行查询
while(<DIC>){
 ($ceci,$startstion,$endstion)=($_=~/(.*)\t([^u4E00-u9FA5]+)\t([^u4E00-u9FA5]+)/);
 Encode::_utf8_off($startstion);
 $startstion=decode("utf8",$startstion);
 Encode::_utf8_off($endstion);
 $endstion=decode("utf8",$endstion);
 #print $ceci."\t".$startstion."\t".$endstion;
 chop($endstion);

#从mysql中查询出该车次的信息
my $ary_ref=$dbh->selectall_arrayref(qq{select * from train2 where ceci="$ceci"},{Slice=>{}});

foreach my $rc (@$ary_ref){
 #对用时(yongshi)字段处理,这里涉及对中文的匹配,最终是将其计算为分钟数
 $rc->{yongshi}=~/^(\d+)[^u4E00-u9FA5](\d+)/;
 my $totime=60*$1+$2;
 #构造hash数组,key为'fazhan->dazhang',value为其它一些相关的字段
 $h->{$rc->{fazhan}.'->'.$rc->{dazhan}}={fazhan=>$rc->{fazhan},dazhan=>$rc->{dazhan},type=>$rc->{type},facheshijian=>$rc->{facheshijian},dazhanshijian=>$rc->{dazhanshijian},yongshi=>$totime};
}#end of foreach

#该车次经过的站点计数,定义为state类型,这样可以在递归循环时保持其值不被重置
state $stno=0;

#主函数,计算与该站相邻(用时最少)的站点
sub get_short_stion{
 my $stion=shift;
 Encode::_utf8_off($stion);
 $stion=decode("utf8",$stion);
 #定义了上一站、下一站,用时
 my ($lastion,$nextion,$minst)=('','',0);
 #对该车次的所有站点进行循环
 foreach (keys %{$h}){
  #只有对该站打头的键才会被处理,加'\b'匹配规则是为了防止出以同一站点打头的情况,让匹配更精确(像成都与成都东这两个站)
  next unless $_=~/^\b$stion\b/;
  #取得用时信息
  $minst=$h->{$_}->{yongshi} unless($minst);
  #只有最小用时的站点才是符合是下一站的规则
  if($minst>=$h->{$_}->{yongshi}){
   $minst=$h->{$_}->{yongshi},$lastion=$h->{$_}->{fazhan},$nextion=$h->{$_}->{dazhan},$ttype=$h->{$_}->{type},$st_time=$h->{$_}->{facheshijian},$ed_time=$h->{$_}->{dazhanshijian};
  }
  #在hash数组中删除用过的键,这样既能减少下一次循环的次数,又能因匹配成功而再度循环而造成的死循环(像环线站之类)
  #delete the key has used for simair place
  delete $h->{$_};
 } #end of foreach
 
 #将分钟转换为'x时y分'格式
 $minst=mintohm($minst);
 #自定义sql语句
 my $insql;
 #打印出站点信息
 printf("%.2d\t%-10.4s\t%-10.6s\t%-10s\t%-10s\t%-10s\n",++$stno,$lastion,$minst,$st_time,$ed_time,$ttype);
 $insql='insert into test.train4 values('."\'$ceci\',\'$startstion\',\'$endstion\',\'$stno\',\'$lastion\',\'$minst\',\'$st_time\',\'$ed_time\',\'$ttype\'".');';
 #say $insql;
 #插入到数据库中
 #$dbh->do($insql);
 #终点站处理,最后一站的到站时间设置为'00:00'
 if($nextion eq $endstion){
  ($lastion,$minst,$st_time,$ed_time)=($nextion,decode("utf8",'终点站'),$ed_time,'00:00');
  printf("%.2d\t%-10.4s\t%-10.6s\t%-10s\t%-10s\t%-10s\n",++$stno,$lastion,$minst,$st_time,$ed_time,$ttype);
  $insql='insert into test.train4 values('."\'$ceci\',\'$startstion\',\'$endstion\',\'$stno\',\'$lastion\',\'$minst\',\'$st_time\',\'$ed_time\',\'$ttype\'".');';
  say $insql;
  #$dbh->do($insql);
 }
 #递归跳出条件,当没有下一站,即终点站时
 return unless($nextion);
 #如果下一站不是终点站则继续调用本函数进行递归
 get_short_stion($nextion) if($nextion ne $endstion);
}#end fun get_short_stion

#从起点站调用查最近站点函数
get_short_stion($startstion);

#该车次所有正常站点查询完成后,将其所用的变量清空
 $stno=0;
 $h=();
}#end of while

END {
 #程序要结束,关闭用过文件句柄
 close DIC;
 $dbh->disconnect if defined($dbh);
}

附件列表
一些火车的列表记录