Как мне написать больше поддерживаемых регулярных выражений? - PullRequest
40 голосов
/ 02 апреля 2009

Я начал чувствовать, что использование регулярных выражений снижает удобство сопровождения кода. В краткости и силе регулярных выражений есть что-то плохое. Perl сочетает это с побочными эффектами, такими как операторы по умолчанию.

У меня есть привычка документировать регулярные выражения, по крайней мере, с одним предложением, дающим основное намерение, и, по крайней мере, один пример того, что будет соответствовать.

Поскольку регулярные выражения созданы, я чувствую, что абсолютно необходимо комментировать самые большие компоненты каждого элемента в выражении. Несмотря на это, даже мои собственные регулярные выражения заставляют меня чесать голову, как будто я читаю клингон.

Вы намеренно тупите свои регулярные выражения? Разлагаете ли вы, возможно, более короткие и более мощные на более простые шаги? Я отказался от вложенных регулярных выражений. Существуют ли конструкции регулярных выражений, которых вы избегаете из-за проблем с mainainability?

Не позволяйте этому примеру затуманить вопрос.

Если бы у следующего из Майкла Эша была какая-то ошибка, у вас были бы какие-то шансы сделать что-нибудь, кроме как полностью выбросить ее?

^(?:(?:(?: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 | 11-30-2001 | 29.02.2000

Несоответствия 02/29/01 | 13/01/2002 | 11/00/02

Ответы [ 13 ]

32 голосов
/ 02 апреля 2009

Используйте Expresso , который дает иерархическое английское разбиение регулярного выражения.

Или

Это чаевые от Даррена Неймке:

.NET допускает регулярное выражение шаблоны, которые будут созданы с помощью встроенного комментарии через RegExOptions.IgnorePatternWhitespace опция компилятора и синтаксис (? # ...) встроен в каждую строку шаблонная строка.

Это позволяет использовать псевдо-код комментарии должны быть встроены в каждую строку и оказывает следующее влияние на читаемость:

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 применим к общей проблеме, другой вариант - документировать его и отправить в RegExLib , где он будет оценен и прокомментирован. Ничто не сравнится со многими парами глаз ...

Еще одним инструментом RegEx является Регулятор

19 голосов
/ 02 апреля 2009

Я обычно просто пытаюсь обернуть все свои вызовы Регулярного выражения в их собственную функцию со значимым именем и некоторыми основными комментариями. Мне нравится думать о Регулярных выражениях как о языке только для записи, доступном для чтения только тому, кто его написал (если только он не очень прост). Я полностью ожидаю, что кому-то, возможно, понадобится полностью переписать выражение, если ему придется изменить его намерение, и, вероятно, это будет лучше для поддержания обучения Регулярному выражению.

17 голосов
/ 02 апреля 2009

Что ж, вся цель модификатора PCRE / x в жизни состоит в том, чтобы вы могли писать регулярные выражения более наглядно, как в этом тривиальном примере:

my $expr = qr/
    [a-z]    # match a lower-case letter
    \d{3,5}  # followed by 3-5 digits
/x;
8 голосов
/ 02 апреля 2009

Некоторые люди используют RE для неправильных вещей (я жду первого SO вопроса о том, как обнаружить действительную программу C ++ с использованием одного RE).

Я обычно нахожу, что, если я не могу уместить свой RE в пределах 60 символов, лучше быть куском кода, так как он почти всегда будет более читабельным.

В любом случае, я всегда документирую в коде то, чего должен достичь RE, очень подробно. Это потому, что по горькому опыту я знаю, как трудно кому-то другому (или даже мне, через полгода) прийти и попытаться понять.

Я не верю, что они злые, хотя я верю, что некоторые люди, которые их используют, злы (не глядя на вас, Майкл Эш :-). Это отличный инструмент, но, как бензопила, вы отрезаете ноги, если не знаете, как правильно их использовать.

ОБНОВЛЕНИЕ: На самом деле, я только что перешел по ссылке на это чудовище, чтобы проверить даты формата m / d / y между 1600 и 9999 годами. Это классический случай, когда полный взорванный код был бы более читабельным и обслуживаемым.

Вы просто разбиваете его на три поля и проверяете отдельные значения. Я бы почти посчитал это преступлением, достойным прекращения, если бы один из моих миньонов купил это мне. Я, конечно, отправил бы их обратно, чтобы написать это правильно.

5 голосов
/ 02 апреля 2009

Вот то же самое регулярное выражение, разбитое на усваиваемые кусочки. В дополнение к тому, чтобы быть более читабельными, некоторые из под-регулярных выражений могут быть полезны сами по себе. Также значительно проще изменить допустимые разделители.

#!/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;
}
4 голосов
/ 02 апреля 2009

Ух ты, это безобразно. Похоже, что это должно работать, по модулю неизбежная ошибка, имеющая дело с 00 как двухзначный год (это должен быть високосный год четверть времени, но без столетия у вас нет возможности узнать, каким он должен быть). Существует много избыточности, которая, вероятно, должна быть разбита на подвыражения, и я бы создал три под-выражения для трех основных случаев (это мой следующий проект сегодня вечером). Я также использовал другой символ для разделителя, чтобы избежать необходимости экранировать косую черту, изменил чередование отдельных символов на классы символов (что, к счастью, позволяет избежать экранирования) и изменил значения \d на [0-9], так как предыдущие совпадения любой символ (включая U+1815 MONGOLIAN DIGIT FIVE: & # x1815;) в 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;
4 голосов
/ 02 апреля 2009

Я нашел хороший метод - просто разбить процесс сопоставления на несколько этапов. Вероятно, он не выполняется так быстро, но у вас есть дополнительный бонус в том, что вы также можете сказать на более высоком уровне зерна, почему совпадение не происходит.

Другой способ - использовать разбор LL или LR. Некоторые языки не могут быть выражены как регулярные выражения, вероятно, даже с расширениями perl, не относящимися к fsm.

4 голосов
/ 02 апреля 2009

Я научился избегать всего, кроме простейшего регулярного выражения. Я предпочитаю другие модели, такие как сканирование строк в Icon или парсинг-комбинаторы Haskell. В обеих этих моделях вы можете написать пользовательский код, который имеет те же привилегии и статус, что и встроенные строковые операции. Если бы я программировал на Perl, я бы, вероятно, установил несколько Perin-комбинаторов в Perl - я сделал это для других языков.

Очень хорошая альтернатива - использовать грамматику синтаксического разбора, как это сделал Роберто Иерусалимский со своим пакетом LPEG , но, в отличие от комбинаторов синтаксического анализа, это то, что вы не можете разыграть днем. Но если кто-то уже сделал PEG для вашей платформы, это очень хорошая альтернатива регулярным выражениям.

3 голосов
/ 02 апреля 2009

Некоторые люди, когда сталкиваются с проблема, подумай "Я знаю, я буду использовать регулярные выражения. "Теперь у них есть две проблемы. - Джейми Завински в comp.lang.emacs.

Сохраняйте регулярные выражения настолько простыми, насколько это возможно ( KISS ). В вашем примере с датой я бы, вероятно, использовал одно регулярное выражение для каждого типа даты.

Или, что еще лучше, замените его библиотекой (то есть библиотекой анализа дат).

Я бы также предпринял шаги для того, чтобы у входного источника были некоторые ограничения (т.е. только один тип строк даты, в идеале ISO-8601 ).

Кроме того,

  • Одна вещь за раз (с возможным исключением извлечения значений)
  • Расширенные конструкции хороши, если используются правильно (как в упрощении выражения и, следовательно, в сокращении обслуживания)

EDIT:

"передовые конструкции приводят к вопросы технического обслуживания "

Моя первоначальная идея заключалась в том, что при правильном использовании 1030 * это должно привести к более простым выражениям, а не к более сложным. Упрощенные выражения должны уменьшить обслуживания.

Я обновил текст выше, чтобы сказать как можно больше.

Я бы отметил, что регулярные выражения вряд ли можно квалифицировать как сложные конструкции сами по себе. Незнание определенной конструкции не делает ее продвинутой, просто незнакомой. Что не меняет того факта, что регулярные выражения являются мощными, компактными и - при правильном использовании - элегантными. Как и скальпель, он целиком находится в руках того, кто им владеет.

1 голос
/ 04 сентября 2009

Недавно я опубликовал вопрос о комментировании регулярных выражений со встроенными комментариями Были полезные ответы, в частности один из @ mikej

Смотрите пост Мартина Фаулера на ComposedRegex для еще нескольких идей улучшение читаемости регулярных выражений. В Резюме, он выступает за разрушение сложное регулярное выражение на более мелкие части которой можно дать значимую переменную имена. например,

...