记得很久很久以前发了一个帖子,出了一道题目,当时我花了好几十行代码才完成的工作,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__
我花了那么大的篇幅,就介绍了两行代码。不过自己觉得还是很有收获的。