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__

我花了那麼大的篇幅,就介紹了兩行代碼。不過自己覺得還是很有收穫的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章