Perl — переместить указатель в начало строки

У меня есть 2 файла.

  1. Запутанный файл с именем input.txt
  2. Второй файл с именем mapping.txt, состоящий из пар ключ-значение.

Я хочу найти каждое вхождение ключа из mapping.txt в input.txt и заменить его значением, соответствующим ключу.

Обратите внимание, что я хочу перезаписывать содержимое строки в input.txt каждый раз, когда происходит успешное совпадение.

Я написал следующий код:

#! /usr/bin/perl

use strict;
use warnings;

(my $mapping,my $input)=@ARGV;

open(MAPPING,'<',$mapping) || die("couldn't read from the file, $mapping with error: $!\n");

while(<MAPPING>)
{
    chomp $_;
    my $line=$_;
    (my $key,my $value)=split("=",$line);
    open(INPUT,'+<',$input);
    while(<INPUT>)
    {
        chomp $_;
        if(index($_,$key)!=-1)
        {
            $_=~s/\Q$key/$value/g;
            # move pointer to beginning of line
           print INPUT $_."\n";
        }
    }
    close INPUT;
}
close MAPPING;

Краткий обзор кода:

  1. Открывает файл mapping.txt в режиме чтения.
  2. Поскольку каждая строка является парой ключ-значение, она разбивается на ключ и значение.
  3. Открывает файл input.txt в режиме перезаписи.
  4. Проверяет, найден ли ключ в текущей строке.
  5. Если ключ найден, замените ключ значением, игнорируя любые метасимволы в ключе (путем префикса \Q)
  6. В этот момент указатель файла будет находиться в конце строки, так как предыдущий оператор будет сканировать всю строку, чтобы найти ключ и заменить его.
  7. Если бы я мог переместить указатель файла в начало строки, я мог бы перезаписать его:

    напечатать ВВОД $_,"\n"

  8. Я попытался найти функцию поиска, но не смог найти способ использовать ее для этой цели.

Как только это будет сделано, код закроет файл. Он выберет следующую пару ключ-значение из mapping.txt и снова просканирует входной файл, начиная с поиска совпадений и их замены.

Самый важный момент заключается в том, что каждый раз внутренний цикл while будет работать с файлом input.txt, который был изменен в предыдущей итерации внутреннего цикла while. Таким образом, любые успешные операции поиска и замены будут сохраняться в файле input.txt.

Как мне это сделать?

Спасибо.


person Neon Flash    schedule 08.10.2012    source источник
comment
Можете ли вы дать нам пример ввода для обоих текстовых файлов?   -  person matthias krull    schedule 08.10.2012


Ответы (2)


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

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

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

use strict;
use warnings;

my ($mapping, $input) = @ARGV;

open my $infh, '<', $input or die "Unable to open '$input': $!";

while (my $line = <$input>) {

  open my $mapfh, '<', $mapping or die "Unable to open '$mapping': $!";

  while (<$mapfh>) {
    chomp;
    my ($key, $value) = split /=/;
    $line =~ s/\Q$key/$value/g;
  }
  print $line;
}

но ваш вывод отправляется в STDOUT, и вам нужно будет сохранить вывод в файл и соответствующим образом переименовать.

Альтернативой здесь является использование параметра командной строки -I, который заставляет файл автоматически переименовываться и при необходимости сохранять резервную копию. Использование голого -I изменит файл на месте, удалив старый файл и переименовав новый вывод, а присвоение параметру значения, подобного -I.bak, переименует старый файл, добавив .bak вместо его удаления. Параметр -I применяется только к файлам, считанным из ARGV с использованием пустого оператора <>, и установка значения встроенной переменной $^I (или пустой строки '') имеет тот же эффект. Код выглядит следующим образом:

use strict;
use warnings;

my $mapping = shift @ARGV;
$^I = '.bak';

while (my $line = <>) {

  open my $mapfh, '<', $mapping or die "Unable to open '$mapping': $!";

  while (<$mapfh>) {
    chomp;
    my ($key, $value) = split /=/;
    $line =~ s/\Q$key/$value/g;
  }
  print $line;
}

Третий и более удобный вариант — использовать Tie::File, который сопоставляет массив Perl с файлом. содержимое и отражает все изменения массива обратно в исходный файл. Вот пример:

use strict;
use warnings;

use Tie::File;

my ($mapping, $input) = @ARGV;
tie my @input, 'Tie::File', $input or die "Unable to open '$input': $!";

for my $line (@input) {

  open my $mapfh, '<', $mapping or die "Unable to open '$mapping': $!";

  while (<$mapfh>) {
    chomp;
    my ($key, $value) = split /=/;
    $line =~ s/\Q$key/$value/g;
  }
}

Наконец, очень неэффективно постоянно открывать и читать файл сопоставления для каждой строки ввода, и лучше всего построить регулярное выражение из его содержимого и использовать его во всей программе. Эта версия сначала строит хэш %mapping из файла сопоставления, а затем создает регулярное выражение, применяя quotemeta к каждому ключу хэша, чтобы избежать любых метасимволов регулярного выражения, а затем соединяя их с помощью оператора чередования регулярных выражений |. Ключи сортируются по убыванию длины, так что самые длинные совпадения находятся и заменяются в приоритете над более короткими.

use strict;
use warnings;

use Tie::File;

my ($mapping, $input) = @ARGV;

open my $mapfh, '<', $mapping or die "Unable to open '$mapping': $!";
my %mapping = map { chomp; /\S/ ? split /=/ : () } <$mapfh>;
my $regex = join '|', map quotemeta, sort { length $b <=> length $b } keys %mapping;

tie my @input, 'Tie::File', $input or die "Unable to open '$input': $!";

for my $line (@input) {
  $line =~ s/($regex)/$mapping{$1}/g;
}
person Borodin    schedule 08.10.2012
comment
Это так красиво. Спасибо большое :) Вы очень хорошо объяснили, теперь буду экспериментировать :) - person Neon Flash; 08.10.2012

Если бы я мог переместить указатель файла в начало строки, я мог бы перезаписать его:

print INPUT $_,"\n"

Ваша предпосылка неверна: если принять последовательность байтов 00 01 02 и правило 01 = A1 A2, результирующая последовательность байтов будет 00 A1 A2, а не 00 A1 A2 02. Способы обойти это включают:

  • Используйте модуль Tie::File.
  • Запишите в другой файл и переименуйте второй файл в исходный после завершения прохода. Это, вероятно, наиболее эффективно и масштабируемо.

seeking не является хорошей идеей: вы будете ограничены подстановками фиксированной длины, а seek и tell работают с байтами, а не с символами. Если вам действительно нужно использовать редактирование на месте, вы можете использовать этот цикл:

my $beginning_of_line = tell $fh;
while (<$fh>) {
  # do processing
  seek $fh, $beginning_of_line, 0;
  # do update
} continue {$beginning_of_line = tell $fh}

Кроме того, вы делаете несколько проходов по входному файлу. Предполагая последовательность токенов a b c и правила b = d e и d = f, вы получите последовательности a f e c или a d e c в зависимости от порядка правил! Возможно, это не то, что вам нужно.
Также обратите внимание на неоднозначность правил a = c и a b = d для входных данных a b. Получается ли это c b или d?

person amon    schedule 08.10.2012
comment
Из Tie::File документации: Файл не загружен в память, так что это будет работать даже для гигантских файлов - person Borodin; 08.10.2012