正则表达式难于书写、难于阅读、难于维护,经常错误匹配意料不到的文本或者错过了有效的文本,这些问题都是由正则表达式的表现和能力引起的。每个元字符(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”。
理论上,你需要整合整个程序所有的测试到一个测试小组里面。即使你现在还没有测试小组,你的正则表达式测试也会是一个小组的良好基础,现在正是开始创建的好机会。即使现在还不是创建的合适时间,你也应该在每次修改以后运行测试一下正则表达式。这里花费一小段时间将会减少你很多麻烦事。
三、为交替操作分组
交替操作符号(|)的优先级很低,这意味着它经常交替超过程序员所设计的那样。比如,从文本里面抽取Email地址的正则表达式可能如下:
^CC:|To:(.*)
上面的尝试是不正确的,但是这个bug往往不被注意。上面代码的意图是找到“CC:”或者“To:”开始的文本,然后在这一行的后面部分提取Email地址。
不幸的是,如果某一行中间出现“To:”,那么这个正则表达式将捕获不到任何以“CC:”开始的一行,而是抽取几个随机的文本。坦白的说,正则表达式匹配 “CC:”开始的一行,但是什么都捕获不到;或者匹配任何包含“To:”的一行,但是把这行的剩余文本都捕获了。通常情况下,这个正则表达式会捕获大量 Email地址,所有没有人会注意这个bug。
如果要符合实际意图,那么你应该加入括号说明清楚,正则表达式如下:
(^CC:)|(To:(.*))
本当の目的が「CC:」または「To:」で始まるテキスト行の残りの部分をキャプチャすることである場合、正しい正規表現は次のとおりです:
^(CC:|To: )(. *)
これはよくある不完全マッチングのバグです。グループ操作を交互に行う習慣を身に付けていれば、このエラーは回避できます。
4. 緩い量指定子を使用する
多くのプログラマは、「*?」、「+?」などの緩い量指定子を使用することを避けます。 make この式は書きやすく、理解しやすいです。
緩和された量指定子は、できるだけ少ないテキストと一致するため、正確な一致を成功させるのに役立ちます。 「foo(.*?)bar」と書いた場合、量指定子は最後ではなく、最初に「bar」に遭遇したときにマッチングを停止します。これは、「foo###bar++bar」から「###」をキャプチャする場合に重要です。厳密な数量指定子は「###bar++ +」をキャプチャします。
HTML ファイルからすべての電話番号をキャプチャしたいと仮定すると、上で説明した電話番号の正規表現の例を使用できます。ただし、すべての電話番号がテーブルの最初の列にあることがわかっている場合は、緩和された量指定子を使用して、より単純な正規表現を作成できます:
初心者のプログラマの多くは、特定のカテゴリを否定するために緩い量指定子を使用しません。次のコードを書くことができます:
この場合、これは機能します。これは問題ありませんが、キャプチャしたいテキストに、区切られる一般的な文字 (この場合は ; など) が含まれている場合は煩わしい場合があります。緩和された量指定子を使用すると、文字タイプの組み立てにほとんど時間を費やさずに新しい正規表現を生成できます。
緩和された量指定子は、テキストをキャプチャしたいコンテキストの構造がわかっている場合に非常に役立ちます。
5. 利用可能な区切り文字を使用する
Perl 言語と PHP 言語では、Python 言語の先頭と末尾をマークするために左スラッシュ (/) がよく使用されます。開始と終了をマークする一連の引用符。 Perl や PHP で左スラッシュを使用する場合は、式内でスラッシュを使用しないようにします。Python で引用符を使用する場合は、バックスラッシュ () を使用しないようにします。別の区切り文字または引用符を選択すると、正規表現の半分を省略できます。これにより、式が読みやすくなり、記号の回避を忘れることによって引き起こされる潜在的なバグが軽減されます。
Perl 言語と PHP 言語では、数値以外の文字とスペース文字を区切り文字として使用できます。新しい区切り文字に切り替えると、URL または HTML タグ (「http://」や「
」など) を照合するときに左スラッシュを見逃すことを防ぐことができます。
たとえば、「/http://(S)*/」は「#http://(S)*#」と記述できます。
一般的な区切り文字は「#」、「!」、「|」です。角括弧、山括弧、または中括弧を使用する場合は、それらを一致させてください。一般的な区切り文字の例をいくつか示します。
#…# !…! {…} s|…|…| (Perl のみ) s<…> /…/ (Perl のみ)
Python では、正規表現は最初に文字列として扱われます。区切り文字として引用符を使用すると、すべてのバックスラッシュが欠落します。ただし、「r''」文字列を使用すると、この問題を回避できます。 「re.VERBOSE」オプションに 3 つの連続した一重引用符を使用すると、改行を含めることができます。たとえば、regex = "(file://w+)(//d+)" は次の形式で記述できます:
regex = r'''
この記事の提案は主に正規表現の読みやすさに焦点を当てており、開発中にこれらの習慣を身につければ、設計と式の構造についてより明確に考えることができ、バグやコードを減らすことができます。あなた自身がこのコードの保守管理者である場合は、よりリラックスできるでしょう。