42

正規表現を使用するとコードの保守性が低下すると感じ始めました。正規表現の簡潔さと強力さには、どこか悪いところがあります。Perl は、これにデフォルト オペレータのような副作用を加えます。

私は、基本的な意図を示す少なくとも 1 つの文と、一致するものの少なくとも 1 つの例を含む正規表現を文書化する習慣があります。

正規表現は構築されているため、式の各要素の最大のコンポーネントについてコメントすることは絶対に必要だと思います。それにもかかわらず、私自身の正規表現でさえ、クリンゴン語を読んでいるかのように頭を悩ませています。

意図的に正規表現を控えめにしていますか? おそらく短くて強力なものをより単純なステップに分解しますか? 正規表現のネストをあきらめました。保守性の問題のために避けている正規表現構造はありますか?

この例で質問を曇らせないでください。

マイケル・アッシュによる次の記事に何らかのバグがあった場合、それを完全に破棄する以外に何かをする可能性はありますか?

^(?:(?:(?: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})$

リクエストごとに、上記のアッシュ氏のリンクを使用して正確な目的を見つけることができます。

一致01.1.02 | 2001 年 11 月 30 日 | 2000/2/29

不一致02/29/01 | 2002/01/13 | 11/00/02

4

13 に答える 13

5

これは、消化可能な断片に分解された同じ正規表現です。より読みやすくなることに加えて、一部のサブ正規表現は単独でも役立ちます。また、許可されたセパレーターを変更するのも非常に簡単です。

#!/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;
}
于 2009-04-02T10:29:58.687 に答える
4

うわー、それは醜いです。00 を 2 桁の年として処理する避けられないバグを除けば、動作するはずです (4 分の 1 の確率でうるう年になるはずですが、世紀がなければ、それがどうあるべきかを知る方法がありません)。おそらくサブ正規表現に分解されるべき多くの冗長性があり、私は 3 つの主要なケースに対して 3 つのサブ正規表現を作成します (これが今夜の私の次のプロジェクトです)。\dまた、スラッシュをエスケープする必要がないように区切り文字に別の文字を使用し、単一の文字の代替を文字クラスに変更し(これにより、ピリオドをエスケープする必要がなくなります)、[0-9]前者は任意の数字文字 (を含むU+1815 MONGOLIAN DIGIT FIVE: ᠕) Perl 5.8 と 5.10 では。

警告、テストされていないコード:

#!/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;
于 2009-04-02T05:23:04.167 に答える
3

問題に直面したとき、「分かった、正規表現を使おう」と考える人がいます。現在、彼らには 2 つの問題があります。— comp.lang.emacs の Jamie Zawinski。

正規表現はできる限り単純にしてください ( KISS )。日付の例では、日付タイプごとに 1 つの正規表現を使用する可能性があります。

またはさらに良いことに、それをライブラリ (つまり、日付解析ライブラリ) に置き換えます。

また、入力ソースにいくつかの制限があることを確認するための手順も実行します (つまり、1 種類の日付文字列のみ、理想的にはISO-8601 )。

また、

  • 一度に 1 つのこと (値の抽出は例外となる場合があります)
  • 高度な構文は、正しく使用すれば問題ありません (式を単純化してメンテナンスを減らすなど)。

編集:

「高度な構造はメンテナンスの問題につながります」

私の最初のポイントは、正しく使用すれば、より複雑な表現ではなく、より単純な表現につながるはずだということでした. より単純な式はメンテナンスを軽減するはずです。

上記のテキストを更新して、同じことを言いました。

正規表現は、それ自体が高度な構成要素として認められることはほとんどないことを指摘しておきます。特定の構造に慣れていないからといって、それが高度な構造になるわけではなく、なじみのないものになるだけです。これは、正規表現が強力でコンパクトで、適切に使用すればエレガントであるという事実を変えるものではありません。メスのように、それを振るう人の手に完全に委ねられています。

于 2009-04-02T09:14:39.417 に答える
1

最近、コメントが埋め込まれた正規表現のコメントについて質問を投稿しました。有用な回答があり、特に @mikej からの回答がありました。

正規表現の可読性を改善するためのその他のアイデアについては、ComposedRegex に関する Martin Fowler の投稿を参照してください。要約すると、彼は複雑な正規表現を、意味のある変数名を付けることができる小さな部分に分解することを提唱しています。例えば

于 2009-09-04T13:49:34.390 に答える
1

私はまだそれを扱うことができました。私は単にRegulatorを使用します。できることの 1 つは、正規表現をテスト データと共に保存することです。

もちろん、コメントを追加することもあります。


これが Expresso で作成されたものです。以前は使用したことがありませんでしたが、今ではレギュレーターは仕事をしていません。

// System.Text.RegularExpressions を使用;

///
/// C# 用に構築された正規表現: 2009 年 4 月 2 日木曜日、午前 12:51:56
/// Expresso バージョンの使用: 3.0.3276、http://www.ultrapico.com
///  
/// 正規表現の説明:
///  
/// 3 つの選択肢から選択
/// ^(?:(?:(?:0?[13578]|1[02])(\/|-|\.)31)\1|(?:(?:0?[13-9 ]|1[0-2])(\/|-|\.)(?:29|30)\2))(?:(?:1[6-9]|[2-9]\d) ?\d{2})$
/// 行または文字列の先頭
/// 式に一致しますが、キャプチャしません。[(?:(?:0?[13578]|1[02])(\/|-|\.)31)\1|(?:(?:0?[13-9]|1[0- 2])(\/|-|\.)(?:29|30)\2)]
/// 2 つの選択肢から選択
/// (?:(?:0?[13578]|1[02])(\/|-|\.)31)\1
/// 式に一致しますが、キャプチャしません。[(?:0?[13578]|1[02])(\/|-|\.)31]
/// (?:0?[13578]|1[02])(\/|-|\.)31
/// 式に一致しますが、キャプチャしません。[0?[13578]|1[02]]
/// 2 つの選択肢から選択
/// 0?[13578]
/// 0、ゼロ、または 1 回の繰り返し
/// このクラスの任意の文字: [13578]
/// 1[02]
/// 1
/// このクラスの任意の文字: [02]
/// [1]: 番号付きキャプチャ グループ。[\/|-|\.]
/// 3 つの選択肢から選択
/// リテラル /
/// -
/// リテラル .
/// 31
/// キャプチャ番号への後方参照: 1
/// (?:(?:0?[13-9]|1[0-2])(\/|-|\.)(?:29|30)\2)
/// 戻る
/// 改行
/// 式に一致しますが、キャプチャしません。[(?:0?[13-9]|1[0-2])(\/|-|\.)(?:29|30)\2]
/// (?:0?[13-9]|1[0-2])(\/|-|\.)(?:29|30)\2
/// 式に一致しますが、キャプチャしません。[0?[13-9]|1[0-2]]
/// 2 つの選択肢から選択
/// 0?[13-9]
/// 0、ゼロ、または 1 回の繰り返し
/// このクラスの任意の文字: [13-9]
/// 1[0-2]
/// 1
/// このクラスの任意の文字: [0-2]
/// [2]: 番号付きキャプチャ グループ。[\/|-|\.]
/// 3 つの選択肢から選択
/// リテラル /
/// -
/// リテラル .
/// 式に一致しますが、キャプチャしません。[29|30]
/// 2 つの選択肢から選択
/// 29
/// 29
/// 30
/// 30
/// キャプチャ番号への後方参照: 2
/// 戻る
/// 改行
/// 式に一致しますが、キャプチャしません。[(?:1[6-9]|[2-9]\d)?\d{2}]
/// (?:1[6-9]|[2-9]\d)?\d{2}
/// 式に一致しますが、キャプチャしません。[1[6-9]|[2-9]\d]、0 回または 1 回の繰り返し
/// 2 つの選択肢から選択
/// 1[6-9]
/// 1
/// このクラスの任意の文字: [6-9]
/// [2-9]\d
/// このクラスの任意の文字: [2-9]
/// 任意の数字
/// 任意の数字、正確に 2 回の繰り返し
/// 行または文字列の終わり
/// ^(?:0?2(\/|-|\.)29\3(?:(?:(?:1[6-9]|[2-9]\d)?(?: 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(\/|-|\.)29\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48] |[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00)))
/// 0、ゼロ、または 1 回の繰り返し2
/// [3]: 番号付きキャプチャ グループ。[\/|-|\.]
/// 3 つの選択肢から選択
/// リテラル /
/// -
/// リテラル .
/// 29
/// キャプチャ番号への後方参照: 3
/// 式に一致しますが、キャプチャしません。[(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:( ?:16|[2468][048]|[3579][26])00))]
/// 式に一致しますが、キャプチャしません。[(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16 |[2468][048]|[3579][26])00)]
/// 2 つの選択肢から選択
/// (?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])
/// 式に一致しますが、キャプチャしません。[1[6-9]|[2-9]\d]、0 回または 1 回の繰り返し
/// 2 つの選択肢から選択
/// 1[6-9]
/// 1
/// このクラスの任意の文字: [6-9]
/// [2-9]\d
/// このクラスの任意の文字: [2-9]
/// 任意の数字
/// 式に一致しますが、キャプチャしません。[0[48]|[2468][048]|[13579][26]]
/// 3 つの選択肢から選択
/// 0[48]
/// 0
/// このクラスの任意の文字: [48]
/// [2468][048]
/// このクラスの任意の文字: [2468]
/// このクラスの任意の文字: [048]
/// [13579][26]
/// このクラスの任意の文字: [13579]
/// このクラスの任意の文字: [26]
/// (?:(?:16|[2468][048]|[3579][26])00)
/// 戻る
/// 改行
/// 式に一致しますが、キャプチャしません。[(?:16|[2468][048]|[3579][26])00]
/// (?:16|[2468][048]|[3579][26])00
/// 式に一致しますが、キャプチャしません。[16|[2468][048]|[3579][26]]
/// 3 つの選択肢から選択
/// 16
/// 16
/// [2468][048]
/// このクラスの任意の文字: [2468]
/// このクラスの任意の文字: [048]
/// [3579][26]
/// このクラスの任意の文字: [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})$
/// 行または文字列の先頭
/// 式に一致しますが、キャプチャしません。[(?:0?[1-9])|(?:1[0-2])]
/// 2 つの選択肢から選択
/// 式に一致しますが、キャプチャしません。[0?[1-9]]
/// 0?[1-9]
/// 0、ゼロ、または 1 回の繰り返し
/// このクラスの任意の文字: [1-9]
/// 式に一致しますが、キャプチャしません。[1[0-2]]
/// 1[0-2]
/// 1
/// このクラスの任意の文字: [0-2]
/// 戻る
/// 改行
/// [4]: 番号付きキャプチャ グループ。[\/|-|\.]
/// 3 つの選択肢から選択
/// リテラル /
/// -
/// リテラル .
/// 式に一致しますが、キャプチャしません。[0?[1-9]|1\d|2[0-8]]
/// 3 つの選択肢から選択
/// 0?[1-9]
/// 0、ゼロ、または 1 回の繰り返し
/// このクラスの任意の文字: [1-9]
/// 1\d
/// 1
/// 任意の数字
/// 2[0-8]
/// 2
/// このクラスの任意の文字: [0-8]
/// キャプチャ番号への後方参照: 4
/// 式に一致しますが、キャプチャしません。[(?:1[6-9]|[2-9]\d)?\d{2}]
/// (?:1[6-9]|[2-9]\d)?\d{2}
/// 式に一致しますが、キャプチャしません。[1[6-9]|[2-9]\d]、0 回または 1 回の繰り返し
/// 2 つの選択肢から選択
/// 1[6-9]
/// 1
/// このクラスの任意の文字: [6-9]
/// [2-9]\d
/// このクラスの任意の文字: [2-9]
/// 任意の数字
/// 任意の数字、正確に 2 回の繰り返し
/// 行または文字列の終わり
///  
///
///
public static Regex regex = new Regex(
      "^(?:(?:(?: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.コンパイル済み
    );

于 2009-04-02T04:12:27.550 に答える
1

正規表現を維持するための答えは、コメントや正規表現構造ではあまりないと思います。

あなたが与えた例のデバッグを任された場合、正規表現デバッグ ツール ( Regex Coachなど) の前に座って、処理する必要があるデータの正規表現をステップ実行します。

于 2009-04-02T04:13:49.077 に答える
0

正規表現が判読できるとは思っていないので、そのままにしておき、必要に応じて書き直します。

于 2009-04-02T04:10:41.080 に答える