找到并替换许多单词

时间:2011-11-23 14:31:06

标签: ruby perl bash python-2.7

我经常需要在文件中进行多次替换。为解决此问题,我创建了两个文件old.textnew.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 个答案:

答案 0 :(得分:8)

  • 第6行,sed命令不知道$ number的结束位置。

尝试使用双引号引用变量

  

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中的用户定义变量需要在awk的BEGIN语句中使用或预定义时定义。您可以使用-v选项包含shell变量。

以下是bash中可以满足您需求的解决方案。

Bash解决方案:

#!/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 filereplacement file读取一行,并执行in-line sed替换。

答案 1 :(得分:4)

为什么不

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 :(得分:2)

这个Python 2脚本将旧单词组成一个正则表达式,然后根据匹配的旧单词的索引替换相应的新单词。旧词只有在不同时才会匹配。通过围绕r'\ b'中的单词强制执行这种清晰度,r'\ b'是正则表达式单词边界。

输入来自命令行(它们是我在空闲时用于开发的注释选项)。输出是stdout

在此解决方案中,主文本仅扫描一次。根据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] )

我刚刚对~100K字节文本文件做了一些统计:

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%

该文本是由here生成的250个lorum ipsum段落。我刚刚接受了十个最常出现的单词,并按顺序将它们替换为字符串ONE到TEN。

Python正则表达式解决方案比Jaypal当前选择的最佳解决方案快一个数量级。 Python选择将替换单词后跟换行符或标点符号以及任何空格(包括制表符等)。

有人评论说C解决方案既简单又易创。几十年前,一些聪明的Unix研究员发现通常情况并非如此,并且创建了诸如awk之类的脚本工具来提高生产力。此任务是脚本语言的理想选择,Python中显示的技术可以在Ruby或Perl中复制。

  • 水稻

答案 3 :(得分:2)

我喜欢这类问题,所以这是我的答案:

首先为简单起见,为什么不使用包含源和翻译的文件。我的意思是:(文件名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”表示在源文件中替换,而在s //命令中的“I”表示忽略大小写 - 一个GNU扩展,检查你的sed实现 -

当然请注意,bash while循环比python或类似的脚本语言慢得多。根据您的需要,您可以进行嵌套,一个在源文件上,另一个在内部循环翻译(更改)。与stdout呼应管道灵活性。

#!/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

答案 4 :(得分:2)

我发现一般的perl解决方案可以很好地替换地图中的键及其关联值:

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

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

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

您必须先将两个文件读入地图(显然),但一旦完成,您只需要在每一行上进行一次传递,并为每次替换进行一次散列查找。我只用相对较小的地图(大约1,000个条目)尝试过,所以如果你的地图要大得多,就不能保证。

答案 5 :(得分:1)

  

在第6行,sed命令不知道$ number的结束位置。

linefromnewwords=$(sed -n '${number}p' newwords.txt)

我不确定引用,但$ {number} p会起作用 - 也许是“$ {number} p”

  

$ number变量更改为“0 + 1”,然后更改为“0 + 1 + 1”,更改为“1”,然后变为“2”。

bash中的算术整数求值可以使用$(())来完成,并且优于eval(eval = evil)。

number=$((number + 1))

一般情况下,我建议使用一个带

的文件
s/ ni3 / nǐ /g
s/ nei3 / neǐ /g

依此类推,每行一个sed命令,这是最好的照顾 - 按字母顺序排序,并将其用于:

sed -f translate.sed input > output 

因此,您可以轻松地比较映射。

s/\bni3\b/nǐ/g

可能优先于空白作为显式分隔符,因为\b:=word boundary匹配行的开头/结尾和标点字符。

答案 6 :(得分:1)

这应该通过某种方式减少时间,因为这可以避免不必要的循环。

合并两个输入文件:

假设您有两个输入文件, old.text 包含所有替换 new.text ,其中包含所有替换 EM>。

我们将使用以下sed script单行创建一个新文本文件,作为awk主文件:

awk '{ printf "s/ "$0" /"; getline <"new.text"; print " "$0" /g" }' old.text > merge.text 

[jaypal:~/Temp] cat old.text 
19
20

[jaypal:~/Temp] cat new.text 
A
B

[jaypal:~/Temp] awk '{ printf "s/ "$0" /"; getline <"new.text"; print " "$0" /g" }' old.text > merge.text

[jaypal:~/Temp] cat merge.text 
s/ 19 / A /g
s/ 20 / B /g

注意: 此替换和替换的格式取决于您在单词之间有空格的要求。

将合并文件用作sed脚本:

创建合并文件后,我们将使用-f option实用程序的sed

sed -f merge.text input_file

[jaypal:~/Temp] cat input_file 
 12 adsflljl
 12 hgfahld
 12 ash;al
 13 a;jfda
 13 asldfj
 15 ;aljdf
 16 a;dlfj
 19 adads
 19 adfasf
 20 aaaadsf

[jaypal:~/Temp] sed -f merge.text input_file 
 12 adsflljl
 12 hgfahld
 12 ash;al
 13 a;jfda
 13 asldfj
 15 ;aljdf
 16 a;dlfj
 A adads
 A adfasf
 B aaaadsf

您可以使用>运算符将其重定向到另一个文件。

答案 7 :(得分:1)

这可能对您有用:

paste {old,new}words.txt | 
sed 's,\(\w*\)\s*\(\w*\),s!\\<\1\\>!\2!g,' | 
sed -i -f - text.txt

答案 8 :(得分:1)

这是一个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进行空格处理。

答案 9 :(得分:1)

这是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

答案 10 :(得分:1)

我不确定为什么以前的大多数海报都坚持使用正则表达式来解决这个问题,我认为这比大多数(如果不是最快的方法)要快。

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,结果将打印到 stdout

答案 11 :(得分:1)

编辑 - 我刚注意到像我这样的两个答案已经在这里......所以你可以忽视我的:)

我相信这个perl脚本虽然没有使用花哨的sed或awk的东西,却能很快地完成工作......

我确实冒昧地使用另一种格式的old_word来new_word: csv格式。如果它太复杂了,请告诉我,我将添加一个带有old.txt,new.txt的脚本并构建csv文件。

带上它让我知道!

顺便说一下 - 如果你们这里的任何一位大师都可以建议我做一些更好的方式来做我在这里做的事情,我很乐意阅读评论:

    #! /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;
    }