【问题标题】:Is there a better way to write Perl regexes with /x so the code is still easy to read?有没有更好的方法用 /x 编写 Perl 正则表达式,这样代码仍然易于阅读?
【发布时间】:2009-06-12 15:48:13
【问题描述】:

我在我的一个脚本上运行 Perl::Critic,得到了这条消息:

Regular expression without "/x" flag at line 21, column 26. See page 236 of PBP.

我查阅了政策信息here,了解到在扩展模式下编写正则表达式将对任何查看代码的人有所帮助。

但是,我不知道如何将我的代码转换为使用 /x 标志。

CPAN 示例:

# Match a single-quoted string efficiently...

m{'[^\\']*(?:\\.[^\\']*)*'};  #Huh?

# Same thing with extended format...

m{
    '           # an opening single quote
    [^\\']      # any non-special chars (i.e. not backslash or single quote)
    (?:         # then all of...
        \\ .    #    any explicitly backslashed char
        [^\\']* #    followed by an non-special chars
    )*          # ...repeated zero or more times
    '           # a closing single quote
}x;

如果您只看正则表达式,这是有道理的。

我的代码:

if ($line =~ /^\s*package\s+(\S+);/ ) {

我不确定如何在 if 语句中使用扩展的正则表达式。我可以这样写:

    if (
        $line =~ /
        ^\s*    # starting with zero or more spaces
        package
        \s+     # at least one space
        (\S+)   # capture any non-space characters
        ;       # ending in a semi-colon
        /x
      )
    {

这很有效,但我认为这几乎比原版更难阅读。有没有更好的方法(或最佳实践方法)来写这个?我想我可以使用 qr// 创建一个变量。

我并不是真的在寻找关于重写这个特定正则表达式的建议(尽管如果我能改进它,我会接受建议)——我更多的是在寻找关于如何在 if 中扩展正则表达式的建议声明。

我知道 Perl::Critic 只是一个指导方针,但最好遵循它。

提前致谢!

编辑: 因此,在收到一些答案后,我很清楚使用 cmets 制作正则表达式多行并不总是必要的。了解基本正则表达式的人应该能够理解我的示例在做什么——我添加的 cmets 可能有点不必要和冗长。我喜欢使用扩展正则表达式标志的想法,但仍然在正则表达式中嵌入空格以使正则表达式的每个部分更加清晰。 感谢您的所有意见!

【问题讨论】:

    标签: regex perl perl-critic


    【解决方案1】:

    切勿编写说明代码内容的注释。注释应该告诉你为什么代码说出它所说的。看看这个怪物,没有 cmets 很难看到发生了什么,但是 cmets 清楚什么试图匹配:

    require 5.010;
    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$/;
    

    【讨论】:

      【解决方案2】:

      好吧,我真的不认为你应该在这上面浪费竖屏空间。另一方面,如果我要将此模式写成多行,我会使用大括号并缩进该模式:

      if ($line =~ m{
              \A \s*
              package
              \s+
              (\S+)
              \s* ;
          }x 
      ) {
      

      恕我直言,以下版本非常好:

      if ( $line =~ m{ \A \s* package \s+ (\S+) \s* ; }x  ) {
      

      就获得m//x 的好处而言。

      在这种情况下,完全不需要 cmets,因为您没有做任何棘手的事情。我确实在分​​号前添加了\s*,因为有时人们将分号与包名分开,这不会影响你的匹配。

      【讨论】:

      • 我必须去perl.com/doc/manual/html/pod/perlre.html 看看“\A”是什么意思。这是不是“^”的首选方式?
      • 我想我以前没有想过在单行正则表达式中添加空格。我一直认为“/x”标志只是一个多行标志,但我真的很喜欢你上面的例子。
      • @BrianH:不,不是。只有当你使用 /m 时才会有所不同,而当你使用 /m 时,你通常需要 ^,而不是 \A。另一方面,$ 经常用在人们真正的意思是 \z 的地方。
      • @ysth 我确实喜欢 \A 和 \z 之间的对称性。不过,我上面的 \A 确实是不必要的。
      【解决方案3】:

      对于这些额外信息所增加的价值,这几乎是您的决定。

      有时你是对的,它不会添加任何内容来解释发生了什么,只会让代码看起来很混乱,但对于复杂的正则表达式,x 标志可能是一个福音。

      实际上,这种关于附加信息的附加价值的“打电话”可能相当困难。

      我不记得有多少次我看到过没有维护格式精美的 cmets 的遗留代码,因此偏离了代码正在做的事情。事实上,当我经验不足的时候,我完全走上了错误的道路,因为与一段代码相关的注释已经过时并且没有得到维护。

      编辑:在某些方面,CPAN 示例并没有那么有用。当使用 x 标志添加 cmets 来描述复杂的正则表达式时,我倾向于描述正则表达式试图匹配的组件,而不仅仅是描述正则表达式“位”本身。例如,我会写如下内容:

      • 英国邮政编码的第一个组成部分(地区和区),或
      • 英国的国际区号,或
      • 任何英国手机号码。

      它告诉我的不仅仅是

      • 一个或两个字母,后跟一个数字,可选地后跟一个字母,或
      • 两个四位数字加在一起,或
      • 一个零,后跟四个十进制数字,一个破折号,然后是六个十进制数字。

      我的感觉是在这种情况下将正则表达式 cmets 排除在外。你的直觉是对的!

      【讨论】:

      • 关于描述正则表达式的非常好的编辑。当“捕获包名称”之类的内容可能更清楚时,我陷入了描述正则表达式正在做什么的陷阱(例如“捕获任何非空格字符”)。如果可以的话,我会再次 +1 你的帖子!
      • 谢谢@BrianH。在 C "I++;" 的一行上方找到带有诸如 "# add 1 to i" 之类的 cmets 的代码是一件非常痛苦的事情。 ;-)
      【解决方案4】:

      看到这个话题是关于编写正则表达式的替代方法,有一些方法可以编写复杂的不带变量和不带cmets的正则表达式,它仍然有用。

      我将 Chas Owens 日期验证正则表达式重排为 Perl-5.10 中可用的新声明形式,它有很多好处。

      • 正则表达式中的标记是可重复使用的
      • 稍后打印正则表达式的任何人仍将看到整个逻辑树。

      它可能不是每个人的鱼壶,但对于日期验证等极其复杂的事情,它可以很方便(ps:在现实世界中,请使用模块来处理日期的东西,不要DIY,这只是一个学习的例子)

      #!/usr/bin/perl 
      use strict;
      use warnings;
      require 5.010;
      
      #match the 1st through 28th for any month of any year
      my $date_syntax = qr{
          (?(DEFINE)
              (?<century>
                  ( 1[6-9] | [2-9][0-9] )
              )
              (?<decade>
                  [0-9]{2} (?!\d)
              )
              (?<year>
                  (?&century)? (?&decade)(?!\d)
              )
              (?<leapdecade> (
                  0[48]       | 
                  [2468][048] |
                  [13579][26]
                  )(?!\d)
              )
              (?<leapcentury> (
                  16          | 
                  [2468][048] |
                  [3579][26]
                  )
              )   
              (?<leapyear>
                  (?&century)?(?&leapdecade)(?!\d)
                  |
                  (?&leapcentury)00(?!\d)
              )
              (?<monthnumber>      ( 0?[1-9] | 1[0-2] )(?!\d)                  )
              (?<shortmonthnumber> ( 0?[469] | 11     )(?!\d)                  )
              (?<longmonthnumber>  ( 0?[13578] | 1[02] )(?!\d)                 )
              (?<nonfebmonth>      ( 0?[13-9] | 1[0-2] )(?!\d)                 )
              (?<febmonth>         ( 0?2 )(?!\d)                               )
              (?<twentyeightdays>  ( 0?[1-9] | 1[0-9] | 2[0-8] )(?!\d)         )
              (?<twentyninedays>   ( (?&twentyeightdays) | 29 )(?!\d)          )
              (?<thirtydays>       ( (?&twentyeightdays) | 29 | 30 )(?!\d)     )
              (?<thirtyonedays>    ( (?&twentyeightdays) | 29 | 30 | 31 )(?!\d))
              (?<separator>        [/.-]                              )               #/ markdown syntax highlighter fix
              (?<ymd>
                  (?&leapyear) (?&separator) (?&febmonth) (?&separator) (?&twentyninedays) (?!\d)
                  |
                  (?&year) (?&separator) (?&longmonthnumber) (?&separator) (?&thirtyonedays) (?!\d)
                  |
                  (?&year) (?&separator) (?&shortmonthnumber) (?&separator) (?&thirtydays) (?!\d)
                  |
                  (?&year) (?&separator) (?&febmonth) (?&separator) (?&twentyeightdays) (?!\d)
              )
              (?<mdy>
                  (?&febmonth) (?&separator) (?&twentyninedays) (?&separator) (?&leapyear)  (?!\d)
                  |
                  (?&longmonthnumber) (?&separator) (?&thirtyonedays) (?&separator) (?&year) (?!\d)
                  |
                  (?&shortmonthnumber) (?&separator) (?&thirtydays) (?&separator) (?&year) (?!\d)
                  |
                  (?&febmonth) (?&separator) (?&twentyeightdays) (?&separator) (?&year) (?!\d)
              )
              (?<dmy>
                  (?&twentyninedays) (?&separator) (?&febmonth) (?&separator) (?&leapyear)  (?!\d)
                  |
                  (?&thirtyonedays) (?&separator) (?&longmonthnumber) (?&separator)(?&year) (?!\d)
                  |
                  (?&thirtydays) (?&separator) (?&shortmonthnumber) (?&separator) (?&year) (?!\d)
                  |
                  (?&twentyeightdays) (?&separator) (?&febmonth) (?&separator)  (?&year) (?!\d)
              )
              (?<date>
                  (?&ymd) | (?&mdy) | (?&dmy)
              )
              (?<exact_date>
                 ^(?&date)$
             )
          )
      }x;
      
      my @test = ( "2009-02-29", "2009-02-28", "2004-02-28", "2004-02-29", "2005-03-31", "2005-04-31", "2005-05-31", 
          "28-02-2009","02-28-2009",        
      );
      
      for (@test) {
        if ( $_ =~ m/(?&exact_date) $date_syntax/x ) {
          print "$_ is valid\n";
        }
        else {
          print "$_ is not valid\n";
        }
      
        if ( $_ =~ m/^(?&ymd) $date_syntax/x ) {
          print "$_ is valid ymd\n";
        }
        else {
          print "$_ is not valid ymd\n";
        }
      
      
        if ( $_ =~ m/^(?&leapyear) $date_syntax/x ) {
          print "$_ is leap (start)\n";
        }
        else {
          print "$_ is not leap (start)\n";
        }
      
        print "\n";
      }
      

      注意添加了(?!\d)sn-ps,它们是这样添加的

      “45”不会匹配~= m{(?&amp;twentyeightdays) $syntax},因为'4'匹配0?[4]

      【讨论】:

      • 这让我很期待 Perl6。
      【解决方案5】:

      似乎这更像是一个问题,即如果条件……有很多答案,如何一致地缩进多行。真正重要的是一致性。 如果您使用 perltidy 或其他格式化程序,请与它提供的内容(与您的配置)保持一致。不过,我会将正则表达式的内容从分隔符缩进一级。

      您的帖子显示了通过 Perl::Critic 之类的方式运行现有代码的一个主要缺陷 - you CPAN 示例在原始正则表达式中遗漏了一个 *。如果你做了很多“清理”,你可能会引入错误,所以我希望你有一个好的测试套件。

      【讨论】:

      • 我在哪里遗漏了“*”?我确实有这个脚本的小测试套件,是的。该脚本只是为了在我的系统中搜索已安装的 Perl 模块,因此如果它中断并不太重要 - 但要注意清理现有代码。
      • 哦 - 你说的是缺少“*”的 CPAN 示例。我是直接从search.cpan.org/~elliotjs/Perl-Critic-1.098/lib/Perl/Critic/… 那里得到的——这不是我的代码。但它确实说明了你的观点。
      猜你喜欢
      • 2010-10-06
      • 2023-03-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-14
      • 2023-03-28
      • 2018-05-23
      相关资源
      最近更新 更多