Perl меняет текстовый файл внутри скобок

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

Composition {
  CurrentTime = 0,
  OutputClips = {
    "",
  },
  Tools = {
      Text3 = Text3D {
          NameSet = true,
          Inputs  = {
            Size       = Input { Value = 0.6282723, },
            Font       = Input { Value = "KG Shadow of the Day", },
            StyledText = Input { Value = "Your Text Goes Here 3", },
      },
      ShadowDensity = BrightnessContrast {
          NameSet = true,
          Inputs  = {
            Alpha = Input { Value = 1, },
            Gain  = Input { Value = 0.5, },
            Input = Input {
                SourceOp = "Loader2",
                Source   = "Output",
            },
          },
          ViewInfo = OperatorInfo { Pos = { -220, 82.5, }, },
      },
  },
}

Мне нужно иметь возможность изменить значение в Text3 'StyledText = Input', а также значения ShadowDensity 'Alpha = Input'. И я не могу просто выполнить обычное выражение, чтобы найти 'Alpha = Input', потому что внутри массива есть другие вложенные элементы с таким же точным именем, только в другом инструменте. То же самое с частью «Текст», если у меня есть несколько инструментов «Текст», он не найдет правильный. Любая помощь и предложения приветствуются. Спасибо


person KJones    schedule 18.02.2013    source источник
comment
Хорошо ли структурированы и отформатированы данные? Может быть, какой-нибудь синтаксический анализатор, учитывающий пути, чтобы модифицировать Alpha= только в том случае, если он появляется в Compostion/Tools/ShadowDensity/Inputs.   -  person Jokester    schedule 18.02.2013
comment
Да, данные очень хорошо отформатированы и структурированы. Но непостоянна в своем расположении. Таким образом, инструмент ShadowDensity иногда может быть выше инструмента Text3. Спасибо, я поищу что-нибудь в анализаторах пути.   -  person KJones    schedule 18.02.2013
comment
Почти все зависит от того, как этот файл может быть отформатирован. Можно использовать даже что-то глупое и простое, например s/(\s+StyledText = Input \{ Value = ")[^"]+/$1$your_new_value/`, в противном случае вы должны найти модуль для его разбора или написать свой собственный.   -  person ArtM    schedule 18.02.2013
comment
Спасибо, амон, я рассмотрю эти два варианта и посмотрю, смогу ли я понять, как это сделать.   -  person KJones    schedule 18.02.2013
comment
Любая идея, что формат этих данных? Это выглядит знакомо, я сначала подумал, что это JSON, и есть JSON модуль на Perl. Однако при ближайшем рассмотрении формат не JSON. Формат файла может упоминаться в документации к программе. Очень вероятно, что кто-то использует стандартную библиотеку для разбора этого вывода.   -  person David W.    schedule 19.02.2013


Ответы (2)


Я задумал его как структурированный файл с «событиями», которые вы, возможно, захотите обработать. Поэтому я создал класс/объект структурированного пути "событие" и класс/объект мультиплексора обработчика.

use strict;
use warnings;

{   package LineEvent;  # our "event" class
    use strict;
    use warnings;

    # use overload to create a default stringification for the class/object
    use overload '""' => 'as_string', fallback => 1;

    # Create new path-tracking object    
    sub new { 
        my $self   = bless {}, shift;
        my %params = @_ % 2 ? ( base => @_ ) : @_;
        for ( qw<base delim verbose> ) { 
            $self->{$_} = $params{ $_ };
        }
        $self->{base}  ||= '';
        $self->{delim} ||= '.';
        return $self;
    }

    # pop back to larger named scope
    sub pop { 
        my $self  = shift;
        my $ref   = \$self->{base};
        my $pos   = rindex( $$ref, $self->{delim} );
        if ( $pos == -1 ) { 
            $self->{current} = '!Close';
        }
        else { 
            my $node = substr( $$ref, $pos + 1 );
            substr( $$ref, $pos ) = '';
            $self->{current} = "$node.!Close";
        }
        say qq{After pop, now "$self".} if $self->{verbose};
        return $self;
    }

    # push a new name as the current scope of the path
    sub push { 
        my ( $self, $level ) = @_;
        return unless $level;
        $self->{current} = '!Open';
        my $delim        = $self->{delim};
        $self->{base}
            .= ( substr( $level, 0, length( $delim )) eq $delim ? '' : $delim ) 
            .  $level
            ;
        say qq{After push, now "$self".} if $self->{verbose};
        return $self;
    }

    # push the temporary name sitting as current onto our base
    sub push_current { 
        return $_[0]->push( $_[0]->{current} ); 
    }

    # set a temporary name to identify the current line.
    sub update { 
        my ( $self, $tip ) = @_;
        $self->{current} = $tip // '';
        say qq{After update, now: "$self".} if $self->{verbose};
        return $self;
    }

    sub null_current { delete $_[0]->{current}; }

    # used in overload
    sub as_string {
        my $self  = shift;
        return join( $self->{delim}, grep {; length } @{ $self }{ qw<base current> } );
    }
};

sub pair_up {
    return map { [ @_[ $_, $_ + 1 ] ] } grep { $_ % 2 == 0 } 0..$#_;
}

{   package PathProcessor; # our mux class

    # create a event list and handler, by splitting them into pairs.
    sub new { 
        my $self = bless [], shift;
        @$self   = &::pair_up;
        return $self;
    }

    # process the current path
    sub process_path { 
        my ( $self, $path ) = @_;
        foreach my $pair ( @$self ) {
            my ( $test, $func ) = @$pair;
            next unless ref( $test ) 
                    ? $path =~ /$test/ 
                    : substr( $path, - length( $test )) eq $test
                    ;
            my $v = $func->( $path );
            return $v || !defined( $v );
        }
        return 1;
    }
}

my $path  = LineEvent->new( base => 'x' );

my $processor  
    = PathProcessor->new( 
      '.Text3.Inputs.StyledText' => sub { s/\bText\b/_Styled_ Text/ || 1; }
    , '.ShadowDensity.Inputs.Alpha' => sub { 
          s/(Value \s+ = \s+ )\K(\d+(?:\.\d+)?)/0.5/x || 1;
      }
    #, '.!Close' => sub { 
    #    say 'Closed!';
    #  }
    );

# We only handle a couple of conditions...
while ( <DATA> ) { 
    chomp;
    # ... If there is a keyword as the first thing in line
    if ( m/^ \s* ( \p{IsUpper} \w+ \b )/gcx ) {
        $path->update( $1 );
        # ... if it is followed by a equals sign, an optional name and
        # and open-bracket
        if ( m/\G \s+ = \s+ (?: \p{IsUpper} \w+ \s+ )? [{] \s* $/gcx ) {
            $path->push_current;
        }
    }
    # ... if it's a closing brace with an optional comma. 
    elsif ( m/^ \s* [}] ,? \s* $/x ) { 
        $path->pop;
    }
    else {
        $path->null_current;
    }
    say $path;
    # you can omit a line by passing back a false value
    say if $processor->process_path( $path );
}

__DATA__
Composition {
  CurrentTime = 0,
  OutputClips = {
    "",
  },
  Tools = {
      Text3 = Text3D {
          NameSet = true,
          Inputs  = {
            Size       = Input { Value = 0.6282723, },
            Font       = Input { Value = "KG Shadow of the Day", },
            StyledText = Input { Value = "Your Text Goes Here 3", },
          },
      },
      ShadowDensity = BrightnessContrast {
          NameSet = true,
          Inputs  = {
            Alpha = Input { Value = 1, },
            Gain  = Input { Value = 0.5, },
            Input = Input {
                SourceOp = "Loader2",
                Source   = "Output",
            },
          },
          ViewInfo = OperatorInfo { Pos = { -220, 82.5, }, },
      },
  },
}

Результат был:

x.Composition
Composition {
x.CurrentTime
  CurrentTime = 0,
x.OutputClips.!Open
  OutputClips = {
x.OutputClips.!Text.1
    "",
x.OutputClips.!Close
  },
x.Tools.!Open
  Tools = {
x.Tools.Text3.!Open
      Text3 = Text3D {
x.Tools.Text3.NameSet
          NameSet = true,
x.Tools.Text3.Inputs.!Open
          Inputs  = {
x.Tools.Text3.Inputs.Size
            Size       = Input { Value = 0.6282723, },
x.Tools.Text3.Inputs.Font
            Font       = Input { Value = "KG Shadow of the Day", },
x.Tools.Text3.Inputs.StyledText
            StyledText = Input { Value = "Your _Styled_ Text Goes Here 3", },
x.Tools.Text3.Inputs.!Close
          },
x.Tools.Text3.!Close
      },
x.Tools.ShadowDensity.!Open
      ShadowDensity = BrightnessContrast {
x.Tools.ShadowDensity.NameSet
          NameSet = true,
x.Tools.ShadowDensity.Inputs.!Open
          Inputs  = {
x.Tools.ShadowDensity.Inputs.Alpha
            Alpha = Input { Value = 0.5, },
x.Tools.ShadowDensity.Inputs.Gain
            Gain  = Input { Value = 0.5, },
x.Tools.ShadowDensity.Inputs.Input.!Open
            Input = Input {
x.Tools.ShadowDensity.Inputs.Input.SourceOp
                SourceOp = "Loader2",
x.Tools.ShadowDensity.Inputs.Input.Source
                Source   = "Output",
x.Tools.ShadowDensity.Inputs.Input.!Close
            },
x.Tools.ShadowDensity.Inputs.!Close
          },
x.Tools.ShadowDensity.ViewInfo
          ViewInfo = OperatorInfo { Pos = { -220, 82.5, }, },
x.Tools.ShadowDensity.!Close
      },
x.Tools.!Close
  },
x.!Close
}
person Axeman    schedule 18.02.2013

Это решение использует Marpa::R2 и перегруженные объекты. Это оказалось дольше, чем ожидалось, но выглядит совместимым с поездкой туда и обратно.

Заголовок простой:

use strict; use warnings; use feature 'say';
use Marpa::R2;

use constant DEBUG => 0;

exit main();

Для этого потребуется Perl5 версии 10 или выше. Далее идет подпрограмма parse. Это выполнит токенизацию и вызовет парсер. Большинство токенов указываются в виде данных (а не явного кода), поэтому их можно легко расширить.

$print_diag — анонимный подписчик. Он закрывается над $string и $last_pos и поэтому может выводить соответствующее сообщение об ошибке, похожее на die. Он укажет на контекст проблемы с токенизацией стрелкой HERE-->.

$match, если подобное закрытие. Он перебирает все доступные токены и возвращает соответствующий токен или ложное значение в случае сбоя. Он использует m/\G.../gc регулярных выражений. Они похожи на s/^...//, но не уничтожают строку. Утверждения \G будут соответствовать pos($string). Опция /c гарантирует, что сбой не изменит pos.

Токен строки сопоставляется вручную. Возможно, вы захотите обработать побеги. Я добавил поддержку нескольких популярных escape-последовательности (\\, \", \n, \t и обратную косую черту для продолжения строки).

Цикл TOKEN извлекает токены и вставляет их в распознаватель. Он включает в себя небольшой код и большую обработку ошибок.

Наконец, мы берем первое возможное дерево $parse (их может быть несколько) и проверяем, было ли оно успешным. Если да, то возвращаем структуру данных:

my $grammar; # filled later in INIT block

sub parse {
    my ($string) = @_;
    my ($last_pos, $length) = (0, length $string);
    my $rec = Marpa::R2::Recognizer->new({ grammar => $grammar });

    my $print_diag = sub {
        my ($problem) = @_;
        my ($behind, $ahead) = (15, 30);
        my $start = $last_pos > $behind ? $last_pos - $behind : 0;
        say STDERR "$problem at ", map ">>$_<<", join " HERE-->",
            substr($string, $start,    $behind),
            substr($string, $last_pos, $ahead );
        exit 1;
    };

    my @capture_token = (
        [qr/true|false/     => 'Bool'],     # bool must come before ident
        [qr/-?\d+(?:\.\d+)?/=> 'Number'],   # number must come before ident
        [qr/\w+/            => 'Ident'],
    );
    my @non_capture_token  = (
        [qr/\{/     => 'LCurly'],
        [qr/\}/     => 'RCurly'],
        [qr/=/      => 'Equal'],
        [qr/,/      => 'Comma'],
    );

    my $match = sub {
        # try String manually here:
        if ($string =~ m/\G"( (?: [^"]++ | \\. )*+ )"/gcxs) {
            my $str = $1;
            my %escapes = ( n => "\n", t => "\t", "\n" => '' );
            $str =~ s{\\(.)}{ $escapes{$1} // $1 }esg;
            return String => $str;
        }
        for (@non_capture_token) {
            my ($re, $type) = @$_;
            return $type if $string =~ m/\G$re/gc;
        }
        for (@capture_token) {
            my ($re, $type) = @$_;
            return $type, $1 if $string =~ m/\G($re)/gc;
        }
        return;
    };

    pos $string = $last_pos; # set match start for \G assertion to beginning

    TOKEN: while ($last_pos < $length) {
        next TOKEN if $string =~ m/\G\s+/gc;
        next TOKEN if $string =~ m/\G\#\N+/gc; # skip comments if you have such

        if (my @token = $match->()) {
            say STDERR "Token [@token]" if DEBUG;
            my $ok = $rec->read(@token);
            unless (defined $ok) {
                $print_diag->("Token [@token] rejected");
            }
        } else {
            $print_diag->("Can't understand input");
        }
    } continue {
        $last_pos = pos $string;
    }

    my $parse = $rec->value;
    unless ($parse) {
        say STDERR "Could not parse input";
        say STDERR "The Progress so far:";
        say STDERR $rec->show_progress;
        exit 1;
    }
    return $$parse;
}

Теперь уточняем грамматику. Марпу можно обрабатывать с помощью нотации, подобной БНФ, которую я использую здесь. Это в основном синтаксический сахар над методами более низкого уровня. Я могу указать действия (о которых напишу позже) и принять решение не захватывать токены, помещая их в круглые скобки. На данном этапе я могу работать только с типами токенов, а не со значением токенов. После того, как я укажу грамматику, я должен скомпилировать ее с помощью $grammar->precompute.

INIT {
    $grammar = Marpa::R2::Grammar->new({
        actions         => "MyActions", # a package name
        default_action  => 'first_arg',
        source          => \(<<'END_OF_GRAMMAR'),
        :start  ::= Value

        Value   ::= Bool            action => doBool
                |   Number          # use auto-action
                |   String          # use auto-action
                ||  Array
                ||  Struct

        Struct  ::= Ident (LCurly) PairList (RCurly)    action => doStruct
                |         (LCurly) PairList (RCurly)    action => doStruct1

        Array   ::= Ident (LCurly) ItemList (RCurly)    action => doArray
                |         (LCurly) ItemList (RCurly)    action => doArray1


        ItemList::= Value +         separator => Comma  action => doList
        PairList::= Pair +          separator => Comma  action => doList
        Pair    ::= Ident (Equal) Value                 action => doPair
END_OF_GRAMMAR
    });
    $grammar->precompute;
}

Вышеприведенное находится в блоке INIT, поэтому оно будет выполнено до того, как будет выполнено parse.

Теперь наши действия. Каждое действие будет вызываться с объектом действия в качестве первого аргумента, который нам не нужен (это полезно для более продвинутых методов синтаксического анализа). Другие аргументы — это значения (а не типы) токенов/правил, которые были сопоставлены. Большинство из них отбрасывают или упаковывают аргументы или помещают данные внутрь позже определенных объектов.

sub MyActions::first_arg {
    say STDERR "rule default action" if DEBUG;
    my (undef, $first) = @_;
    return $first;
}

sub MyActions::doStruct {
    say STDERR "rule Struct" if DEBUG;
    my (undef, $ident, $pair_list) = @_;
    my %hash;
    for (@$pair_list) {
        my ($k, $v) = @$_;
        $hash{$k} = $v;
    }
    return MyHash->new($ident, \%hash);
}

sub MyActions::doStruct1 {
    say STDERR "rule Struct sans Ident" if DEBUG;
    my (undef, $pair_list) = @_;
    return MyActions::doStruct(undef, undef, $pair_list);
}

sub MyActions::doArray {
    say STDERR "rule Array" if DEBUG;
    my (undef, $ident, $items) = @_;
    return MyArray->new($ident, $items);
}

sub MyActions::doArray1 {
    say STDERR "rule Array sans Ident" if DEBUG;
    my (undef, $items) = @_;
    MyActions::doArray(undef, undef, $items);
}

sub MyActions::doList {
    say STDERR "List" if DEBUG;
    my (undef, @list) = @_;
    return \@list;
}

sub MyActions::doPair {
    say STDERR "Pair" if DEBUG;
    my (undef, $key, $value) = @_;
    return [$key, $value];
}

sub MyActions::doBool {
    say STDERR "Bool" if DEBUG;
    my (undef, $bool) = @_;
    return MyBool->new($bool);
}

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

Сначала идут два помощника: $My::Indent устанавливает количество пробелов, на которое будет отступать распечатка. My::stringifyHelper просто гарантирует, что объекты приводятся к их строковым представлениям, и что строки (все остальное, что не является числом) заключены в кавычки.

INIT{ $My::Indent = 4 }
sub My::stringifyHelper {
    my (@objects) = @_;
    for (@objects) {
        if (ref $_) {
            $_ = "$_";
        } elsif ( not /\A-?\d+(?:\.\d+)?\z/) {
            $_ = qq("$_");
        }
    }
    return @objects;
}

Вот это тип MyHash. Строковый код уродлив, но, кажется, работает → программирование случайно.

{
    package MyHash;
    sub new {
        my ($class, $type, $hashref) = @_;
        bless [$type, $hashref] => $class;
    }
    sub type {
        my ($self) = @_;
        return $self->[0];
    }
    sub hash {
        my ($self) = @_;
        return $self->[1];
    }
    sub asString {
        my ($self) = @_;
        my @keys = sort keys %{ $self->hash };
        my @vals =
            map { s/\n\K/" "x$My::Indent/meg; $_ }
            My::stringifyHelper @{ $self->hash }{@keys};
        my $string = "";
        for my $i (0 .. $#keys) {
            $string .= (" "x$My::Indent) . "$keys[$i] = $vals[$i],\n";
        }
        return +($self->type // "") . "{\n$string}";
    }
    use overload
        '""'        => \&asString,
        '%{}'       => \&hash,
        fallback    => 1;
}

Это реализует MyArray. Строковая обработка немного менее безобразна, но я представляю объект как хэш. Я недостаточно хорошо разбираюсь в overload, чтобы убедиться, что в противном случае он не будет рекурсивно обращаться к фактическому массиву.

{
    package MyArray;
    sub new {
        my ($class, $type, $aryref) = @_;
        bless { type => $type, array => $aryref } => $class;
    }
    sub type {
        my ($self) = @_;
        return $self->{type};
    }
    sub array {
        my ($self) = @_;
        no overload;
        return $self->{array};
    }
    sub asString {
        my ($self) = @_;
        my @els = My::stringifyHelper @{$self->array};
        my $string = $self->type // "";
        if (@els <= 1) {
            $string .=  "{ @els, }";
        } else {
            my $els = join '', map "$_,\n", @els;
            $els =~ s/^/" "x$My::Indent/meg;
            $string .= "{\n$els}";
        }
        return $string;
    }
    use overload
        '""'        => \&asString,
        '@{}'       => \&array,
        fallback    => 1;
}

Теперь небольшая реализация MyBool. Это должно даже работать как логическое :)

{
    package MyBool;
    sub new {
        my ($class, $str) = @_;
        my $bool;
        if ('true' eq lc $str)      { $bool = 1     }
        elsif ('false' eq lc $str)  { $bool = undef }
        else { die "Don't know if $str is true or false" }
        bless \$bool => $class;
    }
    use overload
        'bool' => sub {
            my ($self) = @_;
            return $$self;
        },
        '""' => sub {
            my ($self) = @_;
            $$self ? 'true' : 'false';
        },
        fallback => 1;
}

Теперь мы почти закончили. А вот и main:

sub main {
    local $/;
    my $data = <DATA>;
    my $dsc = parse($data);

    say "/:";
    say $dsc;

    say "/Tools:";
    say $dsc->{Tools};

    say "/Tools/ShadowDensity/:";
    say $dsc->{Tools}{ShadowDensity};

    say "/Tools/ShadowDensity/Inputs/:";
    say $dsc->{Tools}{ShadowDensity}{Inputs};

    return 0;
}

Это загружает и анализирует данные. Затем он распечатывает весь объект и только определенные его части. Кажется, это работает до сих пор.

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

Сделать:

  • В некоторых частях используется exit 1, где должно быть выдано исключение.
  • Вышеупомянутые доступы работают, но другие пути терпят неудачу (они возвращают undef). Где-то есть ошибка, которую нужно исправить, но я понятия не имею.
  • Было бы здорово улучшить сообщения об ошибках и увеличить разнообразие уровней отладки.
person amon    schedule 18.02.2013