【问题标题】:SED or AWK replace all with patterns from another fileSED 或 AWK 用另一个文件中的模式替换所有内容
【发布时间】:2018-07-21 00:29:51
【问题描述】:

我正在尝试使用 SED 脚本进行模式替换,但它无法正常工作

sample_content.txt

288Y2RZDBPX1000000001dhana
JP2F64EI1000000002d
EU9V3IXI1000000003dfg1000000001dfdfds
XATSSSSFOO4dhanaUXIBB7TF71000000004adf
10Q1W4ZEAV18LXNPSPGRTTIDHBN1000000005egw

patterns.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 中单行可能有多个匹配值要替换
  • Sample_content.txt 和 patterns.txt 的记录超过 100 万条

文件附件链接: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 级,则将 10 位电话号码替换为 1-10、71-80、151-160 和 181-190

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

{   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
}

我想知道,这种方法是可取的还是其他更好的方法?

【问题讨论】:

  • 回复:> 1 Million records && without affecting performance 祝你好运
  • @Сухой27,我完全赞同你。
  • 到目前为止,您获得的两种解决方案都将每个替换应用于整个输入行,而不是在先前替换之后的输入行的其余部分,例如,如果包含 sample_content.txt xay 和 patterns.txt 包括 a bb c,然后工具将输出 xcy - 预期输出还是应该是 xby?无论哪个正确,您都应该在示例输入中包含一个测试该案例的案例。
  • @RavinderSingh13 OP 明确表示Single line may have multiple matching value to replace in sample_content.txt 因此在第一场比赛后跳出循环,虽然速度更快,但不会产生预期的输出。我还认为只对每个匹配的字符串进行 1 次替换,而不是替换每行上每个字符串的所有出现(就像你的两个脚本都使用 sub() 而不是 gsub())是不正确的。
  • @EdMorton,Ed 先生,我明白了,你说得对。

标签: shell perl awk sed gawk


【解决方案1】:

未来参考的基准

测试环境:

使用您的示例文件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 1st 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 1st 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 2nd 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 9000 行的第二个 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 -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,似乎可以加快速度。

对于那些想在自己的 PC 上重复基准测试的人,您可以通过 OP 的链接或我的 github 下载文件 contents.txtpatterns.txt

contents.txt

patterns.txt

【讨论】:

  • 感谢 George 的分享。您能否也测试一下我的第二个解决方案并将其发布在这里,因为我没有那么多行文件无法测试它,先生,将不胜感激。
  • 酷,实际上该链接对我不起作用。再次感谢。
  • 感谢 George,这是因为我尽快在 array(a) 中找到了一个匹配项并从中出来,所以它不会遍历整个数组 :) 我很高兴我可以让它变得更高效。非常感谢您检查它先生。
  • 非常感谢大家。这是我第一次学习这些脚本。从中获得了很多知识。再次感谢。
  • 我想我必须把那个 RE 分割成一个更大但更小的 RE 数组。那好吧。 OP 已经选择了一个答案并且没有回答问题(请参阅我在问题下的评论)所以是时候像他一样继续前进了....再次感谢您运行计时测试。
【解决方案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

就像到目前为止发布的其他解决方案一样,这会将所有替换都应用于整行,因此给定包含 xay 的 sample_content.txt 和包含 a bb c 的 patterns.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 的每一行进行尽可能多的正则表达式比较,而不是对 sample_content.txt 的每一行进行每行 patterns.txt 1 次匹配
  3. 它只在上次替换后的行的左边运行,所以被测试的字符串不断缩小
  4. 它不会更改 $0,因此 awk 不必在每次替换时都重新编译和重新拆分该记录。

所以它应该比原始脚本快很多,假设从patterns.txt构造的正则表达式不是那么大,它会导致性能下降只是因为它的大小。

【讨论】:

    【解决方案3】:

    您能否尝试关注awk,如果这对您有帮助,请告诉我。

    解决方案一:

    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.
    

    解决方案 2: 当找到匹配项时,无需一直遍历数组,就像从数组中出来的第一个解决方案一样:

    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,让我们计算一下它需要多少时间,然后我们可以看到更多我们可以做更多的事情,让我们知道。
    • @Dhanabalan,请耐心等待并测试这两个解决方案,并通过在它们之前添加time 命令记下它们的时间。让我们知道它是怎么回事。我们不能指望在几秒钟内处理数百万行。
    • 这比必要的复杂得多(您从i开始,然后使用match()+substr()得到val,而val将始终与i相同,并且然后您执行sub() 来查找您刚刚执行match() 的相同字符串!)。请参阅我在stackoverflow.com/a/48720400/1745001 发布的第一个脚本,以了解此方法的简单实现。如果同一字符串在一行上多次出现,您的也会失败,因为它只会替换每个字符串的第一次出现。
    【解决方案4】:

    试试这个。应该很快。

    $ 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
    

    然后将上述所有内容通过进程替换 &lt;(...) 到一个简单的 sed 作为脚本文件使用
    sed -f switch = 从文件读取 sed 命令

    $ sed -f <(printf 's/%s/%s/g\n' $(<patterns.txt)) contents.txt
    288Y2RZDBPX9000000003dhana
    JP2F64EI2000000001d
    EU9V3IXI3000000001dfg9000000003dfdfds
    XATSSSSFOO4dhanaUXIBB7TF74000000001adf
    10Q1W4ZEAV18LXNPSPGRTTIDHBN5000000001egw
    

    【讨论】:

    • @Dhanabalan:也许这样更快:sed -f &lt;(sed -E 's|(.*) (.*)|s/\1/\2/|g' patterns.txt) sample_content.txt
    猜你喜欢
    • 2017-09-10
    • 2013-01-10
    • 2014-03-15
    • 1970-01-01
    • 1970-01-01
    • 2013-10-03
    • 2021-04-26
    • 2021-09-14
    • 1970-01-01
    相关资源
    最近更新 更多