Perl学习笔记 — Find Rare Item[解答篇]

记得很久很久以前发了一个帖子,出了一道题目,当时我花了好几十行代码才完成的工作,Todd用2行代码就完成了,今天来总结一下。题目的需求是找出一个Category里面最特别的物品。我们的做法是把一个Category里面所有的物品的标题都打印出来:

算法描述:

  1. 统计所有的单词的出现频率。比如watch:30次,ring:20次,book:15次,blue:6次
  2. 给每一件物品打分,score=SUM(Freq. of word in Title)。就是把该物品的标题的单词一个一个拿出来,如果是watch,就加30分,如果是ring,就加20分…
  3. 给所有的物品按照分数排序,得分就少的物品就是特别的物品。

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__

我花了那么大的篇幅,就介绍了两行代码。不过自己觉得还是很有收获的。

此条目发表在Perl学习笔记分类目录。将固定链接加入收藏夹。

2 Responses to Perl学习笔记 — Find Rare Item[解答篇]

  1. roye说道:

    看了你的学习笔记,我真的怀疑自己是不是学过计算机

  2. arrowpig说道:

    这个东西是有点难读懂的。这个代码是我们组的Todd写的,他自己写的代码,早上我问他的时候他也回忆了一把呢。
    我刚来eBay的时候,不少东西不懂的。Bruce(就是那天和我一起的高高,有点胖的男生)教了我很多的,网络上面的不少技术细节都是Bruce替我扫的盲。最近他还搞了一个memory pool,我国庆的时候准备试试。
    小罗老师可不要谦虚哦,慢慢来嘛,等脚好了,还要去爬长城呢!-:)
     

留下评论