使用SED或AWK从另一个文件中替换所有模式

11
我尝试使用SED脚本进行模式替换,但它不能正常工作。
样本内容.txt
288Y2RZDBPX1000000001dhana
JP2F64EI1000000002d
EU9V3IXI1000000003dfg1000000001dfdfds
XATSSSSFOO4dhanaUXIBB7TF71000000004adf
10Q1W4ZEAV18LXNPSPGRTTIDHBN1000000005egw

模式.txt

1000000001 9000000003
1000000002 2000000001
1000000003 3000000001
1000000004 4000000001
1000000005 5000000001

期望的输出

288Y2RZDBPX9000000003dhana
JP2F64EI2000000001d
EU9V3IXI3000000001dfg9000000003dfdfds
XATSSSSFOO4dhanaUXIBB7TF74000000001adf
10Q1W4ZEAV18LXNPSPGRTTIDHBN5000000001egw

我能够使用单个 SED 替换来实现,比如:

sed  's/1000000001/1000000003/g' sample_content.txt

注意:

  • 匹配模式不在固定位置。
  • 单行可能有多个匹配值需要替换样本内容中的值。
  • sample_content.txt和patterns.txt有超过1百万条记录。

文件附件链接: https://drive.google.com/open?id=1dVzivKMirEQU3yk9KfPM6iE7tTzVRdt_

有没有人能够建议如何在不影响性能的情况下实现这一点?

更新于2018年2月11日

在分析了真实文件之后,我得到一个线索,即在第30和31个位置有一个grade值。它帮助我们确定在哪里进行替换。
如果等级为AB,则在41-50和101-110处替换10位电话号码。
如果等级为BC,则在11-20、61-70和151-160处替换10位电话号码。
如果等级为DE,则在1-10、71-80、151-160和181-190处替换10位电话号码。

像这样,我看到了50个独特的等级,用于200万个样本记录。

{   grade=substr($0,110,2)} // identify grade
{ 
    if (grade == "AB") {
        print substr($0,41,10) ORS substr($0,101,10)
    } else if(RT == "BC"){
        print substr($0,11,10) ORS substr($0,61,10) ORS substr($0,151,10) 
    }

    like wise 50 coiditions
}

请问这种方法可行,还有更好的方法吗?


3
关于“> 1百万条记录”和“不影响性能”的事项,祝好运。 - mpapec
@Сухой27,我完全同意你的看法。 - RavinderSingh13
1
到目前为止,你得到的两个解决方案都是将每个替换应用于整个输入行,而不是在进行前面的替换后剩余的输入行。因此,例如,如果sample_content.txt包含“xay”,patterns.txt包括“a b”和“b c”,那么工具将输出“xcy” - 这是预期的输出吗?还是应该是“xby”?无论哪种情况,你都应该在样本输入中包含一个测试该情况的案例。 - Ed Morton
1
@RavinderSingh13,OP明确表示“单行可能有多个匹配值需要在sample_content.txt中替换”,因此在第一次匹配后跳出循环虽然更快,但不会产生预期的输出。我认为仅仅对每个匹配字符串进行一次替换,而不是像你们两个脚本使用sub()而不是gsub()在每行上替换所有出现的每个字符串,是不正确的。 - Ed Morton
1
@EdMorton,好的Ed先生,我明白了,您说得对。 - RavinderSingh13
显示剩余3条评论
4个回答

7
尝试一下这个。应该很快。
$ sed -f <(printf 's/%s/%s/g\n' $(<patterns.txt)) contents.txt

这个功能可以以下面的方式格式化`patterns.txt`的数据,而不会实际更改`patterns.txt`的内容:
$ printf 's/%s/%s/g\n' $(<patterns.txt)
s/1000000001/9000000003/g
s/1000000002/2000000001/g
s/1000000003/3000000001/g
s/1000000004/4000000001/g
s/1000000005/5000000001/g

以上所有内容都使用进程替换 `<(...)` 提供给一个简单的 `sed` 作为脚本文件,使用 `sed -f` 开关从文件中读取 `sed` 命令。
$ sed -f <(printf 's/%s/%s/g\n' $(<patterns.txt)) contents.txt
288Y2RZDBPX9000000003dhana
JP2F64EI2000000001d
EU9V3IXI3000000001dfg9000000003dfdfds
XATSSSSFOO4dhanaUXIBB7TF74000000001adf
10Q1W4ZEAV18LXNPSPGRTTIDHBN5000000001egw

@Dhanabalan:也许这样更快:sed -f <(sed -E 's|(.*) (.*)|s/\1/\2/|g' patterns.txt) sample_content.txt - Cyrus

7

参考基准

测试环境:

使用您的示例文件patterns.txt,共50,000行,和contents.txt,也共50,000行。

所有解决方案都加载了patterns.txt中的所有行,但只检查contents.txt的前1000行。

测试笔记本电脑配备了双核64位Intel(R)Celeron(R)CPU N3050 @ 2.16GHz,4 GB RAM,Debian 9 64位测试版,gnu sed 4.4gnu awk 4.1.4

在所有情况下,输出都发送到新文件中,以避免在屏幕上打印数据时出现缓慢的开销。

结果:

1. RavinderSingh13第一种awk解决方案

$ time awk 'FNR==NR{a[$1]=$2;next}   {for(i in a){match($0,i);val=substr($0,RSTART,RLENGTH);if(val){sub(val,a[i])}};print}' patterns.txt  <(head -n 1000 contents.txt) >newcontents.txt

real    19m54.408s
user    19m44.097s
sys 0m1.981s

2. EdMorton的第一个awk解决方案

$ time awk 'NR==FNR{map[$1]=$2;next}{for (old in map) {gsub(old,map[old])}print}' patterns.txt <(head -n1000 contents.txt) >newcontents.txt

real    20m3.420s
user    19m16.559s
sys 0m2.325s

3. Sed(我的sed)解决方案

$ time sed -f <(printf 's/%s/%s/g\n' $(<patterns.txt)) <(head -n 1000 contents.txt) >newcontents.txt

real    1m1.070s
user    0m59.562s
sys 0m1.443s

4. Cyrus sed解决方案

$ time sed -f <(sed -E 's|(.*) (.*)|s/\1/\2/|g' patterns.txt) <(head -n1000 contents.txt) >newcontents.txt

real    1m0.506s
user    0m59.871s
sys 0m1.209s

5. RavinderSingh13的第二个awk解决方案

$ time awk 'FNR==NR{a[$1]=$2;next}{for(i in a){match($0,i);val=substr($0,RSTART,RLENGTH);if(val){sub(val,a[i]);print;next}};}1' patterns.txt  <(head -n 1000 contents.txt) >newcontents.txt

real    0m25.572s
user    0m25.204s
sys     0m0.040s

对于像1000行这样的少量输入数据,awk解决方案似乎很好。现在我们来测试一下9000行数据的性能比较。

6.RavinderSingh13的第二个awk解决方案,使用了9000行数据。

$ time awk 'FNR==NR{a[$1]=$2;next}{for(i in a){match($0,i);val=substr($0,RSTART,RLENGTH);if(val){sub(val,a[i]);print;next}};}1' patterns.txt  <(head -9000 contents.txt) >newcontents.txt

real    22m25.222s
user    22m19.567s
sys      0m2.091s

7. 9000行的Sed解决方案

$ time sed -f <(printf 's/%s/%s/g\n' $(<patterns.txt)) <(head -9000 contents.txt) >newcontents.txt

real    9m7.443s
user    9m0.552s
sys     0m2.650s

8. 使用9000行并行Seds解决方案

$ cat sedpar.sh
s=$SECONDS
sed -f <(printf 's/%s/%s/g\n' $(<patterns.txt)) <(head -3000 contents.txt) >newcontents1.txt &
sed -f <(printf 's/%s/%s/g\n' $(<patterns.txt)) <(tail +3001 contents.txt |head -3000) >newcontents2.txt &
sed -f <(printf 's/%s/%s/g\n' $(<patterns.txt)) <(tail +6001 contents.txt |head -3000) >newcontents3.txt &
wait
cat newcontents1.txt newcontents2.txt newcontents3.txt >newcontents.txt && rm -f newcontents1.txt newcontents2.txt newcontents3.txt
echo "seconds elapsed: $(($SECONDS-$s))"

$ time ./sedpar.sh
seconds elapsed: 309

real    5m16.594s
user    9m43.331s
sys     0m4.232s

将任务分成更多的命令,比如三个并行的sed,似乎可以加快速度。
如果您想在自己的电脑上重复基准测试,可以通过OP的链接或我的github下载文件contents.txtpatterns.txtcontents.txt patterns.txt

很酷,实际上那个链接对我来说不知道为什么不能用。再次感谢。 - RavinderSingh13
谢谢George,这是因为我正在数组(a)中查找匹配项并退出,所以它不会遍历整个数组 :) 我很高兴我能使它更有效率。非常感谢您的检查,先生。 - RavinderSingh13
1
非常感谢大家。这是我第一次学习这些脚本。从中获得了很多知识。再次感谢。 - Dhanabalan
@Dhanabalan 请查看并行 seds 解决方案。 - George Vasiliou
1
我想我得把那个正则表达式切割成一个仍然很大但更小的正则表达式数组。哎,无论如何,提问者已经选择了一个答案并且没有回应问题(请看我的评论),所以像他一样继续前进吧...再次感谢你进行时间测试。 - Ed Morton
显示剩余9条评论

3
请尝试以下awk命令并告诉我是否有所帮助。
解决方案1:
awk 'FNR==NR{a[$1]=$2;next}   {for(i in a){match($0,i);val=substr($0,RSTART,RLENGTH);if(val){sub(val,a[i])}};print}' patterns.txt  sample_content.txt

输出将如下所示。
288Y2RZDBPX9000000003dhana
JP2F64EI2000000001d
EU9V3IXI3000000001dfg9000000003dfdfds
XATSSSSFOO4dhanaUXIBB7TF74000000001adf
10Q1W4ZEAV18LXNPSPGRTTIDHBN5000000001egw

第一种解决方案的解释:现在在这里添加解释:
awk '
FNR==NR{                           ##FNR==NR is a condition which will be TRUE when only first Input_file patterns.txt is being read.
                                   ##FNR and NR both represents line number of Input_file(s) where FNR value will be RESET when a new Input_file is getting read on the other hand NR value will be keep increasing till all Input_file(s) read.
  a[$1]=$2;                        ##creating an array a whose index is first field of line and value is 2nd field of current line.
  next                             ##next will skip all further statements for now.
}
{
for(i in a){                       ##Starting a for loop which traverse through array a all element.
  match($0,i);                     ##Using match function of awk which will try to match index if array a present in variable i.
  val=substr($0,RSTART,RLENGTH);   ##Creating a variable named val which contains the substring of current line substring starts from value of variable RSTART till RLENGTH value.
  if(val){                         ##Checking condition if variable val is NOT NULL then do following:
    sub(val,a[i])}                 ##using sub function of awk to substitute variable val value with array a value of index i.
};
  print                            ##Using print here to print the current line either changed or not changed one.
}
' patterns.txt  sample_content.txt ##Mentioning the Input_file(s) name here.

第二种解决方案:不需要像第一种解决方案那样一直遍历数组,当找到匹配项时直接退出数组。
awk '
FNR==NR{                           ##FNR==NR is a condition which will be TRUE when only first Input_file patterns.txt is being read.
                                   ##FNR and NR both represents line number of Input_file(s) where FNR value will be RESET when a new Input_file is getting read on the other hand NR value will be keep increasing till all Input_file(s) read.
  a[$1]=$2;                        ##creating an array a whose index is first field of line and value is 2nd field of current line.
  next                             ##next will skip all further statements for now.
}
{
for(i in a){                       ##Starting a for loop which traverse through array a all element.
  match($0,i);                     ##Using match function of awk which will try to match index if array a present in variable i.
  val=substr($0,RSTART,RLENGTH);   ##Creating a variable named val which contains the substring of current line substring starts from value of variable RSTART till RLENGTH value.
  if(val){                         ##Checking condition if variable val is NOT NULL then do following:
    sub(val,a[i]);print;next}                 ##using sub function of awk to subsitute variable val value with array a value of index i.
};
}
1
' patterns.txt  sample_content.txt ##Mentioning the Input_file(s) name here.

预期输出结果很好,但是它花费了太多时间来完成。 - Dhanabalan
我已经附上了样本输入文件 https://drive.google.com/open?id=1dVzivKMirEQU3yk9KfPM6iE7tTzVRdt_ - Dhanabalan
@Dhanabalan,让我们先计算一下需要多少时间,然后再看看我们能做些什么,让我们在同一个地方知道。 - RavinderSingh13
它仍在运行 :( - Dhanabalan
1
@Dhanabalan,请耐心测试这两个解决方案,并在它们之前添加“time”命令来记录它们的时间。然后告诉我们进展如何。我们不能指望处理数百万行只需要几秒钟,伙计。 - RavinderSingh13
1
这比必要的要复杂得多(你从i开始,然后使用match()+substr()来获取val,而当val始终与i相同时,你会做一个sub()来查找刚刚在match()上执行的相同字符串!)。请参见我在https://dev59.com/DVYM5IYBdhLWcg3wjAgR#48720400发布的第一个脚本,以了解此方法的简单实现。如果同一字符串在1行上出现多次,则您的代码也将失败,因为它只会替换每个字符串的第一个出现。 - Ed Morton

2
简单的方法是:
$ cat tst.awk
NR==FNR {
    map[$1] = $2
    next
}
{
    for (old in map) {
        gsub(old,map[old])
    }
    print
}

$ awk -f tst.awk patterns.txt sample_content.txt
288Y2RZDBPX9000000003dhana
JP2F64EI2000000001d
EU9V3IXI3000000001dfg9000000003dfdfds
XATSSSSFOO4dhanaUXIBB7TF74000000001adf
10Q1W4ZEAV18LXNPSPGRTTIDHBN5000000001egw

与目前为止发布的其他解决方案一样,此解决方案将每个替换应用于整行,因此如果给定包含xaya b以及b c的patterns.txt和sample_content.txt,则该工具将输出xcy而不是xby
或者您可以尝试这个:
$ cat tst.awk
NR==FNR {
    map[$1] = $2
    re = re sep $1
    sep = "|"
    next
}
{
    head = ""
    tail = $0
    while ( match(tail,re) ) {
        head = head substr(tail,1,RSTART-1) map[substr(tail,RSTART,RLENGTH)]
        tail = substr(tail,RSTART+RLENGTH)
    }
    print head tail
}

$ awk -f tst.awk patterns.txt sample_content.txt
288Y2RZDBPX9000000003dhana
JP2F64EI2000000001d
EU9V3IXI3000000001dfg9000000003dfdfds
XATSSSSFOO4dhanaUXIBB7TF74000000001adf
10Q1W4ZEAV18LXNPSPGRTTIDHBN5000000001egw

这种方法有几个优点:

  1. 在我提到的情况下,它会输出 xby (这可能是您真正想要的)
  2. 每行sample_content.txt只进行与之匹配的正则表达式比较,而不是每行patterns.txt都对sample_content.txt的每一行进行1次比较
  3. 它仅操作前一个替换后剩余的部分,因此被测试的字符串会不断缩小
  4. 它不会改变$0,因此awk不必重新编译和重新拆分该记录以进行每个替换。

因此,假设从patterns.txt构建的正则表达式不是非常庞大,以至于仅仅由于其大小就导致性能下降,那么它应该比原始脚本快得多。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接