记得很久很久以前发了一个帖子,出了一道题目,当时我花了好几十行代码才完成的工作,Todd用2行代码就完成了,今天来总结一下。题目的需求是找出一个Category里面最特别的物品。我们的做法是把一个Category里面所有的物品的标题都打印出来:
算法描述:
- 统计所有的单词的出现频率。比如watch:30次,ring:20次,book:15次,blue:6次
- 给每一件物品打分,score=SUM(Freq. of word in Title)。就是把该物品的标题的单词一个一个拿出来,如果是watch,就加30分,如果是ring,就加20分…
- 给所有的物品按照分数排序,得分就少的物品就是特别的物品。
O.K.我们这里不讨论算法,只学习perl的使用技巧。怎么实现上面的算法?先来看看数据:
原始实验数据:
Version:0.1
MatchCount:10000
…
ReturnCount:50
110175200 Perot by Tod Mason
145230996 TOD OLDHAM petite vest — beautiful and rare
16F44B046 2 Life / Death aus Apokalypse !! Leben / Tod
18B1714EB Insel-Buch 1 Die Weise von Liebe und Tod R.M. Rilke
19CDBF8A1 Tod and Copper by Walt Disney (1981)
1A0C8685F Testaments of Israel: Words of Yesterday, Images of Tod
…
第一行代码:
ReturnCount:50以上包含ReturnCount是Response Header。一下部分就是返回的数据,第一列是item id,第二列就是item title。我们不需要统计response header。所以第一步要把header去掉。
use LWP::Simple;
grep {s/.*?ReturnCount:w+n//s} lc get shift
shift等价于 shift @ARGV,假设我们的脚本叫做findrare.pl, 使用的时候是 perl findrare.pl "requestURL", 所以shift @ARGV就相当于拿到第一个参数,也就是query的URL。get是LWP::Simple库的函数,用于简单的HTTP请求。可以perldoc LWP::Simple查看在线帮助。于是get shift就拿到了原始数据,原始数据我已经贴在上面了,注意整个原始数据是作为一个标量字符串返回的。 这里的grep是perl的函数,grep BLOCK LIST 表示对LIST中的每一个元素都使用BLOCK中的语句进行evaluation,如果为真,就返回匹配的元素。我们先使用lc把返回的字符串转成小写,接着perl会自动把标量字符串转成Array,但是这个Array里面只有一个元素,就是返回的内容数据。
{s/.*?ReturnCount:w+n//s} 表示匹配ReturnCount所在的行。这里注意/s,表示允许通配符’.'匹配换行符n。然后把匹配的内容替换成空字符串。通过grep我们就删除了response header。
接着我们要切分单词。方便的办法是调用split ‘s+’, 但是我们不能这样:
split ‘s+’, grep {s/.*?ReturnCount:w+n//s} lc get shift
split是对标量操作的,而grep返回的是Array, perl 在自动将Array转换成Scalar的标准做法是返回Array的元素个数.于是在这里,grep返回只含有一个元素的Array,在split期待scalar输入的上下文中,上面的语句就等价于:
split ‘s+’, 1 #这个显然不是我们需要的,所以一个欺骗perl 的方式就是:
split ‘s+’, join ” , grep {s/.*?ReturnCount:w+n//s} lc get shift
grep返回一个array,我们通过append空字符的方式,把grep返回的array转换成scalar。而字符串内容不变!
现在我们已经切分了所有的单词,统计词频我们使用Hash表:
$WordFreq{$_}++ for split ‘s+’, join ”, grep {s/.*?ReturnCount:w+n//s} lc get shift;
看到厉害了吧,一条语句就完成了词频统计工作。
第二行代码:
接下来的工作就是迭代每一件物品,打分,然后比较大小,打印得分最小物品的item id和item title。这里要对第一条语句稍作修改,我们需要保存 lc get shift的结果,也就是转成小写后的原始数据,假设保存在$lraw = lc get shift 中。
split /n/, $lraw;
由于原始数据中,每一个item都是通过换行符’n'分隔的,所有我们通过split /n/把字符串标量转换成字符串Array,Array中的每一个元素就是一个物品。
map { [$_, sum map $WordFreq{$_}, split 's+'] } split /n/, $lraw;
map 的 用法是 map Expr List 或者 map Block List, 对List中的每一元素应用Expr/Block后返回。所以map {…} split /n/, $lraw 表示对每一个物品 调用{…}中的代码,每次迭代,$_表示当前的物品 。而 map $WordFreq{$_}, split ‘s+’ 表示对每一个物品,先将其标题中的单词一个个拆出,为每一个单词返回其词频。然后使用sum计算总得分。 sum是List::Util中的函数。但是我们不但需要分数,我们的需求是找到物品,所以我们需要保存 (物品,得分)。
注意: 这里 map 返回一个Array, Array的每一个元素都是一个引用,该引用指向一个数组(一个匿名的数组),数组的第一个元素都是item信息,第二个元素是 item的得分。
注意: 上面这个表达式里面的$_,第一个$_是每一个物品信息,第二个$_是当前物品中被拆分出来的当前单词。接着我们就要进行排序:
reduce { $a->[1] <= $b->[1] ? $a : $b } map { [$_, sum map $WordFreq{$_}, split 's+'] } split /n/, $lraw;
reduce是List::Util中的函数,可以使用perldoc List::Util查看在线帮助。reduce的做法是,reduce Block List,把$a等于第一个元素,$b等于等二个元素,然后让$a等于Block运算的结果,$b等于下一个元素。所以这里的操作本质上就是找出分数最小的元素。
注意: $a->[0]是字符串,$a->[1]中以字符串方式存放得分,字符串的比较应该使用gt或者lt,但是这里使用数字的比较符<=,perl很聪明,看到<=,说明我们期待的是数字大小比较,所以perl会自动把字符串得分转换成数字得分进行大小比较。
reduce返回一个标量,这里就返回那个得分最小的物品的引用。注意这里$a,$b都是对于匿名数组内元素的应用。 我们把reduce 的返回结果再送给map:
print map { $_->[0]} … #这样就打印了那个特别的物品了!
完整的代码:
最后,我们把完成这个工作的代码放在一起看一看。
#! /usr/bin/perl -w
use LWP::Simple;
use List::Util qw/sum reduce/;
$WordFreq{$_}++ for
split ‘s+’, join ”, grep {s/.*?ReturnCount:w+n//s} $lraw=lc get shift;
print map { $_->[0]}
reduce { $a->[1] <= $b->[1] ? $a : $b } map { [$_, sum map $WordFreq{$_}, split 's+'] } split /n/, $lraw;
__END__
我花了那么大的篇幅,就介绍了两行代码。不过自己觉得还是很有收获的。