Лучший способ справиться с «неэкранированными скобками в регулярном выражении» внутри регулярного выражения Perl - PullRequest
0 голосов
/ 08 мая 2018

Я недавно начал изучать Perl, чтобы автоматизировать некоторые бессмысленные задачи с данными. Я работаю на машинах Windows, но предпочитаю использовать Cygwin. Написал скрипт на Perl, который делал все, что я хотел, в Cygwin, но когда я попытался запустить его со Strawberry Perl в Windows через CMD, я получил сообщение «Unescaped left brace in regex - здесь недопустимо в regex», ошибка.

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

open(my $fh, '<:encoding(UTF-8)', $file)
    or die "Could not open file '$file' $!";
my $fileContents = do { local $/; <$fh> };

my $i = 0;
while ($fileContents =~ /(.*Part[^\}]*\})/) {
    $defParts[$i] = $1;
    $i = $i + 1;
    $fileContents =~ s/$1//;
}

В основном я ищу в файле совпадения, похожие на:

Part
{
    Somedata
}

Затем сохраняем эти совпадения в массиве. Затем удалите совпадение из $ fileContents, чтобы избежать повторов.

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

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

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

Спасибо!

Ответы [ 2 ]

0 голосов
/ 08 мая 2018

Я бы просто обошел всю проблему и в то же время упростил код:

my $i = 0;
while ($fileContents =~ s/(.*Part[^\}]*\})//) {
    $defParts[$i] = $1;
    $i = $i + 1;
}

Здесь мы просто сначала делаем замену. Если это удастся, он все равно установит $1 и вернет true (точно так же, как обычный /.../), поэтому нет необходимости возиться с s/$1// позже.

Использование $1 (или любой переменной) в качестве шаблона будет означать, что вы должны экранировать все метасимволы регулярных выражений (например, *, +, {, (, | и т. Д.), Если Вы хотите, чтобы это соответствовало буквально. Вы можете сделать это довольно легко с помощью quotemeta или встроенного (s/\Q$1//), но это все еще дополнительный шаг и, следовательно, подверженный ошибкам.

Кроме того, вы можете оставить свой оригинальный код и не использовать s///. Я имею в виду, вы уже нашли совпадение. Зачем использовать s/// для его повторного поиска?

while ($fileContents =~ /(.*Part[^\}]*\})/) {
    ...
    substr($fileContents, $-[0], $+[0] - $-[0], "");
}

Мы уже знаем, где находится совпадение в строке. $-[0] - это позиция начала, а $+[0] - позиция конца последнего совпадения с регулярным выражением (таким образом, $+[0] - $-[0] - это длина совпадающей строки). Затем мы можем использовать substr для замены этого фрагмента на "".

Но давайте продолжим с s///:

my $i = 0;
while ($fileContents =~ s/(.*Part[^\}]*\})//) {
    $defParts[$i] = $1;
    $i++;
}

$i = $i + 1; можно уменьшить до $i++; («приращение $ i»).

my @defParts;
while ($fileContents =~ s/(.*Part[^\}]*\})//) {
    push @defParts, $1;
}

Единственная причина, по которой нам нужно $i, - добавить элементы в массив @defParts. Мы можем сделать это, используя push, поэтому нет необходимости поддерживать дополнительную переменную. Это спасает нас от другой линии.

Теперь нам, вероятно, не нужно уничтожать $fileContents. Если замена существует только в интересах этого цикла (поэтому я не сопоставляю уже извлеченный контент), мы можем сделать лучше:

my @defParts;
while ($fileContents =~ /(.*Part[^\}]*\})/g) {
    push @defParts, $1;
}

Использование /g в скалярном контексте добавляет «текущую позицию» к $fileContents, поэтому следующая попытка совпадения начинается там, где прервано предыдущее совпадение. Это, вероятно, более эффективно, потому что не нужно переписывать $fileContents.

my @defParts = $fileContents =~ /(.*Part[^\}]*\})/g;

... Или мы могли бы просто использовать //g в контексте списка, где он возвращает список всех захваченных групп всех совпадений, и присвоить его @defParts.

my @defParts = $fileContents =~ /.*Part[^\}]*\}/g;

Если в регулярном выражении нет групп захвата, //g в контексте списка возвращает список всех совпадающих строк (как если бы вокруг всего регулярного выражения было ( )).

Не стесняйтесь выбирать любой из них. : -)

0 голосов
/ 08 мая 2018

Что касается вопроса о побеге, вот для чего quotemeta ,

my $needs_escaping = q(some { data } here);
say quotemeta $needs_escaping;

что печатает (на v5.16)

some\ \{\ data\ \}\ here

и работает на $1. Смотрите связанные документы для деталей. Также см. \Q в perlre (поиск \Q), как это используется внутри регулярного выражения, скажем, s/\Q$1//;. \E перестает убегать (что вам не нужно).

Некоторые комментарии.

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

my @defParts;
while ($fileContents =~ /($pattern)/) {
    push @defParts, $1;
    $fileContents =~ s/\Q$1//;
}

, где \Q добавлено в регулярное выражение. Еще лучше, как объяснено в ответе мельпомены замена может быть сделана в самом условии while

push @defParts, $1  while $fileContents =~ s/($pattern)//;

, где я использовал для краткости модификатор оператора (постфиксный синтаксис).

С модификатором /g в скалярном контексте, как и в while (/($pattern)/g) { .. }, поиск продолжается с позиции предыдущего совпадения в каждой итерации, и это обычный способ итерации по всем экземплярам шаблона в строке. Пожалуйста, ознакомьтесь с использованием /g в скалярном контексте, поскольку в его поведении есть детали, о которых нужно знать.

Однако, это сложно (даже несмотря на то, что работает), что строка меняется под регулярным выражением. Если эффективность не имеет значения, вы можете перехватить все совпадения с /g в списке и затем удалить их

my @all_matches = $fileContents =~ /$patt/g;
$fileContents =~ s/$patt//g;

Хотя это неэффективно, поскольку он делает два прохода, это гораздо проще и понятнее.

Я ожидаю, что Somedata не может когда-либо содержать }, например, как вложенный { ... }, правильно? Если это так, у вас есть проблема сбалансированных разделителей , которая гораздо более округлая. Одним из подходов является использование модуля ядра Text :: Balanced . Поиск SO сообщений с примерами.

...