正則表達式難于書寫、難于閱讀、難于維護,經常錯誤匹配意料不到的文本或者錯過了有效的文本,這些問題都是由正則表達式的表現和能力引起的。每個元字符(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 $test/n";
}
else {
print "Failed match on $test/n";
}
}
在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”。
理論上,你需要整合整個程序所有的測試到一個測試小組里面。即使你現在還沒有測試小組,你的正則表達式測試也會是一個小組的良好基礎,現在正是開始創建的好機會。即使現在還不是創建的合適時間,你也應該在每次修改以后運行測試一下正則表達式。這里花費一小段時間將會減少你很多麻煩事。
新聞熱點
疑難解答