我想在一些文本上执行许多查找和替换操作。我有一个UTF-8 CSV文件,其中包含要查找的内容(在第一列中)以及替换内容(在第二列中),从最长到最短排列。
E.g:
orange,fruit2
carrot,vegetable1
apple,fruit3
pear,fruit4
ink,item1
table,item2
原始档案:
"I like to eat apples and carrots"
产生的输出文件:
"I like to eat fruit3s and vegetable1s."
但是,我想确保如果已经替换了一部分文本,那么它不会混淆已经替换的文本。换句话说,我不希望它看起来像这样(它与vegetable1中的“table”匹配):
"I like to eat fruit3s and vegeitem21s."
目前,我使用的方法非常慢,因为我必须完成整个查找和替换两次:
(1)将CSV转换为三个文件,例如:
a.csv b.csv c.csv
orange 0001 fruit2
carrot 0002 vegetable1
apple 0003 fruit3
pear 0004 fruit4
ink 0005 item1
table 0006 item 2
(2)然后,将a.csv
中的file.txt
中的所有项目替换为b.csv
中的匹配列,并在文字周围使用ZZZ
以确保没有错误稍后匹配数字:
a=1
b=`wc -l < ./a.csv`
while [ $a -le $b ]
do
for i in `sed -n "$a"p ./b.csv`; do
for j in `sed -n "$a"p ./a.csv`; do
sed -i "s/$i/ZZZ$j\ZZZ/g" ./file.txt
echo "Instances of '"$i"' replaced with '"ZZZ$j\ZZZ"' ("$a"/"$b")."
a=`expr $a + 1`
done
done
done
(3)然后再次运行相同的脚本,但要将ZZZ0001ZZZ
替换为fruit2
的{{1}}。
运行第一次替换需要大约2个小时,但由于我必须运行此代码两次以避免编辑已经替换的项目,因此需要两倍的时间。是否有更有效的方法来运行查找和替换不会对已替换的文本执行替换?
答案 0 :(得分:6)
这是一个在“一个阶段”中进行替换的perl解决方案。
#!/usr/bin/perl
use strict;
my %map = (
orange => "fruit2",
carrot => "vegetable1",
apple => "fruit3",
pear => "fruit4",
ink => "item1",
table => "item2",
);
my $repl_rx = '(' . join("|", map { quotemeta } keys %map) . ')';
my $str = "I like to eat apples and carrots";
$str =~ s{$repl_rx}{$map{$1}}g;
print $str, "\n";
答案 1 :(得分:3)
Tcl有一个命令就是这样做:string map
tclsh <<'END'
set map {
"orange" "fruit2"
"carrot" "vegetable1"
"apple" "fruit3"
"pear" "fruit4"
"ink" "item1"
"table" "item2"
}
set str "I like to eat apples and carrots"
puts [string map $map $str]
END
I like to eat fruit3s and vegetable1s
这是如何在bash中实现它(对于关联数组需要bash v4)
declare -A map=(
[orange]=fruit2
[carrot]=vegetable1
[apple]=fruit3
[pear]=fruit4
[ink]=item1
[table]=item2
)
str="I like to eat apples and carrots"
echo "$str"
i=0
while (( i < ${#str} )); do
matched=false
for key in "${!map[@]}"; do
if [[ ${str:$i:${#key}} = $key ]]; then
str=${str:0:$i}${map[$key]}${str:$((i+${#key}))}
((i+=${#map[$key]}))
matched=true
break
fi
done
$matched || ((i++))
done
echo "$str"
I like to eat apples and carrots
I like to eat fruit3s and vegetable1s
这不会很快。
显然,如果您以不同方式订购地图,则可能会得到不同的结果。实际上,我认为"${!map[@]}"
的顺序未指定,因此您可能希望明确指定键的顺序:
keys=(orange carrot apple pear ink table)
# ...
for key in "${keys[@]}"; do
答案 2 :(得分:2)
这样做的一种方法是进行两阶段替换:
phase 1: s/orange/@@1##/ s/carrot/@@2##/ ... phase 2: s/@@1##/fruit2/ s/@@2##/vegetable1/ ...
应选择@@ 1 ##标记,以便它们不会出现在原始文本或替换中。
以下是perl中的概念验证实现:
#!/usr/bin/perl -w
#
my $repls = $ARGV[0];
die ("first parameter must be the replacement list file") unless defined ($repls);
my $tmpFmt = "@@@%d###";
open(my $replsFile, "<", $repls) || die("$!: $repls");
shift;
my @replsList;
my $i = 0;
while (<$replsFile>) {
chomp;
my ($from, $to) = /\"([^\"]*)\",\"([^\"]*)\"/;
if (defined($from) && defined($to)) {
push(@replsList, [$from, sprintf($tmpFmt, ++$i), $to]);
}
}
while (<>) {
foreach my $r (@replsList) {
s/$r->[0]/$r->[1]/g;
}
foreach my $r (@replsList) {
s/$r->[1]/$r->[2]/g;
}
print;
}
答案 3 :(得分:1)
做两次可能不你的问题。如果你设法使用基本策略一次,它仍然需要一个小时,对吗?您可能需要使用不同的技术或工具。如上所述,切换到Perl可能会使代码更快(尝试一下)
但是继续走下其他海报的道路,下一步可能是流水线。编写一个替换两列的小程序,然后同时运行该程序两次。第一次运行使用column2中的字符串交换column1中的字符串,然后使用column3中的字符串交换column2中的字符串。
您的命令行将是这样的
cat input_file.txt | perl replace.pl replace_file.txt 1 2 | perl replace.pl replace_file.txt 2 3 > completely_replaced.txt
而且replace.pl就像这样(类似于其他解决方案)
#!/usr/bin/perl -w
my $replace_file = $ARGV[0];
my $before_replace_colnum = $ARGV[1] - 1;
my $after_replace_colnum = $ARGV[2] - 1;
open(REPLACEFILE, $replace_file) || die("couldn't open $replace_file: $!");
my @replace_pairs;
# read in the list of things to replace
while(<REPLACEFILE>) {
chomp();
my @cols = split /\t/, $_;
my $to_replace = $cols[$before_replace_colnum];
my $replace_with = $cols[$after_replace_colnum];
push @replace_pairs, [$to_replace, $replace_with];
}
# read input from stdin, do swapping
while(<STDIN>) {
# loop over all replacement strings
foreach my $replace_pair (@replace_pairs) {
my($to_replace,$replace_with) = @{$replace_pair};
$_ =~ s/${to_replace}/${replace_with}/g;
}
print STDOUT $_;
}
答案 4 :(得分:1)
我猜你的大部分迟缓都来自创建如此多的sed命令,每个命令都需要单独处理整个文件。对当前进程进行一些小的调整可以通过每步每个文件运行1个sed来加快这一速度。
a=1
b=`wc -l < ./a.csv`
while [ $a -le $b ]
do
cmd=""
for i in `sed -n "$a"p ./a.csv`; do
for j in `sed -n "$a"p ./b.csv`; do
cmd="$cmd ; s/$i/ZZZ${j}ZZZ/g"
echo "Instances of '"$i"' replaced with '"ZZZ${j}ZZZ"' ("$a"/"$b")."
a=`expr $a + 1`
done
done
sed -i "$cmd" ./file.txt
done
答案 5 :(得分:1)
bash + sed方法:
count=0
bigfrom=""
bigto=""
while IFS=, read from to; do
read countmd5sum x < <(md5sum <<< $count)
count=$(( $count + 1 ))
bigfrom="$bigfrom;s/$from/$countmd5sum/g"
bigto="$bigto;s/$countmd5sum/$to/g"
done < replace-list.csv
sed "${bigfrom:1}$bigto" input_file.txt
我选择了md5sum,以获得一些独特的令牌。但是也可以使用其他一些机制来生成这样的令牌;比如从/dev/urandom
或shuf -n1 -i 10000000-20000000
答案 6 :(得分:1)
awk + sed方法:
awk -F, '{a[NR-1]="s/####"NR"####/"$2"/";print "s/"$1"/####"NR"####/"}; END{for (i=0;i<NR;i++)print a[i];}' replace-list.csv > /tmp/sed_script.sed
sed -f /tmp/sed_script.sed input.txt
cat + sed + sed方法:
cat -n replace-list.csv | sed -rn 'H;g;s|(.*)\n *([0-9]+) *[^,]*,(.*)|\1\ns/####\2####/\3/|;x;s|.*\n *([0-9]+)[ \t]*([^,]+).*|s/\2/####\1####/|p;${g;s/^\n//;p}' > /tmp/sed_script.sed
sed -f /tmp/sed_script.sed input.txt
的作用机制:
注意:
####<number>####
作为某种模式,输入文件中不存在该模式。如果需要,请更改此模式。cat -n |
不是UUOC:)答案 7 :(得分:1)
这可能适合你(GNU sed):
sed -r 'h;s/./&\\n/g;H;x;s/([^,]*),.*,(.*)/s|\1|\2|g/;$s/$/;s|\\n||g/' csv_file | sed -rf - original_file
将csv
文件转换为sed
脚本。这里的技巧是将替换字符串替换为不会被重新替换的字符串。在这种情况下,替换字符串中的每个字符都由其自身和\n
替换。最后,一旦完成所有替换,\n
将被移除,留下完成的字符串。
答案 8 :(得分:1)
这里有很多很酷的答案。我发布这个是因为我采用了稍微不同的方法,对要替换的数据做了一些大的假设(基于样本数据):
这是一次通过,awk只回答非常少的正则表达式。
它将“repl.csv”文件读入一个关联数组(参见BEGIN {}),然后在单词的长度受键长限制约束时尝试匹配每个单词的前缀,试图避免查看尽可能使用关联数组:
#!/bin/awk -f
BEGIN {
while( getline repline < "repl.csv" ) {
split( repline, replarr, "," )
replassocarr[ replarr[1] ] = replarr[2]
# set some bounds on the replace word sizes
if( minKeyLen == 0 || length( replarr[1] ) < minKeyLen )
minKeyLen = length( replarr[1] )
if( maxKeyLen == 0 || length( replarr[1] ) > maxKeyLen )
maxKeyLen = length( replarr[1] )
}
close( "repl.csv" )
}
{
i = 1
while( i <= NF ) { print_word( $i, i == NF ); i++ }
}
function print_word( w, end ) {
wl = length( w )
for( j = wl; j >= 0 && prefix_len_bound( wl, j ); j-- ) {
key = substr( w, 1, j )
wl = length( key )
if( wl >= minKeyLen && key in replassocarr ) {
printf( "%s%s%s", replassocarr[ key ],
substr( w, j+1 ), !end ? " " : "\n" )
return
}
}
printf( "%s%s", w, !end ? " " : "\n" )
}
function prefix_len_bound( len, jlen ) {
return len >= minKeyLen && (len <= maxKeyLen || jlen > maxKeylen)
}
基于如下输入:
I like to eat apples and carrots
orange you glad to see me
Some people eat pears while others drink ink
它产生的输出如下:
I like to eat fruit3s and vegetable1s
fruit2 you glad to see me
Some people eat fruit4s while others drink item1
当然,当要替换的单词长度= 1或者平均单词长度远远超过要替换的单词时,任何不看替换文件的“节省”都会消失。