2011年12月6日 星期二

如何在PHP中使用正規表示式


何為正規表示式?

幾年前,我對 Web 表單的輸入框做了一些有趣的檢驗。用戶將在此表單中輸入電話號碼。隨後,此電話號碼會按用戶鍵入的形式打印在用戶的廣告中。按照要求,美國的電話號碼可以幾種方式輸入:可以是 (555) 555-5555,也可以是 555-555-5555,但不能接受 555-5555 這樣的形式。
您或許會感到奇怪,為什麼我們不拋開所有的非數字字符,只保證剩餘的字符總數為 10 呢?這種方法確實可行,但無法阻止用戶輸入 !555?333-3333 這樣的內容。
以一名 Web 開發者的眼光來看,這種情況帶來了一項有趣的挑戰。我可以編寫例程來檢查各種不同格式,但我希望能夠找到一種解決方案,假如用戶隨後認可 555.555.5555 這樣的格式,這種解決方案能具備一定的靈活性。
這正是正規表示式(簡稱為 regex)的適用場景。之前我已經將它們剪切並粘貼到了應用程序中,但從未發現任何難以理解的語法問題。Regex 看上去非常像數學表達式。當您看到一個形如 2x2=4 的表達式時,您通常會想到 “2 乘以 2 等於 4”。正規表示式與之非常類似。閱讀過本文後,當您看到一個這樣的正規表示式 ^b$ 時,您就會告訴自己:“一行的開頭是 b,隨後就是行尾”。不僅如此,您還會意識到在 PHP 中使用正規表示式有多麼簡單。

使用 regex 的時機

在有規則可循時,您應使用 regex 來完成搜索和替換操作,但不必具有需要找到或替換的確切字符。舉例來說,在上文中提到的電話號碼的例子中,用戶定義了表明所輸入電話號碼的格式的規則,但並未定義電話號碼中所包含的數字。這同樣適用於有大批用戶輸入的場景。美國州名縮寫可限制為兩個從 A 到 Z 的大寫字母。這裡也可使用正規表示式,您可簡單地將表單中的文本或用戶輸入限制為字母表中的字母,而無需考慮大小寫和長度問題。

不宜使用 regex 的時機

正規表示式功能強大,但也有一些缺陷。其中之一就是要求具備讀寫表達式的相關技能。如果您決定在應用程序中包含正規表示式,就應該對其進行完整的註釋。這樣,此後如果有其他人需要更改表達式,即可在不中斷功能的情況下完成更改。另外,如果您對於使用正規表示式不夠熟悉,可能會發現它們難於調試。
為避免出現這些難題,在更簡單的內置功能足以很好地解決問題時不要使用正規表示式。
POSIX 與 PCRE
PHP 支持兩種正規表示式的實現:Portable Operating System Implementation(POSIX)和 Perl-Compatible Regular Expression(PCRE)。這兩種實現提供了不同的特性,但它們在 PHP 中使用起來一樣簡單。您所使用的 regex 風格取決於您過去在 regex 使用方面的經驗和使用習慣。有一些證據表明,PCRE 表達式的速度比 POSIX 表達式要略微快一點,但在絕大多數應用程序中,這一差別體現得不是那麼明顯。
在本文的示例中,各 regex 方法的語法都包含在註釋中。在函數語法中,regex 為 regex 參數,所搜索的字符串為 string。括號中的參數是可選的,由於本教程主要介紹基礎內容,故不會給出全部可選參數的介紹。

正規表示式語法

儘管 POSIX 和 PCRE 實現在對某些特性和字符類的支持方面有所不同,但它們的語法是相同的。每個正規表示式都是由一個或多個字符、特殊字符(有時也稱為元字符)、字符類和字符組構成的。
POSIX 和 PCRE 使用相同的通配符 —— 在 regex 中以通配符來表示 “此處可為任意內容”。通配符字符為一個英文句號或點(.)。若要查找英文句號或點,可使用轉義字符 \: \.。下文中所討論的其他特殊字符也是如此,例如行錨(line anchor)和限定符。如果一個字符在正規表示式中有特殊含義,那麼必須通過轉義才能表達其原本的文字含義。
行錨 是特殊的元字符,與一行的開頭和結尾相匹配,但不會捕獲任何文本(參見表 1)。例如,如果某一行以字母 a 開頭,那麼表達式 ^a 中的行錨不會捕獲字母 a,而是匹配行的開頭。

表 1. 行錨

錨 描述
 
^ 匹配一行的開頭
$ 匹配一行的結尾
限定符 應用於緊接於其前的表達式(參見表 2)。使用限定符,您可以指定在一次搜索中查找到一個表達式的次數。例如,表達式 a+ 將一次或多次地查找到字母 a。

表 2. 限定符

限定符 描述
? 限定符之前的表達式可被查找到 0 次或 1 次
+ 限定符之前的表達式可被查找到 1 次或多次
* 限定符之前的表達式可被查找到任意次(含 0 次)
{n} 限定符之前的表達式僅可被查找到 n 次
{n,m} 限定符之前的表達式可被查找到 n 次到 m 次之間
在 regex 中,捕獲文本並在替換和搜索操作中引用該文本是一項非常有用的特性(參見表 3)。通過使用捕獲功能,您可以執行搜索,來查找重複的單詞和閉合的 HTML 及 XML 標記。如果您在替換時使用了捕獲功能,那麼可以將找回的文本置入替換字符串內。後面將給出一個示例,展示如何以超鏈接替換電子郵件地址。

表 3. 分組與捕獲

字符類 描述
 
() 分組字符,並能夠捕獲文本
POSIX 字符類
POSIX 正規表示式遵循一些使其可為許多 regex 實現所用的標準(參見表 4)。例如,如果您正在編寫一條 POSIX 正規表示式,您可以在 PHP 中使用它、可以通過 grep 命令使用它,也可以通過許多支持正規表示式的編輯器使用它。

表 4. POSIX 字符類

字符 描述
[:alpha:] 匹配包含字母與數字的字符
[:digit:] 匹配任意數字
[:space:] 匹配任意空白

POSIX 匹配

有兩個使用 POSIX 正規表示式搜索字符串的函數,即 ereg() 和 eregi()。
ereg()
ereg() 方法為特定正規表示式搜索字符串。如果未找到任何匹配項,則返回 0,因此您可以給出如下測試:



清單 1. ereg() 方法

<?php
$phonenbr="555-555-5555";
// Syntax is ereg( regex, string [, out_captures_array])
if (ereg("[-[:digit:]]{12}", $phonenbr)) {
    print("Found match!\n");
} else {
    print("No match found!\n");
}
?>

正規表示式 [-[:digit:]]{12} 查找 12 個為數字或連字符的字符。就處理電話號碼而言,這有些粗略,您也可將其改寫成這樣的形式:^[0-9]{3}-[0-9]{3}-[0-9]{4}$。(在 regex 中,[0-9] 和 [:digit:] 實際上是完全相同的,您可能更願意使用 [0-9] 的形式,因為它更短些。)這種作為替代方案的表達式顯然更為精確。它會查找行的開頭(^),後接一組 3 個數字([0-9]{3})、一個連字符(-)、另外一組 3 個數字、另外一個連字符、一組 4 個數字,然後是行的結尾($)。當您手工編寫表達式時,這會使您瞭解到正規表示式要處理的問題的複雜程度如何,從而有助於預測出使用表達式搜索或替換的數據類型。
eregi()
eregi() 方法類似於 ereg(),不同之處在於它對大小寫不敏感。它將返回一個包含所找到的匹配項長度的整數,但您很可能會將其用於條件語句中,如下所示:

清單 2. eregi() 方法

<?php
$str="Hello World!";
// Syntax is ereg( regex, string [, out_captures_array])
if (eregi("hello", $str)) {
    print("Found match!\n");
} else {
    print("No match found!\n");
}
?>

執行此示例時,將輸出 Found match!,這是因為在忽略大小寫的搜索中找到了 hello。如果您使用的是 ereg,搜索將失敗。


POSIX 替換

ereg_replace() 和 eregi_replace() 這兩種方法用於在文本中進行替換,具有 POSIX 正規表示式的特性。
ereg_replace()
您可以使用 ereg_replace() 方法以 POSIX 正規表示式語法進行大小寫敏感的替換。如下示例描述了如何替換帶有超鏈接的字符串內的電子郵件地址:

清單 3. ereg_replace() 方法

<?php
$origstr = "My e-mail address is: [email]first.last@example.com[/email]";
// Syntax is: ereg_replace( regex, replacestr, string )
$newstr = \
ereg_replace("([.[:alpha:][:digit:]]+@[.[:alpha:][:digit:]]+)", 
    "\\1", $origstr);
print("$newstr\n");
?>

這是一條用於匹配電子郵件地址的正規表示式的不完整版本,但它展示了與 str_replace() 等其他普通替換函數相比,ereg_replace() 的強大之處。在使用正規表示式時,您可定義搜索的規則,而不是搜索文字字符。



eregi_replace()

除忽略大小寫之外,eregi_replace() 函數與 ereg_replace() 是完全相同的:
清單 4. eregi_replace() 函數

<?php
$origstr = "1 BANANA, 2 banana, 3 Banana";
// Syntax is: eregi_replace( regex, replacestr, string )
$newstr = eregi_replace("banana", "pear", $origstr);
print("New string is: '$newstr'\n");
?>

本例將 banana 替換為 pear,替換操作忽略了大小寫。

PCRE 字符類

由於 PCRE 語法支持更短的字符類和更多的特性,因此它比 POSIX 語法更為強大。表 5 列出了 PCRE 中支持而在 POSIX 表達式中沒有的部分字符類。

表 5. PCRE 字符類

字符類 描述
\b 詞邊界,查找詞的開始和結尾
\d 匹配任意數字
\s 匹配任意空白,如 tab 或空格
\t 匹配一個 tab 字符
\w 匹配包含字母與數字的字符

PCRE 匹配

PHP 中的 PCRE 匹配函數與 POSIX 匹配函數類似,但如果您習慣使用 POSIX 表達式,那麼 PCRE 匹配函數的一項特性可能會使您感到棘手:PCRE 函數要求表達式以分隔符開始和結束。在絕大多數示例中,分隔符都是一個 /,可在引號內表達式的開始和結尾處看到。務必牢記,此分隔符並非表達式的一部分。
在 PCRE 中的最後一個分隔符後,您可添加一個修飾符來更改正規表示式的行為。舉例來說,i 修飾符使正規表示式對大小寫不敏感。這是與 POSIX 方法的一項重要差異,在 POSIX 中,您需要按照對大小寫敏感性的需求來調用不同的方法。

preg_grep()

preg_grep() 方法返回一個數組,其中包含通過正規表示式在其中找到匹配項的另外一個數組的全部項目。如果您有一個較大的值集,並希望對其進行搜索以查找匹配項,那麼該方法非常有用。下面是一個示例:
清單 5. preg_grep() 方法

<?php
$array = array( "1", "3", "ABC", "XYZ", "42" );
// Syntax is preg_grep( regex, inputarray );
$grep_array = preg_grep("/^\d+$/", $array);
print_r($grep_array);
?>

在本例中,正規表示式 ^\d+$ 查找行的開始(^)和結尾($)之間包含一個或多個數字(\d+)的數組的所有元素。

preg_match()

preg_match() 函數使用 PCRE 在字符串中查找匹配項,它需要兩個參數:regex 和字符串。您可以選擇提供一個將由匹配項填充的數組、允許您修改匹配操作行為的標誌,還可提供字符串中開始查找匹配項的位置(offset)。示例如下:
清單 6. offset 方法

<?php
$string = "abcdefgh";
$regex = "/^[a-z]+$/i";
// Syntax is preg_match(regex, string, [, out_matches [, flags [, offset]]]);
if (preg_match($regex, $string)) {
    printf("Pattern '%s' found in string '%s'\n", $regex, $string);
} else {
    printf("No match found in string '%s'!\n", $string);
}
?>

本例使用了正規表示式 ^[a-z]+$,在行的開始(^)和結尾($)之間搜索可查找到一次或多次的([a-z]+)、從 a 到 z 的任意字母。

preg_match_all()

preg_match_all() 函數為在字符串中查找到的全部匹配項構建一個數組。下例構建了一個包含句子中全部詞的數組:
清單 7. preg_match_all() 函數

<?php
$string = "The quick red fox jumped over the lazy brown dog";
$re = "/\b\w+\b/";
// Syntax is preg_match_all( regex, string, return_array [, flags [, offset]])
preg_match_all($re, $string, $arrayout);
print_r($arrayout);
?>

正規表示式 \b\w+\b 在詞邊界(\b)間查找可找到一次或多次的(\w+)單詞字符。每個詞都將置入輸出數組 $arrayout 的一個數組元素中。



PCRE 替換

在 PHP 中進行 PCRE 替換與 POSIX 替換類似,不同之處在於使用的是 preg_replace() 而非 ereg_replace() 和 eregi_replace()。

preg_replace()

preg_replace() 函數使用 PCRE 進行替換。它需要這樣幾個參數:正規表示式、替換表達式和原始字符串。您還可以選擇提供希望的最大替換數,以及以所完成的替換數填充的變量。示例如下:
清單 8. preg_replace() 函數

<?php
$orig_string = "5555555555";
printf("Original string is '%s'\n", $orig_string);
$re = "/^(\d{3})(\d{3})(\d{4})$/";
// Syntax is preg_replace( regex, replacement, string \
[, limit [, out_count]] );
$new_string = preg_replace($re, "(\\1) \\2-\\3", $orig_string);
printf("New string is '%s'\n", $new_string);
?>

本例快速演示了捕獲部分文本及使用反向引用 的方法,如 \\1。這些反向引用會插入圓括號內所匹配的任意文本中,在本例中,\\1 匹配第 1 組 (\d{3})。
在示例中,您可使用 substr 將電話號碼分割開來,而對字符串只需進行少量更改,要依靠 substr 來可靠地捕獲正確文本會更加困難。
如果字符串的形式可為 (555)5555555,您可將表達式修改為 ^(?(\d{3}))?(\d{3})(\d{4})$ 以查找任意圓括號。

沒有留言: