本文的建議主要著重於正規表示式的可讀性,在開發中養成這些習慣,你將會更加清晰的考慮設計和表達式的結構,這將有助於減少bug和程式碼的維護,如果你自己就是這個程式碼的維護者你將會倍感輕鬆。大家可以自己看看,在自己實際使用的過程中註意正規表示式的這些經驗。
正規表示式難於書寫、難於閱讀、難於維護,經常錯誤匹配意料不到的文本或錯過了有效的文本,這些問題都是由正則表達式的表現和能力引起的。每個元字元(metacharacter)的能力和細微差別組合在一起,使得程式碼不借助智力技巧就無法解釋。
許多包含一定特性的工具使閱讀和編寫正規表示式變得容易了,但是它們又很不符合習慣。對許多程式設計師來說,書寫正規表示式就是一種魔法藝術。他們堅持自己所知道的特徵並持有絕對樂觀的態度。如果你願意採用本文所探討的五個習慣,你將可以讓你設計的正規表示式經受的住反覆試驗。
本文將使用Perl、PHP和Python語言作為程式碼範例,但本文的建議幾乎適用於任何替換表達式(regex)的執行。
一、使用空格和註釋
對於大部分程式設計師來說,在一個正規表示式環境裡使用空格和縮排排列都不成問題,如果他們沒有這麼做一定會被同行甚至外行人看笑話。幾乎每個人都知道把程式碼擠在一行會難於閱讀、書寫和維護。對於正規表示式又有什麼不同呢?
大部分替換表達式工具都具有擴展的空格特性,這允許程式設計師把他們的正規表示式擴展為多行,並在每一行結尾加上註解。為什麼只有少數程式設計師利用這個特性呢? Perl 6的正規表示式預設就是擴展空格的模式。不要再讓語言替你預設擴展空格了,自己主動利用吧。
記住擴展空格的竅門之一就是讓正規表示式引擎忽略擴充空格。這樣如果你需要匹配空格,你就得明確說明。
在Perl語言裡面,在正規表示式的結尾加上x,這樣「m/foo bar/」變成如下形式:
m/
foo
bar
/x
在PHP語言裡面,在正規表示式的結尾加上x,這樣「"/foo bar/"」變成如下形式:
"/
foo
bar
/x"
在Python語言裡面,傳遞模式修飾參數「re.VERBOSE」得到編譯函數如下:
pattern = r'''
foo
bar
'''
regex = re.compile(pattern, re.VERBOSE)
處理較複雜的正規表示式時,空格和註解更能體現其重要性。假設下面的正規表示式用來符合美國的電話號碼:
(?d{3})? ?d{3}[-.]d{4}
這個正規表示式比對電話號碼如「( 314)555-4000”的形式,你認為這個正規表示式是否符合「314-555-4000」或「555- 4000」呢?答案是兩種都不匹配。寫上這麼一行程式碼隱藏了缺點和設計結果本身,電話區號是需要的,但是正規表示式在區號和前綴之間缺少一個分隔符號的說明。
把這一行程式碼分成幾行並加上註解將把缺點暴露無疑,修改起來顯然更容易一些。
在Perl語言裡面應該是如下形式:
/
(? # 可選圓括號
d{3} # 必須的電話區號
)? # 可選圓括號
[-s.]? # 分隔符號可以是破折號、空格或句點
d{3} # 三位數前綴
[-.] # 另一個分隔符號
d{4} # 四位數電話號碼
/x
改寫的正規表示式現在在電話區號後面有一個可選擇的分隔符號,這樣它應該是匹配“314-555-4000”的,然而電話區號還是必須的。另一個程式設計師如果需要把電話區號變成可選項則可以迅速看出它現在不是可選的,一個小小的改動就可以解決這個問題。
二、書寫測驗
一共有三個層次的測試,每一層為你的程式碼加上一層可靠性。首先,你需要認真想想你需要匹配什麼程式碼以及你是否能夠處理錯誤匹配。其次,你需要利用資料實例來測試正規表示式。最後,你需要正式通過一個測試小組的測試。
決定要匹配什麼其實就是在匹配錯誤結果和錯過正確結果之間尋求一個平衡點。如果你的正規表示式過於嚴格,它將會錯過一些正確匹配;如果它太寬鬆,它將會產生一個錯誤匹配。一旦某個正規表示式發放到實際程式碼當中,你可能不會兩者都注意到。考慮一下上面電話號碼的例子,它將會匹配「800-555-4000 = -5355」。錯誤的配對其實很難發現,所以事先規劃做好測試是很重要的。
還是使用電話號碼的例子,如果你在Web表單裡面確認一個電話號碼,你可能只要滿足於任何格式的十位數字。但是,如果你想從大量文字裡面分離電話號碼,你可能需要很認證的排除不符合要求的錯誤匹配。
在考慮你想匹配的數據的時候,寫下一些案例。針對案例情況寫下一些程式碼來測試你的正規表示式。任何複雜的正規表示式都最好寫個小程式測試一下,可以採用下面的具體形式。
在Perl語言裡面:
#!/usr/bin/perl
my @tests = ( "314-555-4000",
"800-555-4400",
"(314)555-4000",
"314.555.4000",
"555-4000",
"aasdklfjklas",
"1234-123-12345"
);
foreach my $test (@tests) {
if ( $test =~ m/
(? # 可選圓括號
d{3} # 必須的電話區號
)? # 可選圓括號
[-s.]? # 分隔符號可以是破折號、空格或句點
d{3} # 三位數前綴
[-s.] # 另一個分隔符號
d{4} # 四位數電話號碼
/x ) {
print "Matched on $testn";
}
else {
print "Failed match on $testn";
}
}
在PHP語言裡面:
<?php
$tests = array( "314-555-4000",
"800-555-4400",
"(314)555-4000",
"314.555.4000",
"555-4000",
"aasdklfjklas",
"1234-123-12345" );
$regex = "/
(? # 可選圓括號
d{3} # 必須的電話區號
)? # 可選圓括號
[-s.]? # 分隔符號可以是破折號、空格或句點
d{3} # 三位數前綴
[-s.] # 另一個分隔符號
d{4} # 四位數電話號碼
/x";
foreach ($tests as $test) {
if (preg_match($regex, $test)) {
echo "Matched on $test
;";
}
else {
echo "Failed match on $test
;";
}
}
?>;
在Python語言裡面:
import re
tests = ["314-555-4000",
"800-555-4400",
"(314)555-4000",
"314.555.4000",
"555-4000",
"aasdklfjklas",
"1234-123-12345"
]
pattern = r'''
(? # 可選圓括號
d{3} # 必須的電話區號
)? # 可選圓括號
[-s.]? # 分隔符號可以是破折號、空格或句點
d{3} # 三位數前綴
[-s.] # 另一個分隔符號
d{4} # 四位數電話號碼
'''
regex = re.compile( pattern, re.VERBOSE ) for test in tests:
if regex.match(test):
print "Matched on", test, "n"
else:
print "Failed match on", test, "n"
執行測試程式碼將會發現另一個問題:它符合「1234-123-12345」。
理論上,你需要整合整個程式所有的測試到一個測試小組裡面。即使你現在還沒有測試小組,你的正規表示式測試也會是一個小組的良好基礎,現在正是開始創建的好機會。即使現在還不是創建的合適時間,你也應該在每次修改以後運行測試正規表示式。這裡花一小段時間將會減少你很多麻煩事。
三、為交替操作分組
交替操作符號( )的優先順序很低,這意味著它經常交替超過程式設計師所設計的那樣。例如,從文字裡面抽取Email位址的正規表示式可能如下:
^CC: To:(.*)
上面的嘗試是不正確的,但這個bug往往不被注意。上面程式碼的意圖是找到“CC:”或“To:”開始的文本,然後在這一行的後面部分提取Email地址。
不幸的是,如果某一行中間出現“To:”,那麼這個正規表示式將捕獲不到任何以“CC:”開始的一行,而是抽取幾個隨機的文字。坦白的說,正規表示式符合「CC:」開始的一行,但是什麼都捕獲不到;或符合任何包含「To:」的一行,但是把這行的剩餘文字都捕獲了。通常情況下,這個正規表示式會捕捉大量Email位址,而所有沒有人會注意到這個bug。
如果要符合實際意圖,那麼你應該加入括號說明清楚,正則表達式如下:
(^CC:) (To:(.*))
如果真正意圖是捕獲以“CC:”或“To:”開始的文本行的剩餘部分,那麼正確的正規表示式如下:
^(CC: To:)(.*)
這是一個普遍的不完全匹配的bug,如果你養成為交替操作分組的習慣,你就會避免這個錯誤。
四、使用寬鬆數量詞
很多程式設計師避免使用寬鬆數量詞例如“*?”、“+?”和“??”,即使它們會使這個表達式易於書寫和理解。
寬鬆數量詞可以盡可能少的匹配文本,這樣有助於完全匹配的成功。如果你寫了“foo(.*?)bar”,那麼數量詞將在第一次遇到“bar”時就停止匹配,而不是在最後一次。如果你希望從“foo###bar+++bar”中捕獲“###”,這一點就很重要。一個嚴格數量詞將捕獲“###bar++ +”。 ;),這將會帶來很大麻煩。如果你使用了寬鬆數量詞,你只要花上很少的時間組裝字元種類就能產生新的正規表示式。
當你知道你要捕捉文字的環境結構時,寬鬆數量詞是具有很大價值的。
五、利用可用分界符
Perl 和PHP語言常常使用左斜線(/)來標誌一個正規表示式的開頭和結尾,Python語言使用一組引號來標誌開頭和結尾。如果在Perl和PHP中堅持使用左斜線,你將要避免表達式中的任何斜線;如果在Python中使用引號,你將要避免使用反斜線()。選擇不同的分界符或引號可以允許你避免一半的正規表示式。這將使得表達式易於閱讀,減少由於忘記避免符號而潛在的bug。
Perl和PHP語言允許使用任何非數字和空格字元作為分界符。如果你切換到一個新的分界符,在符合URL或HTML標誌(如「http://」或「<br/>;」)時,你就可以避免漏掉左斜線了。
例如,「/http://(S)*/」可以寫成「#http://(S)*#」。
通用分界符是「#」、「!」和「 」。如果你要使用方括號、尖括號或花括號,只要保持前後配對出現就可以了。以下就是一些通用分界符的範例:
#…# !…! {…} s … … (Perl only) s[…][…] (Perl only) s<…>;/…/ (Perl only)
在Python中,正規表示式會先被當作一個字串。如果你使用引號作為分界符,你將會漏掉所有反斜線。但是你可以使用“r''”字串避免這個問題。如果針對「re.VERBOSE」選項使用三個連續單引號,它將允許你包含換行。例如regex = "( file://w+)(//d +)"可以寫出下面的形式:
regex = r'''
(w+)
(d+)
'''