19 12
發新話題

Perl 學習手札

Perl 學習手札


目錄:
1. 關於Perl
1.1 Perl的歷史
1.2 Perl的概念
1.3 特色
1.4 使用Perl的環境
1.5 開始使用 Perl
1.6 你的第一支Perl程式

2. 純量變數(Scalar)
2.1 關於純量
2.1.1 數值
2.1.2 字串
2.1.3 數字與字串轉換
2.2 使用你自己的變數
2.3 賦值
2.3.1 直接設定
2.3.2 還可以這樣
2.4 運算
2.5 變數的輸出/輸入
2.6 Perl預設變數
2.7 defined 與 undef

3. 串列與陣列
3.1 何謂陣列
3.2 Perl 的陣列結構
3.3 push/pop
3.4 shift/unshift
3.5 切片
3.6 陣列還是純量?
3.7 一些常用的陣列運算
3.7.1 sort
3.7.2 join
3.7.3 map
3.7.4 grep

4. 基本的控制結構
4.1 概念
4.1.1 關於程式的流程
4.1.2 真,偽的判斷
4.1.3 區塊
4.1.4 變數的生命週期
4.2 簡單判斷
4.2.1 if
4.2.2 unless
4.2.3 一行的判斷
4.3.4 else/elsif
4.3 重複執行
4.3.1 while
4.3.2 until
4.4 for
4.4.1 像 C 的寫法
4.4.2 其實可以用 ...
4.4.3 有趣的遞增/遞減算符
4.4.4 對於陣列內的元素

5. 雜湊(Hash)
5.1 日常生活的雜湊
5.2 雜湊的表達
5.3 雜湊賦值
5.4 each
5.5 keys跟values
5.6 雜湊的操作
5.6.1 exists
5.6.2 delete
5.7 怎麼讓雜湊上手

6. 副常式
6.1 關於Perl的副常式
6.2 參數
6.3 傳回值
6.4 再談參數
6.5 副常式中的變數使用

7. 正規表示式
7.1 Perl 的第二把利劍
7.2 甚麼是正規表示式
7.3 樣式比對
7.4 Perl 怎麼比對
7.5 怎麼開始使用正規表示式

8. 更多關於正規表示式
8.1 只取一瓢飲
8.2 比對的字符集合
8.3 正規表示式的特別字元
8.4 一些修飾字元
8.5 取得比對的結果
8.6 定位點
8.7 比對與替換
8.8 有趣的字串內交換
8.9 不貪多比對
8.10 如果你有疊字
8.11 比對樣式群組
8.12 比對樣式的控制

9. 再談控制結構
9.1 迴圈操作
9.1.1 last
9.1.2 redo
9.1.3 next
9.1.4 標籤
9.2 switch
9.2.1 如果你有複雜的 if 敘述
9.2.2 利用模組來進行
9.3 三元運算符
9.4 另一個小訣竅

10. Perl的檔案存取
10.1 檔案代號 (FileHandle)
10.2 預設的檔案代號
10.3 檔案的基本操作
10.3.1 開檔/關檔
10.3.2 意外處理
10.3.3 讀出與寫入

11. 檔案系統
11.1 檔案測試
11.2 重要的檔案相關內建函式
11.3 localtime

12. 字串處理
12.1 簡單的字串形式
12.2 uc 與 lc
12.3 sprintf
12.4 排序
12.5 多子鍵排序

13. 模組與套件
13.1 關於程式的重用
13.2 你該知道的 CPAN
13.3 使用CPAN與CPANPLUS
13.4 使用模組
13.5 開始寫出你的套件

14. 參照 (Reference)
14.1 何謂參照
14.2 取得參照
14.3 參照的內容
14.4 利用參照進行二維陣列

15. 關於資料庫的基本操作
15.1 DBM
15.1.1 與DBM連繫
15.1.2 DBM檔案的操作
15.1.3 多重資料
15.2 DB_File
15.3 DBI
15.4 DBIx:assword

16. 用Perl實作網站程式
16.1 CGI
16.2 Template
16.3 Mason

17. Perl與系統管理
17.1 Perl在系統管理上的優勢
17.2 Perl的單行執行模式
17.3 管理檔案
17.4 郵件管理
17.4.1 Mail::Audit + Mail::SpamAssassin
17.4.2 Mail::Sendmail 與 Mail::Bulkmail
17.4.3 POP3Client 及 IMAPClient
17.5 日誌檔
17.6 報表

附錄A. 習題解答

TOP

1. 關於Perl
當你翻開這本書的時候,你也就進入了一個奇幻的世界。Perl確實是一種非常吸引人的程式語言,而之所以這麼引人入勝的原因不單單在於他的功能,也在於他寫作的方式,或說成為一種程式寫作的藝術。即使你只是每天埋首於程式寫作的程式設計師,也不再讓生活過份單調,至少你可以嘗試在程式碼中多一些變化。而且許多Perl的程式設計師已經這麼作了,這也是Perl的理念-「There is more than one way to do it」。

常常遇到有人問我:「Perl到底可以拿來作甚麼呢?」,不過後來我慢慢的發現,這個問題的答案卻是非常的多樣化。因為在不同的領域幾乎都有人在使用Perl,所以他們會給你的答案就會有很大的差異了。有人會覺得Perl拿來用在生物資訊上真是非常方便,有人也來進行語料的處理,資料庫,網頁程式設計更是有著廣泛的運用。當然,還有許多人把Perl拿來當成系統管理的利器,更是處理系統日誌的好幫手。

1.1 Perl的歷史
由Larry Wall創造出來的Perl在1987年時最早出現在usenet的新聞群組comp.source。從當時所釋出的1.0版本,到3.0版為止,幾乎維持著一年有一次大版本的更新,也就是說在1989年時,Perl已經有了3.0 版。而1991年,Perl開發團隊發展出相當關鍵的4.0版。因為隨著4.0版的釋出,Perl發表了新的版權聲明,也就是Perl Artistic Licence(藝術家授權)。Perl4跟Perl5之間相隔了有三年之久,漸漸的,Perl的架構已經日趨穩定。一直到最近,Perl釋出了新的Perl 5.8版,並且同時進行新一代版本的開發。

1.2 Perl的概念
Perl是非常容易使用的程式語言,或者我們應該說他是方便的程式語言,你可以隨手就寫完一支Perl的程式,就像你在命令列中打一個指令一樣 (註一)。因為Perl的誕生幾乎就在於讓使用者能夠以更好方便的方式去撰寫程式碼,卻不必像寫C一樣的考慮很多細節。
另外,Perl的黏性非常的強 (註二),你可以用Perl把不同的東西輕易的連接起來。而且你可以用Perl解決你大部份的問題,雖然有些時候你並不想這麼作,但這並不表示Perl作不到。

1.3 特色
很多人對Perl的印象就是一種寫CGI(註三)的程式語言,或者直覺的認為Perl只是拿來處理文字的工具。不過就像我們所說的,Perl幾乎可以完成大部份你希望達成的工作。但是不可否認的,正規表示式顯然是Perl足以傲人的部份,這也就是Perl大量被拿來使用作為文字處理的原因之一。
而且Perl對於你希望快速的完成某些工作確實可以提供非常大的幫助。甚至在Unix-like的環境下,還可以直接使用Perl為基礎的Shell,讓你用Perl當指令。而不必像許多程式語言,在還沒正式工作之前,你必須先準備一堆事情,包括你的變數定義,你的資料結構等等。也因此,許多Unix-like系統的管理員都喜歡拿Perl來進行系統管理。畢竟沒人希望要處理一個郵件紀錄檔還要先花一堆時間搞清楚該怎麼把紀錄檔內的東西轉成合適的資料結構。

1.4 使用Perl的環境
雖然大多數的Unix-like系統管理員選擇Perl來幫助他們管理他們的伺服器,可是這絕對不表示Perl只能在這些系統上執行。相反的,Perl可以在絕大多數的作業系統上執行。而目前Windows上的Perl則是以Active Perl(訪客無法瀏覽此圖片或連結,請先 註冊登入會員 這家公司所提供的直譯器為主。Perl的使用在不同的作業系統下會略有不同,本書則以unix-like為執行環境。

1.5 開始使用 Perl
在開始使用Perl之前,必須先確定你的機器上是否已經安裝Perl。在許多unix-like的作業系統中,都預設會安裝Perl,你也可以先執行下面的指令來確定目前系統內的Perl版本。
引用:
[hcchien@Apple]% perl -v

This is perl, v5.8.2 built for darwin-2level

Copyright 1987-2003, Larry Wall

Perl may be copied only under the terms of either the Artistic License or the
GNU General Public License, which may be found in the Perl 5 source kit.

Complete documentation for Perl, including FAQ lists, should be found on
this system using `man perl' or `perldoc perl'.  If you have access to the
Internet, point your browser at 訪客無法瀏覽此圖片或連結,請先 註冊登入會員 , the Perl Home Page.
我們看到在版本的部份,這裡使用的是Perl 5.8.2的版本,然後有著作者Larry Wall的名字,也就是版權擁有著。接下來是Perl的版權說明。另外,你應該要注意 `perldoc perl' 這個部份:直接在你的終端機下打這行指令,就可以看到Perl內附的文件,而且內容非常詳細。
在這裡,我們建議使用Perl 5.8以上的版本,如果你的版本過於老舊,或是系統中還沒有安裝Perl,可以從訪客無法瀏覽此圖片或連結,請先 註冊登入會員 下載,並且安裝Perl。
如果你的系統已經有Perl,並且正常運作,那麼你可以開始使用你的Perl。你可以試著使用所有程式語言都會使用的範例來作為使用Perl的開端:
引用:
[hcchien@Apple]% perl -e 'print "hello world!\n"'
hello world!
不過在Windows上,因為命令列不能使用單引號,所以得這樣寫:
引用:
[hcchien@Apple]% perl -e "print \"Hello world\n\""
hello world!
1.6 你的第一支Perl程式
事實上,你剛剛已經有了你的第一支Perl程式。當然,你可以不承認那是一支Perl程式。不過讓我們真正來寫一支程式吧。如果你習慣於使用許多整合性程式開發工具,你大概會希望知道要安裝甚麼樣的工具來寫Perl。不過你可能要失望了,因為我們全部所需要的就只是一個文字編輯器。你在unix上,可以選擇vi(vim),joe或任何你習慣的編輯器,在Windows上可以使用記事本,或下載UltraEdit(訪客無法瀏覽此圖片或連結,請先 註冊登入會員 。不過請不要使用類似Word的這樣的文書處理工具,因為這樣子你只是讓事情更複雜了。當然,即使你在Windows上,你還是可以選擇Vim或是Emacs這些在Unix世界獲得高度評價的文字編輯器,而且他們還是自由軟體。
現在,我們可以打下第一支程式了:
引用:
#!/usr/bin/perl

print "hello world\n";
相信大家很快就打完了這支程式。先別管裡面到底說了甚麼(雖然妳們應該都看懂了),我們先來執行他吧!
引用:
[hcchien@Apple]% perl ch1.pl
hello world
好極了,結果就像我們直接用命令列執行的樣子。不過至少我們知道了,只要用Perl去執行我們寫出來的程式就可以了,當然,你還可以有更簡單的辦法。你可以讓你的檔案變成可以執行,在Unix下,你只需要利用chmod來達成這樣的目的。當然,我們假設你已經可以操作你的系統,至少能夠瞭解檔案權限。修改完權限之後,你只需要在檔案的所在目錄打:
引用:
[hcchien@Apple]% ./ch1.pl
hello world
那麼第一行又是甚麼意思呢?其實這是Unix系統中,表明這支程式該以甚麼方式執行的表達方式。在這裡,我們希望使用"/usr/bin/perl"這個程式來執行。所以請依照你系統內的實際狀況適時改寫。否則當你在執行的時候,很可能會看到"Command not found"之類的錯誤訊息。
不過在真正開始寫Perl之前,我們還要提醒幾件事情,這些事情對於你要開始寫Perl的程式是非常的重要的。
1. Perl的敘述句是用分號(;)隔開的,因此只要你的敘述句還沒出現分號,Perl就不會把他當成一個完整的結束,除非你的這個敘述句是在一個區塊的最後一句。我們可以在perldoc裡面找到這樣的範例;
引用:
print
"hello world\n"
而且這樣的寫法對 Perl來說並沒有什麼不同,只是對於需要維護你的程式的人來說顯然並不會特別高興。適時的空白確實可以提高程式的可讀性,不過記得不要濫用,造成自己遭受埋怨。
2. Perl是以井字號(#)作為程式的註解標示,也就是只要以井字號開始,到敘述句結束前的內容都會被當成程式註解,Perl並不會嘗試去執行他,或編譯他。對於有些習慣於C程式寫作的程式員而言,能夠使用(/* .... */)來進行程式的註解確實是相當方便的。Perl並沒有正式的定義方式來進行這樣整個區塊的註解,不過卻可以利用其他方式來達到同樣的目的。例如使用pod(plain-old documentation format,簡明文件格式):
引用:
#!/usr/bin/perl

print "hello world\n";

=head1
這裡其實是註解,所以也是很方便的
主要是可以一次放很多行註解
=cut
如果你還想找出其他可能的替代方案,可以直接看perlfaq這份文件,而方法就是直接執行perldoc perlfaq(註四)就可以了。
接下來,我們便要真正進入Perl的環境中了。

習題:
1. 試著找出你電腦上的Perl版本為何。
2. 利用perldoc perl找出所有的perl文件內容
3. 利用Perl寫出第一個程式,印出你的名字


註一:事實上,Perl有所謂的單行模式,你就只需要在命令列中執行Perl的敘述句。
註二:因此也有人戲稱Perl是「膠水程式語言」。
註三:就是所謂的「Common Gateway Interface」,動態網站程式的設計界面。
註四:perldoc裡有著許多非常有用的文件,你可以考慮試著看看perldoc perldoc。

TOP

2. 純量變數(Scalar)
在Perl的世界裡,變數其實是以非常簡單的形式存在。至少比起你必須記憶一大堆int,char等等的資料形態算是方便許多了。對於所有只需要儲存單一變數值的資料結構,在Perl裡面都是使用純量數值來進行。所以你在寫Perl的時候不需要去考慮你的某個變數是要儲存數字或字串,大多數的時候你也不需要煩惱著要進行數字與字串間的轉換(註一)。

2.1 關於純量
一般的資料型態中,大多就是數值與字串兩種型態。當然,其中的數值也還可以分成整數型態跟浮點數型態,字串則是由一個或多個字元組合而成。在許多程式語言中,對這方面的定義非常的嚴格,你不但要考慮程式的流程,還必須隨時注意是否該對你的變數進行資料型態的轉換。不過在Perl中,對於所有這一類型的變數一律一視同仁,因此不論你所要儲存的是數值或字串,在Perl中都只需要使用純量變數。
其實就和我們現實生活中相當接近,我們在使用自然語言時,並不會特別去聲明接下來要使用甚麼樣的描述或解釋,而大多數是取決於當時的語境。而Larry Wall既然是一位自然語言學家,顯然對於這一方面特別在行,而這也造就了Perl能夠以非常接近口語的方式表達電腦程式語言的重要原因。

2.1.1 數值
數值對於Perl的意義在於「整數值」跟「浮點數值」,Perl其實對於數字的看待方式是以倍精度的方式去運算,對於目前大多數的系統而言,這樣的精確度顯然可以應付大部份的需求。否則許多券商都使用Perl進行系統開發,甚至太空科學或DNA運算也都大量的使用Perl,難道他們會想拿石頭砸自己的腳?何況,如果需要的話,Perl也可以支援精確度無限的BigNum(「大數」)運算。
你可以很容易的在Perl中使用數值,不論是整數或浮點數。例如下面都是非常典型的數值表達方式:
引用:
  1
  1.2
  1.0
  1.2e3
  -1
  -1.2
  -1e3
其中的1.2e3是表示1.2乘以10的三次方,這也許在科學或數學上的使用比較多,不過你多知道一些也是有幫助的。
另外,也許你希望使用類似1,300,000來清楚的顯示一個數值的長度,很可惜,這在Perl中會造成誤解,當然也不會達到你的要求。如果你真的期待以區隔的方式來表現數字的長度,你可以嘗試使用1_300_300的形式。不過我個人並不經常使用,也許我處理數字長度都還不足以使用這樣的表達方式。你可以看看在Perl裡面數值的表達方式:
引用:
#!/usr/local/bin/perl

$a = 1_300_300;
$b = 1.3e6;

print "$a\n";
print "$b\n";

執行結果會像這樣

1300300
1300000
你也許會希望使用非十進位的表示方式,例如十六進位的數字就可以在你撰寫網路相關程式的時候提供相當的幫助。這時候,Perl會有一些特殊的方式來幫助你完成這樣的工作。例如:
引用:
#!/usr/local/bin/perl

$a = 0266;
$b = 0xff;

print "$a, $b\n";
printf "%lo, %lx\n", $a, $b;

我們可以看到結果就會像是:

182, 255
266, ff
其中,printf 是以格式化的形式列印,我們稍後會提到。在這裡,我們只要知道這樣的方式可以印出八進位跟十六進位的數字就可以了。

2.1.2 字串
在純量的資料形態中,除了數字,另一個重要的部份則是字串。事實上,Perl的純量值就不外乎這兩種資料型態。在Perl中使用字串也非常的容易,你只需要在所要表達的字串前後加上一對的單引號或雙引號就可以了。
引用:
#!/usr/local/bin/perl

$a = "perl";
$b = 'perl';
$c = "在\tPerl\t中使用字串非常容易";
$d = '在\tPerl\t中使用字串非常容易';
$e = "23";
$f = "";

print "$a\n";
print "$b\n";
print "$c\n";
print "$d\n";
print "$e\n";
print "$f\n";

於是可以印出結果:

perl
perl
在      Perl    中使用字串非常容易
在\tPerl\t中使用字串非常容易
23
在這裡,我們看到一些比較有趣的東西。也是在Perl裡面對字串處理的一些特性:
1. Perl雖然可以使用單引號或雙引號來表示字串,但是兩者的使用卻有些許不同。像我們在$c跟$d兩個變數中所看到的一樣,兩個字串表面上看起來一樣,但是卻因為使用了單引號跟雙引號的差別,使得兩個出來的結果就產生了差異。因為在Perl的單引號中,是無法使用像\n(換行字元),\t(跳格字元)這些特殊字元的,因此在單引號中的這些字元都會被忠實的呈現。就是我們在變數$d的結果所看到的。而這些特殊字元還包括了:
引用:
\a:會發出嗶的警鈴聲
\d:代表一個數字的字元
\D:代表一個非數字的字元
\e:跳脫符號 (escape)
\f:換頁
\n:換行
\s:一個空白字元 (包括空行,換頁,跳格鍵也都屬於空白字元)
\S:非空白字元
\t:跳格字元 (Tab)
\w:一個字母,包括了a-z,A-Z,底線跟數字
\W:非字母
除此之外,我們可以在雙引號中內插變數,可是卻無法在單引號中使用這樣的方式。比如在程式中,我們使用了print這個函數,後面接著字串,字串裡面包含了變數名稱跟換行字元。恰巧這兩者都是無法以單引號呈現的,因此我們可以看看下面的程式表現出他們的差異:
引用:
#!/usr/local/bin/perl

$c = "在Perl中使用字串非常容易";

print '$c\n';
print "$c\n";

我們會得到這樣的結果

[hcchien@Apple]% perl ch2.pl
$c\n在Perl中使用字串非常容易
很明顯的看到第一行的輸出結果就單純的把單引號的內容表現出來,這樣的差異對之後程式的寫作其實有著相當的影響。
2. 利用引號將數字括住,雖然可以清楚的表達你希望將這個數值以字串的方式運算,但是其實這樣的方式有點囉唆,而且Perl的程式設計師並不會希望自己在寫程式時要弄的這麼複雜。因此只要在變數的使用時根據語境的不同而有不同的運算方式。也許可以看看以下的例子:
引用:
#!/usr/local/bin/perl

$a = "number";
$b = 3674;
$c = "4358";
$d = $a.$b;
$e = $b+$c;

print "$d\n";
print "$e\n";

可以得到:

number3674
8032
所以你可以看到變數$c,我們利用引號把數字括起來,但是當我們把變數$b跟變數$c相加時,Perl就會根據語意,自動把$c轉成數字後再進行加法的運算。也因此,如果你的程式把變數寫成像上例的變數$c一樣,雖然語法上不會有錯誤,不過對於有經驗的Perl程式設計師而言,反而會顯得很不習慣。如果他們看到這樣的程式寫法,也許還會發出會心的一笑。
3. 我們可以在定義變數時給定初值,就像上述的例子所使用的方式。而在Perl中,如果你只是定義了變數,而沒有給定初值,那麼這個變數會被Perl視為undef,也就是「尚未定義」的意思。

2.1.3 數字與字串轉換
毫無疑問,大部份我們在寫Perl的時候是不需要特定對數字與字串進行轉換,因為Perl通常會幫我們處理這一類的事情。例如你對兩個包含數字的純量變數進行加法運算以及連接運算則會產生不同的結果,我們可以作個實驗:
引用:
$a = 1357;
$b = 2468;

print $a+$b,"\n";
print $a.$b,"\n";

就可以看到印出:

3825
13572468
不過有時候你必須強制要求Perl使用某種資料型態,那麼就可以使用轉換函式,你可以利用int()函數把變數強制使用整數的型態。不過這樣的機會並不多,因此你只需要注意使用這些變數時用的算符,避免讓Perl產生誤會就可以了。

2.2 使用你自己的變數
在一般的情況下,Perl並不需要事先定義變數後才能使用,因此就像我們的範例中所看到的,你可以直接指定一個數值到某個變數名稱,而Perl大多也會欣然接受這樣的方式。可是在大部份的狀況,程式設計師所出現錯誤的機會遠大於Perl出現錯誤的可能。比如你可能會犯下所有程式員都會犯的錯誤:
引用:
$foo = 3;
$f00 = 6;
print $foo;
程式執行之後會得到'3'這樣的結果,這也許是你想要的,也許不是。不過如果你在編譯訊息裡面要求Perl送出編譯訊息給你(註二),你也許會得到這樣的訊息:
引用:
Name "main::f00" used only once: possible typo at ch2.pl line 4
這並不是錯誤,你也許因為打錯字而造成的結果還不足以影響Perl的執行,但是卻會影響你希望產生的結果。不過,使用編譯的警告參數還有其他用法。你可以在你的程式中加上"use warnings",所不同的是,你在程式的一開始就使用"-w"這個參數,是要求Perl對你程式中每一行都進行檢查。可是有時候會因為使用不同的版本,讓原來一切正常的程式在換為其他版本時產生出警告訊息。因此有些時候你可以單單對於某個區塊進行檢查的動作,這樣的強況下,你就可以使用"use warnings"這樣的方式來要求Perl幫忙。相對於此,我們還有其他的方式來跳過某個區塊的警告訊息。就像這樣:
引用:
#!/usr/local/bin/perl

use warnings;

{
no warnings;
$foo = 3;
$f00 = 6;
}

print $foo;
我們在程式的一開始就使用了"use warnings"這個選項來對我們的程式進行編譯的檢查。不過卻在某個區塊中定義了"no warnings",讓Perl跳過這個區塊進行檢查。這樣一來,我們在執行程式時,Perl就不會再發出上面的那些警告訊息。當然,除非你真的知道自己在做甚麼,否則還是盡量不要省略這樣的檢查才是正途。

另外一個良好的書寫習慣,就是在程式的前面加上use strict的描述,告訴Perl你希望使用比較嚴謹的方式來對程式進行編譯。而一但使用use strict來對你自己的Perl程式進行嚴謹的規則時,你就需要用my這個關鍵字來定義你自己所需要的區域變數。因此假設我們剛剛的程式會寫成這樣:
引用:
use strict;
my $foo = 3;
$f00 = 6;
print $foo;

那麼一但我們想要執行這支程式,Perl就會發生錯誤:

Global symbol "$f00" requires explicit package name at ch2.pl line 5.
Execution of ch2.pl aborted due to compilation errors.
因為我們沒有定義$f00這個變數,Perl不知道你是忘了,或者只是打錯字,這是相當有用的,尤其在你的程式長度已經超過一個螢幕的長度時。因為我們打錯字的機會確實還不少,而且這樣的程式錯誤是非常難以除錯的。因此能夠在程式的一開始就強制使用嚴謹的定義是比較正確的作法。

2.2.1 變數的命名
其實我們已經看了好多例子,裡面都包含了Perl的純量變數,因此相信大家應該都不算陌生了。Perl的變數是以字母,底線,數字為基本元素,你可以用字母或底線作為變數的開始,然後接著其他的字母,底線以及數字。可是在Perl的規定中,是不能以數字開始一個變數名稱的。
而在Perl中,純量變數則是以$符號作為辨識,因此像之前看到的都是屬於純量變數的範圍。
當然,怎麼幫你的變數名稱命名也是必須注意的,因為在Perl中,大小寫的字母是會被視為不同的。因此你如果用了$foo跟$fOO,這在Perl中是屬於兩個不同的變數,只是我們十分不建議這樣的命名方式。否則將來可能維護你的程式碼的人也許會默默的咒罵你,那可別怪我們沒事先警告了。
另外,你應該讓其他看你的程式設計師看到某個變數都大概可以猜出這個變數的作用,你自己想想,如果你看到某支程式裡面的變數名稱是像這樣子:$11,$22,$33,或者像這樣:$100000001,$100000002....。你會不會想要殺了這程式的作者呢?
至於大小寫也是在定義變數時可以運用的另一項特點,例如有人就習慣利用像這樣的方式來定義變數名稱:$ChangeMe。這樣可以避免某些因為連字時造成的混淆,不過一般而言,只要能清楚的表現變數的特性,大小寫與否則視個人的習慣。不過這一切都是為了將來程式維護上的方便,對Perl來說,這些變數的命名對他並沒有特別的意義。不過如果將來維護程式的可能是你自己,還是別找自己的麻煩吧!

2.3 賦值
既然變數就像一個容器,是拿來存放變數,能夠賦予變數他的內容就是非常重要,而且非常基本的一件事了。在程式中,我們經常會對於變數進行賦值運算,否則我們只需要一個定數就好,何必大費周章的使用變數呢?一般來說,要指定一個值給某一個變數的動作並不複雜,而且大概有這兩種主要的方式。

2.3.1 直接設定
這是直接用等號(=)來將某個數值或運算結果指定給變數。例如我們也許會這麼寫:
引用:
$foo = 3;
$foo = 2 + 1;
$bar = "bar";
$foo = $foo + 2;
$bar = $bar." or foo";

就可以看到印出的結果

8
bar or foo
在後面的兩個式子中我們看到左,右兩邊分別出現了兩次相同的變數名稱。這樣的方式是非常常見的,我們在右邊先取變數原來的數值,經過運算之後,把得到的結果指定給左邊的變數。因為Perl的自由度非常高,也許會有人想要把許多賦值的工作一次完成,那麼他可能把程式寫成這樣:
引用:
use strict;
my $foo = 3;
my $bar;
$foo = 3 + $bar = 2;
print "$foo\n";

那麼應該會看到這樣的結果:

Can't modify addition (+) in scalar assignment at ch2.pl line 6, near "2;"
Execution of ch2.pl aborted due to compilation errors.
沒錯,我們雖然說過Perl的語法其實相當自由,但是這樣的寫法確實會讓Perl搞混了。當然,我們可以把程式改寫成:
引用:
use strict;
my $foo = 3;
$foo = 3 + (my $bar = 2);
print "$foo\n";
Perl也會輸出結果為5,可是這樣雖然Perl可以清楚的瞭解你要表達的意思,只怕其他看程式的人還是會一頭霧水,而且這樣並不會讓你的程式執行得更快,當然無法靠這種方式讓你贏得Perl Golf(註三),所以除非你有很好的理由,否則還是少用吧!

2.3.2 還可以這樣
就像我們剛剛看到的,我們可以在賦值前先在等號右邊取出變數的值進行運算,就像這樣:$foo=$foo+3。可是其實某些二元算符可以有更方便的賦值方式,我們可以寫成這樣:
引用:
use strict;
my $foo = 3;
print "$foo\n";
$foo+=3;  # 其實就是 $foo = $foo + 3
print "$foo\n";
$foo*=3;  # 乘號也可以這樣使用
print "$foo\n";
$foo/=3;  # 其實所有的二元運算符都可以這麼用
print "$foo\n";

可以發現這樣的形式確實非常方便:

3
6
18
6
2.4 運算
其實變數的運算就跟一般的數值是一樣的,我們可以利用大部份我們所熟知的運算符號來對變數進行運算。例如我們當然可以把兩個變數相加,然後賦值給另一個變數,就像這樣:$third=$first+$second。或者更複雜的運算,這從過去我們學到的數學中都可以看到。當然,在Perl裡面也的運算式也符合現實生活中的規則,Perl會先乘號,除號進行運算,然後在把結果作相加或相減(如果你的運算式內有這些算符的話)。所以$foo=3*8+2*4就應該是32,而不是106。
不過Perl裡面並沒有數學中的中括號或大括號,而所有你希望先行計算的部份,都是由小括號將他括起來,例如你可以改寫剛剛的算式成:$foo=3*(8+2)*4,那麼結果顯然就變成120了。而運算符的優先順序正是你需要進行運算時非常最要的部份,雖然你已經知道乘號與除號的優先順序高於加號跟減號。而在這個時候,你還可能需要知道的某些算符的優先順序依照他們的優先性大概有下列幾種:
引用:
++, --
**
*,/,%,x
+,-,.
&
&&
||
+=,-=,*=,/=...
當然,Perl的算符並不只有這些,不過我們後面陸續會提到。如果你現在想要知道更多關於Perl算符的說明,可以看一下perldoc perlop這份文件。

2.5 變數的輸出/輸入
當我們寫了一堆程式之後,我們當然希望程式運算的結果可以被看到,否則即使程式運作的結果讓人非常滿意,你也無從得知。當然,換個角度想,如果你的程式錯的一踏糊塗,也不會有人知道。不過如果如此,那何必還花了大量的時間寫這支程式呢?如果你無法從程式得到任何結果。
最簡單的輸出方式,其實我們已經看了很多了,那就是利用print這個Perl的內建函數。而且用法非常直覺,你只需要把你要的結果透過print送出到標準輸出(STDOUT),當然,通常標準輸出指的就是螢幕,除非你自己動了甚麼手腳。我們可以來看看下面的例子會有甚麼結果:
引用:
use strict;
my $foo = 3;
print $foo;
print $foo*3;
print "列印字串\n";
print $foo, $foo+3, $foo*3;

很簡單的,我們就可以看到:

39列印字串
369
從範例中我們很清楚的就發現,我們可以單純的列印一個變數,一個運算式,一個字串,或者一堆用逗點(,)分隔開來的運算式。因為我們可以在print後面連接一個運算式,所以我們當然也可以寫成這樣:
print "foo = ", $foo;
或者你希望最後的輸出結果還可以換行,那麼你可以這麼寫:
print "foo = ", $foo, "\n";
可是如果你有三個變數,那麼你寫起來也許會像這樣:
print "first = ", $first, "second = ", $second, "third = ", $third, "\n";
好吧,雖然吃力,而且可能容易產生錯誤,不過畢竟你做到了。只是如果現在又多了一倍,那困難度可又增加了不少。

2.5.1 變數內插
我想你大概可以慢慢感受到,以Perl的程式設計師的個性,他們絕對不希望這樣的事情發生,因為這些程式設計師總是不希望自己的時間浪費在打字這件事情上,因此當然要有方法能夠少打一些字,又容易維持程式的正確性。而變數的內插就提供了這樣的福音。
我們之前提過對於字串的表示中,單引號與雙引號之間的差異。其實這兩者之間還有一個重要的差異,就是雙引號中可以進行內插變數,而單引號依然很真實的呈現引號內的字串內容。我們可以看看其中的差異:
引用:
my $foo = 3;
print "foo = $foo\n";
print 'foo = $foo\n';

很明顯的,輸出後就有了極大的不同:

foo = 3
foo = $foo\n
在雙引號中,不但特殊字元\n會被轉換為換行字元,變數名稱 $foo 也會被取代為變數的值後輸出。反觀利用單引號的時候,不論變數名稱或特殊字元都會被完整而原始的表示。不過如果你希望輸出這樣的字串呢?
print "$ 表示錢字符號\n"
在雙引號中,如果你希望正確的表達某些符號,例如用來提示特殊字元的倒斜線,表示變數的符號時,你必須用以一個倒斜線來讓原來符號的特殊意義消失,看看下面的寫法:
print "\$ 用來提示純量變數, \@ 則是陣列";
那麼你就可以正確的顯示你要的結果。除此之外,我們也許還有一些好玩的技巧,記得Perl的名言嗎?「辦法不只一種」。
讓我們來看下面的程式:
引用:
print "\$ 用來提示純量變數, \@ 則是陣列,還有 \"\n";
print qq/\$ 用來提示純量變數, \@ 則是陣列,還有 \"\n/;
print qq|\$ 用來提示純量變數, \@ 則是陣列,還有 \"\n|;

結果看來都是一樣的

$ 用來提示純量變數, @ 則是陣列,還有 "
這確實非常神奇,首先我們提示一下,為了避免Perl誤以為你要結束某個字串,因此如果你要印出雙引號時,記得先讓Perl知道,於是就是利用\"的方式來解除雙引號原來的作用。可是一但如此,你的字串內也許會變得難以判讀,尤其常常雙引號又是成雙的出現時。這時候,你可以利用qq來描述字串,而在範例中,我們用了qq//,qq||,其實qq後面可以接任何成對的符號。如果有興趣也可以自己動手試試。
接下來,我們可以來談談怎麼接受使用者的輸入,也就是讓程式可以根據使用者的需求而有不同的反應。
經常被使用的方式應該是程式在進行時,停下來等使用者輸入,當接收到換行字元時,程式就繼續往下執行。這時候我們就是大多就是依賴的方式。很顯然的就是STDOUT的對應,也就是所謂的標準輸入,而我們常用的應該大多就是鍵盤。因此,Perl在遇到時便會等待輸入,我們可以用這個簡單的例子來試試:
引用:
print "please enter your name:";
my $name = ;
print "\n";
print "hello, $name\n";

當我們執行時,就會有這樣的結果:

please enter your name:hcchien

hello, hcchien
看起來,王子跟公主似乎過著幸福,快樂的日子。可是唯一小小的缺憾,卻是Perl連我們在結束字串輸入的換行字元也一併當成字串的一部份了。這樣的動作有時候會有很大的影響,因此我們也許要考慮把這樣的錯誤彌補過來。這時候,chomp函數就派上用場了。這個函數生下來似乎就只為了進行這項工作,至少我們再也不用擔心使用者的換行字元該怎麼辦。他的用法顯然也不特別困難,只要把你需要修正的字串當成傳入值就可以了。
chomp($name);
所以我們只要把這一行加到剛剛的程式裡面,我想你應該就會發現一些變化。沒錯,原來我們的輸出最後還多了一行空行,那是因為我們輸入時打了最後一個換行字元,不過經過chomp的修正,那個字元果然就沒有了。那麼chomp有沒有甚麼資訊可以讓我們參考呢?既然他是一個函數,他就會有一個回傳值,而chomp的回傳值就是被移除的換行字元個數。例如我們如果有一個字串:
my $name = "hcchien\n";
那麼一但我執行了
chomp($name);
理論上就會傳回1的值。實際上也確實如此,那麼我們可以再來試試,如果變數$name的值變成
$name = "hcchien\n\n";
然後我們發現回傳值還是1,也就是說,chomp只對字串結尾的那個換行字元有效。因此如果我們只執行了一次chomp,並不是把字串後面的換行字元全部取消,而只是移除了一個。

2.6 Perl預設變數
很多時候,剛學Perl的程式設計師似乎常常會遇到一些問題,也就是不容易看懂其他的Perl程式。這其中的原因當然很多,例如Perl的寫作形式非常自由,同樣的需求可以利用各種方式達成,有些程式設計師常常會用非常簡略的語法而讓易讀性降低。另外,許多Perl的預設變數對於初學者也是一個問題。你常常會看到一堆符號在程式裡飛來飛去,卻完全不知道他們在說甚麼,你當然可以利用Perl的線上文件perlvar去找到你要的答案,不過我們會適時的在不同的章節提到一些Perl常用的預設變數。

2.7 defined 與 undef
你也許有過經驗,當你在寫一支程式的時候,你定義了一個變數,就像我們平常作的:
my $foo;
或者你可能寫成這樣:
my $foo = "";
於是在你的程式過程中,這樣兩種方式所定義出來的變數在你的程式並沒有產生不同。於是程式平靜的結束,你開始想像你多打了好幾個字,只為了告訴Perl $foo這個變數是個空字串。可是所有的事情一如你所預測的一般,你還是沒想想透,到底宣告變數是空字串到底有沒有意義呢?其實大部份的時候,你是不需要在定義變數時宣告為空字串,因為當這個變數被定義時,Perl會指定他為undef。而當這個變數被作為數值時,他瞬間就被當成零,同樣的,在被當成字串運算時,他則會被作為空字串。
不過,如果你的程式開啟了warnings參數,而打算列印一個undef的變數,可是會遭到警告的,因為Perl顯然很難理解你為甚麼需要列印一個沒有被定義的變數。而這很可能是你的程式有某部份發生了問題,所以千萬別隨便忽略Perl的警告,再仔細檢查你的程式吧!
所以你也許要確認你的程式在某些敘述是否正如你所期待的正確的進行了某些運算,這時候有一個函數就可以派上用場了,那就是defined()。你可以用defined來確定某個變數是否是經過定義,很簡單的就像這樣:
defined($name);
而許多程式的寫作中,也經常使用這個函數來進行判斷。例如你可以這麼寫:
引用:
my $name;
if (defined($name)) {
    print $name;
} else {
    print "it's undefined";
}

我們可以很清楚的看到這樣的宣告一個變數會被設為undef。
而且,undef在perl中也是個關鍵字,你可以直接指定某個變數是undef,就像你在賦值給任何變數一樣。所以你可以很簡單的寫成:
$name=undef;

習題:
1. 使用換行字元,將你的名字以每個字一行的方式印出。
2. 印出'\n', \t'字串。
3. 讓使用者輸入姓名,然後印出包含使用者姓名的招呼語(例如:hello xxx)。


註一:有些時候是必須強制進行轉換,Perl才知道你真正需要的是甚麼。
註二:你可以在程式的最前面寫成這樣「#!/usr/bin/perl -w」,告訴Perl你希望啟動編譯警告。
註三:一種長期並不定時舉行的Perl程式設計遊戲,以程式碼最短者獲勝,就像高爾夫球,最少稈者獲勝,詳細可以參考訪客無法瀏覽此圖片或連結,請先 註冊登入會員

TOP

3. 串列與陣列

在我們已經知道怎麼使用純量變數之後,我們就可以處理非常多的工作。可是有些時候,當我們要使用純量變數來儲存許多性質相近的變數時,卻很容易遭遇瓶頸。例如我希望儲存某個班級四十位學生的數學期末成績,這時候如果每個學生的成績都需要用單獨的一個變數來儲存的話,那會讓資料難以處理,也許你從此再也不想寫程式了,而且你的程式大概會長的像這樣子:
引用:
my $first = '40';
my $second = '80';
my $third = '82';
...
...
沒錯,這樣的寫法雖然可能可以讓我們比過去使用紙張的方式正確率高一些,可是卻未必會省事。另外,如果我希望從資料庫找出今天總共有多少人在我的網路留言板留言,那這時候的留言個數是未知的,要怎麼批次處理這些資料就很花腦筋了,所以要有適當的資料結構可以作這樣的處理。
很顯然的,陣列的運用非常的廣泛,幾乎大部分撰寫程式的時候都會使用陣列來進行資料的存取,在許多程式語言中,陣列的結構相當的複雜,這確實是必要的。因為陣列的使用要必須足夠靈活,才能夠發揮它的功能,可是如果太過複雜卻也是造成入門者的進入門檻。Perl對於這方面卻有一些不同的做法,它提供的陣列結構非常簡單,如果你用最入門的方式去看它,很多第一次接觸的人甚至也可以輕易上手。可是Perl的陣列卻也可以利用非常強大的方式擴展開來,讓許多第一次看到Perl陣列結構卻非常失望的人也能重拾對Perl的信心。當然,使用這些技巧來進行Perl陣列的擴充,不但可以像其他程式語言一般,可以進行多維陣列之外,還可以能精準的結合某些資料結構,當然,這部份我們不會在一開始介紹陣列時就把大家嚇走,不過如果你已經對陣列的方式有些熟悉,可以在後面的章節慢慢看出Perl在這方面設計的巧妙。

3.1 何謂陣列
對於我們剛剛提出來的資料結構需求,希望能把相同的東西簡單的存取,並且讓它們能被歸納在一起。陣列正是解決這個問題的方案,也就是把一堆性質接近的變數放在同一個資料結構裡,這樣可以很方便的處理跟存取。就像一疊盤子一樣,他們都是性質相接近的東西,於是我們就把盤子碟子一疊,而屬於不同性質的東西就分放在其他地方,比如我們就不太應該把碗跟盤子放在同一疊裡面。在Perl裡面,你可以定義一個陣列,而陣列裡面存放的就是純量,當然存放的個數可以由零個到許多個,至於實際可以儲存的個數則依據每部電腦不同而有所差別,因為Perl依然依循它自己的個性,並不對程式設計師進行太多的限制,因此它可以允許你使用系統上所有的資源,換句話說,你可能會因為一個陣列過大而佔用系統的所有資源。

3.2 Perl 的陣列結構
我們先來看看怎麼在Perl裡面定義一個陣列。在Perl中,陣列變數是以@符號開頭,例如你可以定義一個變數名稱叫做@array。然後利用$array[0],$array[1]...的方式來存取陣列裡的元素。也就是說,你在定義了陣列@array之後,你可以指定陣列裡面的值,就像這樣的方式:
引用:
my @array;
$array[0] = 'first';
$array[1] = 'second';
$array[2] = 'third';
....
這樣比起剛剛我們一個一個變數慢慢的指定雖然方便了不少,至少我們可以很清楚的瞭解這些數值都是屬於同一個群組的,因為它們被放在同一個陣列中(註一)。不過這樣的寫法實在太辛苦了,尤其當你已經知道你陣列中的元素個數,以及他們個別的值,你就可以用簡單一點的方式來把陣列的值指定給你的陣列,就像這樣:

my ($array[0], $array[1], $array[2]) = qw/first second third..../;

其中,qw/first second third.../這一串東西就被稱為串列,例如:
my ($one, $two, $three) = (1, 2, 3);
也就是把一個串列一次指定給三個變數。利用qw也是同樣的方式,因此剛剛那一行程式其實也可以寫成:
my ($first, $second, $third) = qw/first second third/;
這樣的方式,就是我們把串列的值指定給變數,所以當然這些變數也可以是陣列的元素。不過既然我們確定要把串列的值指定給某個陣列,我們顯然可以更簡單的這麼作:
my @array = qw/first second third/;
這樣的方式就是直接利用串列賦值給陣列的方式,而類似的方式還可以寫成這樣:
引用:
my @array = (1...10);
my @array = (0, 1, 2, 4...8, 10);
my @array2 = (3, -1, @array, 13);
my @array2 = qw/3, -1, @array, 13/;   # 這應該不是你想要的東西
當然,如果你定義了一個陣列,但是卻沒有賦值給他,那麼這個陣列就會是一個空陣列。相同的狀況,你也可以指定任意的陣列大小給Perl,當然前提是你的電腦有足夠的能力承受。這當然也是Perl的傳統之一。
Perl從來就不是一個嚴謹的程式語言,因此對於陣列的部份也採取同樣的規定。你不需要在程式的一開始就規定你的陣列長度,因此你可以在程式裡面隨時新增元素到你的陣列中。例如你的程式也許會寫的像這樣子:
引用:
my @array = qw/第零 第一 第二/;
$array[3] = '第三';
$array[4] = '第四';
沒錯,你可以使用串列形式來指定陣列的值,也可以直接把值指定給陣列的某個索引值,就像我們剛剛所使用的方式。另外,你也會發現,如果你這麼寫的話,Perl也不會阻止你:
引用:
$array[15] = '一下子就到 15 了';
那麼Perl會直接幫你的陣列程度擴充到15,也就是陣列的索引值會變成從0-14,而陣列大小變為16。至於陣列中間沒有被指定的值,Perl都會自動幫你設為undef,所以你的陣列中,有許多還沒定義的值。好吧,很多人也許對於這樣的設計不以為然,不過有時候這樣還是很方便的,不是嗎?想像你已經預測你的陣列會有20個元素,可是你現在只知道最後一個元素的值,你總不希望必須先把前面十九個元素值填滿之後才能開始使用你期待已久的那個元素值吧?
當然,對於那些認為應該嚴謹的定義程式語言語法,不能讓程式設計師為所欲為的人來說,Perl顯然不是他們會選擇的工具。而且這樣的戰爭已經持續了很長的一段時間,也不是我們可以在這裡解決的。讓我們暫且跳開風格爭議,繼續回來看Perl在陣列中的用法吧。
有時候我們需要知道陣列中的元素個數,比如我們希望在陣列中依序取出陣列中的元素並且進行運算,那麼我們就可以利用下面的方式來進行:
引用:
my @array = qw{first second third};  
# 記得利用qq賦值給字串的作法嗎?用qw賦值給陣列也是類似
$array[4] = 'fifth';                    # 我們跳過索引值3
print $#array;                                # 這裡取得的是最後一個索引值
print $array[3];               # 這裡應該不會有任何結果
既然$#array是陣列中最後一個索引值,所以我們可以利用($#array + 1)得到目前陣列中的元素個數(註二)。不過如果你打算利用這個索引值來確定目前陣列的長度,並且加入新的元素,就像這樣:
引用:
my @array = qw/first second third/;
$array[$#array+1] = 'forth';         # 把新的值放到現在最大索引值的下一個
當然,如果你這樣寫也是可以被接受的:
引用:
my @array = qw/first second third/;    # 一開始,你還是有三個元素值
$array[$#array+1] = 'forth';         # 這時候的 $#array 其實是 2
$array[$#array+1] = 'fifth';         # 可是這時候 $#array 已經變成 3 了

print @array;
3.3 push/pop
沒錯,我是說那樣的寫法可以被接受,可是好像非常辛苦,尤其當你已經被一大堆程式搞到焦頭爛耳,卻還要隨時注意現在的陣列到底發展到多大,接下來你應該把最新的值放到那裡,這樣顯然非常辛苦。你一定也猜到了,Perl不會讓這種事情發生的。所以Perl提供了push這個指令把你想要新增的值「推」入陣列中,同樣的,你也可以利用pop從陣列中取出最後一個元素。不過為甚麼要使用push/pop這樣的指令,這當然和整個陣列的資料結構是具有相關性的,如果你弄清楚了陣列的形式也許就很容易理解了。我們可以把陣列的儲存看成是一疊盤子,因此如果你要放新的盤子,或者是拿盤子,都必須從最上面動作。這也就是為甚麼我們可以利用push/pop來對陣列新增,或是取出元素的最重要原因。我們可以從下面的例子看到 push跟pop的運作:
引用:
my @array = qw{first second third};
push @array, 'fourth';
print $#array;                  # 這裡印出來的是3,表示'fortuh'已經被放入陣列
pop @array;
print $#array;                  # 至於pop,則是把元素從陣列中取出
而且利用pop取出元素一律是從陣列的最後一個元素取出,也就是「後進先出 (last in, first out)」的原則。當然,pop的回傳值也就是被取出的陣列元素,以上面的例子來看,取出的就是'fourth'這個元素。
另外,在使用push時,也不限定只能放入一個元素,你可以放入一整個陣列。那麼就像這樣的寫法:
引用:
my @array = qw{first second third};
my @array2 = qw/fourth fifth/;
push @array, @array2;
print @array;                  # 現在你有五個元素了
3.4 shift/unshift
沒錯,push/pop確實非常方便,他讓我們完全不需要考慮目前陣列的大小,只需要把東西堆到陣列的最後面,或者把陣列裡的最後一個元素拿掉。不過我們也發現了,這樣的操作只能針對陣列的最後一個元素,實在有點小小的遺憾。其實我們想想,如果我把陣列中非結尾的某個元素去掉,那會發生甚麼事呢?比如我現在有一個陣列,他目前總共有三個元素,因此索引值就是0..2。如果我想要把索引值為1的那個元素取消,那麼索引值是不是也就需要作大幅更動。尤其當陣列的元素相當多的時候,其實也會有一些困擾。
不過Perl還是允許我們從「頭」對陣列進行運算,也就是利用shift/unshift的指令。如果我們已經知道push/pop的運作,那麼我們可以從範例中輕鬆的瞭解shift/unshift對陣列的影響:
引用:
my @array = (1...10);
shift @array;                  # 我把1拿掉了
unshift @array, 0;          # 現在補上0
print @array;                  # 現在陣列的值變成了(0, 2...10)
現在你的陣列進行了大幅度的改變,我們應該來檢查一下,當我們在進行shift運算過程中,陣列元素的變化。
我們還是用剛剛的陣列來看看完整的陣列內容:
引用:
my @array = (1...10);                  # 我們還是使用這個陣列
shift @array;                          # 我把1拿掉了
print "$_\t$array[$_]" for (0...9);  # 現在陣列的值變成了(0, 2...10)
好極了,我們看到了輸出的結果:
引用:
0       2
1       3
2       4
3       5
4       6
5       7
6       8
7       9
8       10
Use of uninitialized value in concatenation (.) or string at ch3.pl line 7.
9
沒錯,我們看到了錯誤訊息。因為我們的陣列個數少了一個,因此索引值9目前並不存在,Perl也警告了我們。所以我們發現了,Perl在進行shift的時候,會把索引也重新排列過。不過你能不能從中間插入一個值,並且改變陣列的索引排列,或是攔腰砍斷,取走某些元素,然後希望Perl完全不介意這件事呢?目前看來似乎沒有辦法可以這麼作的。不過有些方式可以讓你單讀取出陣列中某些連續性的元素,也就是使用切片的方式。

3.5 切片
就如我們之前提到,我們總是把一堆串列放入陣列中,雖然放入的方式不盡相同,但是至少我們可以在陣列中找出0個以上的元素所組成的陣列。沒錯,如果我們知道一個陣列中的元素,而且我希望取出這個陣列中的某些連續性元素是不是可行呢?例如有一個陣列的元素是(2003...2008),那麼如果我希望取得的是這個陣列中2004-2006這三個元素,並且把這三個元素拿來進行其他運算或運用,我是不是應該這樣寫:
引用:
my @year = (2003...2008);
my ($range[0], $range[1], $range[2]) = ($year[1], $year[2], $year[3]);
其實如果你真的這麼寫了,也不會有人說你的程式有錯誤,雖然這樣的寫法總是很容易讓人產生錯誤。即使不是語法上的錯誤,也容易因為打字的原因而產生可能的邏輯錯誤。既然如此,我們顯然應該找出容易的方法來作這件事。我們用一個很容易看清楚的例子來說明吧:
引用:
my @array = (0...10);
my @array2 = @array[2...4];
print @array2;                          # 沒錯,你拿到了(2, 3, 4) 三個元素
這個方法,我們就稱為切片,就像我們把生魚片取出其中的一片。可是如果我要的範圍並不屬於連續性的話,還能切片嗎?其實就像你一個一個取出陣列中的元素,只是有些部份是連續的,你不希望把每個元素都打一次。所以如果你希望多切幾片,可以考慮這麼作:
引用:
my @array = (0...10);
my @array2 = @array[2...4, 6];
這時候,你拿到的不但是(2, 3, 4)三個元素,也包含了6這一個元素。這樣是不是非常方便呢?

3.6 陣列還是純量?
如果你已經開始自己試著寫一些Perl程式,不知道你有沒有遇到這個問題,你有一個陣列@array,你想新增一個陣列,元素跟原來的陣列@array相同,於是你想寫了這樣一個式子:
my @array2 = @array;
沒想到一時手誤,把這個式子打成這樣︰
my $array2 = @array;
這時候,Perl卻沒有傳回錯誤給你,可是程式會傳回什麼結果呢?我們可以來實驗看看,只要打這幾行:
引用:
my @array = (0...10);
my $array2 = @array;
print $array2;                          # 程式傳回 11
這個值恰好就是陣列@array的元素個數,所以我們似乎發現好方法來找到陣列的元數個數了。不過也許應該來研究一下,為什麼Perl對於資料型態能夠進行這樣的處理。這其實是非常重要的一個部份,也就是語境的轉換。這很像我們在之前曾經遇過的例子,當我有兩個變數,分別是:

my $a = 4;
my $b = 6;

可是當我使用 $a.$b 跟 $a+$b 兩個不同的運算子時,Perl也會自動去決定這時候該把兩個變數使用字串,或是變數進行處理。因為語境的不同,讓運算的方式也有所不同,這在Perl當中是非常重要的觀念。不過這個觀念絕非由Perl所獨創,相反的,這樣的用法在現實生活中是屢見不鮮。比如有人問你平常用甚麼寫程式,你也會依照當時聊天的情況回答你是用甚麼編輯器,或者是用甚麼程式語言。因此在語言的使用中,如何選對適當的語境確實相當重要,而既然Larry Wall就是研究語言的專家,把這種方法運用在Perl裡面也是再自然不過了。
我們再來看看剛剛的例子,我們指定一個陣列,並且指定這個陣列的元素包括一個從0到10的串列,而當我們把這個陣列賦值給一個純量變數時,Perl便會把串列元素個數指定為這個純量變數的值。這也就表示Perl正以純量變數的語境在處理你的運算,而對一個陣列以純量變數的語境進行運算時,Perl就如我們所看到的,以陣列中串列元素的個數表示。所以你可以寫出這樣的運算式:
引用:
my @array = (1...10);                  # 利用串列賦值給陣列
my $scalar = @array + 4;          # 在純量語境中進行
my @scalar_array = @array + 4;         
# 先以純量語境進行運算,然後以串列方式賦值給陣列
這樣看起來會不會有一點眼花繚亂?程式第一行的中,就像我們所熟知的狀況,我們把一個串列賦值給陣列。接下來,我們利用純量語境把陣列內串列元素的個數取出,並進行運算,然後把結果放到一個純量變數里,這裡全部都是以純量變數的方式在進行。第三行就比較複雜一點了,我們先用純量語境,取出陣列的串列元素個數,以純量方式進行運算,接下來把這個得到的結果以串列的方式指定給陣列@scalar_array。所以最後一行其實也像是這樣:
引用:
my @array = (1...10);
my $scale = @array + 4;                  # 這裡是純量語境
my @scalar_array = ($scale);          # 把得到的結果放進串列中,並且賦值給陣列 @scalar_array
其實就像這裡所看到的,如果你的需求是一個串列,而你卻只能得到一個純量,那麼Perl就會給你一個只有一個元素的串列。其實要訣就是仔細看看你希望得到甚麼樣的東西,而Perl可以給你甚麼東西。而有時候,當理想與現實有些落差的時候,也許就會有些undef產生。假如我們把剛剛的例子改成這樣:
引用:
my @array = (0...10);
my ($scalar1, $scalar2) = (@array + 4);
當我們要求的串列無法獲得滿足時,Perl就會幫忙補上undef。

3.7 一些常用的陣列運算
既然我們總是喜歡把性質類似的變數放在一起變成陣列,那麼很多時候我們就會希望對這一整個陣列進行某些運算。例如排序,過濾,一起帶入某個公式中進行運算等等。這時候我們經常利用迴圈來幫我們處理這一類的事情,不過有些常用的運算,Perl已經幫我們設想好了,我們只需要輕鬆的一個式子就可以進行一些繁複的工作。

3.7.1 sort
排序總是非常必要的,我們在舉陣列的時候有提到,如果我們要把某個班級學生的數學成績放入陣列,那麼我們也許會希望利用這些成績來排序。這時候,sort就非常有用了。我們可以這樣作:
引用:
my @array = qw/45 33 75 21 38 69 46/;
@array = sort { $a <=> $b } @array;
這樣Perl就會幫我們把陣列重新排列成為
21      33      38      45      46      69      75

其實,如果你這樣寫也是有相同的效果:

@array = sort @array;

當然,如果你需要比較複雜的排序方式,就要把包含排序的區塊加入,所以你也可以寫成:

@array = sort { $b <=> $a } @array;
其中$a跟$b是Perl的預設變數,在排序時被拿來作為兩兩取出的兩個數字。而<=>則是表示數字的比較,如果陣列中的元素是字串,則必須以cmp來進行排序。
我們可以用接下來的例子來說明怎麼樣進行更複雜的排序工作。
引用:
my @array = qw/-4 45 -33 8 75 21 -15 38 -69 46/;
@array = sort { ($a**2) <=> ($b**2) } @array;          # 這次我們以平方進行排序

所以得到的結果會是:

-4      8      -15     21      -33     38      45      46      -69      75
3.7.2 join
有時候,你也許會希望把串列裡面的元素值用某種方式連接成一個字串。比如也許你想要把串列中的元素全部以','來隔開,然後連接成一個字串,那麼join就可以幫上忙了。你可以在串列中這麼用:
引用:
print join ',',  qw/-4 45 -33 8 75 21 -15 38 -69 46/;

這一行顯然也可以寫成:

my @array = qw/-4 45 -33 8 75 21 -15 38 -69 46/;
print join ',', @array;
和join函數相對應的的則是split,他可以幫忙你把一個字串進行分隔,並且放進陣列中。

3.7.3 map
很多人會使用Excel的公式,而公式的作用就是針對某一行/列進行統一的運算。比如小時候在學校考試的時候,老師常常會因為全班成績普遍太差,而進行所謂「開平方乘以十」的計算。這時候,如果可以用map就顯得很方便了。
引用:
my @array = map { sqrt($_)*10 } qw/45 33 8 75 21 15 38 69 46/;
我們可以看到,串列裡面是學生的成績,所謂map就是把陣串列裡的元素一一提出,並進行運算,然後得到另外一個串列,我們就把所得到的串列放到陣列中。於是就可以得到這樣的一個陣列:
引用:
67.0820393249937
57.4456264653803
28.2842712474619
86.6025403784439
45.8257569495584
38.7298334620742
61.6441400296898
83.0662386291807
67.8232998312527
當然,map還有許多有趣的使用範例,而且如果能適時運用,確實能大幅降低你寫程式的時間,也可以讓你的程式更加乾淨俐落。

3.7.4 grep
我們既然可以針對串列中的每一個元素進行運算,並且傳回另一個串列,那麼是否可以針對串列進行篩選呢?例如我希望選出串列中大於零的元素,或者以字母開始的字串元素,那麼我可以怎麼作呢?這時候,grep就會是我們的好幫手。如果各位是Unix系統的使用者,應該大多用過系統的grep指令,而Perl的grep函數雖然不盡相同,不過精神卻是相近的。我們可以利用grep把串列中符合我們需求的元素保留下來。就像這樣:
引用:
my @array = qw/6 -4 8 12 -22 19 -8 42/;          # 指定一個串列給陣列 @array
my @positive = grep {$_ > 0} @array;          # 把@array裡大於零的數字取出
print "$_\n" for @positive;                    # 印出新的陣列 @positive
而且答案就正如我們所想像的,Perl能夠正確的找出這個陣列中大於零的數字。
也許你會有一些不錯的想法,如果我們想要把剛剛的陣列中所找出大於零的數字取得平方值之後印出,那麼我們應該怎麼做比較容易呢?當然,一般的情況下,我們就會想到迴圈,而這也正是我們接下來要說的部份。

習題:
1. 試著把串列 (24, 33, 65, 42, 58, 24, 87) 放入陣列中,並讓使用者輸入索引值 (0...6),然後印出陣列中相對應的值。
2. 把剛剛的陣列進行排序,並且印出排序後的結果。
3. 取出陣列中大於40的所有值。
4. 將所有陣列中的值除以 10 後印出。


註一:當然,你也可以把程式中的所有純量變數全部放在一個陣列中,不過很快的,你會發現連你自己都不想再看到這支程式了。
註二:別忘了,Perl的索引值是由零開始。

TOP

4. 基本的控制結構

4.1 概念
大部份的時候,程式總不會跟著你寫程式的順序,一行一行乖乖的往下走。尤其是當你的程式由平鋪直敘漸漸變成有些起伏,這時候,怎麼確定你的程式到底應該往那裡走,或者他們現在到底到了那裡。如果你無法掌握程式的流程,只怕他們很快就會離你而去。你從此再也無法想像你的程式會怎麼運作,當然也很有可能你就寫出了會產生無窮迴圈的程式了。

4.1.1 關於程式的流程
在程式的進行當中,你經常會因為過程發生了不同的事件,因為結果的不同而必須進行不同的運算,這個時候,你就必須進行程式中的流程控制。或者,你會需要對某些工作進行重複性的運算,這時候,重複性的流程控制就可以大大的幫助你減輕工作負擔。因此,你常常會發現,流程的控制在你的程式之中確實是非常重要而且經常被使用的。雖然Perl的流程控制跟其他程式語言並沒有太大的差異,不過我們還是假設大家並沒有這方面的基礎。所以還是從頭來看看最基本的流程控制應該怎麼作呢!

4.1.2 真,偽的判斷
流程的基本控制主要在於判斷某個敘述句是否成立,並藉以判斷在不同情況下該怎麼進行程式的流程。比如我們可以用簡單的例子來認識一下流程控制的進行:
引用:
my $num = <STDIN>;
chomp($num);

if ($num<5) {
    print "small";
} else {
    print "big";
這樣看起來是不是非常簡單呢?
不過Perl比較特殊的部份在於他並不存在一種獨立的布林資料型態,而是有他獨特對於真,偽值的判定方式。所以我們應該先要知道,在Perl中,那些值屬於真,那些值屬於偽,這樣一來,我們才能知道判斷句是否成立。
引用:
* 0 屬於偽值
* 空字串屬於偽值
* 如果一個字串的內容是"0",也會被視為偽值。
* 一個undef的值也屬於偽值。
當然,有些運算式也是透過這些方式來判斷,我們可以輕易的找到例子來觀察Perl的處理方式。例如你可以看看Perl對這樣的判斷式怎麼處理:
引用:
my $true = (1 < 2);
print $true;
沒錯,回傳值是1,表示這是個真值,因此如果你在流程控制中用這樣的判斷式,很清楚可以知道流程的方向。

4.1.3 區塊
在開始進入正式的判斷式之前,我們應該先來說說Perl程式中的區塊。在Perl中,你可以用一對大括號{}來區分出一個Perl區塊,這樣的方式在程式的流程控制中其實非常常見。
在Perl的語法中,區塊中的最後一個敘述不必然要加上分號的,比如你可以這麼寫:
引用:
my $num = 3;
{
    my $max = $num;
    print $num
}
不過如果你未必覺得自己足夠細心的話,也許你該考慮留下這個分號,因為一但你的程式略有修改,你也許會忘了加上該有的分號。那你花在加這些分號的時間可能會讓你覺得應該隨時記得替你的敘述句加上分號才是。另外,區塊本身是不需要以分號作為結束的,不過你可別把區塊所使用的大括號跟雜湊所使用的混在一起。

4.1.4 變數的生命週期
既然提到區塊,我們似乎應該在這裡稍微提起Perl裡面關於變數的生命週期。一般來說,Perl的生命週期都是以區塊來作為區別的。這和有些程式語言的定義方式似乎有些差距,當然,Perl的區分方式應該是屬於比較簡單的一種,所以一般而言,你只需要找到相對應的位置,就很容易可以知道某個變數現在是否還存在他的生命週期中。我們可以看個範例:
引用:
my $num = 3;
{
    my $max = $num;
    print $max;
}
print $max;
在程式裡,我們在區塊中宣告了變數$max的存在,並且把變數$num的值給了他。就在這一切的運算結束之後,我們進行了兩次的列印動作。而兩次列印分別在大括號的結束符號前後,表示一個列印是在區塊中進行,另一個則是在區塊結束後才列印。不過當我們試著執行這支程式時,發生了一個錯誤:
引用:
Global symbol "$max" requires explicit package name at ch2.pl line 12.
Execution of ch2.pl aborted due to compilation errors.
沒錯,我們在區塊內定義了變數$max,也因此,變數$max的生命週期也就僅止於區塊內,一但區塊結束之後,變數$max也就隨之消失了。另外,我們也可以看看這個類似的例子:
引用:
my $num = 3;
{
    my $max = $num;
    print "$max\n";
}
{
    my $max = $num*3;
    print "$max\n";
}
這個例子中,我們看到了變數$max被定義了兩次,可是這兩次卻因為分屬於不同的區塊,因此Perl會把他們視為是完全獨立的個體。也不會警告我們有個叫做$max的變數被重複定義了。這看起來非常簡單吧?!這讓你可以在你需要的區塊裡,定義屬於那個區塊自己的同名變數,可是有時候其實你會把自己搞的頭暈,不信的話,你可以看看接下來的寫法:
引用:
my $a = 3;
my $b = 9;
{
    print "$a\n";  # 屬於外層的區塊,所以你會看到 3
    my $b = 6;     # 定義了這區塊內自己的變數
    print "$b\n";  # 於是你看到的這個$b的值其實是6
}
{
    print "$a\n";  # 這個區塊沒有自己的$a
    print "$b\n";  # 也沒有自己的$b
                   # 所以你在這裡看到的值其實是上一層的變數值
}
print "$a\n";      # 這裡似乎毫無疑問
print "$b\n";     # Perl還是印出期待中的3跟9
4.2 簡單判斷
好極了,現在我們已經知道甚麼是真值,甚麼是偽值。這樣就可以運用在程式的流程判斷了。

4.2.1 if
if的判斷非常的直覺,也就是說,只要判斷式傳回真值,程式就會執行條件狀況下的內容。這是一個非常簡單的例子:
引用:
my $num = 3;
if ($num < 5) {
    print "這是真的";
}
沒錯,這個程式雖然簡單,但卻很清楚的表達出if判斷式的精神。在($num < 5)裡,Perl傳回一個真值,於是我們就可以執行接下來的區塊,也就是列印出字串"這是真的"。
引用:
提示:
由於這些判斷式會用到大量的二元運算符,為了避免執行上產生難以除錯的問題,我們在這裡提醒各位一些容
易忽略的部份。
"<",">",">=","<=","==","!=":這些算符都是在針對數字時用到的比較算符。
"eq","lt","gt","le","ge","ne":如果你是對字串進行比對,請記得使用這些比較算符。
4.2.2 unless
和if相對應的,就是unless了。其實在其他程式語言,很少使用unless的方式來進行判斷。因為我們可以使用if的否定來進行同樣的工作例如你可以用
if (!($a < 3))
這樣的方式來描述一個否定的判斷句。可是利用否定的運算符"!"來進行判斷顯然不夠直覺,也因此比較容易出錯。這個時候unless就顯得方便多了。從口語來看,if敘述就是我們所說的「假如...就...」,而unless就變成了「除非...就...」。這樣在運算式看起來,就顯得清楚,也清爽多了。所以你可以寫成:
unless ($a <3)
這樣的寫法跟上面的那個例子是一樣的效果,不過在易讀性上明顯好了許多。尤其當你的判斷式稍微複雜一些,你就更可以感受到unless的好用之處了。

4.2.3 一行的判斷
在許多時候,我們會用非常簡單的判斷來決定程式的走向。這時候,我們便希望能以最簡單的方式來處理這個敘述句。尤其當我們進行了判斷之後,只需要根據判斷的結果來執行一行敘述時,使用區塊的方式就顯得有點冗長了,比方你有一個像這樣的需求:
引用:
if ($num < 5) {
   $num++;
}
這樣的寫法確實非常工整,可是對於惜字如金的Perl程式員來說,這樣的寫法似乎非常不經濟。於是一種簡單的模式被大量使用:
引用:
$num++ if ($num < 5);
你沒看錯,確實就是如此,把判斷句跟後續的運算句合併為一個運算式。而且這種用法不僅止於if/unless判斷式,而是被大量使用在許多Perl的運算式中。我們以後還會有很多機會遇到。不過先讓我們繼續往下看。

4.3.4 else/elsif
你總是有很多機會使用到if/unless判斷式,而且常常必須搭配著其他的判斷才能完整的讓你的程式知道他該做甚麼事。這個時候,比如你也許會想這麼寫:
引用:
if ($num == 1) { ... }
if ($num != 1) { ... }
這樣的程式雖然也對,不過總覺得那裡不太對勁,畢竟這兩個判斷式顯然正在對同一件事進行判斷,不過卻必須分好幾個敘述句。當然,如果你的判斷還是簡單(像我們的例子所寫的)也就還能手工進行。可是如果你的判斷式長的像這樣呢?
引用:
if ($num == 1) { ... }
if ($num == 2) { ... }
if ($num == 3) { ... }
......
if (($num != 1) && ($num != 2) && ($num != 3) && ...) { ... }
沒錯,你現在可以想像人工進行這件事情的複雜度了吧!所以如果有簡單的方式來進行,同時還能增加程式的易讀性,似乎是非常好的主意,而else/elsif就是這個問題的解答。
如果我們在一個,或一大堆if判斷式的最後希望能有一個總結,表示除了這些條件之外,其他所有狀況下,我們都要用某個方式來處理,那麼else就是非常好的助手。最簡單的形式大概就會像這樣:
引用:
if ($num == 1) {
   ...
} else {  # 其實這裡就是 if ($num != 1) 的意思了
   ...
}
不過如果我們有超過一個判斷式的時候,就像之前的例子,我希望$num在1..3的時候,能有不同的處理方式,甚至我如果進行一個禮拜七天的工作,我希望每天都能有不同的狀況,那只有else顯然不夠。我總是不希望每次都來個if,到然後還要判斷使用者打錯的情況。這時候,elsif就派上用場了,你每次在其他條件下,如果還要訂下其他的條件,那麼你就可以寫成像這樣:
引用:
if ($date eq '星期一') {
   ....
} elsif ($date eq '星期二') {
   ....
} elsif ($date eq '星期三') {
   ....
   ....
} else {
   print "你怎麼會有$date\n";
}
其實,利用if/else/elsif已經可以處理相當多的問題,可是在許多程式語言中還可以利用switch/case來進行類似的工作。在Perl中,也有類似的方式,這是由Damian Conway寫的一個模組,目前已經放進Perl的預設套件裡了。不過我們並不打算在這裡增加所有人的負擔。

4.3 重複執行
我們剛剛所提到的只是對於某個條件進行判斷,並藉由判斷的結果來決定程式的流程,因此條件的不同會讓程式往不同的地方繼續前進。不過很多時候我們需要在某些條件成立的時候進行某些重複的運算,比如我們希望算出10!,也就是10的階乘。這就表示只要我們指定的數字不超過10,就讓這個運算持續進行,這時候,我們顯然需要進行重複的運算。

4.3.1 while
while就是一個很好的例子,讓我們來看看怎麼利用while來完成階乘的例子:
引用:
my $num = 1;
my $result = 1;       # 小心,這裡一定要指定$result為1
while ($num <= 10) {   # 確定你是否超過範圍
      $result*=$num;
      $num = $num + 1;
}
看起來不難吧!你只要掌握幾個原則,理論上就可以很容易讓while迴圈輕鬆上手。
首先,你總得讓你的迴圈有正常運作的機會。當然你如果不希望這個迴圈有任何機會執行,Perl也不會在你的耳邊大叫,不過維護你的程式的人大概會很難理解這一段不可能執行到的程式碼有甚麼功用吧。
其次,別忘了讓你的程式有機會離開他的迴圈,除非你知道自己在作甚麼否則你的程式會不斷的持續進行,當然,那就是所謂的無限迴圈。例如你寫了一個非常簡單的程式:
引用:
while (1) {               # 在這裡,程式會得到永遠的真值
    print "這是無限迴圈";
}
第三,在這裡,你還是可以讓只有單一敘述的while迴圈利用倒裝句達成:
我們假設你完全知道剛剛的程式會發生甚麼事,而那正是你所希望達成的,那麼我們就可以來改寫一下,讓他變得更簡潔:
引用:
print "這是無限迴圈" while (1);
別怪我們太囉唆,不過這樣的寫法確實讓程式乾淨許多,而且許多Perl程式設計師(也包括我自己在內)非常喜歡這樣的用法,如果你有機會讀到別人的程式,還是先在這裡熟悉一下吧。

4.3.2 until
類似if/unless的相對性,你也可以用until來取代while的反面意義,例如你可以用until來作剛剛階乘的同樣程式。語法其實跟while一樣:
引用:
until (判斷式) {
    ....
}
雖然語法看起來完全一樣,不過如果是剛剛的階乘,判斷式就會變成這樣:
引用:
my $num = 1;
my $result = 1;
until ($num > 10) {
      $result*=$num;
      $num = $num + 1;
}
4.4 for
for迴圈也是非常有用的迴圈,尤其在你使用陣列時,你可以很方便的取出所有陣列中的元素。而且你幾乎不需要知道現在陣列中有多少元素,聽起來非常神奇不是嗎?不過先讓我們來看看for到底是怎麼用的呢?

4.4.1 像 C 的寫法
如果你寫過C語言,你應該對這樣的寫法非常熟悉,所以你應該可以直接跳過這一小段。當然,我們假設大部份的人都不熟C,那麼我以為,如果你覺得太累,也可以晚一點再回來看這一段。因為作者個人的偏見,以為這雖然是Perl的基本語法,可是在實際程式寫作時用的機會卻比其他方式少了一些。不過我想大家都是好學生,還是讓我們來看看基本的for迴圈應該怎麼寫呢?還是維持我們的傳統,來看看這個例子吧:
引用:
my $result = 1;
for (my $num = 1; $num <= 10; $num = $num + 1) {
    $result *= $num;
}
跟剛剛的while/until終於有些不太一樣,而且主要的差別似乎在於這一行:
for (my $num = 1; $num <= 10; $num = $num + 1)
沒錯,這一行確實就是for迴圈的奧秘所在。首先我們看到小括號裡面的三個敘述,這三個分別代表迴圈的初值,迴圈的條件,以及每次迴圈進行後所作的改變。從這個例子來看,我們先定義了一個變數$num,初始值是1。接下來要求迴圈執行,只要$num在不超過10的狀況下就不斷的執行,最後一個則表示,當迴圈每做一次,$num的值就被加1。
當然,這三個部份都是獨立的,所以如果你有比較特殊的判斷方式,也可以隨意修改。例如你可以用總和超過/不超過多少來作為判斷的依據。或者每次在執行完一次迴圈就把$num的值加3,這樣的寫法對於應付比較複雜的狀況顯然非常好用。
不過如果我們大部份的時候都只是非常有規律的遞增,或遞減,並且以此為判斷迴圈是否應該結束的依據。既然如此,也許我們還有更清楚,而且簡單的方式嗎?

4.4.2 其實可以用 ...
大家對於Perl程式設計師喜歡簡單的體會應該已經非常深刻了,因此我們就來看看怎麼樣可以讓非常具有規則性的for迴圈可以用更簡單的方式來表達吧!例如像我們前面提到的例子:
for (my $num = 1; $num <= 10; $num = $num + 1)
這麼簡單的for迴圈還要寫這麼長一串真是太累人了,記得有一種非常簡單的語法嗎?
for $num = 1 to 10
沒錯,如果Perl也可以這樣寫那就非常口語化了。而且再也不用那麼長的敘述句只為了告訴Perl:「給我一個1到10的迴圈吧!」不過很顯然,Perl的程式設計師找到了更簡便的方法,你只要用...就可以表示你需要的迴圈範圍了。
引用:
for my $num (1...10) {   # 這就是表示$num從1到10
    print $num;
}

這其實可以寫成:

for (1...10) {           # $_ 經常被拿來作為迴圈的預設變數
    print $_;
}

更簡化的寫法:

print for (1...10);
覺得很神奇嗎?其實一點也不會,因為這正是Perl程式設計師經常在使用的方式。而你需要更熟悉的也許就是要習慣於這些人對於習慣寫作的方式。

4.4.3 有趣的遞增/遞減算符
如果你寫過C語言程式,或是你看過其他人寫Perl的迴圈,應該會常常看到這樣的敘述句:
引用:
print for ($i = 1; $i <= 10; $i++);          # 印出 1...10
可是對於遞增(++)與遞減(--)運算子卻又是似懂非懂。那麼這兩個運算子到底在說些甚麼呢?不過顧名思義,他們主要的工作就是對數字變數進行遞增或遞減的運算。例如你也許會這麼用:
引用:
my $i = 1;
while ($i <= 10) {
      print $i;
      $i++;                                  # 把 $i 加上 1
}
沒錯,於是在 while 迴圈中,$i就會由1到10,靠的正是這個遞增運算子。當然,在這裡你也可以把這個式子替換為:

$i = $i + 1; # 或是 $i += 1

不過看起來總是沒有$i++簡潔吧!同樣的,遞減運算子(--)也是進行類似的工作,也就是每次把你的數值減1。不過遞增或遞減運算子總有時候會讓人感覺困擾,讓我們來看看以下的例子:
引用:
my $i = 1;
while ($i <= 10) {
    print ++$i."\n";                          # 印出 2...11
}

my $j = 1;
while ($j <= 10) {
    print $j++."\n";                          # 印出 1...10
}
從這個例子來看,應該比較清楚,在第一個迴圈中,Perl會先幫$i加1之後印出,也就是根據遞增(遞減)運算子的位置來決定如何運算。當然,我們就可以確定在第二個迴圈中,遞增運算子是怎麼運作的。當遞增運算子在前時,Perl會先對運算元進行運算,就像第一個迴圈的狀況,反之,則是在進行完原來的運算式之後,才進行遞增(或遞減)的運算,也就是像第二個迴圈中所看到的結果。

4.4.4 對於陣列內的元素
我們當然可以用 for (;;) 這樣的方式來取出陣列中的所有元素來運算。不過這時候你只是依照陣列的索引順序,因此你還需要根據索引的值來取得陣列中的元素值。就像這樣子:
引用:
my @array = qw/1 2 3 4 5/;
for (my $i = 0; $i < $#array; $i++) {
    print $array[$i];
}
顯然這樣的寫法太過繁瑣,我們其實可以利用foreach來進行更簡單的取值動作。那麼剛剛的迴圈部份可以寫成:
引用:
print foreach (@array);
不要懷疑,就是這麼簡單,不過讓我們先來解釋一下整個迴圈的運作。
當你對一個陣列使用foreach迴圈時,Perl就會自動取出這個迴圈每一個值。接下來,你可以指定Perl把取得的值放到某個變數中,例如你可以寫成:
引用:
foreach my $element (@array)
不過這時候我們就用到了Perl最常用到的預設變數$_,當我們在迴圈中沒有指定任何變數時,Perl就會把取出來的值放入預設變數$_中。緊接著我們希望把迴圈取得的值列印出來,也就是執行
print $_;
這樣的式子,當然,這樣的式子在Perl出現的機率真是太少,因為大部份的時候,如果你只要列印單一的$_,Perl程式員也就會省略$_這個變數。而因為我們在迴圈中只打算執行print這個指令,所以倒裝句也就順勢產生。看起來應該顯得非常簡潔吧!現在你應該還不習慣這樣的寫法,不過如果你有機會接觸其他Perl程式員寫的程式,那千萬要慢慢接受這樣的寫法。

那麼還有一個問題,那就是foreach是否只能用在迴圈的取值,或是foreach跟for該怎麼區別他們的用法呢?
這個問題倒很容易,因為在Perl之中,foreach跟for所進行的工作基本上是一模一樣,或說他們之中的任何一個都只是另一個的別名。因此只要你可以使用for的地方,即使你將他替換為froeach也完全可以被Perl接受,不過有時候也許你會希望以foreach來表達你的算式能夠讓可能維護你的程式的人比較容易接受,當然,很多時候Perl程式員確實不願意多打四個字母,所以你被需按照當時的語境確定實際執行的是for或foreach的語意了。


還記得我們在上一章提出的問題嗎?如果我們有一個含有整數串列的陣列,而我們想取出其中大於零的整數然後取他們的平方值,那麼我們應該怎麼作呢?現在我們可以嘗試來玩玩這個問題。
我們先用迴圈的方式來解決這個問題:
引用:
my @array = qw/6 -4 8 12 -22 19 -8 42/;
my @positive;
for (@array) {                                  # 針對陣列的每個元素檢察
    push @positive, ($_**2) if ($_ > 0);  # 如果大於零就取平方值
}

print for (@positive);
那如果使用我們在上一章介紹的函數呢?
引用:
my @array = qw/6 -4 8 12 -22 19 -8 42/;
my @positive = map { $_**2 }
               grep { $_  > 0 } @array;         
# 倒裝,把@array的元素先放進 grep 檢查,再把通過檢查的結果利用map取得平方值放進新的陣列

print for (@positive);
相當有趣吧,這也就是Perl的名言:「解決事情的方法不只一種。」

習題:
1. 算出1+3+5+...+99的值
2. 如果我們從1加到n,那麼在累加結果不超過100,n的最大值應該是多少?
3. 讓使用者輸入一個數字,如果輸入的數字小於50,則算出他的階乘,否則就印出數字太大的警告。

TOP

5. 雜湊(Hash)
雜湊對一般使用者大概都非常不熟悉,尤其是沒接觸過Perl的人來說,雜湊對他們來說都是全新名詞。但是在現實生活中,雜湊卻是不斷出現在一般人的生活之中。因此只要搞懂雜湊到底在講甚麼,你就會覺得這個東西用起來真是自然極了,而且沒有了雜湊還可能讓很多事情顯得不知所措,因為你要花大量的時間跟精力才能利用其他資料結構做出雜湊所達到的結果。
聽起來雜湊確實非常吸引人,那我們先來瞭解一下甚麼是雜湊。所謂的雜湊,其實用最簡單的話來說,也就是一對鍵值(key-value),沒錯,就是這麼簡單,一個鍵搭配著一個值的對應方式。當然,你可以搭配Perl所提供複雜的方式來建立多層的雜湊來符合程式的需要,不過那不是基本的雜湊,而且所謂複雜的結構,還是依循著最簡單的原理,也就是鍵跟值的相對應關係。

5.1 日常生活的雜湊
沒錯,如果我們只說雜湊是一對鍵值的組合,那要讓人真正理解顯然並不容易。所以如果我們可以使用一般人常用的詞彙來解釋雜湊這個東西,顯然應該會容易許多。既然如此,我們就來看看大家每天接觸的資料中,有甚麼是能夠以雜湊精確的表現出來的。
最簡單的例子應該算是身份證字號了吧,我們可以很容易的用身份證字號知道一個人的姓名,其中的身份證字號就是雜湊的鍵(key),而利用這個鍵所得到的值(value)就是姓名。而且鍵是這個雜湊中唯一的值,也就是一個雜湊中,不能有重複的鍵。這也應該很明顯,如果有兩個一模一樣的身份證字號,那麼我們要怎麼確認使用者希望找到的是那一個呢?所以這也是雜湊中的限制,我們必須要求雜湊中的鍵值必須是不重複的,很顯然,這樣的限制是非常合理的。另外,我們每個人的行動電話中也藏著使用雜湊的好素材。如果你曾經使用行動電話的電話簿功能,那麼你也許每天都會接觸這種非常雜湊式的結構,因為電話簿功能也是雜湊足以發揮功用的地方。電話簿就是一整個的雜湊,他裡面的鍵是以姓名為主,值則是這個人的電話。所以你必須為每一個人鍵入一個獨特的鍵,大多也就是名字,以及這個鍵所對應的值,當然就是電話了。因為我們只要找到鍵(姓名)就可以查到依附在這個鍵的值(電話)。如此一來,我們應該很容易可以理解雜湊的代表意義了。

5.2 雜湊的表達
雜湊在Perl中是以百分比符號(%)作為表示,變數的命名方式則維持一貫原則,也就是可以包含字母,數字及底線的字串,但是不能以數字作為開頭。所以你可以像這樣的方式定義一個雜湊變數:
引用:
my %hash;          # 基本的命名方式
my %ID_Hash;          # 包含底線的變數
my %id_hash;          # 大小寫還是被認為是不同的字串
my %_underline;          # 以底線開始的變數名稱
my %2hash;          # 程式會產生錯誤,因為這不是合法的變數
雜湊的存取,我們可以利用大括號來進行,因此我們把所想要取得的雜湊鍵放入大括號中,就可以藉此找到相對應的值。同樣的方式,我們也可以利用這樣的形式指定某一對鍵值,這樣的作法非常接近我們存取陣列的形式:
引用:
my %hash;
$hash{key} = 'value';  # 最簡單的賦值形式
print $hash{key};
就如我們說的,我們是使用大括號({})來標示所要存取的雜湊鍵,這和使用陣列是不同的。不過更重要的是千萬別把你的程式寫得像這個樣子:
引用:
my $var = 1;
my @var = (1, 2, 3, 4, 5, 6);
my %var;
$var{1} = 2;
$var{3} = 4;
$var{5} = 6;

print $var[2];
這樣的形式對於Perl來說當然是合法的,不過我們顯然不希望你用這樣的形式來寫程式,否則即使Perl可以很容易的分辨出來,只怕寫程式或維護的人自己還先搞混了。
有時候,我們會忽略一些小地方,那就會讓自己找不到雜湊中的值,其中有一個非常重要的部份,也就是雜湊鍵的資料型態。Perl會把雜湊鍵全部轉為字串,這樣的轉換其實是有些道理的。我們來研究一下這樣的程式會發生甚麼狀況呢:
引用:
my %hash;
$hash{2} = 'two';                  # 指定雜湊的一對鍵值
$hash{'4/2'} = '這是字串 4/2';          # 注意引號的使用
print $hash{4/2};                  # 先運算後轉為字串的鍵
你認為Perl會輸出甚麼樣的結果呢?答案是'two'。沒錯,很有趣吧,所以你可以在雜湊鍵的地方放上一個運算式,那麼Perl會先進行運算,然後把運算結果轉為字串,所以上面的例子,我們所要求Perl輸出的其實是$hash{2},否則你可以利用引號來指定字串,就像$hash{'4/2'}這樣的方式。我們再看看另一個例子:
引用:
my %hash;
for (1...5) {
    $hash{$_*2} = $_**2;
}
那我們可以得到的雜湊就是像是這個樣子:
引用:
$hash{2} = 1;
$hash{4} = 4;
$hash{6} = 9;
$hash{8} = 16;
$hash{10} = 25;
沒錯,正如我們所預料的,Perl會把運算出來的結果轉為字串後當成雜湊的鍵。還記得我們可以利用字串的內插方式來插入變數到字串嗎?你可以猜測以下的程式會產生出甚麼不同的結果:
引用:
my %hash;
for (1...5) {
    $hash{"$_*2"} = $_**2;
}
如果你可以想辦法看到雜湊的內容,你會發現你得到的雜湊鍵變成了 "1*2","2*2"......。沒錯,因為他們被視為一個字串了。所以如果你以為你可以利用$hash{2}或$hash{4}來得到雜湊內的值,恐怕會失望了。所以當你要開始使用雜湊時,可就要小心別搞混了。

5.3 雜湊賦值
我們剛剛學到了利用 $hash{2} = 4; 這樣的方式來指定一對鍵值給雜湊,沒錯,這是賦值給雜湊的最基本方式,不過就跟我們使用陣列一樣,我們經常需要一次指定大量的雜湊鍵值,想必Perl的開發者一定也會遇到相同的問題,而且應該有一些合理的解決方案。既然如此,我們應該有其他方式可以一次指定超過一組的鍵值。利用串列的方式賦值給雜湊就是其中之一,而且當你在定義某個雜湊時就預先知道他的一些鍵值時特別有用,看看下面的例子:
引用:
my %hash = qw/1 one 2 two 3 three/;
這樣的賦值方式看起來跟處理陣列時候的方式非常接近,我們利用qw//來指定一個串列,並且將這個串列賦值給雜湊。這時候,Perl會按照串列的順序,分別為【鍵】,【值】,並且賦予雜湊。所以在這個例子中,所得到的結果就跟我們這麼寫是一樣的:
引用:
$hash{1} = 'one';
$hash{2} = 'two';
$hash{3} = 'three';
或許你會想到某個狀況,也就是鍵值的個數不一的時候。這時候,Perl會把最後一個鍵所對應的值設為undef(註二),你可以利用這個程式來確認:
引用:
my %hash = (1, 2, 3, 4, 5);
print 'false' unless defined($hash{5});
當然利用串列賦值的方式是方便了一些,可是就像我們剛剛遇到的問題,有時候會發現利用串列賦值的情況似乎比較容易發生錯誤。尤其當一個串列的元素足夠多的時候,你要怎麼確認某個串列中的元素應該是鍵,還是值呢?最簡單的方式大概就是進行人工比對,所以你或許可以考慮用另外的方式來賦值給雜湊,就像這樣的寫法:
引用:
my %hash = (
            1 => 'one',
            2 => 'two',
            3 => 'three',
            );
在這裡,我們利用箭號(=>)來表示雜湊中鍵跟值的相對關係,而且在一對鍵值的後面加上逗號作為區隔。這樣的方式就顯得方便、也直覺了許多。不過當你在使用箭號進行指定時,你可能會發現一些不同。因為箭號左邊的雜湊鍵已經完全被視為一個字串,所以你如果使用這樣的方式:
引用:
my %hash = (
            4/2 => 3,
            );
print $hash{'4/2'};
print $hash{2};
別忘了,跟之前的狀況一樣,Perl還是會幫你先把箭號左邊的運算式算出結果,然後轉成字串,作為雜湊的鍵。所以當你在取值時使用了引號確保你要找雜湊鍵等於'4/2'的值時,你就沒辦法找到任何結果,因為目前雜湊中只有一個雜湊鍵為'2'的值。
要從雜湊中取出現有的值以目前的方式應該足夠方便,你只需要知道雜湊中的鍵,就可以取得他的內容值。不過這樣顯然還不夠,因為雜湊跟陣列還是有著相當的差異。在陣列中,你可以很清楚的知道陣列的索引值是從0到最後一個陣列的大小減1,可是在雜湊中卻並不是這麼一回事。如果你沒辦法知道雜湊的鍵,又怎麼取出他的值呢?那麼這個時候,你應該考慮先把整個雜湊讀過一次。

5.4 each
就像在陣列當中,你可以使用foreach這樣的迴圈來找到陣列中的每一個值,當然我們也經常需要在雜湊中進行類似的工作,我們希望可以在雜湊中能一次取出所有的鍵,值。所以你必須仰賴類似foreach的工具來幫助你,那就是each函數。例如你可以利用下面的寫法讀出剛剛我們所建立起來的雜湊:
引用:
while (my ($key, $value) = each (%hash)) {         
      # 取出雜湊中的每一對鍵值,並且分別放入$key, $value
      print "$key => $value\n";
}
很明顯的,每次each函數都會送回了一個包含兩個值的串列,其中這兩個值分別是一個雜湊鍵跟相對應的值。因此我們把取回的串列指定給$key和$value兩個變數,接著印出結果,就可以看到一對一對的鍵值了。而當傳回空陣列時,while判斷就會變成偽值,while迴圈也就結束了。利用這樣的函式對我們有很大的幫助,如果我們想要整理一個雜湊的內容,我們可以在完全不知道雜湊中有什麼內容的狀況下開始進行處理。使用each函數在處理雜湊時是讓事情顯得容易許多,可是有時候還是有點不方便的地方,舉例來說:如果我有一個包含著主機ip跟主機名稱的雜湊,雖然我不知道雜湊裡面到底有多少資料,可是我卻希望能找出所有的雜湊鍵值,然後取出以192開始的ip位址。這時候如果使用each來作,那就必須先把所有的鍵值取出,然後再一一進行比對,所以也許程式就像這樣:
引用:
my %hash = (
            '168.1.2.1' => 'verdi',
            '192.1.2.2' => 'wagner',
            '168.1.2.3' => 'beethoven',
            );                                  # 定義主機跟ip 的對應
my @hostname;
while (my ($key, $value) = each (%hash)) {
    if ($key =~ /^192/) {                  # 要找出ip以192開頭的部份
        push @hostname, $value;                  # 找到之後放入新的陣列中
    }
}

print @hostname;
很顯然,這樣的寫法確實可以讓程式正確的找出我們要的結果,不過我們總是還會繼續思考可以有更乾淨俐落的寫法,畢竟使用Perl的程式設計師都不太喜歡拉拉雜雜的程式。所以有甚麼方法可以讓過濾出需要的鍵值可以顯得方便些呢?

5.5 keys跟values
如果我們可以用簡單的方式一次取得雜湊的所有鍵(keys),那麼要進行過去的過程就非常容易,而我們所需要的就是過濾後留下來的鍵,跟他們的相對值。當然,有某些時候,你可能只想要拿到雜湊中的所有值,這時候你就不需要擔心他們是屬於什麼鍵的相關。為了因應這樣的需求,有兩個函數可以滿足我們,他們分別是keys跟values。很顯然的,這兩個函數所作的工作就是取出雜湊的鍵跟值。和使用 each相當不同的是:你可以只單讀取出所有的鍵,或所有的值,而不需要一次全部取出。
例如我們可以用這樣來把雜湊鍵放在同一個陣列中:
引用:
my @keys = keys(%hash);
如果你希望取出所有的值,那麼不妨這樣寫:
引用:
my @values = values(%hash);
當然,你可以用他來完成each的工作,就像這樣:
引用:
my @keys = keys(%hash);
for (@keys) {
    print "$_ => $hash{$_}\n";
}

其實跟這麼寫是一樣的效果:

while (my ($key, $value) = each(%hash)) {
      print "$key => $value\n";
}
不過你顯然會發現,有時候用keys/values比較簡單,有時候用each比較方便,當然,至於要使用何者是完全取決於你所想要得出的結果,或者你認為最省力,簡潔,或是效率比較好的寫法。

在雜湊中使用keys/values這兩個函數都傳回串列,因此我們可以把我們所得到的串列輕易的放入陣列,接下來再以陣列的方式進行運算。這樣的方便之處在於我們可以有很多可供利用的陣列函數,所以我們可以把剛剛的那個例子改寫成這樣:
引用:
my %hash = (
            '168.1.2.1' => 'verdi',
            '192.1.2.2' => 'wagner',
            '168.1.2.3' => 'beethoven',
            );

my @keys =  map { $hash{$_} }
            grep { (m/^192/) } keys(%hash);

print @keys;
這樣的寫法比起之前的方式看起來是不是乾淨許多了呢?我們來看看最關鍵的一行,結果到底怎麼產生的:我們先用keys函數取出雜湊中的所有鍵,就如我們所說的,這個函數傳回一個串列。然後我們對所得到的串列進行過濾,利用grep取出串列中以192開頭的ip子串列,最後利用map一一比對得出雜湊中以對應這些ip的主機名稱。

5.6 雜湊的操作
毫無疑問,雜湊這樣的資料結構對於程式的寫作有著莫大的幫助,但是我們必須能熟悉對雜湊的操作才能夠讓我們更容易發揮雜湊的功能。其中最重要的大概就是exists跟delete兩個函數了,這兩個函式能讓我們有效的掌握雜湊的元素,同時它們也是perl內建相關於雜湊函數的最後兩個(註一)。

5.6.1 exists
我們就繼續用ip跟主機的雜湊當例子吧。假如我有一個ip,我不確定我是否有這部主機的資料,如果我們只用剛剛的方法,那我們就必須取得所有的ip,然後把手上的ip跟取得的ip串列一一比對,以便確定自己有沒有這個ip的主機資料。所以我們的程式也許長的像這樣:
引用:
my %hash = (
            '168.1.2.1' => 'verdi',
            '192.1.2.2' => 'wagner',
            '168.1.2.3' => 'beethoven',
            );
my $ip = '192.1.2.2';
print "bingo" if ($hash{$ip});
在這裡,我們有一個雜湊,其中三個鍵分別是'168.1.2.1','192.1.2.2','168.1.2.3',而我們希望判定目前手上的一組ip'192.1.2.2'是不是我們主機所擁有的ip。於是我們利用這個ip作為雜湊鍵,並判斷如果取得的值為真,那麼我們就說這個ip屬於雜湊的其中一個鍵,這樣的想法似乎暫時解決了我們的需求。不過我們來看看下面的例子:
引用:
my %hash = (
            'cd' => 2,
            'book' => 10,
            'video' => 0,
            );
my $media = 'video';
print "bingo" if ($hash{$media});
我們假設這是某個社區圖書館目前外借的東西數量,其中的鍵就是代表則可以外借的圖書館資產,其中包含了CD,書跟錄影帶。而所對應到的值則是他們目前被借出的數量。我們看到,CD被借走了兩套,書被借走了十本,而錄影帶則是原封不動,一卷也沒被借走。是的,大家都不喜歡錄影帶了。
這時候,我們希望知道圖書館是否提供錄影帶外借,也就是要檢查video這個鍵是否存在。於是我們利用剛剛的方式,看看$hash{$media}是否傳回真值。很遺憾,因為錄影帶這個鍵目前的值是0,因此當我們利用錄影帶當成鍵來取的相對應的值時,Perl會傳回0給我們。而我們知道0其實是個偽值。於是我們以為'video'這個鍵並不存在於這個雜湊中,也就是說這個圖書館並沒有錄影帶出借,但是這樣的結果跟我們的認知有所不同,因為取得的值為0只是代表目前沒人借出。所以我們發現這個方法並不正確,至少我們已經知道他會產生錯誤的結果。所以我們必須嘗試其他方法,例如利用keys找到包含所有索引鍵的串列,然後進行一一的比對。就像這樣:
引用:
print "exist" if (grep { $_ eq 'video' } keys (%hash));
這樣就可以確定某個鍵是否存在於這個雜湊,可是程式還是有點長,而且我們也許必須經常去判斷某個值是否為雜湊的鍵。所幸Perl提供了簡潔的函式可以使用,所以利用exists這個函式讓我們有了極佳的判斷方式。有了exist之後,對於剛剛那一行程式,我們只需要這麼改寫:
引用:
print "exists" if (exists $hash{video});
這樣的寫法顯然輕鬆了許多。

5.6.2 delete
有些時候,我們也會遇到某些鍵值我們不再需要的狀況,這時候如果可以把這些沒有必要的鍵值移除似乎是非常必要的。所以Perl也提供了移除雜湊鍵值的函式,也就是delete。這個函式的使用其實非常容易,你只需要指定想要刪除的某一個雜湊鍵,就像這樣:
引用:
delete $hash{video};
當然,所謂的移除是指這個鍵將不再存在於這個雜湊,而不是指讓這個鍵對應的雜湊值消失。所以並不是把需要被delete的這對鍵值設為undef。也就是說,即使有一個鍵所對應的雜湊值為undef,那這個鍵依然被視為存在(exists)的,這在剛剛解釋exists這個函數的例子中就可以瞭解了。

5.7 怎麼讓雜湊上手
在Perl中要使用雜湊,有一些重點也許還是應該提醒大家的。首先,Perl對於雜湊的大小限制依然採取了「放任」的態度,也就是以最沒有限制的方式。只要電腦可以容量的大小,Perl都可以接受。因此程式設計師可以有很大的揮灑空間,只是也必須注意避免讓系統因為被Perl佔用太多資源而導致無法正常運作。
另外,使用者可以利用任何的純量值來表示雜湊中的鍵與值。可是在雜湊鍵的部份,Perl會把所有的鍵轉換為字串。所以如果你在不注意的情況下把運算式當成雜湊的鍵,Perl會幫你先進行運算,然後利用運算所得的結果作為雜湊鍵,這樣的情況可能會有出乎意料的結果。當然,如果你使用運算式來作為雜湊的鍵值,那就應該有些準備,因此應該會更小心的注意,而我們也在前面提到了不少例子。
另外,你還會希望知道自己甚麼時候該用雜湊,這就必須依賴你對於雜湊的感覺,最基本的原則還是以雜湊的特性來看,如果你有一個可以辨識的鍵,而且希望藉由這個鍵找到相關連的值,這時候你幾乎就可以放心的使用雜湊了,只不過這裡所謂的值當然不限定單指特定的值,而可能是任何一種純量值,也就是因為這個特性,可以讓我們搭建出複雜的雜湊結構,不過這個部份則是屬於進階的內容,我們就不在這裡解釋。就像我們所舉的例子,你可以利用ip作為每一部主機的辨識,那麼你可以藉由ip找到那部機器的相關資料。
還有一個常常被搞混的問題,也就是雜湊的順序。許多人想當然爾,以為雜湊的順序是依照新增的順序來決定的。其實事實並非如此,雜湊的排列方式並非按照使用者加入的順序,而是Perl會依照內部的演算法找出最佳化的排列。

習題:
1. 將下列資料建立一個雜湊:
John => 1982.1.5
Paul => 1978.11.3
Lee => 1976.3.2
Mary => 1980.6.23
2. 印出1980年以後出生的人跟他們的生日。
3. 新增兩筆資料到雜湊中:
Kayle => 1984.6.12
Ray => 1978.5.29
4. 檢查在不修改程式碼的情況下,能否達成第二題的題目需求。


註一:可以利用perldoc perlfunc來查看perl所提供的函數。
註二:其實,如果你在程式裡打開了警告訊息的選項,這樣的指定會讓Perl產生警告訊息:"Odd number of elements in hash assignment"。

TOP

6. 副常式
當你的程式在很多時候總是不斷在進行類似的工作時,而且你總是為了這些工作在寫相同的重複程式碼時,你應該考慮替你自己把這些段落用更簡潔的方式來萃取出一個可以重複使用的區塊。這時候,你就可以考慮使用副常式,不過如果要更清楚的瞭解副常式的意義,也許我們應該先來看看這樣的例子:
引用:
my @array = qw/6 8 -4 18 -9 -22 36 48/;
my @new_array = map { ($_**2) }
                grep { $_ > 0 } @array;
print @new_array;
print "\n";
my @array2 = qw/16 8 -24 8 -12 20 16 28/;
my @new_array2 = map { ($_**2) }
                grep { $_ > 0 } @array2;
print @new_array2;
從這幾行程式,你應該很容易看出一些端倪,因為我們發現在這個小小的程式中,幾乎除了陣列中的元素不同之外,其他的幾行程式顯然都在進行相同的工作。也許你覺得只有兩個陣列需要運算還算容易,不過當你需要進行相同運算的陣列達到十個,或二十個的時候,你總不希望又把相同的程式碼進行大量的複製吧?
也許你會認為複製,貼上這樣的動作總比弄懂副常式來得簡單許多,不過你也許需要考慮,當你的程式需要進行修改怎麼辦?讓我們來舉一個例子吧,如果你的程式在取平方值的地方現在希望可以取立方值,或甚至進行更複雜的運算,而你現在總共複製了一百份的程式碼在做同樣的運算。沒錯,恭喜你,你現在就因為這樣小小的改變而必須修改這一百個地方,而且這對程式的除錯會變成非常大的負擔。除非你認為你的工作時間就應該花在這樣的工人智慧上,否則你確實要考慮來使用副常式了。當然,如果你還是堅持不用副常式,千萬別找別人去維護你的程式。
使用副常式不但可以增加程式的可重用性,降低維護成本,當然還可以提昇程式的可讀性。就像剛剛的例子,你對於每次需要進行某個運算時,只需要使用不同的參數,而如果運算部份的程式需要修改時,也只需要修改一次。對於想要看程式碼的人來說,他也可以清楚的看懂這一部份到底進行甚麼樣的工作。因此適當的在自己的程式中使用副常式確實是非常必要而且提高效率的方式。

6.1 關於Perl的副常式
在Perl中,使用副常式並不困難,尤其當你曾經使用其他程式語言寫過副常式,那麼Perl的副常式對你來說更是容易上手。不過我們假設你從來沒寫過任何程式語言,那麼我們準備從Perl來學習副常式了。
在Perl中,我們可以用&來表明副常式,而識別字的命名方式也是和其他變數的命名方式相同。也就是可以用數字,底線和字母組成,但是不能以數字開始。關於副常式,大概有兩個部份你必須特別注意的,也就是副常式的定義跟叫用。也就是副常式本身的區塊以及使用副常式。就像我們剛剛說到的,副常式的本身是以&符號作為辨識,所以如果你有一個叫做DoSub的副常式,那麼你就可以利用&DoSub的方式來叫用。當然,利用&符號叫用副常式本身,並不是絕對必要的,除非你的副常式名稱和Perl內建的函式名稱有所重疊,否則你其實可以省略&,也就是說,你可以直接使用DoSub來叫用你自己寫的DoSub副常式。當然,有時候我們會建議你在叫用副常式時盡量加上&符號,除非你能夠非常確定你使用的副常式名稱和Perl並不重複。但是當你看到許多程式設計師都省略&符號時,可別以為他們寫錯了,他們也許都是經驗老練的高手,已經能輕易確定自己所使用的函式名稱不會發生衝突。如果你還是不太瞭解我們解釋的意思,那還是讓我們來看看這樣的寫法吧:
引用:
my $num = 12;
print hex($num),"\n";          # 這是Perl提供的hex函式
print &hex($num),"\n";          # 我們自己寫的hex副常式


sub hex {
    my $param = shift;
    $num*2;
}
這個例子應該就可以非常簡單的看出&帶來的不同,我們第一次使用了hex來呼叫函式,因為Perl內建了hex這個函式,所以Perl會直接使用內建的hex函式,而第二次我們使用&hex呼叫時,才真正叫用了我們自己定義的副常式hex。
不過說了這麼多,我們還是必須在開始叫用副常式之前,先嘗試寫出你自己的副常式。也就是主要運作的那一個部份,你可以使用sub這個Perl的關鍵字來定義一個副常式,而且既然我們已經使用了sub這個關鍵字,你總不會還以為我們會喜歡多打一個&字元吧,所以最典型的副常式大多會長的像這樣子:
引用:
sub subroutine {
    ...
    ...
}
當然,副常式內的縮排並非絕對必要的,不過為了保持程式的可讀性跟維持好的程式寫作習慣,我們還是極力建議各位在進行程式寫作時,能夠養成區塊內的縮排。在Perl中,一般的使用情況下(註一),你可以把副常式放在程式中的任何位置,只要你叫用的時候,能夠讓程式本身不至於找不到副常式而發生錯誤就可以。雖然筆者自己習慣把副常式放在最後,不過對於已經有其他程式語言寫作習慣的人來說,也許有規定副常式的位置,而養成在程式的一開始就定義出程式中用了那些副常式的習慣。不過不管如何,Perl對於這些情況都是允許的,所以你可以試著找到自己習慣的方式。
另外,在Perl中使用副常式還有一個特點,也就是對於程式中全域變數的存取。由於副常式也是屬於程式的一部份(對Perl來說,那就是另一個程式中的區塊),因此在Perl的設計中,你可以任意的存取程式中的全域變數,就像我們之前使用的那些變數。對很多使用其他程式語言的人來說,這實在非常不可想像,當然,也因此持反對意見的人應該也不在少數。不過保留這樣的功能卻未必就是鼓勵使用者以這樣的方式來寫程式,而只是保留某種彈性的空間。筆者還是建議各位能盡量使用參數的方式,並且使用副常式中的私有變數,這樣的建議當然是有一些理由的,因為你很可能在程式的發展過程中寫了一些副常式,並且把他們放在程式之中,等到程式慢慢成熟之後,你也許就可以把這些副常式放進模組裡,以方便建立程式的可重用性(註二),以及屬於自己的函式庫。這時候,如果你的副常式足夠獨立的話,那麼搬移的工作就可以輕鬆許多,也不容易產生一些難以除錯的狀況。相反的,如果你在開始使用副常式的時候就大量使用全域變數時,你可能會發現要把這些副常式放入模組中就顯得特別困難。不過有時候能夠在副常式中使用全域變數也是非常方便的,例如有些模組中,你可能會有一些不提供給外部使用的副常式,這時候你也許會直接叫用程式的全域變數。
現在,我們來寫我們的第一個副常式:
引用:
sub hello {
    print "hello\n";
}
沒錯,這個副常式雖然簡單,但是卻能夠讓我們一窺副常式的奧秘,所以當你叫用這個副常式時,他只會印出"hello"這個字串。那我們就來試試:
引用:
&hello;          # 印出 hello
&hello;          # 再印一次

sub hello {
    print "hello\n";
}
你應該發現了,副常式就是這麼簡單。

6.2 參數
沒錯,副常式的用法其實並不太困難,不過要能發揮副常式更重要的功能,可就還要下些功夫了,也就是讓副常式能根據我們的需求進行不同的回應。所以我們應該想辦法讓副常式能根據我們的需求來進行一些調整,進行不同的運算。首先,我們需要的是參數,所謂的參數也就是需求不同的那一個部份,利用參數來告訴副常式我們所需要的調整。使用參數,當然會有傳入跟接收的部份。發送端也就是叫用的部份,也就是我們要告訴副常式,我們需要進行調整的內容,我們只要直接把所要傳送的值放進小括號內,就像這樣:

&hello('world');

不過只有傳送當然是不夠的,我們的副常式也需要知道外面的世界發生了什麼事,它需要接收一些資訊。那我們來看看傳送的資訊去哪裡了,我們先來做個實驗:
引用:
&hello("world");          # 我們傳了參數 "world"

sub hello {
    print @_;                  # 原來參數傳到這裡了
}
我們可以看到,當我們呼叫副常式,並且把參數傳給副常式時,參數會被放到預設的陣列變數@_裡。這樣我們就可以叫用參數來進行操作了。既然如此,我們來改寫hello這個副常式吧!
引用:
&hello("world");          # 傳參數"world"

sub hello {
    my $name = shift @_;  # 把參數從預設陣列拿出來
    print "hello $name\n";  # 根據參數不同印出不同的招呼
}
6.3 傳回值
大多數的時候,我們除了參數,還會希望副常式可以有回傳值,也就是讓副常式利用我們的參數運算之後,也能夠傳回運算結果給我們,比如我們想要寫一個找階乘(註四)的副常式,因此我們告訴副常式,我們希望找到某個數的階乘,而當然也期望從副常式得到運算的結果,也就是我們需要副常式的回傳值。最簡單的方式,就是在副常式中使用return這個指令來要求副常式回傳某個值。我們可以試著把階乘的副常式寫出來:
引用:
my $return = &times(4);                  # 把回傳值放到變數$return
print $return;

sub times {
    my $max = shift;                  # 把參數指定為變數$max
    my $total = 1;                  # 如果不指定,預設會是0,那乘法會產生錯誤
    for (1...$max) {                  # 從 1 到 $max
  $total *= $_;                        # 進行階乘的動作
    }
    return $total                  # 傳回總數
}
在這裡,又有一些簡便的使用方式來處理Perl的傳回值,因為Perl會把副常式中最後一個運算的值當成預設的回傳值,所以你可以省略在進行運算後還必須再進行一次return的動作。就像這樣的寫法:
引用:
my $return = &square(4);
print $return;

sub square {
    my $base = shift;
    $base**2;
}
這時候,我們看到副常式的最後一次運算是把參數進行了一次取平方的動作,而這個運算結果就會直接被Perl當為回傳值,所以你就不需要再另外進行回傳的動作。這樣確實可以簡化寫副常式時的手續,繼續維持了Perl的簡樸風格。當然,如果你還是不太熟悉這種回傳的方式,你還是可以加上return的敘述,不過當你在看其他Perl程式設計師的程式時,可別被這樣的寫法搞混了。

6.4 再談參數
我們已經知道了在副常式中怎麼使用參數及回傳值,而且我們還看到了Perl在處理參數時所使用的預設陣列。聰明的讀者應該早就猜到,當我們使用超過一個的參數時,應該就是依照陣列的規則一個一個被填入預設的陣列中,因此我們也可以按照這樣的原則來取出使用。我們可以用剛剛的概念,很容易的理解多個參數時的運用:
引用:
my $return = &div(4, 2);          # 這時候有兩個參數
print $return;

sub div {
    $_[0]/$_[1];                  # 只是進行除法
}
這樣的寫法其實非常粗糙,不過我們只是舉例來說明副常式的參數運用。這次我們直接取出預設陣列中的元素來進行預算,因為只有一個運算式,所以運算結果也自然的被當成回傳值了。這樣的運用方式非常的簡明,所以當你在寫副常式的時候,你便可以使用許多組的參數。不過如果我們在叫用副常式的時候傳了三個參數,就像:

&div(4, 2, 6);

那會產生什麼結果呢?其實回傳值就跟原來的一樣,因為Perl並不會去在意參數的個數問題。不過如果你的程式有需要,應該去確認參數的個數,避免參數個數無法應付需要,以確保程式能正常而順利的進行。
既然Perl的參數是以陣列的方式儲存,而我們也知道,Perl的陣列並沒有大小的限制,也就是以系統的限制為準。那麼我們很容易的可以傳入多個參數,而且還可以正確的運算並且回傳運算的結果。就像這樣:
引用:
my $return = &adv(4, 2, 6, 4, 9);  # 我們一次傳入五個參數
print $return;

sub adv {
    my $total;
    for (@_) {                          # 針對預設陣列進行運算
  $total += $_;                        # 加總
    }
    $total/($#_+1);                  # 除以總數 (取平均)
}
這時候,不論你的參數個數多少,Perl都可以輕鬆的應付,然後算出所有參數的平均值而且這時候。而且我們所需要的就是不管使用者有多少參數,都可以正確的算出他們的平均值。不過使用不定個數的時機或許不像固定參數個數來得頻繁,很多時候,我們都會使用固定的參數個數,然後確定每個參數的用途。當然這樣的用法有時候會讓人產生一些困擾,尤其在你的程式會被大量重用時(註五),不過要考慮這個問題還需要對Perl有更深入的瞭解,所以暫時我們就先不討論這種深入的用法。

6.5 副常式中的變數使用
就像大部分人所想的,副常式也是一個區塊,所以有屬於這個區塊自己的變數,也就是副常式的私有變數。不過就如我們所說的,副常式是可以使用程式中的全域變數,就像程式中的其他區塊一般。因此我們只需要在副常式中宣告my變數,也就是定義了副常式的私有變數。那麼就像我們知道的,變數將會維持到這個區塊的結束,也就是你無法在程式的其他地方存取這個變數。
另外,在副常式中,還有一種相當特殊的變數,也就是利用local來定義變數。不過這個部份目前用的人已經非常的少,所以你可以記著副常式裡面有這樣的用法,然後跳過一個部份。而我們打算在這裡提出來的原因是因為各位也許會有機會在某些程式裡面看到這樣的用法,為了避免大家看到這種用法卻又不知道它的作用,我們就在這裡簡單的介紹local的用法,讓大家未來有機會看到時可以能有一些參考的資料。
其實local的用途在於確認某些變數是在副常式中私用的,可是因為副常式會有機會被其他程式引用,所以你無法預期在某個引用的程式之中是否也有名稱相同的變數。因此使用local來確立這是副常式中的私有變數,而如果原來的程式中有相同的變數名稱時,就把主程式的變數放入堆疊,也就是先暫時儲存了主程式的這個變數,然後把相同的變數名稱清空以提供副常式使用。一但離開了副常式之後,Perl就會復原原來被儲存,並且清空的變數了。這樣子看起來,local和my的用法看起來似乎非常接近。
當然,你會發現這跟my之間會有什麼差異呢?我們先來看看這個程式:
引用:
$var1 = "global";
&sub1;                                  # 印出 sub1
print "$var1\n";                  # 印出 global
&sub2;                                  # 現在變成 sub2
print "$var1\n";                  # 又回到 global

sub sub1 {
    my $var1 = "sub1";
    print "$var1\n";
}

sub sub2 {
    local $var1 = "sub2";
    print "$var1\n";
}
看起來沒什麼不同,好像兩者之間沒有太大的差別,可是如果我們改寫一下程式︰
引用:
$var1 = "global";
$var2 = "for local";
&sub1;                                  # 印出 local, for local
&sub2;                                  # 印出 global, for local

sub sub1 {
    local $var1 = "local";
    my $var2 = "my";
    &sub2;
}

sub sub2 {
    print "var1=$var1\tvar2=$var2\n";
}
從這裡,我們好像可以發現一些不同。差別就在於當我們先呼叫sub1的時候,sub1會把原來的變數$var1放進堆疊,清空後把新的值"local"放入。而在呼叫sub2的時候,因為還在sub1的區塊內,因此local還佔用著$var1這個變數。所以印出"local"的值,可是使用my就有所不同的。雖然我們在sub1使用了my來定義區域變數$var2,可是my卻不會把佔用原來$var2變數的空間。所以當我們呼叫sub2時,會使用sub2里的$var2變數。而在sub2里面因為沒有定義$var2,所以Perl直接叫用全域變數,也就印出了"for local"的字串。

習題:
1. 下面有一段程式,包含了一個陣列,以及一個副常式diff。其中diff這個副常式的功能在於算出陣列中最大與最小數值之間的差距。請試著將這個副常式補上。
引用:
#!/usr/bin/perl -w

use strict;

my @array = (23, 54, 12, 64, 23);
my $ret = diff(@array);
print "$ret\n";          # 印出 52 (64 - 12)
my @array2 = (42, 33, 71, 19, 52, 3);
my $ret2 = diff(@array2);
print "$ret2\n";  # 印出 68 (71 - 3)
2. 把第四章計算階乘的程式改寫為副常式型態,利用參數傳入所要求得的階乘數。

註一:如果你想要了也更複雜的副常式使用方式,可以參考perldoc perlsub。
註二:就像你弄出了小螺絲釘,你總不希望每次遇到一樣的需要就重作一次螺絲釘。
註三:筆著第一次學Perl的時候就是被預設變數$_打敗的。(XXX 正文中沒有出現)
註四:階乘就是從一乘到某個數,比如4的階乘就是1x2x3x4。
註五:也就是你的副常式被放入模組中,而會不斷被重用時。那麼你固定的參數的個數及順序,一但將來副常式要改寫時,很容易影響過去使用的程式碼,而產生無法正確執行的問題。不過這屬於進階的問題,我們並不在這裡討論。

TOP

7. 正規表示式
正規表示式其實並不是Perl的專利,相反的,在很多Unix系統中都一直有不少人使用正規表示式在處理他們日常生活的工作。尤其在許多Unix系統中的log更是發揮正規表示式的最好歷練,系統把所有發生過的狀況都存在log檔之中,可是你應該怎麼找出你要的資訊,並且統計成有用的資料。當然,大部份的Unix管理員可以求助許多工具,不過很大多數的狀況下,這些工具也是利用正規表示式在進行,所以如果說一個足夠深入管理Unix的系統管理員都曾經直接,或是間接的使用過正規表示式,我想應該很少人會反對吧。不過這顯然也充分的表達出正規表示式的重要性。
7.1 Perl 的第二把利劍
沒錯,正規表示式並不是Perl所獨有,或由Perl首創。可是在Perl之中卻被充分發揮,還有人說如果Perl少掉了雜湊跟正規表示式,那可就甚麼都不是了。情況也許沒有這麼誇張,可是卻可以從這裡明顯感覺出來正規表示式在Perl世界中所佔有的地位。對於許多人而言,聽到Perl的時候總不免聽其他人介紹Perl的文字處理能力,而這當然也大多是拜正規表示式所賜。
7.2 甚麼是正規表示式
講了那麼多,那到底甚麼是正規表示式呢?簡單的說,就是樣式比對。大部份的人用過各種文字處理器,文書編輯器,應該或多或少都用過編輯器裡面的搜尋功能,或是比對的功能吧!我彷彿聽到有人回答:那是基本功能啊。是啊,而且那也是最基本的樣式表示。就像我要在一大堆的文字中找到某個字串,這確實是非常需要的功能。不過如果你寫過其他程式語言,那麼你不妨回想一下,這樣的需求你應該怎麼表達呢?或者假設你現在是公司的網路管理員,如果你拿到一個郵件伺服器的log檔案,你希望找到所有寄給某個同事的所有郵件寄送資料,而你現在手上也許正在使用C或Java,或其他程式語言,你要怎麼完成你的工作呢?這樣說好像太過抽像,也許我們應該來舉個文件中搜尋關鍵字的例子。
例如我希望在perlfunc這份Perl文件中找sort這個字串,這樣的需求很簡單,大部份的時候你也都可以完成這樣的程式。可是我如果希望找到sort或者delete呢?好吧,雖然麻煩,不過多花點時間還是沒問題的。不過實際去找了之後,我發現找出來的結果真是非常的多。於是我看到某些找到的結果是這樣的:
引用:
  sort SUBNAME LIST
         sort BLOCK LIST
         sort LIST
沒錯,這些正是我想要找的結果,可是如果一個一個找也實在太辛苦了。所以如果我可以把這些東西寫成一個樣式,讓程式去辨別這樣樣式,符合樣式條件的才傳回來,這樣一來,應該比較符合我們的期待了。而所謂符合的條件,也就是我們所希望的「樣式」,於是我們開始想像這個樣式會是甚麼樣子,在這個例子中,我們開始設計我們需要的樣式:以sort開始,中間可能有一些其他的字,可能沒有,最後接著一個LIST,於是符合這樣的樣式都是我們所要搜尋的結果。相反的,如果在文章中其他地方出現的sort,可是並沒有符合我們的樣式,那麼也不能算是成功的比對。
就這樣,當我們再度拿起其他程式語言時,好像忽然覺得很難下手,因為要完成這樣的工作,顯然是非常的艱辛。不過在Perl的正規表示式中,這才是剛開始。因為你也許會希望在浩瀚的網路中找到你想要的某些資料,你也許知道某個網站有你所需要的資訊,比如每天的股票收盤價格,而你希望程式每天自動收集這些資訊之後自動去分析股票的走勢。當然,也許你已經可以每天派出機器人去各大新聞網站收集最新的消息,可是你也許需要利用正規表示式去萃取對你有幫助的新聞內容。或者你根本就想模仿google,去進行新聞的比對,然後過濾掉相同的新聞,利用機器人完成一份足夠動人的報紙。當然,並不是用了正規表示式就可以輕易完成這些工作,不過相較於其他開發工具,Perl在這方面顯然佔有相當大的優勢。

7.3 樣式比對
在Perl中,你要進行比對前,應該先產生出一個你所需要的「樣式(pattern)」,也就是說,你必須告訴Perl:在尋找的目標裡,如果發現存在著我所指定的樣式,就回傳給我。也就是說,你必須告訴Perl,我需要的東西大概長的像這個樣子,如果你有任何發現,就回傳給我。
所以樣式的寫法與精準與否就會影響比對的結果,通常而言,如果你發現比對出來的結果跟你的想像有所差距,那麼你顯然應該從比對的樣式著手,看看樣式上到底出了甚麼差錯。因為當你把結果反過來跟原來所寫的樣式比對,就會發現這些回傳結果確實還是符合比對的樣式。當然,要寫出正確的樣式是必須很花精神的,或者應該說要非常小心的。
如果我們要以簡單的方式來描述樣式的模型,那麼我們可以說樣式其實是由一個個單一位元所組成出來的一個比對字串。例如最簡單的一個單字是一個樣式,就像你寫了一個"Perl",他就是一個樣式。可是在樣式中也可能有一些特殊符號,他們雖然沒辦法用一般的字元來表示,可是使用了特殊符號之後,在Perl的比對中,他們還是逐字元的進行比對。很常見的就是我們在列印程式結果也會用到的"\t"或是"\n"等等。所以如果你寫了這樣的一個字串,他也算是一個比對的樣式:
"Perl\tPython\tPHP"
另外,你還可能會用到一些量詞,也就是用來表達數量。量詞的使用對於Perl的正規表示式中是佔有重要地位的,因為使用了比對量詞,你就可以讓你的比對樣式開始具有彈性。例如你想在你的比對字串內找到一個字,這個字可能是:
引用:
wow
woow
wooow
不過你又不想要把每一個字都放到你的比對樣式中,所謂的每一個字就也許包含了'wow','woow','wooow'...,而且也許他們有可能會變成"wooooooow",甚至中間夾雜了更多的"o",甚至在你寫程式的時候也都還無法預測中間會出現多少次的'o',這時候就是你需要使用量詞的時候了。另外還有許多技巧跟參數,例如你希望進行忽略大小寫的比對,或是你希望這個樣式只出現在句首或句尾等等,而這種種的東西都是拿來描述比對的樣式,讓Perl能更精準的比對出你所需要的字串。而在Perl之中使用正規表示式其實有許多的技巧,我們接下來就是要來討論該怎麼學習這些技巧。

7.4 Perl 怎麼比對
我們之前提過,Perl所使用的是逐字元比對,也就是說,Perl根據你的樣式去目標內容一個字元一個字元進行比對。例如你的目標內容是字串 "I am a perl monger",而你的樣式是字串"monger"。那麼Perl會根據樣式中的第一個字元"m"去字串中比對,當他瀏覽過"I",空白鍵,"a"之後,他遇到了句子中的第一個"m"字元。於是Perl拿出樣式字串中的第二個字元"o",可是目標字串的下一個字元卻是另一個空白鍵,於是Perl退回到比對字串的第一個字元"m"繼續比對。
就這樣繼續前進,一直到Perl找到下一個"m"。於是又拿出比對樣式的第二個字元"o",發現也符合目標字串的下一個字元。然後繼續往前進,等到Perl把整個比對字串都完成,並且在目標字串對應到相同的字串,整個比對的結果就傳回1,也就是進行了成功的比對。
也許我們可以用圖示的方式來表達Perl在正規表示式中的比對方式。


[圖]

7.5 怎麼開始使用正規表示式
如果你對Perl進行比對的方式有點理解,那麼要怎麼開始寫自己的正規表示式呢?
首先,我們要先知道,Perl使用了一個比對的運算子(=~),也就是利用這個運算子來讓Perl知道接下來是要進行比對。接下來,就要告訴Perl你所要使用的樣式,在Perl中,你可以用m//來括住你的樣式。而就像其他的括號表達,//也可以替換為其他成對出現的符號,例如你可以用m{},m||,或是m!!來表達你的樣式。不過對於習慣使用傳統的m//作為樣式表達的程式設計師來說,Perl倒是允許他們可以省略"m"這個代表比對(match)的字元。所以下面的方式都可以用來進行正規表示式:
引用:
$string =~ m/$patten/
$string =~ m{$patten}
$string =~ m|$patten|
$string =~ m!$patten!
$string =~ /$patten/
Perl在完成比對之後,會傳回成功與否的數值,所以你可以將正規表示式放到判斷式中,作為程式流程控制的決定因素。不過也僅止於此,也就是說,當比對成功時,正規表示式就會結束,而且傳回比對成功的結果。當然,如果Perl比對到字串結束還是沒有找到符合比對樣式的字串,那麼比對依然會結束,然後Perl會傳回比對失敗的結果。例如下面的例子就是一個利用正規表示式來控制程式的例子:
引用:
my $answer = "monger";
until ((my $patten = <STDIN>) =~ /$answer/) {   
    # 持續進行,直到使用者輸入含有 monger 的字串
    print "wrong\n";             # 在這裡,表示比對失敗
};
我們首先定義了一個字串"monger",並且把這個字串作為我們的比對樣式,其實我們也可以直接把這個樣式放到正規表示式裝,不過我們在這裡只是讓大家可以比叫清楚的分辨出樣式的內容。。接下來,我們從標準輸入裝置(一般就是鍵盤)讀取使用者輸入的字串,並且把讀進來的字串放到變數$patten中,接下來再去判斷使用者是否輸入含有"monger"的字串,如果沒有,就一直持續等候輸入,然後繼續進行比對,一直到比對成功才結束這個程式。
當然,如果正規表示式只能作這麼簡單的比對,那就真的太無趣了。而且如果他的功能這麼陽春,也實在稱不上是Perl的強力工具。還記得我們提過的量詞嗎?他可以讓我們的比對樣式變得更有彈性,現在我們可以用最簡單的量詞來重新描述我們的樣式。我們繼續使用剛剛的例子來看看:
引用:
my $answer = "mo*r";                          # 使用量詞
while (1) {                                    # 所以其實是無限迴圈
    if ((my $patten = <STDIN>) =~ /$answer/) {  # 判斷是否比對成功
        print "*match*\n";
    } else {
        print "*not match*\n";
    }
};

我們試著來執行看看

[hcchien@Apple]% perl ch3.pl
mor
*match*
mooor
*match*
moor
*match*
mar
*not match*
mur
*not match*
muur
*not match*
在這裡,我們用了這一次的量詞來進行比對。也就是"*"這個比對的量詞,它代表零次以上的任何次數,在這裡因為他接在字母"o"的後面,也就表示了"o"這個字元出現零次以上次數都符合我們所想要的樣式。所以我們看到前面幾次的比對都是比對成功即使我們只有輸入"mr"這個字串,但是因為這個字串中,"m"跟"r"之間,"o"總共出現了零次,因此對Perl而言,這也算是比對成功的。不過至少我們可以開始更有彈性的使用比對的樣式了,可是該怎麼要求Perl能夠最少比對一個"o"呢?在正規表示式中,'+'就表示至少出現一次,所以這時候我們就可以把"*"換成"+"符號。也就是說,我們如果以剛剛的例子來看,當我們把比對樣式改成"mo+r",原來可以成功比對的"mr"就不再成立了。
既然可以要求某個字元出現0次或1次,那麼如果我希望"o"至少出現二次,或其他更多的次數,有沒有辦法可以做到呢?答案也是肯定的,我們可以使用另一種方式來表示所需要的量詞數目,也就是說可以讓你限定次數的量詞,而它的表示方式會像這個樣子:
引用:
{min, max}
讓我們還是繼續以剛剛的例子來看,如果你希望掌握"o"出現的次數在某個區間內,那你就可以用這樣的方式。讓我們來改寫一下剛剛的程式變成這樣:
引用:
my $answer = "mo{2,4}r";  # 新的比對樣式
while (1) {
    if ((my $patten = <STDIN>) =~ /$answer/) {
        print "*match*\n";
    } else {
        print "*not match*\n";
    }
};
我們試著執行看看:
引用:
[hcchien@Apple]% perl ch3.pl
mor
*not match*
mooor
*match*
mr
*not match*
moor
*match*
很顯然的,比對樣式和剛剛有了明顯的變化。我們利用o{2,4}來限制了"o"只能出現兩次至四次,所以只要"o"出現的次數少於兩次或大於四次,我們都無法接受。而從執行的結果來看,Perl也符合我們的期待,因為當我們輸入"mor"或"mooooor"時,Perl都傳回比對失敗的訊息。不過如果"m"跟"r"中間能夠比對到二到四次的"o",那也就成功的比對了我們的樣式。
我們當然可能只需要設定某一邊的限制,例如我也許只要求某個字元出現三次以上,至於最多可能出現多少次我並不在意。這時候我們可以用這樣的樣式:mo{3,}r。很顯然,我們也可以這麼寫:mo{,8}r,這也就是表示我們並不限制"o"出現的最少次數,即使沒出現也可以,可是最多卻不能能出現超過八次。
另外,我們剛剛都一直在討論某個位元使用量詞的比對,可是我們還希望能同時對某個字串使用量詞進行比對。就像這樣的字串"wowwow",他也可能是"wow"或是"wowwowwow"。那麼我們應該怎麼來使用量詞呢?這時候,我們就需要定義某個群組了,而在正規表示式中,我們可以利用小括號()來把我們想要進行一次比對的字串全部拉進來,成為一個群組。所以如果我們希望比對出現一次以上的"wow"字串,那麼我們應該這麼寫:
引用:
my $answer = "(wow)+";  # 新的比對樣式
while (1) {
    if ((my $patten = <STDIN>) =~ /$answer/) {
        print "*match*\n";
    } else {
        print "*not match*\n";
    }
};
沒錯,當我們定義了群組(wow)之後,接下來Perl的比對每次都會以(wow)這個字串為主,也就是必須這個字串同時出現才算是比對成功。當然,你還是可以利用群組比對作限定量詞的方式,只要把剛剛的比對樣式改成(wow){2,4},那麼跟比對單一字元是一樣的方式。Perl還是會比對"wow"這個字串是不是出現二到四次之間,就像我們比對單一字元的狀況一樣。
我們好像講了不少關於Perl正規表示式的技巧,不過這只是一小部份,其實關於正規表示式中還有許多技巧可以善加利用的。不過我們把這些留在下一章再來討論,這時候也許是該喝杯茶休息一下了。

習題:
1. 讓使用者輸入字串,並且比對是否有Perl字樣,然後印出比對結果。
2. 比對當使用者輸入的字串包含foo兩次以上時(foofoo 或是 foofoofoo 或是 ...),印出比對成功字樣。

TOP

8. 更多關於正規表示式
正規表示式確實能夠完成很多字串比對的工作,可是當然也需要花更多的時間去熟悉這個高深的學問。如果你從來沒有用過正規表示式,你可以在學Perl時學會用Perl,然後在很多其他Unix環境下的應用程式裡面使用。當然,如果你曾經用過正規表示式,那麼可以在這裡看到一些更有趣的用法。我們在上一章已經介紹了正規表示式的一些基本概念,千萬別忘記,那些只是正規表示式最基本的部份,因為Perl能夠妥善的處裡字串幾乎就是仰賴正規表示式的強大功能。所以我們要來介紹更多關於正規表示式的用法。
8.1 只取一瓢飲
當你真正使用了正規表示式去進行字串比對的時候,你會發現,有時候會有可選擇性的比對。比如我希望找「電腦」或「資訊」這兩個詞是否在一篇文章裡,也就是只要「電腦」或「資訊」中任何一個詞出現在文章裡都算是比對成功,那麼我們就應該使用管線符號/|/來表式。所以我們的樣式應該試寫成這樣:/電腦|資訊/。
還有可能,你會想要找某個字串中部份相等的比對,就像這樣:
引用:
/f(oo|ee)t/                  # 找 foot 或 feet
/it (is|was) a good choice/  # 在句子中用不同的字
/on (March|April|May)/          # 顯然也可以多個選擇
8.2 比對的字符集合
在Perl中的所有的命名規則都必須以字母或底線作為第一個字元,那麼我們如果要以正規表示式來描述這樣的規則應該怎麼作呢?你總不希望你的樣式表達寫成這個樣子吧?

(/a|b|c|d|.......|z|A|B|C|D|......|_|)

這樣的寫法也確實是太過壯觀了一些。那麼我們應該怎麼減少自己跟其他可能看到這支程式的程式設計師在維護時的負擔呢?Perl提供了一種不錯的方式,也就是以「集合」的方式來表達上面的那個概念。因此剛剛的寫法以集合的方式來表達就可以寫成這樣:
[a-zA-Z_]
很顯然的,有些時候我們希望比對的字元是屬於數字,那麼就可以用[0-9]的方式。如果有需要,你也可以這麼寫[13579]來表示希望比對的是小於10的奇數。
有時候你會遇到一個問題,你希望比對的字元也許是各種標點,也就是你在鍵盤上看到,躲在數字上緣的那一堆字符,所以你想要寫成這樣的集合:
[!@#$%^&*()_+-=]
可是這時候問題就出現了,我們剛剛使用了連字號(-)來取得a-z,A-Z的各個字符,可是這裡有一個[+-=]會變成甚麼樣子呢?這恐怕會產生出讓人意想不到的結果。所以我們為了避免這種狀況,必須跳脫這個特殊字元,所以如果你真的希望把連字號放進你的字符集合的話,就必須使用(\-)的方式,所以剛剛的字符集合應該要寫成:
[!@#$%^&*()_+\-=]
另外,在字符集合還有一個特殊字元^,這被稱為排除字元。不過他的效用只在集合的開始,例如像是這樣:
[^24680]
這就表示比對24680以外的字元才算符合。

8.3 正規表示式的特別字元
就像我們在介紹正規表示式的概念的時候所說的,Perl是逐字元在處理樣式比對的。可是對於某一些字元,我們卻很難使用一般鍵盤上的按鍵去表達這些字元。所以我們就需要一些特殊字元的符號。這些就是Perl在處理正規表示式時常用的一些特殊字元:
引用:
\s:很多時候,我們回看到要比對的字串中有一些空白,可是很難分辨他們到底是空格,跳格符號或甚至是
換行符號 (註一),這時候我們可以用\s來對這些字元進行比對。而且\s對於空白符號的比對掌握非常的
高,以處理(\n\t\f\r )這五種字元。除了原來的空白鍵,以及我們所提過的跳格字元(\t),換行字元
(\n)外,\s還會毊??藉以表示回行首的\r跟換頁字元\f。
\S:在大部份的時候,正規表示式特殊字元的大小寫總是表示相反的意思,例如我們使用\s來表示上面所
說的五種空白字元,那麼\S也就是排除以上五種字元。
\w:這個特殊字元就等同於[a-zA-Z]的字符集合,例如你可以比對長度為3到10的英文單字,那就要寫成:
\w{3,10},同樣的,你就可以比對英文字母或英文單字了。\W:同樣的,如果你不希望看到任何在英文
字母範圍裡的字符,不妨就用這個方式避開。
\d:這個特殊的字元就是字符集合[0-9]的縮寫。
\D:其實你也可以寫成[^0-9],如果你不覺得麻煩的話。
這些縮寫符號也可以放在中括號括住的集合內,例如你可以寫成這樣:[\d\w_],這就表示字母,數字或底線都可以被接受。而且看起來顯然比起[a-zA-Z0-9_]舒服多了。
另外,你也可以這麼寫[\d\D],這表示數字或不是數字,所以就是所有字元,不過既然要全部字元,那就不如用"."來表示了。

8.4 一些修飾字元
現在是不是越來越進入裝況了呢?我們已經可以使用一般的比對樣式來對需要的字串進行比較了。於是我們拿到了一篇文字,就像這樣:
引用:
I use perl and I like perl. I am a Perl Monger.
我們現在希望找出裡面關於Perl的字串,這樣該相當簡單,所以我們把這串文字定義為字串$content。然後只要用這樣的樣式來比對:

$content =~ /perl/;

不過好像不太對勁,或許我們應該改寫成這樣:

$content =~ /Perl/;

可是萬一我們打算從檔案裡面取出一篇文字,然後去比對某個字串,這時候我們不知道自己會遇到的是Perl或perl。既然如此,我們可以用字符集合來表示,就像我們之前說過的樣子:

$content =~ /[pP]erl/;

可是我要怎麼確定不會寫成PERL呢?其實你可以考慮忽略大小寫的比對方式,所以你只要這樣表示:

$content =~ /perl/i;

其中的修飾字元i就是告訴Perl,你希望這次的比對可以忽略大小寫,也就是不管大小寫都算是比對成功。所以你有可能比對到Perl,perl,PERL。當然也可能有pErL這種奇怪的字串,不過有時候你會相信沒人會寫出這樣的東西在自己的文章裡。
Perl在進行比對的修飾字元,除了/i之外,我們還有/s可用。我們剛剛稍微提到了可以使用萬用字元點號(.)來進行比對,可是使用萬用字元卻有一個問題,也就是如果我們拿到的字串不在同一行內,萬用字串是沒辦法自動幫我們跨行比對,就像這樣:
引用:
my $content = "I like perl. \n I am a perl monger. \n";
if ($content =~ /like.*monger/) {
    print "*$1*\n";
}
我們想要找到like到monger中間的所有字元,可是因為中間多了換行符號(\n),所以Perl並不會找到我們真正需要的東西。這時候我們就可以動用/s來要求Perl進行跨行的比對。因此我們只要改寫原來的樣式為:

$content =~ /like.*monger/s

那麼就可以成功的進行比對了。可是如果有人還是喜歡用Perl Monger或是PERL MONGER來表達呢?我們當然還是可以同時利用忽略大小寫的修飾字元,因此我們再度重寫整個比對樣式:

$content =~ /like.*monger/is

這兩個修飾字元對於比對確實非常有用。

8.5 取得比對的結果
雖然樣式比對的成功與否對我們非常有用,可是很多時候我們並無法滿足於這樣的用法。尤其當我們使用了一些量詞,或修飾字元之後,我們還會希望知道自己到底得到了甚麼樣的字串。就以剛剛的例子來看,我的比對樣式是表示從like開始,到monger結束,中間可以有隨便任何字元。可是我要怎麼知道我到底拿到了甚麼呢?這時候我就需要取得比對的結果了。
Perl有預設變數來讓你取得比對的結果,就是以錢號跟數字的結合來表示,就像這樣:($1,$2,$3....)。
而用法也相當簡單,你只要把需要放入預設變數的比對結果以小括號刮起括就可以了,就以我們剛剛的例子來看,你只要改寫比對樣式,就像這樣:
引用:
my $content = "I like perl. \n I am a perl monger. \n";
if ($content =~ /(like.*monger)/s) {
    print "$1\n";
}
這裡的$1就是表示第一個括號括住的的比對結果。所以Perl會送出這樣的結果:
引用:
[hcchien@Apple]% perl ch3.pl
like perl.
I am a perl monger
當然,預設的比對變數也是可以一次擷取多個比對結果,就像下面的例子:
引用:
my $content = "I like perl. \n I am a perl monger. \n";
if ($content =~ /(perl)\s(monger)/s) {             # $1 = "perl", $2 = "monger"
    print "$1\n";                          # 印出 perl
}


不過我們如果再把這個小程式改寫成這樣呢?


my $content = "I like perl. \n I am a perl monger. \n";
if ($content =~ /((perl)\s(monger))/s) {
    print "$1\n$2\n$3\n";
}
結果非常有趣:
引用:
[hcchien@Apple]% perl ch3.pl
perl monger
perl
monger
看出來了嗎?我們用括號拿到三個比對變數,而Perl分配變數的方式則是根據左括號的位置來進行。因此最左邊的括號是整個比對結果,也就是"perl monger",接下來是"perl",最後才是"monger"。相當有趣,也相當實用。
不過在使用這些暫存變數有一些必須注意的部份,那就是這些變數的生命週期。因為這些變數回被放在記憶體中,直到下次比對成功,要注意,是比對成功。所以如果你的程式是這麼寫的話:
引用:
my $content = "Taipei Perl Monger";
$content =~ /(Monger$)/;    # $1 現在是 Monger
print $1;
$content = /(perl)/;        # 比對失敗
print $1;                   # 所以還是印出 Monger
當你第一次成功比對之後,Perl會把你所需要的結果放如暫存變數$1中,所以你第一次列印$1時就會看到Perl印出Monger,於是我們繼續進行下一次的比對,這次我們希望比對perl這個字串,並且把比對要的字串同樣的放入$1之中。可惜我們的字串中,並沒有perl這個字串,而且我們也沒有加上修飾符號去進行忽略大小寫的比對,因此這次的比對是失敗的,可是Perl並不會先清空暫存變數$1,因此變數的內容還是我們之前所比對成功的結果,也就是Monger,這從最後印出來的時候就可以看出來了。
比較容易的解決方式就是利用判斷式去根據比對的成功與否決定是否列印,就像這樣:
引用:
my $content = "Taipei Perl Monger";
print $1 if ($content =~ /(Monger$)/);    # 因為比對成功,所以會印出Monger
print $1 if ($content = /(perl)/);        # 這裡就不會印出任何結果了
8.6 定位點
要能夠精確的描述正規表示式,還有一項非常重要的工具,就是定位點。其中你可以指定某個樣式必須要被放在句首或是句尾,比如你希望比對某個字串一開始就是"Perl"這個字串。那麼你可以把你的樣式這樣表示:

/^Perl/

其中的^就是表示字串開始的位置,也就是只有在開始的位置比對到這個字串才算成功。 當然,你可以可以使用$來表示字串結束的位置。以這個例子來看:
引用:
my $content = "Taipei Perl Monger";
if ($content =~ /Monger$/s) {                  # 以定位字元進行比對
    print "*Match*";                              # 在這裡可以成功比對
}
8.7 比對與替換
就像很多編輯器的功能,我們不只希望可以找到某個字串,還希望可以進行替換的功能。當然正規表示式也有提供類似的功能,甚至更為強大。不過其實整個基礎還是基於比對的原則。也就是必須先比對成功之後才能開始進行替換,所以只要你能瞭解整個Perl正規表示式的比對原理,接下來要置換就顯得容易多了。現在我們先來看一下在Perl的正規表示式中該怎麼描述正規表示式中的替換。
我們可以使用s///來表示替換,其中第一個部份表示比對的字串,第二個部份則是要進行替換的部份。還是舉個例子來看會清楚一些:
引用:
my $content = "I love Java";
print $content if ($content =~ s/Java/Perl/);  # 假如置換成功,則印出替換過的字串
當然,就像我們所說的,置換工作的先決條件是必須完成比對的動作之後才能進行,因此如果我們把剛剛的程式改寫成
引用:
my $content = "I love Java";
print $content if ($content =~ s/java/perl/);
那就甚麼事情也不會發生了。當你重新檢查字串$content時,就會發現正如我們所預料的,Perl並沒有對字串進行任何更動。
不過有時候我們會有一些問題,就像這個例子:
引用:
my $content = "水果對我們很有幫助,所以應該多吃水果";
print $content if ($content =~ s/水果/零食/);  # 把水果用零食置換
看起來好像很容易,我們把零食取代水果,可是當結果出來時,我們發現了一個問題。Perl的輸出是:「零食對我們很有幫助,所以應該多吃水果」。當然,這跟我們的期待是不同的,因為我們實在想吃零食啊。可是Perl只說了零食對我們有幫助,我們還是得吃水果。
沒錯,我們注意到了,Perl只替換了一次,因為當第一次比對成功之後,Perl就接收到比對成功的訊息,於是就把字串依照我們的想法置換過,接著....收工。好吧,那我們要怎麼讓Perl把整個字串的所有的「水果」都換成「零食」呢?我們可以加上/g這個修飾字元,這是表示全部置換的意思。所以現在應該會是這個樣子:
引用:
my $content = "水果對我們很有幫助,所以應該多吃水果";
print $content if ($content =~ s/水果/零食/g);  # 把水果全部換成零食吧
就像我們在比對時用的修飾字元,我們在這裡也可以把那些修飾字元再拿出來使用。就像這樣:
引用:
my $content = "I love Perl. I am a perl monger";
print $content if ($content =~ s/perl/Perl/gi);
我們希望不管大小寫,所有字串中的Perl一律改為Perl,所以就可以在樣式的最後面加上/gi兩個修飾字元。而且使用的方式和在進行比對時是相同的方式。

8.8 有趣的字串內交換
這是個有趣的運用,而且使用的機會也相當的多,那就是字串內的交換。這樣聽起來非常難以理解,舉個例子來看看。
我們有一個字串,就像這樣:

$string = "門是開著的,燈是關著的"

看起來真是平淡無奇的一個句子。可是如果我們希望讓門關起來,並且打開燈,我們應該怎麼作呢?
根據我們剛剛學到的替換,這件事情好像很簡單,我們只要把門跟燈互相對調就好,可是應該怎麼作呢?如果我們這麼寫:

$string =~ s/門/燈/;

那整個字串就變成了「燈是開著的,燈是關著的」,那接下來我們要怎麼讓原來「燈」的位置變成「門」呢?所以這種作法似乎行不通,不過既然要交換這兩個字,我們是不是有容易的方法呢?利用暫存變數似乎是個可行的方法,就像這樣:
引用:
my $string = "門是開著的,燈是關著的";

print $string if ($string =~ s/(門)(.*)(,)(燈)(.*)/$3$2$1$4/);
看起來好像有點複雜,不過卻非常單純,我們只要注意正規表示式裡面的內容就可以了。在樣式表示裡面,非常簡單,我們要找門,然後接著是「門」和「燈」中間的那一串文字,緊接在後面的就是「燈」,最後的就全部歸在一起。按照這樣分好之後,我們希望如果Perl比對成功,就把每一個部份放在一個暫存變數中。接下來就是進行替換的動作,我們把代表「門」跟「燈」的暫存變數$1及$3進行交換,其餘的部份則維持不變。我們可以看到執行之後的結果就像我們所期待的一樣。
當然,這樣只是最簡單的交換,如果沒有正規表示式,那真的會非常的複雜,不過現在我們還可以作更複雜的交換動作。

8.9 不貪多比對
其實在很多狀況下,我們常常不能預期會比對甚麼樣的內容,就像我們常常會從網路上抓一些資料回來進行比對,這時候我們也許有一些關鍵的比對樣式,但是大多數的內容卻是未知的。因此比對的萬用字元(.)會經常被使用,可是一但使用了萬用字元,就要小心Perl會一路比對下去,一直到不合乎要求為止,就像這樣:
引用:
<table>
    <tr><td>first</td></tr>
    <tr><td>second</td></tr>
    <tr><td>third</td></tr>
</table>
這是非常常見的HTML語法,假設我們希望找到其中的三個元素,所以就必須過濾掉那些HTML標籤。如果你沒注意,也許會寫成:
引用:
my $string = "<table><tr><td>first</td></tr><tr><td>second</td></tr><tr><td>
third </td></tr></table>";

if ($string =~ m|<tr><td>(.+)<\/td><\/tr>|) {
    print "$1";
}
可是當你看到執行結果時可能會發現那並不是你要的結果,因為程式印出的$1居然是:

first</td></tr><tr><td>second</td></tr><tr><td>third

讓我們來檢查一下程式出了甚麼問題。我們的比對樣式中告訴Perl,從<tr><td>開始比對,然後比對所有字元,一直到遇到</td></tr>時結束。 而且Perl也很符合我們的期望,他找到了符合我們需求的最大集合。這就是重點了,Perl預設會去找到符合需求的最大集合。因此在這裡他就取得了比對結果"first</td></tr><tr><td>second</td></tr><tr><td>third"。可是我們要的卻是「從<tr><td>開始,遇到</td></tr>就結束」,而不是「找出字串中<tr><td>到</td></tr>的最大字串」。
可是我們剛剛的比對樣式中並沒有告訴Perl:「遇到</td></tr>就停下來」,所以他會一直比對到字串結束,然後找出符合樣式的最大字串,這就是所謂的貪多比對。相對於此,我們就應該告訴Perl,請他以不貪多的方式進行比對。所以我們就在比對的量詞後面加上問號(?)來表示不貪多,並且改寫剛剛的比對樣式:
引用:
$string =~ m|<table><tr><td>(.+?)<\/td><\/tr>|
如此一來,就符合我們的要求了。

8.10 如果你有疊字
在正規表示式中,有一種比對的技巧稱為回溯參照 (backreference)。我們如果可以用個好玩的例子來玩玩回溯參照也是不錯的,比如我們有個常見的句子:「庭院深深深幾許」。如果我希望比對中間三個深,我可以怎麼作呢?當然,直接把「深深深」當作比對的樣式是個方法,不過顯而易見的,這絕對不是個好方法。至少你總不希望看到有人把程式寫成這樣吧:
引用:
my $string = "庭院深深深幾許";
print $string if ($string =~ /深深深/);          # 這樣寫程式好像真的很糟
這時候回溯參照就是一個很好玩的東西,我們先把剛剛的程式改成這樣試試:
引用:
my $string = "庭院深深深幾許";
print $string if ($string =~ /(深)\1\1/);
你應該發現了,我們把「深」這個字先放到暫存變數中,然後告訴Perl:如果有東西長的跟我們要比對的那個變數里的東西一樣的話,那麼就用來繼續比對吧。可是這時候你卻不能使用暫存變數$1,因為暫存變數是在比對完成之後才會被指定的,而回溯參照則是在比對的期間發生的狀況。剛剛那個例子雖然可以看出回溯參照的用途,可是要瞭解他的有用之處,我們似乎該來看看其他的例子:
引用:
my $string = "/Chinese/中文/";
if ($string =~ m|([\/\|'"])(.*?)\1(.*?)\1|) {          # 這時候我們有一堆字符集合
    print "我們希望用 $3 來替換 $2 \n";
}
看出有趣的地方了嗎?我們在字符集合裡面用了一堆符號,因為不論在字符集合裡的那一個符號都可以算是正確比對。但是我們在後面卻不能照舊的使用[\/\|'"]來進行比對,為甚麼呢?你不妨實驗一下這個例子:
引用:
my $string = "/Chinese|中文'";
if ($string =~ m|([\/\|'"])(.*?)[\/\|'"](.*?)[\/\|'"]|) {
    print "我們希望用 $3 來替換 $2 \n";
}
很幸運的,我們在這裡還是比對成功。為甚麼呢?我們來檢查一下這次的比對過程:首先我們有一個字符集合,其中包括了/|'"四種字符,而這次我們的字串中一開始就出現了/字符,正好符合我們的需求。接下來我們要拿下其他所有的字元,一直到另一個相同的字符集合,不過我們這次拿到了卻是|字符,最後我們拿到了一個單引號(')。顯然不單是方便,因為沒有使用回溯參照的狀況下,我們拿到了錯誤的結果。
那我們回過頭來檢查上一個例子就會清楚許多了,我們一開始還是一個字符集合,而且我們也比對到了/字符。接下來我們要找到跟剛剛比對到相同的內容 (也就是要找到下一個/),然後還要再找最後一次完全相同的比對內容。我們經常會遇到單引號(')或雙引號(")必須成對出現,而利用回溯參照就可以很容易的達成這樣的要求。

8.11 比對樣式群組

我們剛剛說了關於回溯參照的用法,不過如果我們的比對並沒有那麼複雜,是不是也有簡單的方式來進行呢?我們都知道很多人喜歡用blahblah來進行沒有什麼意義的留言,於是我們想把這些東西刪除,可是他們可能是寫"blahblah"或是"blahblahblah"等等。這時候使用回溯參照可能會寫成這樣:
引用:
my $string = "blahblahblah means nothing";

if ($string =~ s/(blah)\1*//) {
    print "$string";
}
當然這樣的寫法並沒有錯,只是好像看起來比較礙眼罷了,因為我們其實可以用更簡單的方法來表達我們想要的東西,那就是比對樣式群組。這是小括號(())的另外一個用途,所以我們只要把剛剛的比對樣式改成這樣:/(blah)+/就可以了。這樣一來,Perl就會每次比對(blah)這個群組,然後找尋合乎要求的群組,而不是單一字元(除非你想把某一個字元當群組,只是我們並不覺得這樣的方式會有特殊的需求)。而當我們設定好某個群組之後,他的操作方式就跟平常在寫比對樣式沒什麼兩樣了,我們就可以利用/(blah)+/找出(blah)這個群組出現超過一次的字串。如果你覺得不過癮,/(blah){4,6}/來確定只有blah出現四到六次才算比對成功也是可以的。

8.12 比對樣式的控制
一開始使用正規表示式的人總有一個疑問,為甚麼要寫出正確比對的樣式這麼不容易。而比對錯誤的主要原因通常在於得到不必要的資料,也就是比對樣式符合了過多的文字,當然,還有可能是比對了不被我們期待的文字。就像我們有這樣的一堆字串:
引用:
I am a perl monger
I am a perl killer
it is so popular.
如果你的比對樣式是/p.*r/,那麼你會比對成功:
引用:
perl monger
perl killer
popular
可是這跟我們的需求好像差距太大,於是你希望用這樣的樣式來進行比對:/p\w+\s\w+r/,那你也還會得到
引用:
perl monger
perl killer
這兩種結果。所以怎麼在所取得的資訊中寫出最能夠精確比對的樣式確實是非常重要,也需要一些經驗的。

習題:
1. 延續第七章的第一題,比對出perl在字串結尾的成功結果。
2. 繼續比對使用者輸入的字串,並且確定是否有輸入數字。
3. 利用回溯參照,找出使用者輸入中,引號內(雙引號或單引號)的字串。
4. 找出使用者輸入的第一個由p開頭,l結尾的英文字。



註一:有時候因為作業系統的不同,換行符號並不會被忠實的呈現。

TOP

9. 再談控制結構
程式中的控制結構是用來控制程式進行方向的重要依據,所以有足夠靈活的程式控制結構能節省程式設計師的大量時間。可是也必須謹慎的使用,否則如果一個程式裡面到處充滿了迴圈控制,然後中斷,轉向,反而讓程式變得非常沒有結構,在程式的結構遭到破壞之後,一旦程式出了問題而需要要開始追蹤整個程式的行進就變成要花大量的時間。很顯然的,如果沒有保持程式良好的結構性,對於日後的維護將會是一大負擔。
不過如果能夠小心使用,這些控制程式流程的工具會是程式設計師重要的工具。所以我們就來看看除了之前提過的for,while,until,if等等各式各樣的流程控制之外,我們還能有什麼其他的方式可以方便的操控Perl吧。

9.1 迴圈操作
既然是流程控制,我們有時候會因為程式的狀況而希望離開迴圈或某些條件敘述的判斷區塊,也可能需要省略迴圈中的某次運算,或進行其他的跳躍。聽起來這些方式好像很容易讓人搞的眼花繚亂,現在我們就要來看看這些功能到底有什麼幫助。

9.1.1 last
顧名思義,這也就是最後的意思,因此Perl看到這個關鍵字就會相當高興,表示他距離休息又靠近了一步。不過這樣可以提前結束迴圈的函數到底扮演著甚麼樣的角色呢?讓我們來試試:
引用:
for (1...10) {
    last if ($_ == 8);
    print;                          # 這樣會印出  1...7
}
好玩吧,當你在迴圈內加上了另外一個判斷式,並且允許迴圈在某個條件下跳出迴圈的執行,這時候,Perl就提早下班,回家休息了。當然,last的使用是有限制的,也就是他只允許在可以執行結束的區塊內,其中最常被應用的是在迴圈內,而這裡的迴圈指的是for,foreach,while,until,另外也可以在單獨的區塊中使用,就像這樣:
引用:
print "start\n";
{
    print "last 前執行\n";          # 會順利印出
    last;
    print "這裡就不執行了\n";          # 所以這一行永遠不會被執行
}
print "然後就結束了\n";
沒錯,這個例子的程式確實非常無趣,因為我們寫了一行永遠不會執行的程式,不過卻讓我們透過這個小程式清楚的看到last的執行過程。所以我們可以輕易的讓Perl跳出某個區塊,而既然可以提前結束區塊的執行範圍,我們有時候也需要Perl可以重複執行迴圈內的某些條件這時候redo這個函式就派上用場了。

9.1.2 redo
雖然我們可以用last讓Perl提早下班,同樣的,我們也可以要求Perl加班,也就是利用redo這個函式讓Perl重新執行迴圈中的某些條件。例如我們在迴圈中可以利用另一個判斷敘述來決定目前的狀況,並且利用這個額外的敘述判斷來決定是否要讓迴圈中的某個條件重複執行。例如我們有一個迴圈,我們可以很容易的要求perl在迴圈中的某個階段重複執行一次,就像下面的例子:
引用:
for (1...10) {
    $_++;                          # redo 其實會來這裡
    redo if ($_ == 8);                  # 我們希望 redo 的條件
    print;                               # 會印出 2, 3, 4, 5, 6, 7, 9, 9, 10, 11
}
我們可以研究一下redo的過程,在上面那個程式的第三行,我們要求Perl在迴圈的變數等於8的時候就執行redo。所以當我們在迴圈內的條件符合redo的要求時,Perl就會跳到迴圈的第一行,也就是說當迴圈的值進行到7的時候,經過$_++的運算就會讓$_變成8,這時候就符合了redo的條件,因此還來不及印出來變數$_,Perl就被要求回到迴圈中的第一行,於是$_變成了9,這就是第一次9的出現。接下來迴圈恢復正常,就接連印出9...11。也就是我們看到的結果了。
不過這裡要注意的是我們迴圈的使用方式,我們使用了for(1...10),而不是使用for ($_ = 1; $_ <= 10); $_++)這樣的敘述,而這兩者有著相當的差異。如果各位使用了後者的迴圈表示,結果就會有所不同。
我們還是實際上來看看使用for (;;)來檢查redo的效果時,也可以藉此看看兩者的差異了:
引用:
for ($_ = 1; $_ <= 10; $_++) {
    $_++;                          # 我們還是先把得到的元素進行累加
    redo if ($_ == 8);                  # 遇到8的時候就重複一次
    print $_;                             # 印出目前的 $_,我們得到2, 4, 6, 9, 11
}
很有趣吧,我們來看看這兩者有什麼不同,首先我們看到第一個例子中,Perl是拿出串列1...10的元素,並且把得到的元素放進變數$_中。接下來就像我們在迴圈中所看到的樣子了,所以迴圈並不是以$_作為計數的依據。這樣的方式就像這樣的寫法:
引用:
for (my @array = (1...10))
可是當我們看到第二個例子的時候,我們卻是指定了$_作為迴圈的計數標準。所以我們在迴圈中對$_進行累加,就完全影響到迴圈的執行。因此我們一開始拿到$_等於1,可是一進迴圈就馬上又被累加了一次,我們就印出了2,接著Perl又執行迴圈的遞增,讓我們取得3,我們自己又累加了一次,也就印出4,等我們累加到8的時候,迴圈被要求執行redo,因此我們又累加一次,$_變成9,緊接著最後一次的迴圈,經過累加之後,我們印出了11。看起來好像非常複雜,不過你只要實際跟著迴圈跑一次應該就可以看出其中的變化了。
不過在使用redo的時候必須非常小心,因為你很可能因為設定了redo的條件而產生無窮迴圈。就像剛剛的例子,如果我們改寫成:
引用:
for (1...10) {
    redo if ($_ == 8);
    print $_;
}
在這個迴圈裡,我們希望迴圈的控制變數在8的時候可以進行redo,於是它就一直卡在8而跳不出來了。就像我們說的,這裡變成了無窮迴圈,你的程式也就有加不完的班了。

9.1.3 next
我們剛剛使用了last來結束某個區塊,也透過redo來重複執行迴圈中的某個條件敘述,那麼既然可以在迴圈內重複執行某個條件的敘述,那麼略過某個條件下的敘述也應該不是太難的問題。是的,其實只要利用next,那麼我們可以在某些情況下直接結束這次的執行,也就是省略迴圈中某一些狀況的執行。當然,描述還是不如直接看看實例,我們還是利用簡單的例子來瞭解next的作用:
引用:
for (1...10) {
    next if ($_%2);                  # 以串列值除以2的餘數判斷
    print $_;
}
這個例子裡面,我們會印出1...10之間的所有偶數。首先,這是一個從1到10的迴圈,主要的工作在於印出目前迴圈進行到的值,不過就在列印之前,我們使用了一個next函式,而決定是否執行next的判斷是以串列值除以2的餘數來作為條件
。如果餘數為真(在這裡的解釋就是:如果餘數為1),就直接結束這次的執行,當然,如果餘數為0(在這個程式中,我們可以解釋為「遇到偶數時」),就會印出串列的值,所以程式會印從1到10的所有偶數值。
雖然有些時候,你會發現next的好用之處,可是如果你會因為next而造成追蹤程式的困擾時,那就可能要修改一下你的使用方式了。例如改變迴圈的判斷條件或是索引的遞增方式等等,就像上面的例子,我們也許可以改用while來判斷,或者使用for(;;),而不是使用foreach加上next來增加程式的複雜性,不過這些都必須依賴經驗來達成。

9.1.4 標籤
標籤的作用主要就是讓Perl知道他該跳到哪裡去,這樣的寫法並不太常被使用,主要是因為對於程式的結構會有一定程度的破壞,因為你可以任意的設置一個標籤位置,然後要求Perl跳到標籤的位置,當然,他確實有一些使用上的要求,而不是完全漫不限制的隨便下一個標籤就讓Perl轉換執行的位置,至少這並不是goto在做的工作。不過撇開這個暫且不談,我們先來看看怎麼使用標籤。下面的例子應該可以讓大家能夠看出輪廓:
引用:
LABEL: for my $outter (1...5) {
    for (1...10) {
  if ($_ > 2) { next LABEL; } else { print "inner $_ \n"; }
    }
    next LABEL if ($outter%2);
    print $_;
}
當我們有時候單單利用next或last無法逃離迴圈到正確的地方時時,使用標籤就能夠幫助我們找到出路。就像我們的例子中,我們一共有兩個for迴圈,兩個if判斷,我們要怎麼讓Perl不會在裡面迷路呢?這時候標籤的使用就很方便了,就像我們在內部的for迴圈中根據得到的值來決定是否要跳出上一層的for迴圈。
可是使用標籤時有一個特別需要注意的部份,就是標籤的使用並非針對程式中的某一個點,而必須是一個迴圈或是區塊。否則整個標籤的使用就會太過混亂,你會發現要檢查程式的錯誤變成了「不可能的任務」。當然,如果你在你的迴圈中插了大量的標籤也會讓其他人非常困擾,因為就算是Perl可以處理這樣的標籤,只怕你自己也會搞的頭暈。這又是寫程式時的風格問題了。
標籤可以配合我們之前所提的幾種控制指令來運用,因此你可以要求使用next,redo,last加上標籤來標明迴圈的方向。就像上面的例子,我們先在第一行的地方加上標籤'LABEL',表明接下來如果需要,要求Perl直接來這裡。接下來我們用了一個foreach迴圈,其中的值是從1到10。可是在這個迴圈中,我們又使用了next,要求如果變數$_大於2就執行next,而且是跳到標籤LABEL的位置。也就是說,他除了跳過裡面的迴圈之外,也會跳出外層迴圈的其他敘述。所以當內層迴圈的$_變數大於2的時候,程式中最後面的兩行敘述都不會被執行。當然,大家應該還是想要知道這樣的程式會產生出甚麼樣的結果:
引用:
inner 1
inner 2
inner 1
inner 2
inner 1
inner 2
inner 1
inner 2
inner 1
inner 2  
你應該發現了,程式一直都只執行了一部份,因為當內圈的變數$_大於2的時候,Perl就急著要回去LABEL的地方,所以就連裡面的迴圈都沒辦法完整執行,外面的迴圈更是被直接略過,這樣應該就很容易理解了。

9.2 switch
如果你用過其他程式語言,例如C或Java,你現在也許會很好奇,為甚麼我們到目前為止還沒有提到Switch這個重要的流程控制函式,主要是因為Perl在最初的設計是沒有放入Switch的。其實很多人對於Perl沒有提供switch都覺得非常不可思議,不過Larry Wall顯然有他的理由,至於這些歷史原因,我們也沒必要在這裡討論。
好吧,我聽到一陣嘩然,為甚麼Perl沒有這個可以為程式畫上彩妝的工具呢?其實我個人也覺得Switch用來進行各種條件判斷的流程控制確實是非常方便,而且會讓程式看起來相當整齊,不過大部份的時候,你有甚麼流程控制非得需要Switch才能完成呢?因為我們在進行Switch的時候,其實也就是希望表達出許多層的if {} elsif {} elsif {} ..... 。也就是說,if敘述其實已經可以滿足我們的需求了,那麼Switch就真的是幫助我們取得比較整齊,易讀的程式碼。不過在大部份的情況下,你想要用漂亮的程式碼來吸引Perl的黑客(註一)們協助完成一項工作,倒不如告訴他們怎麼樣可以少打一些字。

9.2.1 如果你有複雜的 if 敘述
Switch之所以受到歡迎,當然有過人之處,雖然我們也可以用其他方式達到同樣的目的,可是至少對我來說,程式的易讀性似乎還是以Switch來得好些,不過這部份可就是見仁見智了。就像我們說的,如果你有一大堆if {} elsif {} elsif {} .... 的敘述時,你的程式看起來也許看起來會像這樣:
引用:
my $day = <STDIN>;
chomp($day);
if ($day eq 'mon') {
   ...
} elsif ($day eq 'tue') {
  ...
} elsif ($day eq 'wed') {
  ...
} elsif ($day eq 'thu') {
  ...
} elsif ($day eq 'fri') {
  ...
}
其實這樣的程式碼也沒甚麼不妥,可是你也許會覺得這樣的寫法有點麻煩。當然,對這些人來說,如果可以把上面這段程式碼利用Switch寫成這樣,那好像看起來更讓人感覺神清氣爽:
引用:
my $day = <STDIN>;
chomp($day);
swich ($day) {
      case ('mon') { ... }
      case ('tue') { ... }
      case ('wed') { ... }
      case ('thu') { ... }
      case ('fri') { ... }
}
以可讀性來講,使用Switch確實比用了一大堆的 if {...} elsif {...} elsif {...} 要好的多,那麼我們要怎麼樣可以使用Switch來寫我們的程式呢?

9.2.2 利用模組來進行
很顯然的,還是有許多Perl的程式設計師對於switch的乾淨俐落難以忘懷。因此有人寫了perl模組,我們就可以利用這個模組來讓我們的程式認識switch。
利用Switch模組,我們就可以寫出像上面一樣的語法,讓你的程式看起來更簡潔有力。而且switch的使用上,不單可以比對某個數字或字串,你還可以使用正規表示式進行複雜的比對來決定程式的進行方向。我們在這裡只是告訴大家一些目前已經存在的解決方案,而不應該在這裡講太多關於模組的使用,以免造成大家的負擔。
另外,還有部份程式設計師不太喜歡目前Switch的運作,認為破壞了原來Perl在流程控制的結構而也會因此而破壞原來Perl程式的穩定性。因為不管如何,這些意見都是僅供參考。不過既然「辦法不只一種」,那麼就看個人的接受度如何了。

9.3 三元運算符
另外也有一種非常類似 if {...} else {...} 的運算符,我們稱為三元運算符。他的寫法也就是像這樣:
引用:
my ($a, $b) = (42, 22);
my $max = ($a > $b) ? $a : $b;
print "$max\n";
首先我們把串列 (42, 22) 指定給變數 $a 跟 $b,接著我們要找到兩個值中較大的一個,於是利用判斷式 ($a > $b) 來檢查兩個數字之間的關係。如果 $a > $b 成立,那麼 $max 就是 $a,否則就是 $b。所以很明顯的,上面的三元運算符也可以改寫成這樣:
引用:
my ($a, $b) = (44. 22);
if ($a > $b) { $max = $a } else { $max = $b }
print "$max\n";
以上面兩個例子來看,相較之下,三元運算符的方式應該簡單許多,只是這樣的方式並不夠直覺,對於剛開始寫Perl的人而言可能會有點障礙。不過我們還是必須提醒,這樣的寫法很可能常常出現在其他的程式裡,所以即使你只想依賴 if {...} else {...} 來完成同樣的工作,至少你也要知道別人的程式碼中表達的是甚麼。
而且,其實利用三元運算符也可以完成不少複雜的工作。例如你可以在判斷式的地方用一個副常式,並且根據回傳的結果來決定你要的值等等。因此一但有機會,也許你也可以試試。在這裡,我們可以再舉一個怎麼增加便利性的寫法的例子:
引用:
my $return = cal(5);
print "$return\n";

sub cal {
    my $param = shift;
    ($param > 4) ? $param*2 : $param**2;  # 利用參數來判斷回傳值的運算方式
}
9.4 另一個小訣竅
接下來我們來點飯後甜點,也就是 || 算符。其實不只 || 算符,其他的邏輯算符也可以拿來作流程控制的小小螺絲釘。不過首先我自己偏愛使用 || (也是使用機會比較高的),而且我們只打算來個甜點,這時候顯然不適合大餐了。
我們有時候會希望某些變數可以有預設值,例如副常式的參數,或是希望使用者輸入的變數等等。所以你當然可以這樣寫:
引用:
sub input {
    my $key = shift;
    $key = "預設值" unless ($key);
    print "$key\n";
}
這個副常式甚麼也沒作,就只拿了使用者傳來的參數,然後印出來。可是我們還可以讓他更簡單一些,我們把他改成這樣:
引用:
sub input {
    my $key = shift || "預設值";
    print "$key\n";
}
這時候,|| 算符被我們拿來當一個判斷的工具。我們先確定使用者有沒有傳入參數,也就是平常我們所使用的shift,如果@_中是空陣列,那麼 $key = shift 就會得到偽值,這時候 || 就會啟動,讓我們的預設值產生效果。因此我們就得到 $key = "預設值"。
另外,|| 還常常被用來進行意外處理。因為我們必須知道,如果某個運算式失敗,那麼我們就可以讓程式傳回錯誤訊息。就像這樣:
引用:
output() || die "沒有回傳值";

sub output {
    return 0;
}
我們在程式裡面呼叫 output 這個副常式,不過因為回傳值是 0,於是 || 也發生效用,就讓程式中斷在這裡,並且印出錯誤訊息。


習題:
1. 陸續算出 (1...1) 的總和,(1...2) 的總和,...到 (1...10) 的總和。但是當得到總和大於50時就結束。
2. 把下面的程式轉為三元運算符形式:
引用:
#!/usr/bin/perl -w

use strict;

chomp(my $input = <STDIN>);
if ($input < 60) {
    print "不及格";
} else {
    print "及格";
}
註一:其實我們指的就是hacker,不過現今大多數人都誤用cracker(指潛入或破壞其他人系統者)為hacker(指對某些領域有特別研究的人)

TOP

 19 12
發新話題

本站所有圖文均屬網友發表,僅代表作者的觀點與本站無關,如有侵權請通知版主會盡快刪除。