讀古今文學網 > 精通正則表達式(第3版) > 使用正則表達式匹配文本 >

使用正則表達式匹配文本

Matching Text with Regular Expressions

Perl可以以多種方式使用正則表達式,最簡單的就是檢查變量中的文本能否由某個正則表達式匹配。下面的代碼檢查$reply中所含的字符串,報告這個字符串是否全部由數字構成:

第一行的代碼也許有些奇怪:正則表達式是「^[0-9]+$ 」,兩邊的 m/…/告訴 Perl 該對這個正則表達式進行什麼操作。m代表嘗試進行「正則表達式匹配(regular expression match)」,斜線用來標記界限(注2)。之前的=~用來連接m/…/和欲搜索的字符串,即本例中的$reply。

請不要混淆=~、=和==。運算符==用來測試兩個數字是否相等(我們將會看到,運算符 eq用來測試兩個字符串是否相等)。運算符=用來給變量賦值,例如$Celsius=20。最後,=~用來連接正則表達式和待搜索的目標字符串。在這個例子中,要搜索的正則表達式是m/^[0-9]+$/,而目標字符串是$reply。此程序在其他語言中的思路有所不同,我們會在下一章看到例子。

把=~讀作「匹配(matches)」可能比較省事,所以

if ($reply=~m/^[0-9]+$/)

讀作:

「如果變量$reply所含的文本能夠匹配正則表達式「^[0-9]+$」,那麼…」

如果「^[0-9]+$」能夠匹配$reply的內容,$reply=~m/^[0-9]+$/的返回值就為true,否則為false。if語句使用true/false值來決定輸出什麼信息。

請注意,如果$reply中包含任意的數字字符,$reply=~m/[0-9]+/(相比之前的表達式,去掉了開頭的脫字符和結尾的美元符)的返回值就是 true。兩端的「^…$」保證整個$reply只包含數字。

現在把上面兩個例子結合起來。首先提示用戶輸入一個值,接收這個輸入,用一個正則表達式來驗證,確保輸入的是一個數值。如果是,我們就計算相應的華氏溫度,否則,我們輸出一條報警信息:

請注意最後的print語句有兩個轉義的雙引號,它們的作用並不是標記引用字符串的邊界。對大多數語言的文字字符串(literal string)來說,有時候需要轉義某些字符,做法跟正則表達式中元字符的轉義很相似。在 Perl 中,字符串與正則表達式的區別並非很重要,但是在Java、Python 等語言中卻極為重要。「一點題外話——數量豐富的元字符」這一節(☞44)更詳細地討論了這個問題(VB.NET 是個明顯的例外,在那裡轉義雙引號用『」」』而不是『」』)。

如果我們把這段程序保存為c2f,則運行結果如下:

哎呀,看來(至少在某些系統上),Perl的簡單的print並不能很好地處理浮點數。

我不想在本章中討論Perl的細節,但是我告訴你用printf(「格式化輸出(print formatted)」)可以解決這個問題:

printf \"%.2f C is%.2f Fn\",$celsius,$fahrenheit;

這裡的printf類似C語言中的printf,或者Pascal、Tcl、elisp和Python中的format。它不會更改變量的值,而只是改變顯示的方式。現在的結果好看多了:

向更實用的程序前進

Toward a More Real-World Example

讓我們擴展這個例子,容許輸入負數和可能出現的小數部分。這個問題的計算部分沒問題——Perl通常情況下不區分整數和浮點數。不過我們需要修改正則表達式,容許輸入負數和浮點數。我們添加一個「-?」來容許最前面的負數符號。實際上,我們可以用「[-+]?」來處理開頭的正負號。

要容許可能出現的小數部分,我們添加「(.[0-9]*)?」。轉義的點號匹配小數點,所以「.[0-9]*」用來匹配小數點和後面可能出現的數字。因為「.[0-9]*」被「(…)?」所包圍,整個子表達式都不是匹配成果所必須的(請注意,它與是截然不同的,對後一個表達式中,即使「.」無法匹配,「[0-9]*」也能夠匹配接下來的數字)。

把這些綜合起來,就得到這樣的條件判斷語句:

它能夠匹配 32、-3.723、+98.6這樣的文字。不過還不夠完善:它不能匹配以小數點開頭的數(例如.357)。當然,用戶可以添加一個整數位 0 來匹配(例如 0.357),所以我認為這並不是一個嚴重的問題。這個浮點數問題處理起來得靠些訣竅,我們會在第 5 章詳細講解(☞194)。

成功匹配的副作用

Side Effects of a Successful Match

我們再進一步,讓這個表達式能夠匹配攝氏和華氏溫度。我們讓用戶在溫度的末尾加上 C或者 F 來表示。我們可以在正則表達式的末尾加上「[CF]」來匹配用戶的輸入,但還需要修改程序的其他部分,以便識別用戶輸入的溫度類型,並進行相應的轉換。

在第1章,我們看到過某些版本的egrep支持作為元字符的「1」、「2」、「3」,用來保存前面的括號內的子表達式實際匹配的文本(☞21)。Perl 和其他許多支持正則表達式的語言都支持這些功能,而且匹配成功之後,在正則表達式之外的代碼仍然能夠引用這些匹配的文本。

我們會在下一章看到各種語言是如何做到這一點的(☞137),但是 Perl 的辦法是通過變量$1、$2、$3等等,它們指向第一組、第二組、第三組括號內的子表達式實際匹配的文本。這未免有點奇怪,它們都是變量,而變量名則是數字。正則表達式匹配成功一次,Perl就會設置一次。

總結一下,在嘗試匹配時,正則表達式中的元字符「1」指向之前匹配的某些文本,匹配成功之後,在接下來的程序中用$1來引用同樣的文本。

為了保持例子的簡潔,集中表現新的地方,我先不考慮小數部分,之後再來看它。所以,我們來看$1,請比較:

添加的括號改變了正則表達式的意義嗎?為了回答這個問題,我們需要知道,這些括號是否改變了星號或者其他量詞的作用對象,或是「|」的意義。答案是,都沒有改變,所以這個表達式仍然能夠匹配相同的文本。不過,他們確實圍住了我們期望匹配字符串中「有價值」文本的子表達式。如圖2-1 所示,$1保存那些數字,而$2保存 C或者 F。下一頁的圖2-2是程序的流程圖,我們發現,這個圖讓我們很容易地決定匹配之後應該幹什麼。

圖2-1:捕獲型括號

圖2-2:溫度轉換程序的邏輯流程

示例2-1:溫度轉換程序

如果上一頁的程序名叫convert,我們可以這樣使用:

錯綜複雜的正則表達式

Intertwined Regular Expressions

在 Perl 之類的高級語言中,正則表達式的使用與其他程序的邏輯是混合在一起的。為了說明這一點,我們對這個程序做三點改進:像之前一樣能夠接收浮點數,容許 f或者 c是小寫,容許數字和字母之間存在空格。這三點全都完成之後,程序就能夠接收『98.6·f』的輸入。

我們已經知道,添加「(.[0-9]*)?」就能夠處理浮點數:

請注意,它添加在第一個括號內部,因為我們用第一組括號內的子表達式來捕獲溫度的值,我們當然希望它能夠包含小數部分。不過,增加了這組括號之後,即使它只是用來分組問號限定的對象,也會影響到引用捕獲文本的變量。因為這組括號的開括號在整個表達式中排在第二位(從左向右數),所以它匹配的文本存入$2(見圖2-3)。

圖2-3:嵌套的括號

圖2-3說明了括號的嵌套關係。在「[CF]」之前添加一組括號並不會直接影響整個正則表達式的意義,但是會產生間接的影響,因為現在「[CF]」所在的括號排在第 3 位。這也意味著我們需要把$type的賦值從$2改為$3(如果不希望這麼做,可以參考下一頁的補充內容)。

接下來,我們要處理數字和字母之間可能出現的空格。我們知道,正則表達式中的空格字符正好對應匹配文本中的空格字符,所以「·*」能夠匹配任意數目的空格(但並不是必須出現空格):

但這樣還不夠靈活,而我們希望的是開發一個能夠實際應用的程序,所以必須容許其他的空白字符(whitespace)。例如常見的製表符(tabs)。但是「*」並不能匹配空格,所以我們需要一個字符組來匹配兩者「[]*」。

請把上面這個子表達式與「(·*|*)」進行對比,你能發現這其中的巨大差異嗎?ϖ請翻到下一頁查看答案。

本書中空格字符和製表符都很常見,因為我使用·和來表示它們。不幸的是,在屏幕上卻不是如此。如果你見到*,在沒有實際測試過以前,只能猜測這是空格符還是製表符。為了方便使用,Perl提供了「t」這個元字符。它能夠匹配製表符——相比真正的製表符,它的好處就在於看得更清楚,所以我會在正則表達式中採用這個元字符。於是「[· ]*」變成「[·t]*」。

Perl還提供了一些簡便的元字符,例如「n」(表示換行符),「f」(ASCII的進紙符formfeed),和「b」(退格符)。不過,確切地說,「b」在某些情況下是退格符,有些情況下又表示單詞分界符。它怎麼能身兼數職呢?下一節我們會看到。

一點題外話——數量豐富的元字符

在前面的例子裡我們見到了n,但是n都出現在字符串而不是正則表達式中。就像多數語言一樣,Perl的字符串也有自己的元字符,它們完全不同於正則表達式元字符。新程序員常犯的錯誤就是混淆了這兩個概念(VB.NET是個例外,因為其中字符串的元字符少得可憐)。字符串的元字符中有一些跟正則表達式中對應的元字符一模一樣。你可以在字符串中用t加入製表符,也可以在正則表達式中用元字符「t」來匹配製表符。

這種相似性無疑方便了使用,但是我必須強調區分這兩種元字符的重要性。對於t這樣簡單的情況來說或許並不重要,但對於我們將要看到的各種不同的語言和工具來說,知道在什麼情況下應該使用什麼元字符是極其重要的。

我們已經見過不同的字符組之間的衝突。在第1章,使用egrep時,我們把正則表達式包含在單引號中。整個egrep命令行寫在command-shell提示符,shell能夠認出它自己的元字符。例如,對shell來說,空格符就是一個元字符,它用來分隔命令和參數,或者參數與參數。在許多shell中,單引號是元字符,單引號內的字符串中的字符不需要被當作元字符處理(DOS使用雙引號)。

在shell中使用引號容許我們在正則表達式中使用空格。否則,shell會把空格認作參數之間的分隔符,而不是把整個正則表達式傳遞給egrep。許多shell能夠識別的元字符包括$、*、?之類——我們在正則表達式中也會用到這些元字符。

目前,所有關於shell的元字符和Perl字符串的元字符的討論都還與正則表達式本身沒有任何關聯,但它們會影響到現實環境中正則表達式的使用。隨著閱讀的深入,我們會見到許多(有時候還很複雜)情況,我們需要同時在不同層級上使用元字符交互(multiple levels of simultaneously interacting metacharacters)。

那麼「b」的情況呢?這是一個正則表達式的問題:在Perl的正則表達式中,「b」通常是匹配一個單詞分界符的,但是在字符組中,它匹配一個退格符。單詞分界符作為字符組的一部分則沒有任何意義,所以Perl完全可以用它來匹配其他的字符。第1章曾提醒我們,字符組「子語言」的規範不同於正則表達式主體,這條規則也適用於Perl(包括任何其他流派的正則表達式)。

用s匹配所有「空白」

討論空白的問題時,我們最後使用的是「[·t]* 」。這樣做沒問題,但許多流派的正則表達式提供了一種方便的辦法:「s」。「s」看起來類似「t」,「t」代表製表符,而「s」則能表示所有表示「空白字符(whitespace character)」的字符組,其中包括空格符、製表符、換行符和回車符。在我們的例子中,換行符和回車符並不需要特別考慮,但是「s*」顯然比「[·t]*」要簡潔。而且不久你就會習慣這種表示法,在複雜的表達式中,「s*」更加易於理解。

現在我們的程序變成:

最後,我們還必須能夠處理表示溫度制式的小寫字母。簡單的辦法是直接把小寫字母添加到字符組中,「[CFcf]」。不過,我更願意使用另一種辦法:

$input=~m/^([-+]?[0-9]+(.[0-9]*)?)s*([CF])$/i

添加的這個i稱作「修飾符(modifier)」,把它放在m/…/結構之後,告訴Perl進行不區分大小寫的匹配。修飾符其實不是正則表達式的一部分,而是m/…/結構的一部分,這個結構告訴Perl使用者的意圖(應用一個正則表達式),以及採用的正則表達式(在斜線之間的部分)。我們曾看到過這種功能,即egrep的-i參數(☞15)。

時時刻刻說「i修飾符(the i modifier)」有點麻煩,所以我們通常說「/i」,即使真正的寫法並不是「/i」。/i 只是在 Perl 中指定修飾符的辦法之一——在下一章,我們會看到其他的辦法,以及其他語言實現此功能的寫法。在本章後面的部分,我們還會看到其他的修飾符,例如/g(表示「全局匹配(global match)」)以及/x(表示「寬鬆排列的表達式(free-form expressions)」)。

現在,我們已經做了不少修改了,來看看新的程序:

哎呀!你是否注意到了,第二次運行時我們輸入的是攝氏50度,結果被認成了華氏50度?看看程序的邏輯,你找出問題了嗎?

再來看程序的片段:

雖然我們的正則表達式能夠接受小寫的f,程序的其他部分卻沒有相應的修改。在這個程序裡,只有$type是『C』的時候,才作為攝氏度處理。因為程序同樣可以接受小寫的 c,我們需要修改$type的判斷:

if ($type eq \"C\" or $type eq \"c\") {

實際上,因為本書是關於正則表達式的,我或許這樣做:

if ($type=~m/c/i) {

現在,大小寫的情況都能應付了。最終的程序如下所示。這個例子告訴我們,正則表達式的使用方式,可能會影響到程序的其他部分。

示例2-2:溫度轉換程序——最終版本

暫停片刻

Intermission

儘管本章的大部分篇幅是關於熟悉Perl的,但也遇到了許多新的關於正則表達式的知識。

1.許多工具都有自己的正則表達式流派。Perl 和egrep 可能屬於同一個流派,但是Perl 的正則表達式中的元字符更多。許多其他的語言,類似Java、Python、.NET和TCL,它們的流派類似Perl。

2.Perl用$variable=~m/regex/來判斷一個正則表達式是否能匹配某個字符串。m表示「匹配(match)」,而斜線用來標注正則表達式的邊界(它們本身不屬於正則表達式)。整個測試語句作為一個單元,返回true或者false值。

3.元字符——具有特殊意義的字符——的定義在正則表達式中並不是統一的。之前在關於shell和雙引號引用的字符串的例子中我們講過,元字符的含義取決於具體的情況。瞭解具體情況(shell、正則表達式、字符串),其中的元字符及其作用,對學習和使用Perl、PHP、Java、Tcl、GNU Emacs、awk、Python 或其他高級語言是非常重要的(當然,在正則表達式內部,字符組有自己的「子語言」,其中的元字符是不同的)。

4.Perl和其他流派的正則表達式提供了許多有用的簡記法(shorthands):

5./i修飾符表示此測試不區分大小寫。儘管寫法是「/i」,其實「i」只是跟在表示結尾的斜線之後。

6.「(?:…)」這個麻煩的寫法可以用來分組文本,但並不捕獲。

7.匹配成功之後,Perl可以用$1、$2、$3之類的變量來保存相對應的「(…)」括號內的子表達式匹配的文本。使用這些變量,我們能夠用正則表達式從字符串中提取信息(其他的語言所使用的方式有所不同,我們會在下一章看到例子)。

子表達式的編號按照開括號的出現先後排序,從 1 開始。子表達式可以嵌套,例如「(Washington(·DC)?)」。如果只是希望分組,也可以使用「(…)」,但副作用是,它們捕獲的文本仍然會保存到特殊的變量中。