RegEx для замены только слева от комментариев LaTeX - PullRequest
1 голос
/ 15 мая 2019

Файл, который я преобразовываю (LaTeX), содержит комментарии, которые лежат справа от%. Любой неэкранированный знак процента помечает комментарий.

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

s/dog/CAT/g

но только в некомментированном тексте. Таким образом, линии

Одна собака съела крысу, но 5% собак съели яблоко% собаки ??

Моя собака умнее твоей ученицы

будет преобразовано в

Одна кошка съела крысу, но 5% кошек съели яблочную собаку ??

Моя кошка умнее вашей чести, ученика

Вот, конечно, как сопоставить неэкранированный знак процента:

bash: cat aaa
dog % cat
dog \% cat
bash: cat aaa | perl -n -e 'use strict; use warnings; print if (m/(?<!\x5c)%/)'
dog % cat
bash: 

Это должен быть общеизвестный вопрос, но я не нашел правильных условий поиска, чтобы найти ответ. Можно ли не сделать это в Perl с одним регулярным выражением? Очевидно, мое регулярное выражение подстановки заменяет каждые dog на CAT, даже в комментариях.

Ответы [ 4 ]

2 голосов
/ 15 мая 2019

В одну сторону: извлечь весь текст до (неэкранированного) %, затем выполнить замену в этом

s/ (.*?) ([^\\]%.*) /$r=$2; $1=~s{dog}{CAT}gr . $r/egx;

Модификатор /e позволяет заменить сторону замены как код, и мы запускаем в нем регулярное выражение.

Там нам нужно сначала спасти "остаток" строки (после %), захваченный в $2, так как $2 будет очищен в следующем регулярном выражении.

Модификатор /r в этом регулярном выражении заставляет его возвращать преобразованную строку, что удобно для формирования значения, которое будет использоваться в качестве замены (путем объединения его с остальной частью строки). Кроме того, наличие оригинала без изменений в /r позволяет нам использовать подстановку в $1 (только для чтения).


Для [^\\] выше требуется символ, отличный от \, предшествующий %, для начала комментария. Однако, поскольку он запрашивает символ, он сопоставляет все регулярные выражения, если строка начинается с % , а имеет дальнейшее отсутствие экранирования %, что неверно. Это вполне возможно: строка имеет некоторый закомментированный текст (%...), а в некоторый момент также полностью закомментируется.

Если это действительно проблема, используйте вместо этого отрицательный взгляд

s{ (.*?) ((?<!\\)%.*)? $ }{ $r=($2//''); $1=~s{dog}{CAT}gr . $r}egx;

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

Тест, с входным файлом data.txt

One dog 5\% of dogs % dog
%dog more than 10\% of % dogs
dogs \% and dogs

Однострочник

perl -nwe'
    s{ (.*?) ((?<!\\)%.*)? $}{$r=($2//""); $1=~s{dog}{CAT}gr . $r}egx; print
' data.txt

печать

One CAT 5\% of CATs % dog
%dog more than 10\% of % dogs
CATs \% and CATs
1 голос
/ 15 мая 2019

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

use strict;
use warnings;
my $str = 'One dog ate a rat but 5\% of dogs ate the apple % dog??';
if (my ($first, $second) = $str =~ m/\A(.*?)((?<!\\)%.*)?\z/s) {
  $first =~ s/dog/CAT/g;
  $str = defined $second ? "$first$second" : $first;
}

При этом используется отрицательный lookbehind , чтобы найти первый неэкранированный знак процента, даже если это первый символ строки, и делает комментарий наполовину необязательнымтак что он все равно заменит, если нет комментариев.Однако это все равно потребует большого количества возвратов , поэтому, если производительность вызывает беспокойство, может быть предпочтительнее более обширная реализация.делать что-то регулярное выражение не очень хорошо.Вы хотите найти вещи в строке на основе контекстного состояния.«Лучший» способ сделать это - разобрать строку в токены, что обычно делается с помощью цикла, который сохраняет состояние и регулярное выражение (что хорошо в этой части);даже если это просто токены «строки без комментариев», «начала комментария», «строки комментария».Тогда вы можете легко оперировать только строками без комментариев.

Вот как может выглядеть расширенный алгоритм, я попытался упростить его до объема синтаксического анализа, необходимого для этого случая, и он, безусловно, может быть использован в дальнейшем.Ключ должен использовать m/\G.../g для поэтапного анализа строки (\G привязывает совпадение к концу последнего совпадения с модификатором /g в скалярном контексте) и полагаться на механизм регулярных выражений, выбирающий первый вариант чередования, которыйсоответствует этой точке в строке.Таким образом, вы последовательно проходите строку без обратного отслеживания и сохраняете состояние вне цикла.

use strict;
use warnings;
my $str = 'One dog ate a rat but 5\% of dogs ate the apple % dog??';
my $in_comment;
my ($text, $comment) = ('','');
while ($str =~ m/\G(((?<!\\)%)|%|[^%]+)/g) {
  my ($token, $start_comment) = ($1, $2);
  $in_comment = 1 if defined $start_comment;
  if ($in_comment) {
    $comment .= $token;
  } else {
    $text .= $token;
  }
}
$text =~ s/dog/CAT/g;
$str = "$text$comment";

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

my $escaping;
while ($str =~ m/\G((\\+)|(%)|[^\\%]+)/g) {
  my ($token, $backslashes, $percent) = ($1, $2, $3);
  $in_comment = 1 if defined $percent and !$escaping;
  $escaping = (defined $backslashes and length($backslashes) % 2) ? 1 : 0;

Parser :: MGC является абстракцией этой концепции для интерфейса объекта.

(Также: этот методне всегда будет быстрее, чем одно регулярное выражение, особенно с более простым анализом и более короткими строками.)

0 голосов
/ 17 мая 2019
#!/usr/bin/perl
# Default input record separator: one line at a time.
# Read through a LaTeX file line by line. Distinguish comment from text.
# Parse each line into exactly 2 tokens. 
# Boundary between tokens is the first non-escaped %.
# $text: everything up to, but excluding, boundary if exists; else entire line.
# $comment: possibly null, from the first non-escaped % to end of line. 
# Last (pathological) line might not end in LF, hence LF is excluded from tokens and appended at the end.
# Consequently, output will end in LF whether input did or not.
use strict;
use warnings;
use 5.18.2;
my $text;
my $comment;
while (<>) {
    # Non-greedy: match until first non-escaped %
    # Without final ([\n]?), pathological last line would not match and an entire last line of comment would be mistaken for text.
    if (m/(^.*?)((?<!\x5c)%.*)([\n]?)/) {
        $text=$1;
        $comment="$2";
    }
    else {
        s/\n//g; # There can be at most one LF, at the end; remove it if it exists.
        $text=$_;
        $comment="";
    }
    # Here, 
    # (1) examine $text for LaTeX-illegal characters; if found, exit with informative error
    # (2) identify LaTeX environments such as \verbatim and \verb, which are to be left alone
    # (3) perform any desired global changes on remaining text
    $text=~s/dog/CAT/g;
    # Add LF back in which we explicitly removed above 
    print "$text$comment\n";
}
0 голосов
/ 15 мая 2019

Более подробное и подробное решение, основанное на zdim's:

bash: cat aaa
dog and dogs and many many dogs% dog
dog and dogs and many many dogs\% dog
bash: cat aaa | perl -n -e 'use strict; use warnings; my $r; s/ (.*?) ((?<!\x5c)%.*) /$r=$2; $1=~s{dog}{CAT}gr . $r/egx; print;'
CAT and CATs and many many CATs% dog
dog and dogs and many many dogs\% dog 

Обратите внимание, это позволяет маркеру комментария сразу после текста без комментариев; он не требует пробела перед%.

...