我如何编写更多可维护的正则expression式?
我开始觉得使用正则expression式会降低代码的可维护性。 正则expression式的简洁和力量是邪恶的。 Perl会像默认操作符那样带有副作用。
我有养成正则expression式的习惯,至less有一个句子给出了基本意图,至less有一个例子说明什么可以匹配。
由于build立了正则expression式,所以我觉得评论expression式中每个元素的最大组成部分是绝对必要的。 尽pipe如此,即使我自己的正则expression式,让我仿佛在读克林贡一样,挠挠脑袋。
你有意贬低你的正则expression式吗? 你是否可以将更短更强大的分解成更简单的步骤? 我放弃了嵌套正则expression式。 是否存在由于可维护性问题而避免的正则expression式构造?
不要让这个例子云问题。
如果迈克尔·阿什 ( Michael Ash)的以下内容存在某种缺陷,你会有什么前景可以完全抛弃它吗?
^(?:(?:(?:0?[13578]|1[02])(\/|-|\.)31)\1|(?:(?:0?[13-9]|1[0-2])(\/|-|\.)(?:29|30)\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:0?2(\/|-|\.)29\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:(?:0?[1-9])|(?:1[0-2]))(\/|-|\.)(?:0?[1-9]|1\d|2[0-8])\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$
根据要求,可以使用Ash先生的链接find确切的目的。
匹配 01.1.02 | 11-30-2001 | 2000年2月29日
非匹配 02/29/01 | 13/01/2002 | 11/00/02
我通常只是尝试将所有正则expression式调用包装在自己的函数中,并且使用有意义的名称和一些基本的注释。 我喜欢把正则expression式看作只写语言,只有写它的人才能读(除非真的很简单)。 我完全期望有人可能需要完全重写expression,如果他们不得不改变它的意图,这可能是更好地保持正则expression式培训活着。
使用Expresso ,它给出了正则expression式的分级,英文分解。
要么
Darren Neimke的这个技巧 :
.NET允许通过RegExOptions.IgnorePatternWhitespace编译器选项和embedded在模式string每行中的(?#…)语法,使用embedded式注释来创build正则expression式模式。
这允许在每行中embedded类似于伪代码的注释,并且对可读性具有以下影响:
Dim re As New Regex ( _ "(?<= (?# Start a positive lookBEHIND assertion ) " & _ "(#|@) (?# Find a # or a @ symbol ) " & _ ") (?# End the lookBEHIND assertion ) " & _ "(?= (?# Start a positive lookAHEAD assertion ) " & _ " \w+ (?# Find at least one word character ) " & _ ") (?# End the lookAHEAD assertion ) " & _ "\w+\b (?# Match multiple word characters leading up to a word boundary)", _ RegexOptions.Multiline Or RegexOptions.IgnoreCase Or RegexOptions.IgnoreWhitespace _ )
这是另一个.NET示例(需要RegexOptions.Multiline
和RegexOptions.IgnorePatternWhitespace
选项):
static string validEmail = @"\b # Find a word boundary (?<Username> # Begin group: Username [a-zA-Z0-9._%+-]+ # Characters allowed in username, 1 or more ) # End group: Username @ # The e-mail '@' character (?<Domainname> # Begin group: Domain name [a-zA-Z0-9.-]+ # Domain name(s), we include a dot so that # mail.somewhere is also possible .[a-zA-Z]{2,4} # The top level domain can only be 4 characters # So .info works, .telephone doesn't. ) # End group: Domain name \b # Ending on a word boundary ";
如果您的RegEx适用于常见问题,则另一种select是将其logging在案并提交给RegExLib ,然后在该处进行评估和评论。 没有什么能击败许多双眼睛
另一个RegEx工具是The Regulator
那么,PCRE / x修饰符在生活中的全部目的就是让你更易于编写正则expression式,就像这个简单的例子一样:
my $expr = qr/ [az] # match a lower-case letter \d{3,5} # followed by 3-5 digits /x;
有些人使用RE来做错误的事情(我正在等待第一个关于如何使用单个RE来检测有效的C ++程序的SO问题)。
我通常会发现,如果我不能在60个字符之内适合我的RE,最好是一段代码,因为它几乎总是更具可读性。
无论如何,我总是在代码中loggingRE应该实现的细节。 这是因为我从痛苦的经历中知道,对于其他人(或者甚至六个月以后),进入并试图理解是多么困难。
我不相信他们是邪恶的,尽pipe我相信有些使用他们的人是邪恶的(不看着你,Michael Ash :-)。 他们是一个伟大的工具,但是,就像电锯一样,如果你不知道如何正确使用它们,你会割掉你的腿。
更新:其实,我只是跟着这个怪物的链接,它是validation1600年至9999年之间的m / d / y格式的date。这是一个典型的情况下,成熟的代码将更易于阅读和维护。
您只需将其分成三个字段并检查各个值。 我几乎认为这是一个值得终止,如果我的一个奴才买了这个给我。 我一定会把他们发回来写得很好。
这是相同的正则expression式分解成易消化的片断。 除了更具可读性之外,一些子正则expression式可以自己使用。 更改允许的分隔符也很容易。
#!/usr/local/ActivePerl-5.10/bin/perl use 5.010; #only 5.10 and above use strict; use warnings; my $sep = qr{ [/.-] }x; #allowed separators my $any_century = qr/ 1[6-9] | [2-9][0-9] /x; #match the century my $any_decade = qr/ [0-9]{2} /x; #match any decade or 2 digit year my $any_year = qr/ $any_century? $any_decade /x; #match a 2 or 4 digit year #match the 1st through 28th for any month of any year my $start_of_month = qr/ (?: #match 0?[1-9] | #Jan - Sep or 1[0-2] #Oct - Dec ) ($sep) #the separator (?: 0?[1-9] | # 1st - 9th or 1[0-9] | #10th - 19th or 2[0-8] #20th - 28th ) \g{-1} #and the separator again /x; #match 28th - 31st for any month but Feb for any year my $end_of_month = qr/ (?: (?: 0?[13578] | 1[02] ) #match Jan, Mar, May, Jul, Aug, Oct, Dec ($sep) #the separator 31 #the 31st \g{-1} #and the separator again | #or (?: 0?[13-9] | 1[0-2] ) #match all months but Feb ($sep) #the separator (?:29|30) #the 29th or the 30th \g{-1} #and the separator again ) /x; #match any non-leap year date and the first part of Feb in leap years my $non_leap_year = qr/ (?: $start_of_month | $end_of_month ) $any_year/x; #match 29th of Feb in leap years #BUG: 00 is treated as a non leap year #even though 2000, 2400, etc are leap years my $feb_in_leap = qr/ 0?2 #match Feb ($sep) #the separtor 29 #the 29th \g{-1} #the separator again (?: $any_century? #any century (?: #and decades divisible by 4 but not 100 0[48] | [2468][048] | [13579][26] ) | (?: #or match centuries that are divisible by 4 16 | [2468][048] | [3579][26] ) 00 ) /x; my $any_date = qr/$non_leap_year|$feb_in_leap/; my $only_date = qr/^$any_date$/; say "test against garbage"; for my $date (qw(022900 foo 1/1/1)) { say "\t$date ", $date ~~ $only_date ? "matched" : "didn't match"; } say ''; #comprehensive test my @code = qw/good unmatch month day year leap/; for my $sep (qw( / - . )) { say "testing $sep"; my $i = 0; for my $y ("00" .. "99", 1600 .. 9999) { say "\t", int $i/8500*100, "% done" if $i++ and not $i % 850; for my $m ("00" .. "09", 0 .. 13) { for my $d ("00" .. "09", 1 .. 31) { my $date = join $sep, $m, $d, $y; my $re = $date ~~ $only_date || 0; my $code = not_valid($date); unless ($re == !$code) { die "error $date re $re code $code[$code]\n" } } } } } sub not_valid { state $end = [undef, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; my $date = shift; my ($m,$d,$y) = $date =~ m{([0-9]+)[-./]([0-9]+)[-./]([0-9]+)}; return 1 unless defined $m; #if $m is set, the rest will be too #components are in roughly the right ranges return 2 unless $m >= 1 and $m <= 12; return 3 unless $d >= 1 and $d <= $end->[$m]; return 4 unless ($y >= 0 and $y <= 99) or ($y >= 1600 and $y <= 9999); #handle the non leap year case return 5 if $m == 2 and $d == 29 and not leap_year($y); return 0; } sub leap_year { my $y = shift; $y = "19$y" if $y < 1600; return 1 if 0 == $y % 4 and 0 != $y % 100 or 0 == $y % 400; return 0; }
我发现一个很好的方法是简单地将匹配过程分解成几个阶段。 它可能不会执行得如此之快,但是您还有额外的好处,就是能够在更精细的粒度上说出为什么没有发生匹配。
另一条路线是使用LL或LRparsing。 有些语言甚至不能用perl的非fsm扩展名来表示为正则expression式。
我已经学会了避免所有的,但最简单的正则expression式。 我更喜欢其他模型,如图标的string扫描或Haskell的parsing组合器。 在这两种模型中,您都可以编写具有与内置string操作相同的权限和状态的用户定义代码。 如果我使用Perl进行编程,那么我可能会在Perl中安装一些parsing组合器—我已经为其他语言编写了它。
一个非常好的select是使用parsingexpression式语法,就像Roberto Ierusalimschy用他的LPEG包完成的那样 ,但是与parsing器组合器不同的是,这是一个你在一个下午不能动弹的东西。 但是如果有人已经为你的平台做了PEG,那么它是正则expression式的一个非常好的select。
哇,那很丑。 它看起来应该起作用,将00作为一个两位数的年份(这应该是四分之一的闰年,但没有世纪你无法知道它应该是什么)模仿一个不可避免的bug。 有很多冗余可能被分解成子正则expression式,我会为三个主要情况(这是我今晚的下一个项目)创build三个子正则expression式。 我还使用了一个不同的字符作为分隔符,以避免必须转义正斜杠,将单个字符的变化转换成字符类(这很好地让我们避免了逃避时间段),并将\d
更改为[0-9]
因为前者匹配Perl 5.8和5.10中的任何数字字符(包括U+1815
MONGOLIAN DIGIT FIVE
:᠕)。
警告,未经testing的代码:
#!/usr/bin/perl use strict; use warnings; my $match_date = qr{ #match 29th - 31st of all months but 2 for the years 1600 - 9999 #with optionally leaving off the first two digits of the year ^ (?: #match the 31st of 1, 3, 5, 7, 8, 10, and 12 (?: (?: 0? [13578] | 1[02] ) ([/-.]) 31) \1 | #or match the 29th and 30th of all months but 2 (?: (?: 0? [13-9] | 1[0-2] ) ([/-.]) (?:29|30) \2) ) (?: (?: #optionally match the century 1[6-9] | #16 - 19 [2-9][0-9] #20 - 99 )? [0-9]{2} #match the decade ) $ | #or match 29 for 2 for leap years ^ (?: #FIXME: 00 is treated as a non leap year #even though 2000, 2400, etc are leap years 0?2 #month 2 ([/-.]) #separtor 29 #29th \3 #separator from before (?: #leap years (?: #match rule 1 (div 4) minus rule 2 (div 100) (?: #match any century 1[6-9] | [2-9][0-9] )? (?: #match decades divisible by 4 but not 100 0[48] | [2468][048] | [13579][26] ) | #or match rule 3 (div 400) (?: (?: #match centuries that are divisible by 4 16 | [2468][048] | [3579][26] ) 00 ) ) ) ) $ | #or match 1st through 28th for all months between 1600 and 9999 ^ (?: (?: 0?[1-9]) | (?:1[0-2] ) ) #all months ([/-.]) #separator (?: 0?[1-9] | #1st - 9th or 1[0-9] | #10th - 19th or 2[0-8] #20th - 28th ) \4 #seprator from before (?: (?: #optionally match the century 1[6-9] | #16 - 19 [2-9][0-9] #20 - 99 )? [0-9]{2} #match the decade ) $ }x;
有些人在遇到问题时,会想:“我知道,我会用正则expression式”。 现在他们有两个问题。 – 在comp.lang.emacs中的Jamie Zawinski。
保持正则expression式尽可能简单( KISS )。 在你的date示例中,我可能会为每个datetypes使用一个正则expression式。
或者甚至更好,用库(即dateparsing库)replace它。
我也会采取措施确保input源有一些限制(即只有一种types的datestring,最好是ISO-8601 )。
也,
- 当时有一件事(提取值可能是个例外)
- 如果正确使用高级构造是可以的(如简化expression式并因此减less维护)
编辑:
“先进的结构导致维护问题”
我原来的观点是,如果使用得当 ,应该会导致更简单的expression,而不是更困难的expression。 简单的expression式应该减less维护。
我已经更新了上面的文字说尽可能多。
我想指出的是,正则expression式本身并不具备先进的构造。 不熟悉某种结构并不能使它成为一种先进的结构,而仅仅是一种不熟悉的结构。 这并不能改变正则expression式function强大,简洁,使用得当的优点。 就像手术刀一样,它完全掌握在使用者的手中。
我认为维护正则expression式的答案与评论或正则expression式没有多大关系。
如果我负责debugging你给出的例子,我会坐在正则expression式debugging工具(如正则expression式教程 )的前面,并通过它正在处理的数据的正则expression式。
我仍然可以使用它。 我只是使用Regulator 。 它允许你做的一件事就是保存正则expression式和testing数据。
当然,我也可以添加评论。
这是Expresso生产的。 我以前从来没有用过,但现在,监pipe机构已经失去了工作:
//使用System.Text.RegularExpressions; /// ///为C#build立的正则expression式:星期四,四月2,2009,12:51:56 AM ///使用Expresso版本:3.0.3276,http://www.ultrapico.com /// ///正则expression式的描述: /// ///从3个选项中select /// ^(?:(?:( ?: 0?[13578] | 1 [02])(\ / | - | \。)31)\ 1 |(?:( ?: 0?[13-9 ] | 1 [0-2])(\ / | - |。?\)(?: 29 | 30)\ 2))(:( ?: 1 [6-9] | [2-9] \ d) ?\ d {2})$ ///行或string的开始 ///匹配expression式,但不捕获它。 [(:( ?: 0 [13578] | 1 [02]?)?(\ / | - |。\)31)\ 1 |(:( ?: 0 [13-9] | 1 - [O- 2])(\ / | - |。\)(?: 29 | 30)\ 2)] ///从2个select中select ///(?:(?:0?| 1 [02])(\ / | - | \。)31)\ 1 ///匹配expression式,但不捕获它。 [(?:?0 [13578] | 1 [02])(\ / | - | \)31] ///(?:0?| 1 [02])(\ / | - | \。)31 ///匹配expression式,但不捕获它。 [0 [13578] |λ1 [02]] ///从2个select中select /// 0?[13578] /// 0,零次或一次重复 ///这个类中的任何字符:[13578] /// 1 [02] /// 1 ///此类中的任何angular色:[02] /// [1]:一个编号的捕获组。 [\ / | - | \。] ///从3个选项中select ///文字/ /// - ///文字。 /// 31 ///反向引用捕获号码:1 (/: - | \。)(?: 29 | 30)\ 2)///(?:( ?: 0?[13-9] | 1 [0-2]) ///返回 /// 新队 ///匹配expression式,但不捕获它。 [(?:?0 [13-9] | 1 [0-2])(\ / | - | \)(?: 29 | 30)\ 2] ///(?:0?[13-9] | 1 [0-2])(\ / | - | \。)(?: 29 | 30)\ 2 ///匹配expression式,但不捕获它。 [0 [13-9] |λ1 [0-2]] ///从2个select中select /// 0?[13-9] /// 0,零次或一次重复 ///这个类中的任何字符:[13-9] /// 1 [0-2] /// 1 ///这个类中的任何字符:[0-2] /// [2]:编号的捕获组。 [\ / | - | \。] ///从3个选项中select ///文字/ /// - ///文字。 ///匹配expression式,但不捕获它。 [29 | 30] ///从2个select中select /// 29 /// 29 /// 30 /// 30 ///反向引用捕获号码:2 ///返回 /// 新队 ///匹配expression式,但不捕获它。 [(?:1 [6-9] | [2-9] \ d)?\ d {2}] ///(?:1 [6-9] | [2-9] \ d)?\ d {2} ///匹配expression式,但不捕获它。 [1 [6-9] | [2-9] \ d],零个或一个重复 ///从2个select中select /// 1 [6-9] /// 1 ///这个类中的任何字符:[6-9] /// [2-9] \ d ///这个类中的任何字符:[2-9] ///任何数字 ///任何数字,正好2个重复 ///行结束或string /// ^(?: 0?2(\ / | - | \。)29 \ 3(?:(?:( ?: 1 [6-9] | [2-9] \ d)?(?: 0 [48] | [2468] [048] | [13579] [26])|(:( ?: 16 |?[2468] [048] | [3579] [26])00))))$ ///行或string的开始 ///匹配expression式,但不捕获它。 [0 2 | - |(?:( :( ?: 1 [6-9] | [2-9] \ d)?(\ / \)29 \ 3(?: 0 [48] | [ 2468] [048] | [13579] [26])|(:( ?: 16 |?[2468] [048] | [3579] [26])00)))] /// 0?2(\ / | - | \。)29 \ 3(?:(?:( ?: 1 [6-9] | [2-9] \ d)?(?: 0 [48] | [2468] [048] | [13579] [26])|(:( ?: 16 |?[2468] [048] | [3579] [26])00))) /// 0,零次或一次重复2 /// [3]:一个编号的捕获组。 [\ / | - | \。] ///从3个选项中select ///文字/ /// - ///文字。 /// 29 ///反向引用捕获号码:3 ///匹配expression式,但不捕获它。 [(:( ?: 1 [6-9] | [2-9] \ d)?(?:0 [48] | [2468] [048] | [13579] [26])|?(:( ?:16 | [2468] [048] | [3579] [26])00))] ///匹配expression式,但不捕获它。 [(?:1 [6-9] | [2-9] \ d)?(?:0 [48] | [2468] [048] | [13579] [26])|(:( ?: 16 | [2468] [048] | [3579] [26])00)] ///从2个select中select ///(?:1 [6-9] | [2-9] \ d)?(?: 0 [48] | [13579] [26]) ///匹配expression式,但不捕获它。 [1 [6-9] | [2-9] \ d],零个或一个重复 ///从2个select中select /// 1 [6-9] /// 1 ///这个类中的任何字符:[6-9] /// [2-9] \ d ///这个类中的任何字符:[2-9] ///任何数字 ///匹配expression式,但不捕获它。 [0 [48] | [2468] [048] | [13579] [26]] ///从3个选项中select /// 0 [48] /// 0 ///此类中的任何angular色:[48] /// [048] ///这个类中的任何字符:[2468] ///这个类中的任何字符: /// [13579] [26] ///此类中的任何字符:[13579] ///此类中的任何angular色:[26] ///(?:( ?: 16 | 0079] [0068] 00) ///返回 /// 新队 ///匹配expression式,但不捕获它。 [(?:16 | [2468] [048] | [3579] [26])00] ///(?:16 | | 26])00 ///匹配expression式,但不捕获它。 [16 | [2468] [048] | [3579] [26]] ///从3个选项中select /// 16 /// 16 /// [048] ///这个类中的任何字符:[2468] ///这个类中的任何字符: /// [3579] [26] ///此类中的任何字符:[3579] ///此类中的任何angular色:[26] /// 00 ///行结束或string /// ^(?:( ?: 0?[1-9])|(?:1 [0-2]))(\ / | - | \。)(?: 0?[1-9] | 1 \ d | 2 [0-8])\ 4(:( ?: 1 [6-9] |?[2-9] \ d)\ d {2})$ ///行或string的开头 ///匹配expression式,但不捕获它。 [(:0 [1-9]?)|(?:1 [0-2])] ///从2个select中select ///匹配expression式,但不捕获它。 [0?[1-9]] /// 0?[1-9] /// 0,零次或一次重复 ///这个类中的任何字符:[1-9] ///匹配expression式,但不捕获它。 [1 [0-2]] /// 1 [0-2] /// 1 ///这个类中的任何字符:[0-2] ///返回 /// 新队 /// [4]:一个编号的捕获组。 [\ / | - | \。] ///从3个选项中select ///文字/ /// - ///文字。 ///匹配expression式,但不捕获它。 [0 [1-9] |≤1\ d | 2 [0-8]] ///从3个选项中select /// 0?[1-9] /// 0,零次或一次重复 ///这个类中的任何字符:[1-9] /// 1 \ d /// 1 ///任何数字 /// 2 [0-8] /// 2 ///此类中的任何字符:[0-8] ///反向引用捕获号码:4 ///匹配expression式,但不捕获它。 [(?:1 [6-9] | [2-9] \ d)?\ d {2}] ///(?:1 [6-9] | [2-9] \ d)?\ d {2} ///匹配expression式,但不捕获它。 [1 [6-9] | [2-9] \ d],零个或一个重复 ///从2个select中select /// 1 [6-9] /// 1 ///这个类中的任何字符:[6-9] /// [2-9] \ d ///这个类中的任何字符:[2-9] ///任何数字 ///任何数字,正好2个重复 ///行结束或string /// /// /// 公共静态正则expression式正则expression式=新正则expression式( “^(:( :( ?: 0 [13578] | 1 [02]???)(\\ / | - |。\\)31)\\ 1 | \ r \ N(:( ?: 0? 〔13-9〕“+ “| 1 [0-2])(\\ / | - |。\\?)(?: 29 | 30)\\ 2)个)\ r \ N(:( ?: 1 [6-9] | [ 2-9] \\ d)\\?d“+ “{2})$ | ^(?: 0 2(\\ / | - |?。\\)?29 \\ 3(:( :( ?: 1 [6-9] | [2-9] \\ d)?(?:0 [“+ “48] | [2468] [048] | [13579] [26])| \ r \ N(:( ?: 16 |?[2468] [048] | [3579] [2” + “6])00))))$ | ^(:( ?: 0 [1-9])|(:???1 [0-2]))\ r \ N(\\ / | - | \ \。)(?:0?[1-9" + “] | 1 \\ d | 2 [0-8])\\ 4(:( ?: 1 [6-9] |?[2-9] \\ d)\\ d {2})$” , RegexOptions.CultureInvariant | RegexOptions.Compiled );
我最近发表了一个关于评论带embedded式注释的正则expression式的问题有一些有用的答案,尤其是@mikej
ComposeRegex上的Martin Fowler发表了关于提高正则expression式可读性的更多想法。 总之,他主张将一个复杂的正则expression式分解成可以赋予有意义variables名称的较小部分。 例如
我不希望正则expression式是可读的,所以我只是把它们保持原样,并在需要时重写。