Замените блоки текста с регулярным выражением Perl с условным - PullRequest
2 голосов
/ 07 апреля 2019

У меня есть Perl-скрипт, который выполняет некоторые замены регулярных выражений в текстовом файле, и мне нужно изменить его следующим образом: (a) мне нужно обработать текст как блоки текста, а затем, в зависимости от наличия / отсутствие одной строки, необходимо сделать различные замены. (б) мне нужно добавить текст в конец каждого блока. (это преобразует текст из транскрипционной программы в код LaTeX)

Это должны быть две колонки:
Слева - как выглядит вход, справа - каким он должен стать:

ORIGINAL INPUT               EXPECTED OUTCOME

# Single line blocks: label to be replaced and \xe added to en
txt@#Name  Text text text    \ex[exno=\spkr{Name}] \txt  Text text text 
                             \xe

nvb@#Name  Text text text    \ex[exno=\spkr{Name}] \nvb  Text text text 
                             \xe

# Multi-line blocks: labels to be replaced and \xe added to end
txt@#Name  Text text text    \ex[exno=\spkr{Name}] \txt  Text text text 
fte@#Name  Text text text    \freetr Text text text
                             \xe

txt@#Name  Text text text    \ex[exno=\spkr{Name}] \txt  Text text text 
SD (0.0)                     \silence{0.0}
                             \xe

txt@#Name  Text text text    \ex[exno=\spkr{Name}] \txt  Text text text 
tli@#Name  Text text text    \translit   Text text text
fte@#Name  Text text text    \freetr    Text text text
                             \xe

# Multi-line block that has the mrb@... line (must start with txt): 
txt@#Name  Text text text    \ex[exno=\spkr{Name}] \begingl \glpreamble  Text text text // 
mrb@#Name  Text text text    \gla Text text text //
gle@#Name  Text text text    \glb Text text text //
fte@#Name  Text text text    \glft Text text text //
SD (0.0)                     \endgl 
                             \silence{0.0}
                             \xe
# The tricky thing here is that (a) the labels get replaced differently, the txt line gets two commands, \begingl and \glpreamble, all lines have to end with  // and they end with \endgl and \xe.  In case there is an SD (silence duration) line then that needs to go between the \endgl and the \xe. (but not all have the SD). 



Блоки разделены дополнительной пустой строкой. Первая строка каждого блока начинается с метки txt@..., nvb@... или event и может сопровождаться или не сопровождаться последующими строками, начинающимися с разных меток. Каждая метка должна быть заменена чем-то другим, здесь это выполняется посредством регулярных выражений, как в примере ниже (плюс некоторые другие замены, это просто минимально для целей объяснения). И тогда мне нужно отметить конец каждого блока.

Кроме того, мне нужно иметь одно условие где-то там: если в блоке есть строка, начинающаяся с метки mrb @ (как и в шестом блоке выше), применяются разные шаблоны замены.

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

#!/usr/bin/perl
use warnings;
use strict;

open my $fh_in, '<', $ARGV[0] or die "No input: $!";
open my $fh_out, '>', $ARGV[1] or die "No output: $!";

print $fh_out "\\begin{myenv}\n\n"; # begins group at beginning of file

while (<$fh_in>) 
{
    # general replacements for everything except if block includes a "mrb@" line:
    s/^txt@#(\S*)\s+(.*)/\\ex[exno=\\spkr{$1}] \\txt $2 /g; 
    s/^nvb@#(\S*)\s+(.*)/\\ex[exno=\\spkr{$1}] \\txt $2 /g;  
    s/^tli@#\S*\s+(.*)/\\translit $1 /g; 
    s/^fte@#\S*\s+(.*)/\\freetr $1 /g; 
    s/^SD\s*\((\d*)\.(\d*)\)/\\silence{\($1\.$2\)}/g; 

    # after each block I need to add "\\xe" 

    # replacements if block includes a "mrb@" line: 
    s/^txt@#(\S*)\s+(.*)/\\ex[exno=\\spkr{$1}] \\begingl \\glpreamble $2 \/\/ /g; 
    s/^mrb@#\S*\s+(.*)/\\gla $1 \/\/ /g; # 
    s/^gle@#\S*\s+(.*)/\\glb $1 \/\/ /g; # 
    s/^fte@#\S*\s+(.*)/\\glft $1 \/\/ /g; # 
    s/^tli@#\S*\s+(.*)/\\translit $1 \/\/ /g; #
    s/^fte@#\S*\s+(.*)/\\freetr $1 \/\/ /g; # 
    s/^SD\s*\((\d*)\.(\d*)\)/\\silence{\($1\.$2\)}/g;
    # after each block with a "mrb@" line I need to add "\\endgl" and "\\xe"
    # if there is a line starting with SD at the end of the block it needs to go between "\\endgl" and "\\xe"


    print $fh_out $_;    
} 

print $fh_out "\\end{myenv}"; # ends group

Любая помощь высоко ценится!

1 Ответ

4 голосов
/ 08 апреля 2019

Детали обработки, по-видимому, сложны; давайте сначала разберемся, как обрабатывать блоки.

Один из способов - построчно и накапливать строки для блока, пока не дойдете до пустой строки. Затем вы обрабатываете свой блок, очищаете буфер и продолжаете. Например

use warnings;
use strict;
use feature 'say';

sub process_block {
    say "Block:"; say "\t$_" for @{$_[0]};
}

my $file = shift // die "Usage: $0 filename\n";  #/

open my $fh, '<', $file or die "Can't open $file: $!";

my @block;
while (<$fh>) {
    chomp;
    if (not /\S/) {
        if (@block) {                # the first empty line
            process_block(\@block);
            @block = (); 
        }
        next;
    }   

    push @block, $_; 
}
process_block(\@block) if @block;    # last block may have remained

Вызов process_block после цикла while не срабатывает для показанного примера, поскольку перед концом файла есть пустые строки, поэтому последний блок обрабатывается внутри цикла. Но нам нужно убедиться, что последний блок обрабатывается, когда в конце также нет пустых строк.

Внутри process_block теперь вы можете проверить, содержит ли @block mrb@#Name, применить другие (очевидно сложные) условия, выполнить регулярное выражение и напечатать обработанные строки.

Вот пример, следующий за разъяснениями, но все же оставляющий некоторые детали

use List::Util qw(any);  # used to be in List::MoreUtils

sub process_block {
    my @block = @{ $_[0] };  # local copy, to not change @block in caller

    if ($block[0] =~ /^txt\@/ and any { /^mrb\@/ } @block) {
        for (@block) {
            s{^txt\@#(\S*)\s+(.*)}
             {\\ex[exno=\\spkr{$1}, exnoformat=X] \\begingl \\glpreamble $2 // }g;  #/
            s{^mrb\@#\S*\s+(.*)}{\\gla $1 // }g;
            # etc
        }   
        if ($block[-1] =~ /^\s*SD/) {
            my $SD_line = pop @block;
            push @block, '\endgl', $SD_line, '\xe';
        }
        else {
            push @block, '\endgl', '\xe';
        }
    }
    else {
        for (@block) {
            s/^txt\@#(\S*)\s+(.*)/\\ex[exno=\\spkr{$1}, exnoformat=X] \\txt $2 /g; 
            s/^tli\@#\S*\s+(.*)/\\translit $1 /g;
            # etc
        }
        push @block, '\xe';
    }
    say for @block;
    say "\n";        # two lines to separate blocks
}

Примечание по эффективности.

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

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

my %repl_non_mrb = ( 
    'txt@' => sub { s/^txt\@#(\S*)\s+(.*)/\\ex[exno=\\spkr{$1}, exnoformat=X] \\txt $2 /g }
    'tli@' => sub { s/^tli\@#\S*\s+(.*)/\\translit $1 /g },
    ...
);
my %repl_mrb = ( ... );

, а затем используйте его в соответствии с

# For blocks without 'mrb@'
for (@block) {
    # Capture key: up to # for 'txt@' (etc), up to \s for 'SD'. Other cases?
    my ($key) = /^(.*?)(?:#|\s)/; 
    if ($key and exists $repl_non_mrb{$key}) {
        $repl_non_mrb{$key}->();                  # run the coderef
    }
    else { say "No processing key (?) for: $_" }  # some error?
}

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


Другой способ, о чем вы спрашиваете

Я знаю, что Perl может делать блок за блоком

что можно сделать, установив переменную $/ . Он устанавливает то, что затем используется в качестве разделителя между входными записями. Если вы установите его на \n\n, здесь вы получите блок, обслуживаемый для каждого чтения, в строке

open my $fh, '<', $file or die "Can't open $file: $!";

PROCESS_FILE: { 
    local $/ = "\n\n";
    while (my $block = <$fh>) { 
        chomp $block;
        say "|$block|"; 
    }
};

Я поместил это в блок (названный PROCESS_FILE просто так), чтобы мы могли изменить $/, используя local . Затем его предыдущее значение восстанавливается при выходе из блока, и файлы снова читаются нормально.

Однако я не вижу в этом преимущества, поскольку у вас теперь есть блок в скалярной переменной, в то время как то, что вам нужно сделать, кажется ориентированным на строки. Поэтому я бы порекомендовал первый подход.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...