现在的位置: 首页 > 综合 > 正文

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

2013年01月29日 ⁄ 综合 ⁄ 共 3353字 ⁄ 字号 评论关闭

记得很久很久以前发了一个帖子,出了一道题目,当时我花了好几十行代码才完成的工作,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__

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

 

抱歉!评论已关闭.