Нахождение и замена многих слов - PullRequest
5 голосов
/ 23 ноября 2011

Мне часто приходится делать много замен в файлах.Чтобы решить эту проблему, я создал два файла old.text и new.text.Первый содержит список слов, которые необходимо найти.Второй содержит список слов, которые должны заменить их.

  • Все мои файлы используют UTF-8 и используют различные языки.

Я создал этот скрипт, который я надеялся сделать замену.Сначала он читает old.text по одной строке за раз, а затем заменяет слова в этой строке в файле input.txt соответствующими словами из файла new.text.

#!/bin/sh
number=1
while read linefromoldwords
do
    echo $linefromoldwords
    linefromnewwords=$(sed -n '$numberp' new.text)
    awk '{gsub(/$linefromoldwords/,$linefromnewwords);print}' input.txt >> output.txt
    number=$number+1
echo $number
done <  old.text

Однако мое решение не позволяетхорошо работать.Когда я запускаю скрипт:

  • В строке 6 команда sed не знает, где заканчивается $number.
  • Переменная $number меняется на "0+1 ", затем" 0 + 1 + 1 ", когда он должен измениться на" 1 ", затем" 2 ".
  • Строка с awk, похоже, не делает ничего, кроме копированияinput.txt точно такой же, как и output.txt.

У вас есть предложения?

Обновление:

Отмеченный ответ работает хорошо, однако я используюЭтот сценарий много, и это займет много часов, чтобы закончить.Поэтому я предлагаю вознаграждение за решение, которое может выполнить эти замены намного быстрее.Решение в BASH, Perl или Python 2 будет в порядке, при условии, что оно по-прежнему совместимо с UTF-8.Если вы думаете, что какое-то другое решение, использующее другое программное обеспечение, обычно доступное в системах Linux, будет быстрее, то это тоже может быть хорошо, если не требуются огромные зависимости.

Ответы [ 12 ]

8 голосов
/ 23 ноября 2011
  • В одной строке 6 команда sed не знает, где заканчивается число $.

Попробуйте заключить переменную в кавычки

linefromnewwords = $ (sed -n "$ number" p newwords.txt)

  • Переменная $ number меняется на «0 + 1», затем «0 + 1 + 1», когда она должна измениться на «1», затем «2».

Сделайте это вместо:

number = `expr $ number + 1`

  • Строка с awk, похоже, не выполняет ничего, кроме копирования input.txt точно так же, как и output.txt.

awk не будет выводить переменные за пределы своей области видимости. Определенные пользователем переменные в awk должны быть либо определены, когда они используются, либо предопределены в выражении BEGIN в awk. Вы можете включить переменные оболочки, используя опцию -v.

Вот решение в bash, которое будет делать то, что вам нужно.

Bash Solution:

#!/bin/bash

while read -r sub && read -r rep <&3; do
  sed -i "s/ "$sub" / "$rep" /g" main.file
done <old.text 3<new.text

Это решение читает по одной строке за раз от substitution file и replacement file и выполняет замену in-line sed.

4 голосов
/ 23 ноября 2011

Почему бы не

paste -d/ oldwords.txt newwords.txt |\
sed -e 's@/@ / @' -e 's@^@s/ @' -e 's@$@ /g@' >/tmp/$$.sed

sed -f /tmp/$$.sed original >changed

rm /tmp/$$.sed

?

2 голосов
/ 15 декабря 2011

Общее решение Perl, которое, как мне показалось, хорошо работает для замены ключей на карте их соответствующими значениями, таково:

my %map = (
    19 => 'A',
    20 => 'B',
);

my $key_regex = '(' . join('|', keys %map) . ')';

while (<>) {
    s/$key_regex/$map{$1}/g;
    print $_;
}

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

2 голосов
/ 12 декабря 2011

Мне нравятся вопросы такого рода, поэтому вот мой ответ:

Сначала для простоты, почему бы не использовать только файл с источником и переводом.Я имею в виду: (имя файла changeThis)

hello=Bye dudes
the morNing=next Afternoon
first=last

Тогда вы можете определить правильный разделитель в скрипте.(файл replaceWords.sh)

#!/bin/bash

SEP=${1}
REPLACE=${2}
FILE=${3}
while read transline
do
   origin=${transline%%${SEP}*}
   dest=${transline##*${SEP}}
   sed -i "s/${origin}/${dest}/gI" $FILE
done < $REPLACE

Возьмите этот пример (файл changeMe)

Hello, this is me. 
I will be there at first time in the morning

Назовите его с помощью

$ bash replaceWords.sh = changeThis changeMe 

И вы получите

Bye dudes, this is me.
I will be there at last time in next Afternoon

Обратите внимание на развлечение "я" с sed.«-i» означает заменить в исходном файле, а «I» в команде s // означает игнорировать регистр -a расширение GNU, проверить вашу реализацию sed-

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

#!/bin/bash

SEP=${1}
TRANSLATION=${2}
FILE=${3}
while read line
do
   while read transline
   do
      origin=${transline%%${SEP}*}
      dest=${transline##*${SEP}}
      line=$(echo $line | sed "s/${origin}/${dest}/gI")
   done < $TRANSLATION
   echo $line
done < $FILE
2 голосов
/ 11 декабря 2011

Этот скрипт Python 2 формирует старые слова в одно регулярное выражение, а затем заменяет соответствующее новое слово на основе индекса старого слова, которое соответствует.Старые слова сопоставляются, только если они различны.Эта особенность обеспечивается окружением слова в r '\ b', которое является границей слова регулярного выражения.

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

Основной текст сканируется только один раз в этом решении.С ответом от Jaypals, вывод такой же.

#!/bin/env python

import sys, re

def replacer(match):
    global new
    return new[match.lastindex-1]

if __name__ == '__main__':
    fname_old, fname_new, fname_txt = sys.argv[1:4]
    #fname_old, fname_new, fname_txt = 'oldwords.txt oldwordreplacements.txt oldwordreplacer.txt'.split()

    with file(fname_old) as f:
        # Form regular expression that matches old words, grouped in order
        old = '(?:' + '|'.join(r'\b(%s)\b' % re.escape(word)
                               for word in f.read().strip().split()) + ')'
    with file(fname_new) as f:
        # Ordered list of replacement words 
        new = [word for word in f.read().strip().split()]
    with file(fname_txt) as f:
        # input text
        txt = f.read()
    # Output the new text
    print( re.subn(old, replacer, txt)[0] )

Я только что сделал некоторые статистические данные для текстового файла размером ~ 100 Кбайт:

Total characters in text: 116413
Total words in text: 17114
Total distinct words in text: 209
Top 10 distinct word occurences in text: 2664 = 15.57%

Текст состоял из 250 абзацевlorum ipsum, сгенерированный из здесь Я просто взял десять наиболее часто встречающихся слов и заменил их на строки от ОДНА до ДЕСЯТИ по порядку.

Решение регулярного выражения Python на порядок быстрее, чемНа данный момент выбрано лучшее решение от Jaypal.Выбор Python заменит слова, за которыми следует символ новой строки или пунктуация, а также любые пробелы (включая вкладки и т. Д.).

Кто-то заметил, что решение на C будет простым и быстрым в создании.Несколько десятилетий назад некоторые мудрые юноши из Unix заметили, что это обычно не так, и создали инструменты для создания сценариев, такие как awk, для повышения производительности.Это задание идеально подходит для языков сценариев, и техника, показанная в приложении Python, может быть воспроизведена в Ruby или Perl.

  • Paddy.
1 голос
/ 14 декабря 2011

РЕДАКТИРОВАТЬ - Я только что заметил, что два ответа, как мой, уже здесь ... так что вы можете просто игнорировать мой:)

Я считаю, что этот скрипт на Perl, хотя и не использует причудливые вещи sed или awk, делаетработа довольно быстрая ...

Я позволил себе использовать другой формат от old_word до new_word: формат csv.если это слишком сложно сделать, дайте мне знать, и я добавлю скрипт, который берет ваш old.txt, new.txt и создает файл csv.

запустите его и дайте мне знать!

кстати - если кто-нибудь из вас, гуру Perl, может предложить более изощренный способ сделать что-то, что я делаю здесь, я с удовольствием прочту комментарий:

    #! /usr/bin/perl
    # getting the user's input
    if ($#ARGV == 1)
        {
        $LUT_file = shift;
        $file = shift;
        $outfile = $file . ".out.txt";
        }
    elsif ($#ARGV == 2)
        {
        $LUT_file = shift;
        $file = shift;
        $outfile = shift;
        }
    else { &usage; }

    # opening the relevant files

    open LUT, "<",$LUT_file or die "can't open $signal_LUT_file for reading!\n : $!";
    open FILE,"<",$file or die "can't open $file for reading!\n : $!";
    open OUT,">",$outfile or die "can't open $outfile for writing\n :$!";

    # getting the lines from the text to be changed and changing them
    %word_LUT = ();
    WORD_EXT:while (<LUT>)
        {
        $_ =~ m/(\w+),(\w+)/;
        $word_LUT{ $1 } =  $2 ;
        }
    close LUT;

    OUTER:while ($line = <FILE>)
        {
        @words = split(/\s+/,$line);
        for( $i = 0; $i <= $#words; $i++)
            {
            if ( exists ($word_LUT { $words[$i] }) ) 
                {
                $words[$i] = $word_LUT { $words[$i] };
                }

            }
        $newline = join(' ',@words);
        print "old line - $line\nnewline - $newline\n\n";
        print OUT $newline . "\n";

        }   
    # now we have all the signals needed in the swav array, build the file.

        close OUT;close FILE;

    # Sub Routines
    #
    #

    sub usage(){
    print "\n\n\replacer.pl Usage:\n";
    print "replacer.pl <LUT file> <Input file> [<out file>]\n\n";
    print "<LUT file> -    a LookUp Table of words, from the old word to the new one.
    \t\t\twith the following csv format:
    \t\t\told word,new word\n";
    print "<Input file>       -    the input file\n";
    print "<out file>         -    out file is optional. \nif not entered the default output file will be: <Input file>.out.txt\n\n";

    exit;
    }
1 голос
/ 14 декабря 2011

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

use warnings;
use strict;

open (my $fh_o, '<', "old.txt");
open (my $fh_n, '<', "new.txt");

my @hay = <>;
my @old = map {s/^\s*(.*?)\s*$/$1/; $_} <$fh_o>;
my @new = map {s/^\s*(.*?)\s*$/$1/; $_} <$fh_n>;

my %r;
;  @r{@old} = @new;

print defined  $r{$_} ? $r{$_} : $_ for split (
  /(\s+)/, "@hay"
);

Использование: perl script.pl /file/to/modify, результат выводится на стандартный вывод .

1 голос
/ 13 декабря 2011

Вот решение на Perl.Это может быть упрощено, если вы объедините свои списки входных слов в один список: каждая строка содержит карту старых и новых слов.

#!/usr/bin/env perl

# usage:
#   replace.pl OLD.txt NEW.txt INPUT.txt >> OUTPUT.txt

use strict;
use warnings;

sub read_words {
    my $file = shift;

    open my $fh, "<$file" or die "Error reading file: $file; $!\n";
    my @words = <$fh>;
    chomp @words;
    close $fh;

    return \@words;
}

sub word_map {
    my ($old_words, $new_words) = @_;

    if (scalar @$old_words != scalar @$new_words) {
        warn "Old and new word lists are not equal in size; using the smaller of the two sizes ...\n";
    }
    my $list_size = scalar @$old_words;
    $list_size = scalar @$new_words if $list_size > scalar @$new_words;

    my %map = map { $old_words->[$_] => $new_words->[$_] } 0 .. $list_size - 1;

    return \%map;
}

sub build_regex {
    my $words = shift;

    my $pattern = join "|", sort { length $b <=> length $a } @$words;

    return qr/$pattern/;
}

my $old_words = read_words(shift);
my $new_words = read_words(shift);
my $word_map = word_map($old_words, $new_words);
my $old_pattern = build_regex($old_words);

my $input_file = shift;
open my $input, "<$input_file" or die "Error reading input file: $input_file; $!\n";
while (<$input>) {
    s/($old_pattern)/$word_map->{$&}/g;
    print;
}
close $input;
__END__

Файл старых слов:

$ cat old.txt 
19
20

Новые словафайл:

$ cat new.txt 
A
B

Входной файл:

$ cat input.txt 
12 adsflljl
12 hgfahld
12 ash;al
13 a;jfda
13 asldfj
15 ;aljdf
16 a;dlfj
19 adads
19 adfasf
20 aaaadsf

Создать вывод:

$ perl replace.pl old.txt new.txt input.txt
12 adsflljl
12 hgfahld
12 ash;al
13 a;jfda
13 asldfj
15 ;aljdf
16 a;dlfj
A adads
A adfasf
B aaaadsf
1 голос
/ 12 декабря 2011

Вот скрипт Python 2, который должен быть эффективным с точки зрения пространства и времени:

import sys
import codecs
import re

sub = dict(zip((line.strip() for line in codecs.open("old.txt", "r", "utf-8")),
               (line.strip() for line in codecs.open("new.txt", "r", "utf-8"))))

regexp = re.compile('|'.join(map(lambda item:r"\b" + re.escape(item) + r"\b", sub)))

for line in codecs.open("input.txt", "r", "utf-8"):
    result = regexp.sub(lambda match:sub[match.group(0)], line)
    sys.stdout.write(result.encode("utf-8"))

Вот оно в действии:

$ cat old.txt 
19
20
$ cat new.txt 
A
B
$ cat input.txt 
12 adsflljl
12 hgfahld
12 ash;al
13 a;jfda
13 asldfj
15 ;aljdf
16 a;dlfj
19 adads
19 adfasf
20 aaaadsf
$ python convert.py 
12 adsflljl
12 hgfahld
12 ash;al
13 a;jfda
13 asldfj
15 ;aljdf
16 a;dlfj
A adads
A adfasf
B aaaadsf
$

РЕДАКТИРОВАТЬ: Шляпа подсказки @ Paddy3118 для обработки пробелов.

1 голос
/ 11 декабря 2011

Это может работать для вас:

paste {old,new}words.txt | 
sed 's,\(\w*\)\s*\(\w*\),s!\\<\1\\>!\2!g,' | 
sed -i -f - text.txt
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...