程式扎記: [Perl 學習手冊] CH14 : 字串與排序

標籤

2010年9月23日 星期四

[Perl 學習手冊] CH14 : 字串與排序


前言 :
在 Perl 擅長處理的問題, 其中 90% 與文字處理有關, 雖然 Perl 的正規表示式很 Powerful, 但對你某些使用情況可能太過花俏, 而你所需要的只是更簡單的字串處理功能.

以 index 來找尋子字串 :
使用 index 函式(find a substring within a string) 可以幫你找出某字串($small)在一段長字串($big)出現的位置, 用法如下 :
$where = index($big, $small);

Perl 會在 $big 字串中找尋 $small 字串首次出現的地方, 並且傳回一個整數, 代表出現頭一個字符的位置 - 而傳回字符位置是從零開始算起. 在下面例子裡, $where 的值為 6:
  1. my $stuff = "Howdy world!";  
  2. my $where = index($stuff, "wor");  
另一種理解傳回值的方法, 就是把它想成你要取得子字串之前, 需要跳過的字符數目. 因為 $where 是 6, 所以我們知道必須跳過前面的六個字符, 才會找到 wor. index 函式永遠會回傳首次出現字串的位置. 不過你可以加上第三個參數來指定開始搜尋的位置 ; 這樣一來它就不會從字串的開頭開始尋找 :
  1. my $stuff = "Howdy world!";  
  2. my $where1 = index($stuff, "w");  # where1 的值為2  
  3. my $where2 = index($stuff, "w", $where1 + 1); # $where2 的值為6  
  4. my $where3 = index($stuff, "w", $where2 + 1); # $where3 的值為 -1 (找不到)  
有的時候你會想要知道子字串最後出現的時間, 這項資訊可以利用 rindex 函式來取得, 下面的例子我們可以找到最後一個倒斜線出現的位置 :
my $last_slash = rindex("/etc/passwd", "/"); # 值為 4

以 substr 來操作子字串 :
substr 函式(get or alter a portion of a stirng)的用法如下 :
$part = substr($string, $initial_position, $length);

它需要三個引數 : 一個字串, 一個從零開始的起始值以及子字串的長度. 它傳回值是個子字串 :
  1. my $mineral = substr("John K Lee"73);  #  傳回 "Lee"  
  2. my $rock = substr "John K Lee"51000; # 傳回 "K Lee"  
  3. print $rock;  
上面的例子你可能注意到, 假如所要求的長度 (此例為 1000) 超出字串的結尾, Perl 不會抱怨, 如果你想要一直取到字串結尾, 不論字串長短的話只要省略第三個參數 (子字串長度) 就行了, 作法如下 :
my $peddle = substr "Fred J. Flintstone", 13; # 得到 "stone"

一個字串中, 子字串的起始位置可以為負值. 表示從字串尾開始倒數 (意思是說位置 -1 就是最後一個字符). 在下面的例子中, 位置 -3 是從字串結尾算起來第三個字符, 也就是自母 i 的位置 :
my $out = substr("some very long string", -3, 2); # $out 為 "in"

另外透過 index, 可以更方便的使用 substr, 下面的例子我們會取出以字符 "l" 的位置開頭的子字串 :
  1. my $long = "some very very long string";  
  2. my $right = substr($long, index($long"l"));  # 得到 "long string"  
接下來就很有趣了 : 假如字串是個變數, 你就可以變動該字串被選取的部分 :
my $string = "Hello, world!";
substr($string, 0, 5) = "Goodbye";
 # $string 變成 "Goodbye, world!"

如你所見, 用來取代的 (子) 字串長度, 並不一定要與被取代的子字串相同 ; 字串會自行調整長度. 其實你還可以用繫結運算符 (=~) 來只對字串的某部分進行操作. 下面的例子只會處理字串的最後 20 個字符, 將所有的 john 代換成 peter :
substr($string, -20) =~ s/john/peter/g;

基本上 substr 與 index 能辦到的事, 多半也能使用正規表示式做到. 但是通常使用 substr 與 index 通常會快一點, 因為它們沒有正規表示式引擎的額外負擔 : 它們永遠不會使用 "不區分大小寫" 的功能, 它們沒有中介字符而且也不會設定任何的比對變數. 如果不想賦值給 substr 函式, 你也可以使用傳統作法以四個引數的版本來使用它, 其中第四個變數代表想要代換成的子字串 :
my $previous_value = substr($string, 0, 5, "Goodbye"); #傳回值代表置換前的字串

用 sprintf 來編排資料 :
sprintf 函式與 printf 有著相同的引數, 但是 sprintf 會傳回所要求的字串, 而不是將它印出來. 此函式方便之處在於你能將好的字串放在變數裡, 以供後續使用 :
my $date_tag = sprintf
"%4d/%02d/%02d/ %2d:%02d:%02d",
$yr, $mo, $da, $h, $m, $s;


在上面的例子, $data_tag 會得到類似 "2038/01/19 3:00:08" 的結果.

- 用sprintf 來產生貨幣數值
sprintf 的一種常用作法就是用來編排小數點後具有固定位數的數值, 如果有個貨幣數值, 大到需要放上好幾個逗號才有辦法顯示它的大小, 你或許會覺得下面這個函式很有用 :
  1. sub big_money{  
  2.     my $number = sprintf "%.2f", shift @_;  
  3.     # 在一個空迴圈中, 每次加上一個逗號  
  4.     1 while $number =~ s/^(-?\d+)(\d\d\d)/$1,$2/;  
  5.     # 在正確位置上放上錢號  
  6.     $number =~ s/^(-?)/$1\$/;  
  7.     $number;  
  8. }  
  9. $number = &big_money("12345678.90");  
  10. print $number . "\n";  
裡面的 while 迴圈的 1 只是個佔位符, 真正迴圈的用途是在加上逗號. 在第一次的比對, 比對變數 $1 會是 "5", 而$2 會是 "678", 所以這個置換運算會讓 $number 變成 "12345,678.90". 接著以此類推直到沒有滿足的字串. 那為什麼我們不就直接使用 /g 修飾符來進行 "全域" 搜尋與代換, 以省下 1 while 迴圈所造成的麻煩與混淆呢? 這是因為必須從小數點倒回處理, 而不是從字串開頭依序處理, 所以不能這樣做.

進階排序 :
在 sort 函式, 提供以 ASCII 編碼順序來對串列排序. 如果你想要以數值大小進行排序呢? 或是以不區分大小寫的方式進行排序? 或者你可能想要以儲存在雜湊內的資訊進行排序? Perl 能以任何你所需要的順序來為串列排序 : 你可以建立 "排序法定義副常式" 告訴 Perl 你想要的排序方式. 事實上它的觀念相當簡單, 它不負責排序而是負責排序的"順序".
排序用的副常式與一般的副常式使用方法相同, 它會對傳入的兩個元素進行比較並且將之存在變數 $a 與 $b 並且賦值給它們. 而副常式會傳回一個值, 用來描述兩個元素之間的順序. 假如 $a 應該在 $b 之前, 排序用的副常式就會傳回 -1, 如果 $b 應該在 $a 之前, 它應該傳回 1. 最後如果 $a 與 $b 的順序無關緊要, 則回傳 0. 接個我們來參考一個範例 :
  1. sub by_number{  
  2.     # 使用副常式處理 $a 與 $b 兩個引數  
  3.     if($a < $b){-1}elsif($a > $b){1else {0}  
  4. }  
要使用排序用的副常式, 請將它的名稱放在 sort 關鍵字與所要排序的串列之間. 下面範例會把 "依數值大小排序好的結果串列" 放進 @result 裡 :
my @result = sort by_number @some_numbers;

請注意在排序用的副常式中, 我們並不需要花功夫來宣告並設定 $a 與 $b - 如果我們這麼做的話反而該副常式無法正常運作. 事實上我們可以讓它更簡單 (也更有效率). 因為常常需要用到這樣的三路比較, 所以 Perl 提供了一個方便的簡寫. 在這個例子可以使用太空船運算符 (<=>). 這個運算符會比較兩個數值, 並且傳回 -1, 0 與 1. 好讓它們依數值排序, 所以我們可以把排序用副常式寫得更好看, 如下 :
sub by_number { $a <=> $b }

除了太空船還有另一個三路字串比較運算符 : cmp. 這兩個運算符非常易記而且 cmp 所提供的順序與 sort 預設排序方法相同. 不過 cmp 可以用於建立更複雜的排序順序, 像是不區分大小寫的排序 :
sub case_insensitive {"\L$a" cmp "\L$b" }

在這個例子裡, 我們比較了 $a(強制轉成小寫) 裡的字串與 $b 裡的字串 (強制轉成小寫), 以產生不區分大小寫的排序順序. 要注意的是我們並不更動元素本身 ; 我們只是使用它們的值而以. 這一點很重要 : 基於效率的理由, $a 與 $b 並不是資料項目的副本. 它們實際上是原始串列元素新的暫用別名. 所以如果我們更動它們的話, 就會弄亂原始的資料. 當排序用副常式像上面一樣簡單時, 其實我們可以直接在使用 sort 的地方展開 :
my @numbers = sort { $a <=> $b } @some_numbers;

另外除了使用 reverse 函式來將串列反轉外, 我們還可以直接變更排序副函式裡面的 $a 與 $b 的位置, 就可以達到同樣的效果. 參考如下 :
my $descending = reverse sort { $a <=> $b } @some_numbers;
equals
my $descending = sort { $b <=> $a } @some_numbers;

- 以雜湊值排序雜湊
考慮下面雜湊, 鍵為人名, 而鍵值為其對應的分數, 現在你希望讓雜湊根據分數來進行排序, 也就是分數告的人排在前面 :
  1. my %score = ("john" => 82"ken" => 98"mary" => 77);  
  2. my @winners = sort by_score keys %score;  
雖然上面可以得到依分數排序的人名串列, 但是實際上還不是雜湊排序 ; 而這個排序用的排序副常式可以參考如下 :
  1. sub by_score {  
  2.     $score{$b} <=> $score{$a};  
  3. }  
- 以多個鍵來排序
如果考慮到同分的話, 希望可以按照人名的 ASCII 編碼順序來進行排序的話, 則排序的副常式可以參考如下 :
  1. sub by_score {  
  2.     $score{$b} <=> $score{$a} # 依分數遞減  
  3.     or  
  4.     $a cmp $b; # 依名字的 ASCII 編碼排序      
  5. }  
這裡當太空船運算符比對兩邊的分數相同時, 會回傳 0, 在 or 運算符看起來是假, 因此 or 運算符的右邊運算式接著就會執行並得到我們依照人名 ASCII 排序的結果.
This message was edited 6 times. Last update was at 08/09/2010 13:22:16

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!