Saturday, January 21, 2012

perlfaq6 — регулярные выражения http://beshenov.ru/perlfaq/6.html

http://beshenov.ru/perlfaq/6.html

perlfaq6 — регулярные выражения

Этот раздел оказался неожиданно маленьким, потому что остальной FAQ забит ответами, использующими регулярные выражения. Например, декодирование URL или проверка, является ли скаляр числом, проводится с регулярными выражениями, но это описано в других разделах.
Как использовать регулярные выражения, не создавая нечитаемый и неподдерживаемый код?
Шаблон не работает больше чем с одной строкой текста. Что не так?
Как вырезать строки между двумя шаблонами, которые сами стоят на разных строках?
Как регулярными выражениями осуществлять поиск по XML, HTML и прочим ужасным вещам?
Я занес в $/ регулярное выражение, но это не работает. Что не так?
Как сделать нечувствительную к регистру подстановку, чтобы результат сохранял искомый регистр?
Как заставить \w соответствовать национальной кодировке?
Как использовать в шаблоне соответствующую локальным установкам версию /[a-zA-Z]/?
Как экранировать переменную для использования в регулярном выражении?
Зачем нужно /o?
Как при помощи регулярных выражений удалить из кода комментарии в стиле C?
Как использовать регулярные выражения Perl для поиска по сбалансированному тексту?
Что подразумевается под «жадными регулярными выражениями»? Как их сделать нежадными?
Как обработать каждое слово в каждой строке?
Как сделать сводку по частоте вхождения слов или строк?
Как сделать нечеткий поиск по шаблону?
Как сделать эффективный поиск сразу по нескольким шаблонам?
Почему не работает поиск с разделителем слов \b?
Почему использование $&, $` и $' замедляет программу?
Зачем в регулярном выражении \G?
Регулярные выражения в Perl — это ДКА или НКА? Они POSIX-совместимы?
Почему не следует использовать grep в пустом контексте?
Как сделать поиск по строке с многобайтовыми символами?
Как осуществить поиск по шаблону, записанному в переменной?

Как использовать регулярные выражения, не создавая нечитаемый и неподдерживаемый код?

Для создания поддерживаемых и понятных регулярных выражений можно использовать три приема.

Комментарии вне регулярного выражения

Описывайте обычными комментариями Perl, что и как делаете.
# Заменить строку на первое слово, двоеточие и # количество символов до конца s/^(\w+)(.*)/ lc($1) . ":" . length($2) /meg;

Комментарии внутри регулярного выражения

Модификатор /x включает пропуск пробельных символов в шаблоне (кроме пробелов в классе символов), а также позволяет использовать обычные комментарии. Это сильно помогает.
/x позволяет превратить шаблон
s{<(?:[^>'"]*|".*?"|'.*?')+>}{}gs;
в следующий:
s{ <                 # открывающая угловая скобка     (?:                 # группирующие скобки, не порождающие обратную ссылку         [^>'"] *            # любой символ, кроме > ' ", 0 и более раз             |                   #    ИЛИ         ".*?"               # подстрока между двойными кавычками (нежадный поиск)             |                   #    ИЛИ         '.*?'               # подстрока между одинарными кавычками (нежадный поиск)     ) +                 #    всё встречается один и более раз     >                   # закрывающая угловая скобка }{}gsx;              # заменить пустой строкой, то есть, удалить
Это не столь же понятно, как обычный текст, но очень полезно при описании значения каждой части шаблона.

Различные разделители

Хотя обычно шаблоны заключаются в символы /, можно использовать практически любой разделитель — смотрите perlre. Например, выше использовались фигурные скобки. Иногда вместо того, чтобы экранировать символы в шаблоне, лучше использовать другой разделитель:
s/\/usr\/local/\/usr\/share/g;    # разделитель выбран неудачно s#/usr/local#/usr/share#g;        # это лучше

Шаблон не работает больше чем с одной строкой текста. Что не так?

Либо в рассматриваемой переменной только одна строка, либо вы используете неправильные модификаторы шаблона.
Записать в переменную несколько строк можно несколькими способами. Если хотите сделать это автоматически при считывания ввода, то следует установить $/ таким образом, чтобы позволить считывать за раз больше одной строки (например, задать '' для считывания абзацами и undef — для считывания файла целиком).
Посмотрите perlre, чтобы решить, что следует использовать: /s/m, либо оба модификатора. /s допускает совпадение . с переводом строки, а /m допускает соответствие ^ и $ границам строк, а не только началу и концу переменной. При этом вы должны убедиться, что в переменной действительно содержится несколько строк.
Например, эта программа определяет повторяющиеся слова, даже если повторы распространяются за переводы строк (но не параграфов). Здесь не требуется /s, потому что мы не используем в регулярном выражении символ ., который мог бы соответствовать переводу строки. Не требуется и /m, потому что не нужно, чтобы ^ и $ соответствовали границам внутренних строк. Но $/ обязательно нужно задать значение, отличное от значения по умолчанию, иначе мы не будем считывать многострочные записи.
$/ = '';          # считывать абзацами, а не строками while ( <> ) {     while ( /\b([\w'-]+)(\s+\1)+\b/gi ) {      # слово начинается с буквы         print "Повтор $1 в абзаце $.\n";     } }
Вот код для поиска строк, начинающихся с «From » (что будет искажено большинством почтовых программ):
$/ = '';          # считывать абзацами, а не строками while ( <> ) {     while ( /^From /gm ) { # /m заставляет ^ соответствовать началу внутренней строки в $_     print "в абзаце $ есть строка с началом 'From'.\n";     } }
Вот код для поиска в абзаце текста между START и END:
undef $/;          # считать файл целиком, а не строками или абзацами while ( <> ) {     while ( /START(.*?)END/sgm ) { # /s заставляет . переходить через границы строк         print "$1\n";     } }

Как вырезать строки между двумя шаблонами, которые сами стоят на разных строках?

Можно использовать странный оператор .., задокументированный в perlop:
perl -ne 'print if /START/ .. /END/' file1 file2 ...
Если нужен текст, а не строки, то используйте
perl -0777 -ne 'print "$1\n" while /START(.*?)END/gs' file1 file2 ...
Но если нужно обработать вложенные блоки START-END, то вы столкнетесь с задачей поиска по сбалансированному тексту, описанной в другом вопросе FAQ.
Вот другой пример использования ..:
while (<>) {     $in_header =   1  .. /^$/;     $in_body   = /^$/ .. eof; # теперь выбираем из них } continue {     $. = 0 if eof;    # поправить $. }

Как регулярными выражениями осуществлять поиск по XML, HTML и прочим ужасным вещам?

(Отвечает Брайан Д. Фой)
Если нужно просто решить задачу, то используйте тот или иной модуль и забудьте про регулярные выражения. Можно начать с XML::Parser и HTML::Parser, хотя каждое пространство имен содержит другие модули разбора, заточенные под особые задачи и методы решения. Откройте поиск по CPAN, и вы будете удивлены тем, что всю работу за вас уже сделали :-)
Языки вроде XML сложно разбирать, потому что они содержат сбалансированный текст с несколькими уровнями вложенности, но некоторые элементы сбалансированным текстом не являются — например, пустой тег (вроде <br/>). И даже если вы думаете, что ваш шаблон подходит для входного текста, могут встречаться непредвиденные ситуации.
Если хотите пойти по сложному пути, продираться к правильному решению, постоянно натыкаться на неудачи, быть заваленным сообщениями об ошибках, мучаться огромное количество времени, которое придется тратить на изобретение кривого колеса, то перед тем, как окончательно сдаваться, нужно
решить задачу сбалансированного текста из другого вопроса perlfaq6;
попробовать рекурсивные регулярные выражения, появившиеся в Perl 5.10 (см. perlre);
попробовать определить грамматику при помощи (?DEFINE) из Perl 5.10;
разбить задачу на составные и не пытаться написать единое регулярное выражение;
уговорить всех не использовать первым делом XML или HTML.
Удачи!

Я занес в $/ регулярное выражение, но это не работает. Что не так?

В $/ должна быть строка. Если действительно нужно, то можете использовать следующие примеры.
Если есть File::Stream, то всё просто.
use File::Stream;  my $stream = File::Stream->new(     $filehandle,     separator => qr/\s*,\s*/,     );  print "$_\n" while <$stream>;
Если File::Stream нет, то нужно сделать немного больше.
Можно использовать вариант вызова sysread с четырьмя аргументами, чтобы постоянно дописывать в буфер. После добавления в буфер проверяйте при помощи регулярного выражения, имеется ли полная строка.
local $_ = ""; while( sysread FH, $_, 8192, length ) {     while( s/^((?s).*?)your_pattern/ ) {         my $record = $1;         # ...     } }
То же самое можно сделать с foreach, поиском по шаблону с флагом c и якорем \G, если вы не против, чтобы под конец в память был занесен весь файл.
local $_ = ""; while( sysread FH, $_, 8192, length ) {     foreach my $record ( m/\G((?s).*?)your_pattern/gc ) {         # ...     } substr( $_, 0, pos ) = "" if pos; }

Как сделать нечувствительную к регистру подстановку, чтобы результат сохранял искомый регистр?

Вот красивое решение в духе Perl от Ларри Рослера. Оно использует свойства побитового исключающего ИЛИ для ASCII-строк.
$_= "this is a TEsT case";  $old = 'test'; $new = 'success';  s{(\Q$old\E)} { uc $new | (uc $1 ^ $1) .     (uc(substr $1, -1) ^ substr $1, -1) x     (length($new) - length $1) }egi;  print;
А вот процедура, созданная на основе кода выше:
sub preserve_case($$) {     my ($old, $new) = @_;     my $mask = uc $old ^ $old;      uc $new | $mask .         substr($mask, -1) x (length($new) - length($old)) }  $string = "this is a TEsT case"; $string =~ s/(test)/preserve_case($1, "success")/egi; print "$string\n";
Это выводит
this is a SUcCESS case
Если нужно сохранить регистр слова замены, когда оно длиннее исходного, можно использовать код Джеффа Пиньяна:
sub preserve_case {     my ($from, $to) = @_;     my ($lf, $lt) = map length, @_;      if ($lt < $lf) { $from = substr $from, 0, $lt }     else { $from .= substr $to, $lf }      return uc $to | ($from ^ uc $from);     }
Это заменяет строку на «this is a SUcCess case».
Если вам больше нравятся решения в духе C, то следующий скрипт показывает, как программирующие на C могут создавать код C на любом языке, и делает абсолютно ту же подстановку. (А еще это работает примерно на 240% медленнее, чем код в духе Perl.) Если в подставляемом слове больше символов, чем в заменяемом, то для оставшейся части используется регистр последнего символа.
# Код Натана Торкингтона, переделанный Джефри Фридлом # sub preserve_case($$) {     my ($old, $new) = @_;     my ($state) = 0;    # 0 = без изменений; 1 = lc; 2 = uc     my ($i, $oldlen, $newlen, $c) = (0, length($old), length($new));     my ($len) = $oldlen < $newlen ? $oldlen : $newlen;      for ($i = 0; $i < $len; $i++) {         if ($c = substr($old, $i, 1), $c =~ /[\W\d_]/) {             $state = 0;         } elsif (lc $c eq $c) {             substr($new, $i, 1) = lc(substr($new, $i, 1));             $state = 1;         } else {             substr($new, $i, 1) = uc(substr($new, $i, 1));             $state = 2;         }     }     # завершить оставшейся частью $new (если $new длиннее $old)     if ($newlen > $oldlen) {         if ($state == 1) {             substr($new, $oldlen) = lc(substr($new, $oldlen));         } elsif ($state == 2) {             substr($new, $oldlen) = uc(substr($new, $oldlen));         }     }     return $new; }

Как заставить \w соответствовать национальной кодировке?

Добавьте в скрипт use locale;, чтобы класс символов \w брался из текущих локальных установок.
Подробности см. в perllocale.

Как использовать в шаблоне соответствующую локальным установкам версию /[a-zA-Z]/?

Можете использовать синтаксис POSIX для классов символов /[[:alpha:]]/, задокументированный в perlre.
Вне зависимости от локальных установок, символы алфавита содержатся в \w за вычетом цифр и подчеркивания. Это соответствует шаблону /[^\W\d_]/. Его дополнение, неалфавитные символы — всё в \W, а также цифры и подчеркивание, то есть /[\W\d_]/.

Как экранировать переменную для использования в регулярном выражении?

Парсер Perl раскрывает $variable и @variable в регулярном выражении, если только в качестве разделителя не использована одиночная кавычка. Также помните, что в подстановке s/// шаблон замены считается строкой в двойных кавычках (подробности см. в perlop). Кроме того, все специальные символы для регулярных выражений сохраняют силу, если не используется экранирование \Q. Например:
$string = "Placido P. Octopus"; $regex  = "P.";  $string =~ s/$regex/Polyp/; # теперь в $string записано "Polypacido P. Octopus"
Так как . — специальный символ в регулярных выражениях, соответствующий любому символу, то шаблон P.совпал с "Pl" в исходной строке.
Для экранирования . используется \Q:
$string = "Placido P. Octopus"; $regex  = "P.";  $string =~ s/\Q$regex/Polyp/; # теперь в $string записано "Placido Polyp Octopus"
\Q приводит к тому, что "." считается простым символом, и P. соответствует "P" с точкой.

Зачем нужно /o?

(Отвечает Брайан Д. Фой)
Опция /o для регулярных выражений (задокументирована в perlop и perlreref) указывает, чтобы регулярное выражение компилировалось единожды. Это полезно, только если шаблон включает переменную. Начиная с Perl 5.6, такое происходит автоматически, если шаблон не меняется.
Так как оператор поиска m//, подстановки s///, а также оператор экранирования регулярного выраженияqr// — контексты двойных кавычек, то переменные можно интерполировать в шаблон. Подробности см. в ответе на вопрос «Как экранировать переменную для использования в регулярном выражении?»
Следующий пример принимает регулярное выражение из списка аргументов и печатает строки ввода, соответствующие ему:
my $pattern = shift @ARGV;  while( <> ) {     print if m/$pattern/;     }
Версии Perl до 5.6 могли перекомпилировать регулярное выражение на каждой итерации, даже если $patternне менялся. /o должен предотвратить это: шаблон будет компилироваться только первый раз, а потом использоваться в последующем.
my $pattern = shift @ARGV;  while( <> ) {     print if m/$pattern/o;    # полезно для Perl < 5.6     }
Начиная с версии 5.6, perl не будет перекомпилировать регулярное выражение, если переменная не меняется, поэтому /o скорее не пригодится: не навредит, но и не поможет. Используйте /o в любой версии Perl, если хотите, чтобы шаблон не перекомпилировался, даже если переменная меняется (то есть, чтобы использовалось первоначальное значение).
Чтобы сделать для себя проверку, проходит ли перекомпиляция регулярного выражения, можете посмотреть на движок регулярных выражений Perl в действии. Прагма use re 'debug' (введена в Perl 5.005) показывает подробности. В Perl до версии 5.6 вы должны видеть сообщения о перекомпиляции от re на каждой итерации. Начиная с Perl 5.6, вы должны видеть сообщение от re только для первой итерации.
use re 'debug';  $regex = 'Perl'; foreach ( qw(Perl Java Ruby Python) ) {     print STDERR "-" x 73, "\n";     print STDERR "Trying $_...\n";     print STDERR "\t$_ is good!\n" if m/$regex/;     }

Как при помощи регулярных выражений удалить из кода комментарии в стиле C?

Хотя это возможно, это намного сложней, чем кажется. Так, однострочник
perl -0777 -pe 's{/\*.*?\*/}{}gs' foo.c
будет работать во многих, но не во всех случаях. Это слишком просто для отдельных программ на C, в частности, тех, которые содержат «комментарии» внутри строк. В этом случае нужно нечто вроде скрипта, созданного Джефри Фридлом и доработанного Фредом Кёртисом.
$/ = undef; $_ = <>; s#/\*[^*]*\*+([^/*][^*]*\*+)*/|("(\\.|[^"\\])*"|'(\\.|[^'\\])*'|.[^/"'\\]*)#defined $2 ? $2 : ""#gse; print;
Конечно, это можно записать более понятно с модификатором /x, если отформатировать шаблон и расставить комментарии. Вот развернутая запись, любезно предоставленная Фредом Кёртисом.
s{    /\*         ##  Начало комментария /* ... */    [^*]*\*+    ##  Отличный от * символ с последующим 1 и более *    (      [^/*][^*]*\*+    )*          ##  0 и более подстрок, не начинающихся с /                ##    но заканчивающихся на '*'    /           ##  Конец комментария /* ... */   |         ##     ИЛИ  разные вещи, не являющиеся комментариями:     (      "           ##  Начало строки " ... "      (        \\.           ##  Экранированный символ      |               ##    ИЛИ        [^"\\]        ##  Не "\      )*      "           ##  Конец строки " ... "     |         ##     ИЛИ       '           ##  Начало строки ' ... '      (        \\.           ##  Экранированный символ      |               ##    ИЛИ        [^'\\]        ##  Не '\      )*      '           ##  Конец строки ' ... '     |         ##     ИЛИ       .           ##  Любой другой символ      [^/"'\\]*   ##  Символы, с которых не начинается комментарий, строка, экранированный символ    )  }{defined $2 ? $2 : ""}gxse;
Небольшая доработка также удаляет комментарии C++, не продолжающиеся на нескольких строках при помощи символа продолжения строки:
s#/\*[^*]*\*+([^/*][^*]*\*+)*/|//[^\n]*|("(\\.|[^"\\])*"|'(\\.|[^'\\])*'|.[^/"'\\]*)#defined $2 ? $2 : ""#gse;

Как использовать регулярные выражения Perl для поиска по сбалансированному тексту?

(Отвечает Брайан Д. Фой)
Сначала следует попробовать модуль Text::Balanced, который входит в стандартную библиотеку Perl, начиная с версии 5.8. В Text::Balanced есть много функций по работе с хитрым текстом. МодульRegexp::Common также может помочь готовыми шаблонами.
Начиная с Perl 5.10, можно осуществлять поиск по сбалансированному тексту при помощи рекурсивных шаблонов. До 5.10 приходилось прибегать к разным уловкам вроде включения кода Perl в последовательности (??{}).
Вот пример использования рекурсивного регулярного выражения. Задача — захватить весь текст в угловых скобках, в том числе текст во вложенных угловых скобках. Текст примера содержит две основные группы: с одним уровнем вложенности и с двумя уровнями вложенности. Всего существует пять групп в угловых скобках:
I have some <brackets in <nested brackets> > and <another group <nested once <nested twice> > > and that's it.
Регулярное выражения для поиска по сбалансированному тексту использует новые возможности Perl 5.10, описанные в perlre. Этот пример основан на том, который дается в документации.
Во-первых, добавление + к любому квалификатору ищет самое длинное совпадение без обратного поиска. Это важно, потому что требуется обработать все угловые скобки по рекурсии, а не перебором с возвратами. Группа [^<>]++ ищет один и более символ, отличный от угловой скобки, не привлекая обратный поиск.
Во-вторых, новая конструкция (?PARNO) соответствует подшаблону буфера захвата, определенного PARNO. В следующем регулярном выражении первый буфер захвата ищет (и сохраняет) сбалансированный текст и требуется, чтобы тот же шаблон был применен к содержимому первого буфера для обработки вложенного текста. В этом состоит рекурсия. (?1) использует шаблон во внешнем буфере захвата в качестве независимой части регулярного выражения.
В результате имеем
#!/usr/local/bin/perl5.10.0  my $string =<<"HERE"; I have some <brackets in <nested brackets> > and <another group <nested once <nested twice> > > and that's it. HERE  my @groups = $string =~ m/         (                   # начало буфера захвата 1         <                   # открывающая угловая скобка             (?:                 [^<>]++     # один и более символов, отличных от угловой скобки; без обратного поиска                   |                 (?1)        # встретили < или >, рекурсивно обращаемся к буферу захвата 1             )*         >                   # закрывающая угловая скобка         )                   # конец буфера захвата 1         /xg;  $" = "\n\t"; print "Found:\n\t@groups\n";
Вывод показывает, что Perl нашел две большие группы:
Found:     <brackets in <nested brackets> >     <another group <nested once <nested twice> > >
С небольшой доработкой, можно найти все группы в угловых скобках, даже если они заключены в другие угловые скобки. Каждый раз при нахождении сбалансированного текста удаляйте его внешний разделитель (вы уже нашли его, поэтому не должны встретить повторно) и добавляйте в очередь строк для обработки. Делайте это, пока не получите 0 совпадений:
#!/usr/local/bin/perl5.10.0  my @queue =<<"HERE"; I have some <brackets in <nested brackets> > and <another group <nested once <nested twice> > > and that's it. HERE  my $regex = qr/         (                   # начало скобки 1         <                   # открывающая угловая скобка             (?:                 [^<>]++     # один и более символов, отличных от угловой скобки; без обратного поиска                   |                 (?1)        # рекурсивно обращаемся к скобке 1             )*         >                   # закрывающая угловая скобка         )                   # конец скобки 1         /x;  $" = "\n\t";  while( @queue )     {     my $string = shift @queue;      my @groups = $string =~ m/$regex/g;     print "Found:\n\t@groups\n\n" if @groups;      unshift @queue, map { s/^<//; s/>$//; $_ } @groups;     }
Вывод отображает все группы. Сначала выводятся внешние вхождения, а потом уже — вложенные:
Found:     <brackets in <nested brackets> >     <another group <nested once <nested twice> > >  Found:     <nested brackets>  Found:     <nested once <nested twice> >  Found:     <nested twice>

Что подразумевается под «жадными регулярными выражениями»? Как их сделать нежадными?

Обычно имеется в виду, что выражение соответствует как можно большей строке. Строго говоря, имеются квантификаторы (?*+{}), которые являются более жадными, чем весь шаблон; в Perl предпочтительна локальная жадность и выбор ближайшего соответствия, а не жадность всего шаблона. Чтобы получить нежадные версии тех же квантификаторов, используйте (??*?+?{}?).
Пример:
$s1 = $s2 = "I am very very cold"; $s1 =~ s/ve.*y //;      # I am cold $s2 =~ s/ve.*?y //;     # I am very cold
Заметьте, что во второй подстановке поиск закончился сразу при встрече "y ". Квантификатор *? указывает движку регулярных выражений найти соответствие как можно быстрее и не проверять, что стоит дальше в строке. Это похоже на детскую игру «горячая картошка».

Как обработать каждое слово в каждой строке?

Используйте функцию split:
while (<>) {     foreach $word ( split ) {         # сделать что-то с $word     } }
Заметьте, что это не слова в обычном понимании, а просто последовательности непробельных символов. Для обработки только буквенно-цифровых последовательностей (с символами подчеркивания) используйте
while (<>) {     foreach $word (m/(\w+)/g) {         # сделать что-то с $word     } }

Как сделать сводку по частоте вхождения слов или строк?

Чтобы это сделать, нужно разобрать каждое слово во входном потоке. Пусть под словом понимается последовательность букв, дефисов и апострофов, а не просто непробельные символы, как в предыдущем вопросе:
while (<>) {     while ( /(\b[^\W_\d][\w'-]+\b)/g ) {   # упускает варианты вроде "`sheep'"         $seen{$1}++;     } }  while ( ($word, $count) = each %seen ) {     print "$count $word\n";     }
Если нужно сделать то же со строками, то регулярные выражения не нужны:
while (<>) {     $seen{$_}++;     }  while ( ($line, $count) = each %seen ) {     print "$count $line"; }
Если хотите сделать вывод в отсортированном порядке, то смотрите в perlfaq4 вопрос о сортировке хеша.

Как сделать нечеткий поиск по шаблону?

См. модуль String::Approx из CPAN.

Как сделать эффективный поиск сразу по нескольким шаблонам?

(Отвечает Брайан Д. Фой)
Избегайте компиляции регулярного выражения при каждом обращении к нему. В примере ниже perl должен перекомпилировать регулярное выражение при каждой итерации foreach, так как не может знать, что будет в$pattern.
@patterns = qw( foo bar baz );  LINE: while( <DATA> )     {     foreach $pattern ( @patterns )         {         if( /\b$pattern\b/i )             {             print;             next LINE;             }         }     }
В Perl 5.005 введен оператор qr//. Он компилирует, но не применяет регулярное выражение. Если вы используете предварительно скомпилированное регулярное выражение, то perl делает меньше затрат. Здесь добавлен map, чтобы каждый шаблон переходил в заранее скомпилированный вид. Остальная часть скрипта та же, но работать будет быстрее.
@patterns = map { qr/\b$_\b/i } qw( foo bar baz );  LINE: while( <> )     {     foreach $pattern ( @patterns )         {         if( /$pattern/ )             {             print;             next LINE;             }         }     }
В некоторых случаях можно свести несколько шаблонов в одно регулярное выражение. Однако избегайте ситуаций, требующих обратного поиска.
$regex = join '|', qw( foo bar baz );  LINE: while( <> )     {     print if /\b(?:$regex)\b/i;     }
Подробности насчет эффективности регулярных выражений смотрите в «Mastering Regular Expressions» Джефри Фридла. Там описано устройство движка регулярных выражений, и почему некоторые шаблоны удивительно неэффективны. Если вы будете понимать, как регулярные выражения работают в perl, вы сможете адаптировать их для конкретных случаев.

Почему не работает поиск с разделителем слов \b?

(Отвечает Брайан Д. Фой)
Убедитесь, что вы знаете, что на самом деле делает \b: это разделитель между символом слова \w и чем-то, что не является символом слова — это может быть \W, а также начало или конец строки.
Это не разделитель между пробельным и непробельным символом и не то, что разделяет слова в привычном понимании.
В терминах регулярных выражений, разделитель слов \b — «zero width assertion», он представляет не символ строки, а выполнение определенного условия в данной позиции.
В регулярном выражении /\bPerl\b/ требуется, чтобы разделитель слов был до "P" и после "l". Если до "P" и после "l" стоит что-то помимо \w, шаблон совпадет. Следующие строки соответствуют /\bPerl\b/:
"Perl"    # перед P или после l не стоит символ слова "Perl "   # аналогично, пробел — не символ слова "'Perl'"  # ' — не символ слова "Perl's"  # перед P нет символа слова, после l стоит не символ слова
Следующие строки не соответствуют /\bPerl\b/.
"Perl_"   # _ — символ слова! "Perler"  # перед P символа слова нет, но есть после l
Однако для поиска слов не нужно использовать \b. Можно рассмотреть не-символы слова, окруженные символами слова. Такие строки соответствуют шаблону /\b'\b/.
"don't"   # символ ' заключен между "n" и "t" "qep'a'"  # символ ' заключен между "p" и "a"
Следующие строки не соответствуют /\b'\b/.
"foo'"    # после ' нет символа слова
Также можно использовать \B — дополнение \b, чтобы указать, что разделитель слов не должен встречаться.
В шаблоне /\Bam\B/, до "a" и после "m" должен быть символ слова. Следующие строки соответствуют/\Bam\B/:
"llama"   # "am" окружено символами слова "Samuel"  # аналогично
Следующие строки не соответствуют /\Bam\B/
"Sam"      # до "a" нет ограничителя слова, но есть после "m" "I am Sam" # "am" окружено не-символами слова

Почему использование $&, $` и $' замедляет программу?

(Отвечает Анно Зигель)
Если Perl видит, что где-либо в программе вам требуется одна из этих переменных, то он обеспечивает их для каждого поиска по шаблону. То есть, всякий раз копируется вся строка, частично — в $`, частично — в $&, частично — в $'. Это влечет серьезные неприятности, если вы имеете дело с большими строками или часто обращаетесь к шаблонам. Избегайте использования $&$' и $`, если можете; если не можете, то, использовав единожды, не стесняйтесь использовать где угодно, потому что уже заплатили за это. В некоторых алгоритмах эти переменные действительно полезны. Начиная с версии 5.005, $& уже не такая затратная, как две остальные.
Начиная с Perl 5.6.1, специальные переменные @- и @+ могут заменить $`$& и $'. Эти массивы содержат указатели на начало и конец каждого совпадения (подробности см. в perlvar) — дают ту же информацию, но без риска избыточного копирования строк.
В Perl 5.10 добавились ${^MATCH}${^PREMATCH} и ${^POSTMATCH} для тех же целей, но без общего ущерба для производительности. Perl задает эти переменные, только если регулярное выражение компилируется или выполняется с модификатором /p.

Зачем в регулярном выражении \G?

Якорь \G используется для начала следующего поиска по той же строке в том месте, где произошла остановка. С этим якорем движок регулярных выражений не может пропустить какие-либо символы для поиска следующего совпадения, в этом \G похож на якорь начала строки ^\G обычно используется с флагомg. Он использует значение pos() в качестве позиции для начала следующего поиска. Как только оператор поиска по шаблону находит совпадение, он заносит в pos() позицию следующего за совпадением символа (или первого символа следующего совпадения, это как посмотреть). Каждая строка имеет свое значениеpos().
Пусть нужно найти последовательно все пары цифр в строке вида "1122a44" и прекратить поиск, как только встретится не-цифра. Требуется извлечь 11 и 22; между 22 и 44 встречается буква a и на ней нужно остановиться. Простой поиск пар цифр пропускает a и находит 44.
$_ = "1122a44"; my @pairs = m/(\d\d)/g;   # qw( 11 22 44 )
Если вы используете якорь \G, то требуете, чтобы совпадение после 22 начиналось с a. Регулярное выражение не может дать такое совпадение, потому что в нем нет цифры; поиск прекращается, и оператор возвращает уже найденные пары.
$_ = "1122a44"; my @pairs = m/\G(\d\d)/g; # qw( 11 22 )
Можете также использовать якорь \G в скалярном контексте. Также потребуется флаг g.
$_ = "1122a44"; while( m/\G(\d\d)/g )     {     print "Found $1\n";     }
После того, как поиск обрывается на букве a, perl сбрасывает pos(), и следующий поиск по той же строке начнется сначала.
$_ = "1122a44"; while( m/\G(\d\d)/g )     {     print "Found $1\n";     }  print "Found $1 after while" if m/(\d\d)/g; # находит "11"
Сброс pos() по окончанию поиска можно отключить флагом c, задокументированном в perlop и perlreref. Следующий поиск начинается там, где закончилось предыдущее совпадение (в позиции pos()), даже если поиск по той же строке завершился. В этом случае поиск после цикла while() начнется с a (там, где завершился последний), и, так как в нем не используется \G, то a будет пропущено и мы получим 44.
$_ = "1122a44"; while( m/\G(\d\d)/gc )     {     print "Found $1\n";     }  print "Found $1 after while" if m/(\d\d)/g; # находит "44"
Обычно якорь \G используется вместе с флагом c, когда нужно попробовать другой шаблон, если какой-то не подошел, как это делается в разборе по лексемам. Следующий пример Джефри Фридла работает, начиная с Perl 5.004.
while (<>) {     chomp;     PARSER: {         m/ \G( \d+\b    )/gcx   && do { print "number: $1\n";  redo; };         m/ \G( \w+      )/gcx   && do { print "word:   $1\n";  redo; };         m/ \G( \s+      )/gcx   && do { print "space:  $1\n";  redo; };         m/ \G( [^\w\d]+ )/gcx   && do { print "other:  $1\n";  redo; };     } }
Для каждой строки ввода цикл PARSER сначала пытается найти последовательность цифр, завершающуюся ограничителем слова. Поиск должен начинаться там, где закончился предыдущий (первый — с начала строки). Так как в m/ \G( \d+\b )/gcx использован флаг c, то, если строка не соответствует регулярному выражению, perl не сбрасывает pos(), и следующий поиск начинается в той же позиции, чтобы попробовать другую лексему.

Регулярные выражения в Perl — это ДКА или НКА? Они POSIX-совместимы?

Хотя регулярные выражения в Perl похожи на детерминированные конечные автоматы из программыegrep(1), на самом деле они реализованы как недетерминированные конечные автоматы, чтобы имелась возможность использовать поиск с возвратом (backtracking) и обратные ссылки (backreferencing). Кроме того, они не POSIX-совместимы, потому что POSIX-совместимые выражения гарантируют наихудшее поведение во всех случаях. (Некоторые предпочитают последовательность в работе, даже если она сводится к медленной скорости.) За всеми мыслимыми подробностями по теме обратитесь к книге Джефри Фридла «Mastering Regular Expressions» (выходила в издательстве O'Reilly, полная ссылка есть в perlfaq2).

Почему не следует использовать grep в пустом контексте?

grep создает список для возврата независимо от контекста; вы заставляете Perl создавать список, а потом просто отбрасывать. Если список большой, то вы расходуете время и память. Если хотите пройти по списку, то используйте цикл for.
До perl 5.8.1 такая же проблема была у map, но позже это было исправлено, и map контекстно-зависима; в пустом контексте никаких списков не создается.

Как сделать поиск по строке с многобайтовыми символами?

Начиная с Perl 5.6, имеется какая-то поддержка многобайтовых символов. Рекомендуется версия 5.8 и более поздние. В модуле Encode поддерживается Юникод и традиционные кодировки. См. perluniintroperlunicode иEncode.
Если вы продолжаете работать со старыми версиями Perl, то Юникод поддерживается через модульUnicode::String и преобразование символов в Unicode::Map8 и Unicode::Map. Если вы используете японские кодировки, попробуйте jperl 5.005_03.
Наконец, следующие приемы предложены Джефри Фридлом, чья статья в 5 номере «Perl Journal» подробно рассматривает вопрос.
Пусть имеется странная марсианская кодировка, где пары заглавных букв ASCII обозначают одну марсианскую (так, одну марсианскую букву составляют пары байт "CV""SG""VS""XX", и так далее). Остальные байты представляют однобайтовые символы, как в обычном ASCII.
Поэтому строка на марсианском языке "I am CVSGXX!" использует 12 байт для кодирования девяти символов'I'' ''a''m'' ''CV''SG''XX''!'.
Теперь пусть нужно найти один символ /GX/. Perl не знает марсианского, поэтому найдет два байта "GX" в строке "I am CVSGXX!", даже если такого символа там нет: так кажется из-за стыка "SG" и "XX", но там нет"GX". Это серьезная проблема.
Вот несколько способов обойти это. Все сложные.
# разделить соседние марсианские байты $martian =~ s/([A-Z][A-Z])/ $1 /g;  print "found GX!\n" if $martian =~ /GX/;
Или так:
@chars = $martian =~ m/([A-Z][A-Z]|[^A-Z])/g; # по сути это похоже на @chars = $text =~ m/(.)/g; # foreach $char (@chars) { print "found GX!\n", last if $char eq 'GX'; }
Или так:
while ($martian =~ m/\G([A-Z][A-Z]|.)/gs) {  # возможно, \G не требуется     print "found GX!\n", last if $1 eq 'GX';     }
Вот другой, чуть менее сложный способ Бенджамина Голдберга, использующий (?<!) (zero-width negative look-behind assertion).
print "found GX!\n" if    $martian =~ m/     (?<![A-Z])     (?:[A-Z][A-Z])*?     GX     /x;
Это успешно выполняется, если марсианский символ "GX" есть в строке. Если вам не нравится (?<!), можете заменить (?<![A-Z]) на (?:^|[^A-Z]).
Тут есть недостаток в размещении неподходящих строк в $-[0] и $+[0], но обычно это можно обойти.

Как осуществить поиск по шаблону, записанному в переменной?

(Отвечает Брайан Д. Фой)
Шаблоны не обязательно жестко задавать в операторе поиска или в чём-либо еще, работающим с регулярными выражениями. Можно сохранить шаблон в переменной для последующего использования.
Оператор поиска соответствует контексту двойных кавычек, поэтому переменную можно интерполировать, как и в строке, заключенной в двойные кавычки. Ниже регулярное выражение считывается как пользовательский ввод и сохраняется в переменной $regex, которая затем используется в операторе поиска по шаблону.
chomp( my $regex = <STDIN> );  if( $string =~ m/$regex/ ) { ... }
Все специальные символы регулярных выражений в $regex сохраняют свое значение, и шаблон должен быть валидным, иначе Perl сообщит об ошибке. Так, в следующем шаблоне есть открывающая скобка без закрывающей:
my $regex = "Unmatched ( paren";  "Two parens to bind them all" =~ m/$regex/;
Когда Perl компилирует регулярное выражение, он принимает открывающую скобку за начало запоминаемой группы. Когда закрывающей скобки не находится, выдается сообщение об ошибке:
Unmatched ( in regex; marked by <-- HERE in m/Unmatched ( <-- HERE  paren/ at script line 3.
Вы можете обойти это несколькими способами, в зависимости от ситуации. Если ни один символ не должен считаться специальным, то перед использованием строки экранируйте символы функцией quotemeta.
chomp( my $regex = <STDIN> ); $regex = quotemeta( $regex );  if( $string =~ m/$regex/ ) { ... }
Также это можно сделать напрямую в операторе поиска по шаблону с \Q и \E\Q обозначает начало экранирования специальных символов, а \E — конец (подробности см. в perlop).
chomp( my $regex = <STDIN> );  if( $string =~ m/\Q$regex\E/ ) { ... }
Кроме того, можно использовать оператор экранирования регулярного выражения qr// (подробности см. вperlop), он экранирует и компилирует шаблон. Также к шаблону можно применять флаги регулярных выражений.
chomp( my $input = <STDIN> );  my $regex = qr/$input/is;  $string =~ m/$regex/  # то же самое, что и m/$input/is;
Может оказаться полезным заключить блок в eval и перехватывать любые ошибки:
chomp( my $input = <STDIN> );  eval {     if( $string =~ m/\Q$input\E/ ) { ... }     }; warn $@ if $@;
или
my $regex = eval { qr/$input/is }; if( defined $regex ) {     $string =~ m/$regex/;     } else {     warn $@;     }

No comments:

Post a Comment