1. 程式人生 > >正則表示式高階技巧及例項詳解

正則表示式高階技巧及例項詳解

轉載地址:http://www.blueidea.com/tech/program/2009/6757.asp

正則表示式(Regular Expression, abbr. regex) 功能強大,能夠用於在一大串字元裡找到所需資訊。它利用約定俗成的字元結構表示式來發生作用。不幸的是,簡單的正則表示式對於一些高階運用,功能遠遠不夠。若要進行篩選的結構比較複雜,你可能就需要用到高階正則表示式

本文為您介紹正則表示式的高階技巧。我們篩選出了八個常用的概念,並配上例項解析,每個例子都是滿足某種複雜要求的簡單寫法。如果你對正則的基本概念尚缺乏瞭解,請先閱讀這篇文章,或者 這個教程,或者維基條目

這裡的正則語法適用於PHP,與

Perl相容。

1. 貪婪/懶惰

所有能多次限定的正則運算子都是貪婪的。他們儘可能多地匹配目標字串,也就是說匹配結果會儘可能地長。不幸的是,這種做法並不總是我們想要的。因此,我們新增“懶惰”限定符來解決問題。在各個貪婪運算子後新增“?”能讓表示式只匹配儘可能短的長度。另外,修改器“U”也能惰化能多次限定的運算子。理解貪婪與懶惰的區別是運用高階正則表示式的基礎。

貪婪操作符

操作符 * 匹配之前的表示式零次或零次以上。它是一個貪婪操作符。請看下面的例子:

preg_match( '/<h1>.*<\/h1>/', '<h1>這是一個標題。</h1><h1>這是另一個。</h1>', $matches );
句點(.)能代表除換行符外的任意字元。上面的正則表示式匹配 h1 標籤以及標籤內的所有內容。它用句點(.)和星號(*)來匹配標籤內的所有內容。匹配結果如下:
<h1>這是一個標題。</h1><h1>這是另一個。</h1>

整個字串都被返回。* 操作符會連續匹配所有內容—— 甚至包括中間的 h1 閉合標籤。因為它是貪婪的,匹配整個字串是符合其利益最大化原則。

懶惰操作符

把上面的式子稍作修改,加上一個問號(?),能讓表示式變懶惰:

/<h1>.*?<\/h1>/

這樣它會覺得,只需匹配到第一個 h1 結尾標籤就完成任務了。

另一個有著類似屬性的貪婪操作符是 {n,} 。它代表之前的匹配模式重複n次或n次以上,如果沒有加上問號,它會尋找儘可能多的重複次數,加上的話,則會盡可能少重複(當然也就是“重複n次”最少)。

# 建立字串
$str = 'hihihi oops hi';
# 使用貪婪的{n,}操作符進行匹配
preg_match( '/(hi){2,}/', $str, $matches );  # matches[0] 將是 'hihihi'
# 使用墮化了的 {n,}? 操作符匹配
preg_match( '/(hi){2,}?/', $str, $matches );  # matches[0] 將是 'hihi'

2. 回返引用(Back referencing)

有什麼用?

回返引用(Back referencing)一般被翻譯成“反向引用”、“後向引用”、“向後引用”,個人覺得“回返引用”更為貼切[笨活兒]。它是在正則表示式內部引用之前捕獲到的內容的方法。例如,下面這個簡單例子的目的是匹配出引號內部的內容:

# 建立匹配陣列
$matches = array();
# 建立字串
$str = ""This is a 'string'"";
# 用正則表示式捕捉內容
preg_match( "/(\"|').*?(\"|')/", $str, $matches );
# 輸出整個匹配字串
echo  $matches[0];
它會輸出:
"This is a'
顯然,這並不是我們想要的內容。

這個表示式從開頭的雙引號開始匹配,遭遇單引號之後就錯誤地結束了匹配。這是因為表示式裡說:("|'),也就是雙引號(")和單引號(')均可。要修正這個問題,你可以用到回返引用。表示式\1,\2,…,\9 是對前面已捕獲到的各個子內容的編組序號,能作為對這些編組的“指標”而被引用。在此例中,第一個被匹配的引號就由\1代表。

如何運用?

將上面的例子中,後面的閉合引號替換為1:

preg_match( '/(\"|').*?\1/', $str, $matches );
這會正確地返回字串:
"This is a 'string'"

如果是中文引號,前引號和後引號不是同一個字元,怎麼辦?

還記得PHP函式 preg_replace 嗎?其中也有回返引用。只不過我們沒有用 \1 … \9,而是用了 $1 … $9 … $n (此處任意數目均可)作為回返指標。例如,如果你想把所有的段落標籤<p>都替換成文字:

$text = preg_replace( '/<p>(.*?)</p>/',
"<p>$1</p>", $html );
引數$1是一個回返引用,代表段落標籤<p>內部的文字,並插入到替換後的文本里。這種簡便易用的表示式寫法為我們提供了一個獲取已匹配文字的簡單方法,甚至在替換文字時也能使用。

3. 已命名捕獲組(Named Groups)

當在一個表示式內多次用到回撥引用時,很容易就把事情搞混淆,要弄清那些數字(1 … 9)都代表哪一個子內容是件很麻煩的事。回撥引用的一個替代方法是使用帶名字的捕獲組(下文簡稱“有名組”)。有名組使用(?P<name>pattern)來設定,name代表組名,pattern是配合該有名組的正則結構。請看下面的例子:

/(?P<quote>"|').*?(?P=quote)/
上式中,quote就是組名,"|'是改組匹配內容的正則。後面的(?P=quote)是在呼叫組名為quote的有名組。這個式子的效果和上面的回撥引用例項一樣,只不過是用了有名組來實現。是不是更加易讀易懂了?

有名組也能用於處理已匹配內容之陣列的內部資料。賦予特定正則的組名也能作為所匹配到的內容在陣列內部的索引詞。

preg_match( '/(?P<quote>"|\')/', "'String'", $matches );
# 下面的語句輸出“'”(不包括雙引號)
echo $matches[1];
# 使用組名呼叫,也會輸出“'”
echo $matches['quote'];

所以,有名組並不只是讓寫程式碼更容易,它也能用於組織程式碼。

4. 字詞邊界(Word Boundaries)

字詞邊界是字串裡的字詞字元(包括字母、數字和下劃線,自然也包括漢字)和非字詞字元之間的位置。其特殊之處就在於,它並不匹配某個實在的字元。它的長度是\b 匹配所有字詞邊界。

不幸的是,字詞邊界一般都被忽視掉了,大部分人都沒有在意他的現實意義。 例如,如果你想要匹配單詞“import”:

/import/
注意了!正則表示式有時候很調皮的。下面的字串也能和上面的式子匹配成功:
important
你或許覺得,只要在import前後加上空格,不就可以匹配這個獨立的單詞了:
/ import /
那如果遇上這種情況呢:
The trader voted for the import
當 import 這個詞在字串開頭或者結尾時,修改後的表示式仍然不能用。因此,考慮各種情況是必須的:
/(^import | import | import$)/i
別慌,還沒完呢。如果遇到標點符號了呢?就為了滿足這一個單詞的匹配,你的正則可能就需要這樣寫:
/(^import(:|;|,)? | import(:|;|,)? | import(\.|\?|\!)?$)/i
對於只匹配一個單詞來說,這樣做實在是有點大動干戈了。正因如此,字詞邊界才顯得意義重大。要適應上述要求,以及很多其他情況變種,有了字元邊界,我們所需寫的程式碼只是:
/\bimport\b/

上面所有情況都得到了解決。 \b 的靈活性就在於,它是一個沒有長度的匹配。它只匹配兩個實際字元之間想象出的位置。它檢查兩個相鄰字元是否是一個為單字,另一個為非單字。情況符合,就返回匹配。如果遇到了單詞的開頭或結尾,\b 會把它當成是非單詞字元對待。由於import裡面的 i 仍然被看成是單詞字元,import 就被匹配出來了。

注意,與\b相對,我們還有\B,此操作符匹配兩個單字或者兩個非單字之間的位置。因此,如果你想匹配在某個單詞內部的‘hi’,可以使用:

\Bhi\B
“this”、“hight”,都會返回匹配,而“hi there”則不會返回匹配。

5. 最小組團(Atomic Groups)

最小組團是無捕捉的特殊正則表示式分組。通常用來提高正則表示式的效能,也能用於消除特定匹配。一個最小組團可以用(?>pattern) 來定義,其中pattern是匹配式。
/(?>his|this)/
當正則引擎針對最小組團進行匹配時,它會跳過組團內標記的回溯位置。以單詞“smashing”為例,當用上面的正則表示式匹配時,正則引擎會先嚐試在“smashing”裡尋找“his”。顯然,找不到任何匹配。此時,最小組團就發揮作用了:正則引擎會放棄所有回溯位置。也就是說,它不會嘗試再從“smashing”裡查詢“this”。為什麼要這樣設定?因為“his”都沒有返回匹配結果,包含有“his”的“this”當然就更匹配不了了!

上面的例子並沒有什麼實用性,我們用/t?his?/ 也能達到效果。再看看下面的例子:

/\b(engineer|engrave|end)\b/
如果把“engineering”拿去匹配,正則引擎會先匹配到“engineer”,但接下來就遇到了字詞邊界,\b,所以匹配不成功。然後,正則引擎又會嘗試在字串裡尋找下一個匹配內容:engrave。匹配到eng的時候,後面的又對不上了,匹配失敗。最後,嘗試“end”,結果同樣是失敗。仔細觀察,你會發現,一旦engineer匹配失敗,並且都抵達了字詞邊界,“engrave”和“end”這兩個詞就已經不可能匹配成功了。這兩個詞都比engineer短小,正則引擎不應該再多做無謂的嘗試。
/\b(?>engineer|engrave|end)\b/

上面的替代寫法更能節省正則引擎的匹配時間,提高程式碼的工作效率。

6. 遞迴(Recursion)

遞迴(Recursion)用於匹配巢狀結構,例如括弧巢狀, (this (that)),HTML標籤巢狀<div><div></div></div>。我們使用(?R)來代表遞迴過程中的子模式。下面是一個匹配巢狀括弧的例子:
/\(((?>[^()]+)|(?R))*\)/

最外層使用了反義符的括號“\(”匹配巢狀結構的開端。然後是一個多選項操作符( * | * ),可能匹配除括號外的所有字元 “(?>[^()]+)”,也可能是通過子模式“(?R)”來再次匹配整個表示式。請注意,這個操作符會盡量多地匹配所有巢狀。

遞迴的另一個例項如下:

/<([\w]+).*?>((?>[^<>]+)|((?R)))*<\/\1>/
以上表達式綜合運用了字元分組,貪婪操作符、回溯,以及最小化組團來匹配巢狀標籤。第一個括弧內分組([w]+)匹配出標籤名,用於接下來的應用。若找到這尖括號樣式的標籤,則嘗試尋找標籤內容的剩餘部分。下一個括弧括起來的子表示式和上一個例項非常相似:要麼匹配不包括尖括號的所有字元?>[^<>]+,要麼遞迴匹配整個表示式(?R)。表示式最後的</1>代表閉合標籤。

7. 回撥(Callbacks)

匹配結果中的特定內容有時可能會需要某種特別的修改。要應用多重而複雜的修改,正則表示式的回撥就有了用武之地。回撥是用於函式preg_replace_callback中的動態修改字串的方式。你可以為preg_replace_callback指定某個函式為引數,此函式能接收匹配結果陣列為引數,並將陣列修改後返回,作為替換的結果。

例如,我們想將某字串中的字母全部轉變成大寫。十分不巧,PHP沒有直接轉化字母大小寫的正則操作符。要完成這項任務,就可以用到正則回撥。首先,表示式要匹配出所有需要被大寫的字母:

/\b\w/
上式同時使用了字詞邊界和字元類。光有這個式子還不夠,我們還需要一個回撥函式:
function upper_case( $matches ) {
return strtoupper( $matches[0] );
}
函式upper_case接收匹配結果陣列,並將整個匹配結果轉化成大寫。 在此例中,$matches[0]代表需要被大寫化的字母。然後,我們再利用preg_replace_callback實現回撥:
preg_replace_callback( '/\b\w/', "upper_case", $str );
一個簡單的回撥即有這般強大的力量。

8. 註釋(Commenting)

註釋不用來匹配字串,但確實是正則表示式中最重要的部分。當正則越寫越深入,越寫越複雜,要推譯出究竟什麼東西被匹配就會變得越來越困難。在正則表示式中間加上註釋,是最小化將來的迷糊和困惑的最佳方式。

要在正則表示式內部加上註釋,使用(?#comment)格式。把“comment”替換成你的註釋語句:

/(?#數字)\d/
如果你打算把程式碼公之於眾,為正則表示式加上註釋就顯得尤為重要。這樣別人才能更容易看懂和修改你的程式碼。和其他場合的註釋一樣,這樣做也能為你重訪自己以前寫的程式時提供方便。

考慮使用“x”或“(?x)”修改器來格式化註釋。這個修改器讓正則引擎忽略表示式引數之間的空格。“有用的”空格仍然能夠通過[ ]\s,或者\(反義符加空格)來匹配。

/
\d    #digit
[ ]   #space
\w+   #word
/x
上面的程式碼與下面的式子作用一樣:
/\d(?#digit)[ ](?#space)\w+(?#word)/
請時刻注意程式碼的可讀性。

更多資源(英文)

關於作者

Karthik Viswanathan 是一個喜歡程式設計和做網站的高中生。你可以到他的部落格上檢視他的作品:Lateral Code。你也可以關注一下他的線上Twitter應用