« 6. 副常式 | Perl 學習手札目录 | 8. 更多關於正規表示式 »

7. 正規表示式

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 或是 ...),印出比對成功字樣。