讀古今文學網 > 精通正則表達式(第3版) > 正則表達式的注意事項和處理方式 >

正則表達式的注意事項和處理方式

Care and Handling of Regular Expressions

本章開頭列出的第二點需要注意的就是:正則表達式的句法規則(syntactic packaging),它告訴應用程序:「嘿,這兒有一個正則表達式,我需要你做這些」。egrep是一個簡單的例子,因為正則表達式是作為命令行參數傳過去的。其他的「語法訣竅(syntactic sugar)」,例如我在第1章堅持使用的單引號,是因為考慮到shell,而不是egrep。複雜的系統,例如程序設計語言中的正則表達式,需要更多的包裝,系統才能知道哪些部分是正則表達式,需要如何處理。

下一步是考察我們能夠對匹配結果進行的操作。同樣,egrep在這方面很簡單,因為它做的都是同樣的事情(顯示包含匹配文本的行),但是,我們在前一章的開頭已經說過,真正有意義的是更複雜的操作。其中最基本的是匹配(檢查一個正則表達式是否能匹配一個字符串,或者從字符串中提取信息),以及查找和替換,根據匹配的結果修改字符串。這些操作可以以多種形式進行,不同的語言對此也有不同的規定。

一般來說,程序設計語言有 3 種處理正則表達式的方式:集成式(integrated)、程序式(procedural)和面向對像式(object-oriented)。在第一種方式中,正則表達式是直接內建在語言之中的,Perl就是如此。但是在其他兩種方式中,正則表達式不屬於語言的低級語法。相反,普通的函數接收普通的字符串,把它們作為正則表達式進行處理。由不同的函數進行不同的、關係到一個或多個正則表達式的操作。大多數語言(不包括Perl)採用的都是這兩種方式之一,包括Java、.NET、Tcl、Python、PHP、Emacs、lisp和Ruby。

集成式處理

Integrated Handling

我們已經看過Perl的集成式處理方法,例如第55頁的例子:

為清楚起見,我用斜體標注變量名,正則表達式相關的部分則用粗體標注,正則表達式本身用下畫線標注。Perl 會把正則表達式「^Subject:·(.*)」應用到$line 保存的文本中,如果能夠匹配,則執行下面的程序段。其中,變量$1代表括號內的子表達式匹配的文本,將它們賦值給$subject。

另一個集成式處理的例子是把正則表達式作為配置文件的一部分,例如 procmail(Unix 下的一個郵件處理程序)。在配置文件中,正則表達式用於將郵件信息發佈到對應的處理程序中。這個例子比Perl更簡單,因為不需要指明操作對像(郵件信息)。

這兩個例子背後的原理要複雜一些。集成式處理方法減輕了程序員的負擔,因為它隱藏了一些工作,例如正則表達式的預處理,準備匹配,應用正則表達式,返回結果。省略這些操作減輕了常見任務的完成難度,不過我們之後將會看到,有些情況下,這樣處理反而更慢,更複雜。

不過,在深入細節之前,我們先打量打量其他的處理方式,然後再來揭示這些被隱藏的步驟。

程序式處理和面向對像式處理

Procedural and Object-Oriented Handling

程序式處理和面向對像式處理非常相似。這兩種方式下,正則功能不是由內建的操作符來提供,而是由普通函數(函數式)或構造函數及方法(面向對像式)來提供的。這種情況下,並沒有專屬於正則表達式的操作符,只有平常的字符串,普通的函數、構造函數和方法把這些字符串作為正則表達式來處理。

下面幾節給出了幾個Java、VB.NET、PHP和Python的例子。

Java中的正則處理

現在來看「Subject」例子在Java中的實現方式,使用Sun提供的java.util.regex包(第8章詳細介紹Java)。

我仍然用斜體標注變量名,粗體標注正則表達式相關的元素,下畫線標注正則表達式本身。準確地說,是用下畫線標注表示作為正則表達式處理的普通的字符串。

這個類說明了面向對像式處理方法,它使用Sun提供的java.util.regex包的兩個類——Pattern和Matcher。其中執行的操作有:

∂ 檢查正則表達式,將它編譯為能進行不區分大小匹配的內部形式(internal form),得到一個「Pattern」對象。

● 將它與欲匹配的文本聯繫起來,得到一個「Matcher」對象。

÷應用這個正則表達式,檢查之前與之建立聯繫的文本,是否存在匹配,返回結果。

≠ 如果存在匹配,提取第一個捕獲括號內的子表達式匹配的文本。

任何使用正則表達式的語言都需要進行這些操作,或是顯式的(explicitly)或是隱式的(implicitly)。Perl隱藏了大多數細節,Java的實現方式則暴露這些細節。

函數式處理的例子。不過,Java 也提供了一些函數式處理的「便捷函數(convenience functions)」來節省工作量。用戶不再需要首先聲稱一個正則表達式對象,然後使用該對象的方法來操作。下面的靜態函數提供了臨時對象,執行完之後,這些對象就會被自動拋棄。這個例子用來說明Pattern.matches(…)函數:

這個函數包裝了一個隱式的「^…$」的正則表達式,返回一個Boolean值,說明它是否能夠匹配輸入的字符串。Sun的package同時提供程序式和面向對像式的處理方式是常見的做法。兩種接口的差別在於便捷程度(程序式處理方式在完成簡單任務時更容易,但處理複雜任務則很麻煩)、功能(程序式處理方式的功能和選項通常比對應的面向對像式的要少)和效率(在任何情況下,兩類處理方式的效率都不同——第6章詳細論述這個問題)。

Sun 有時也會把正則表達式整合到 Java 的其他部分,例如上面的例子可以使用 string 類的matches功能來完成:

同樣,這種辦法不如合理使用面向對象的程序有效率,所以不適宜在對時間要求很高的循環中使用,但是「隨手(casual)」用起來非常方便。

VB和.NET語言中的正則處理

儘管所有的正則引擎都能執行同樣的基本操作,但即使是採用同樣方法的各種實現方式(implementation)提供給程序員完成的任務,以及使用服務的方式也各有不同。下面是VB.NET中的「Subject」例子(.NET在第9章詳細論述):

總的來說,它很類似Java的例子,只是.NET將第●和第÷步結合為一步,第≠步需要一個確定的值。為什麼會有這樣的差異?兩者並沒有本質上的優劣之分——只是開發人員採用了自己當時覺得最好的方式(稍後我們會看到這點)。

.NET同樣提供了若干程序式處理的函數。下面的代碼用於判斷空行:

Java 的 Pattern.matches函數會自動在正則表達式兩端添加「^…$」,微軟則提供了更為一般的函數。Java的做法只是對核心對象的簡單包裝,但程序員需要使用的字符和變量更少,而代價只是一點點性能下降。

PHP中的正則處理

下面是使用PHP的preg套件中的正則表達式函數處理「Subject」的例子,這是純粹的函數式方法(第10章詳細介紹PHP)。

Python中的正則處理

最後我們來看Python中「Subject」的例子,Python採用的也是面向對像式的辦法。

這個例子與我們之前看過的非常類似。

差異從何而來

為什麼不同的語言採用不同的辦法呢?可能有語言本身的原因,不過最重要的因素還是正則軟件包的開發人員的思維和技術水準。舉例來說,Java 有許多正則表達式包,因為這些作者都希望提供 Sun 未提供的功能。每個包都有自己的強項和弱項,不過有趣的是,每個軟件包的功能設定都不一樣,所以Sun最終決定自己提供正則表達式包。

另一個關於這種差異的例子是 PHP,PHP 包含了三種完全獨立的正則引擎,每一種都對應一套自己的函數。PHP 的開發人員在開發過程中,因為對原有的功能不滿意,添加新的軟件包和對應的接口函數套件來升級 PHP 核心(一般認為,本書講解的「preg」套件是最優秀的)。

查找和替換

A Search-and-Replace Example

「Subject」的例子太簡單,還不足以說明3種方法之間的差異。在本節我們將看到更複雜的例子,它進一步揭示了不同處理方式在設計上的差異。

在前一章,我們看到了在Perl中利用查找和替換將E-mail地址轉換為超鏈接的例子(☞73):

Perl的查找和替換操作符是「原地生效」的,也就是說,替換會在目標變量上進行。其他大多數語言的替換都是在目標文本的副本上進行的。如果不需要修改原變量,這樣操作就很方便,不過如果需要修改原變量,就得把替換結果回傳給原變量。下面給出了一些例子。

Java中的查找和替換

下面是使用Sun提供的java.util.regex進行查找-替換的例子:

請注意,字符串中的每個『』都必須轉義為『\\』,所以,如果我們像本例中一樣用文本字符串來生成正則表達式,「w」就必須寫成『\\w』。在調試時,System.out.println(r.pattern())可以顯示正則函數確切接收到的正則表達式。我在這個正則表達式中包括換行符的原因是,這樣看起來很清楚。另一個原因是,每個#引入一段註釋,直到該行結束,所以,為了約束註釋,必須設定某些換行符。

Perl使用/g、/i、/x之類的符號來表示特殊的條件(這些修飾符分別代表全局替換、不區分大小寫和寬鬆排列模式☞135),java.util.regex則使用不同的函數(replaceAll而不是replace),以及給函數傳遞不同的標誌位(flag)參數(例如Pattern.CASE_INSENSITIVE和Pattern.COMMENTS)來實現。

VB.NET中的查找和替換

VB.NET的程序與Java的類似:

因為VB.NET的字符串文字(literal)不便於操作(它們不能跨越多行,也很難在其中加入換行符),長一點的正則表達式使用起來不如其他語言方便。另一方面,因為『』不是VB.NET中的字符串的元字符,這個表達式看起來要更清楚些。雙引號是 VB.NET 字符串中的元字符,為了表示這個字符,我們必須使用兩個緊挨著的雙引號。

PHP中的查找和替換

下面是PHP中的查找和替換的例子:

就像Java 和VB.NET 一樣,查找和替換操作的結果必須回傳給$text,除去這一點,這個例子和Perl的很相似。

其他語言中的查找和替換

Search and Replace in Other Languages

下面我們簡要看看其他傳統工具軟件和語言中查找和替換的例子。

Awk

Awk 使用的是集成式處理方法,/regex/,來匹配當前的輸入行,使用「var~…」來匹配其他數據。你可以在 Perl 中看到這種匹配表示法的影子(不過,Perl 的替換操作符模仿的是sed)。Awk的早期版本不支持正則表達式替換,不過現在的版本提供了sub(…)操作符。

sub(/mizpel/,〞misspell〞)

它會把正則表達式「mizpel」應用到當前行,將第一個匹配替換為 misspel。請注意,在Perl(和sed)中的對應做法是s/mizpel/misspell/。

如果要對該行的所有匹配文本進行替換,Awk使用的不是/g修飾符,而是另一個運算符:gsub(/mizpel/,「misspell」)。

Tcl

Tcl採用的是程序式處理方法,對不熟悉Tcl引用慣例(quoting conventions)的人來說可能很迷惑。如果我們要在Tcl中修正錯誤的拼寫,可以這樣:

它會檢查變量var中的字符串,把「mizpel」的第一處匹配替換為misspell,把替換後的字符串存入變量 newvar(這個變量並沒有以$開頭)。Tcl 接收的第一個參數是正則表達式,第二個參數是目標字符串,第三個是 replacement 字符串,第四個是目標變量的名字。Tcl的 regsub同樣可以接收可能出現的標誌位,例如-all用來進行全局替換,而不是只替換第一處匹配文本。

同樣,-nocase選項告訴正則引擎進行不區分大小寫的匹配(它等於 egrep 的-i參數,或者Perl的/i修飾符)。

GNU Emacs

GNU Emacs(下文中簡稱Emacs)是功能強大的文本編輯器,它可以使用elisp(Emacs lisp)作為內建的編程語言。它提供了正則表達式的程序式處理接口,以及數量眾多的函數來提供各種服務。其中主要的一種是「正則表達式搜索-前進(re-search-forward)」,接收參數為普通字符串,將它作為正則表達式來處理。然後從文本的「當前位置」開始搜索,直到第一處匹配發生,或者如果沒有匹配,就一直前進到字符串的末尾(用戶調用編輯器的「正則表達式搜索(regexp search)」的功能時,就會執行re-search-forward)。

如表 3-3(☞92)所示,Emacs 所屬的正則流派嚴重依賴反斜線。例如,「<([a-z]+)([n·t]|<[^>]+>)+1>」是查找重複單詞的表達式,可以用來解決第 1 章的問題。但我們不能直接使用這個正則表達式,因為Emacs的正則引擎不能識別t和n。不過Emacs中的雙引號字符串則可以,它會把這些標記轉換為我們需要的製表符和換行符,傳給正則引擎。在使用普通字符串提交正則表達式時,非常有用。但其缺陷——尤其是elisp的正則表達式的缺陷——在於,此流派過分依賴反斜線了,最終得到的正則表達式好像插滿了牙籤。下面是查找下一組重複單詞的函數:

這段程序加上(define-key global-map〞C-xC-d〞\'FindNextDbl),就可以使用「Control-x+Control-d」來迅速查找重複單詞了。

注意事項和處理方式:小結

Care and Handling:Summary

我們已經看到,函數很多,內部的機制也很多。如果你不熟悉這些語言,可能現在還有些困惑。不過請不必擔心。學習任何特定的工具軟件都比學習原理要容易。