【问题标题】:Speed up sed replacement string read from a file加快从文件中读取的 sed 替换字符串
【发布时间】:2018-03-07 14:14:04
【问题描述】:

第一次发帖,请多多指教。我一次读取一行文件“bar”,并使用 sed 将“foo”中的每一行(从第一行开始)替换为从“bar”读取的行。下面的代码有效,但是当“foo”为 48,890 行而“bar”为 ~24,445 行(正好是 foo 的一半长度)时,它的速度非常慢。

有人对如何加快这个过程有建议吗?

x=1
while read i;do
  sed -i "$x s/^.*$/$i/" foo
  x=$[$x +2]
done < bar

【问题讨论】:

  • 一定要使用sed吗? awkperl 会更好。
  • 每次读取来自bar 的一行时完全重写foo当然会很慢。
  • 顺便说一句,$[ ] 实际上是 1970 年代的语法。自 1990 年代初发布 POSIX sh 标准以来,$(( )) 一直是在 shell 中输入数学上下文的正确语法。虽然 bash 也支持 $[ ],但许多纯 POSIX shell(ash、dash 等)仅支持 $(( ))
  • 感谢$(())@Charles Duffy 的提醒

标签: bash sed


【解决方案1】:

paste 和 awk 交错:

paste -d '\n' bar <(awk 'NR%2==0' foo)

或者,如果进程替换不可用:

awk 'NR%2==0' foo | paste -d '\n' bar -

替换foo:

paste -d '\n' bar <(awk 'NR%2==0' foo) > tmp && mv tmp foo

awk 'NR%2==0' foo | paste -d '\n' bar - > tmp && mv tmp foo

我做了一些基准测试(只是执行时间,忽略了内存需求)。

创建输入文件(大约是问题中的十倍):

$ dd if=/dev/urandom count=500000 | tr -cd [:alpha:] | fold -w 100 |
> sed 's/^/foo /' > foo
$ dd if=/dev/urandom count=250000 | tr -cd [:alpha:] | fold -w 100 |
> sed 's/^/bar /' > bar
$ wc -l foo bar
  539994 foo
  270126 bar
  810120 total

我使用time 来测量执行时间。所有解决方案的输出都被重定向到一个新文件。结果以秒为单位,平均每次尝试五次:

codeforester            9.878
codeforester, mapfile   8.072
Fred                   17.332
Charles Duffy          'Argument list too long"
Claude                 27.448
Barmar                  0.298
Benjamin W.             0.176

Charles 的输入也达到此处使用的 10% 的大小。

【讨论】:

  • 非常感谢您的基准测试!我很惊讶@fred 的解决方案结果很慢。顺便说一句,我的代码中有一个错误会导致输出空行,因为我没有检查 bar 文件的大小。解决它。只是您的解决方案的一个小问题——它们都在最后输出一个空行。
  • 感谢您的周到! @Benjamin W.
  • paste -d '\n' bar &lt;(awk 'NR%2==0' foo) &gt; tmp &amp;&amp; mv tmp foo 工作就像一个魅力!
【解决方案2】:

这是awk 解决方案。它将所有bar 读入一个数组。当它读取foo 时,它会打印该数组的行或下一个元素,具体取决于它是奇数行号还是偶数行号。

awk 'BEGIN {index1 = 1}
     FNR == NR {file1[NR] = $0; next}
     NR % 2 == 1 { print file1[index1++]; next }
     { print }' bar foo > newfoo

【讨论】:

    【解决方案3】:

    我认为您当前解决方案的缓慢是由sed 所需的大量分叉以及重复重写文件导致的大量 I/O 造成的。这是一个零分叉的纯 Bash 解决方案:

    #!/bin/bash
    
    # read "bar" file into an array - this should take less memory than "foo"
    while read -r line; do
      bar_array+=("$line")
    done < bar
    
    
    # traverse "foo" file and replace odd lines with the lines from "bar"
    # we don't need to read the whole file into memory
    i=0
    max_bar="${#bar_array[@]}"
    while read -r line; do
      #
      # we look at bar_array only when we are within the limits of that file
      #
      p="$line"
      if ((i < max_bar && i % 2 == 0)); then
        p=${bar_array[$i]}
      fi
      printf "%s\n" "$p"
      ((i++))
    done < foo
    

    示例运行:

    栏的内容:

    11
    22
    33
    44
    55
    

    foo 的内容:

    1
    2
    3
    4
    5
    6
    7
    8
    

    输出:

    11
    2
    33
    4
    55
    6
    7
    8
    

    使用 Bash 4 及更高版本,读取语句

    while read -r line; do
      bar_array+=("$line")
    done < bar
    

    也可以写成:

    mapfile -t bar_array < bar
    

    【讨论】:

    • file1+=( "$line" ),以避免字符串拆分和通配。您也可以mapfile -t file1 &lt;file1IFS=$'\n' read -r -a file1 &lt; file1,以避免读取的显式循环。
    • 在要求其他人学习的同时忘记基本课程是多么容易。已更正,谢谢@CharlesDuffy。
    • 如果有人喜欢这个解决方案,他们需要感谢 Charles Duffy 和 @barmar 等 SO 成员的才华。我每天都从他们身上学到很多东西。
    • (...我应该有资格证明:mapfile 方法是 bash 4 或更高版本,因此如果需要与 3.x 兼容,那么有充分的理由坚持使用 @987654332 @)。
    • (除此之外,虽然我们给予赞扬:我所知道的关于 bash 的一切,都是从 freenode #bash IRC 频道上的 graycat 那里学到的)
    【解决方案4】:

    其他答案建议基于将整个文件存储在数组中的方法。根据文件大小,这在某些时候会有一些实际限制。

    另一种方法是简单地从两个文件中读取,一次一行,在单独的文件描述符中打开它们。

    #!/bin/bash
    
    exec 3< foo
    exec 4< bar
    
    eof_bar=0
    eof_foo=0
    
    while [[ $eof_bar = 0 ]]
    do
       # Foo line we keep
       IFS= read -r -u 3 foo_line || eof_foo=$?
       [[ "$eof_foo" != 0 ]] || [[ -n "$foo_line" ]] || break
       printf "%s\n" "$foo_line"
       # Bar line we will replace with
       IFS= read -r -u 4 bar_line || eof_bar=$?
       [[ "$eof_bar" = 0 ]] || [[ -n "$bar_line" ]] || break
       # Foo line we skip (line from bar was present)
       IFS= read -r -u 3 foo_line
       [[ "$eof_foo" != 0 ]] || [[ -n "$foo_line" ]] || break
       # Actual replacement (both files had required lines)
       printf "%s\n" "$bar_line"
    done
    
    # Cat the rest of the lines from foo (if any), if bar did not
    # have enough lines compared to foo
    cat <&3
    
    # Close file descriptors
    exec 3>&-
    exec 4>&-
    

    对于来自bar 的每一行,代码从foo 读取两行,并简单地跳过打印在每次迭代时读取的来自foo 的第二行。

    这样做会占用很少的内存,因此可以处理任意大小的文件。

    【讨论】:

    • 看起来有点复杂,但我感谢您为提高 I/O 效率所做的努力!我认为它会为我的答案中的示例打印一些空白行。
    • 想知道为什么这在@BenjaminW. 的实验中表现不佳。
    • @codeforrester 这可能是(1)Bash不是特别快,礼貌(2)基于数组的解决方案只要有足够的内存,旧的空间/时间就会更快权衡(3)我不是特别精通优化速度。我想知道为从文件描述符重定向而打开的文件做了什么样的缓冲。
    【解决方案5】:

    awk 似乎是最好的选择,因为它不会在每一行创建子 shell 以供读取,它将所有文件放在一个进程中,几乎没有修改/复杂性

    # Oneliner for batch or command line
    awk 'FNR==NR{b[NR]=$0;next}{if(NR%2==1)$0=b[((NR+1)/2)];print}' bar foo
    

    相同代码,但自我注释以供理解

    awk '# when reading first file (bar)
         FNR == NR {
            # load line content into an array
            bar[ NR] = $0
            # cycle to next line (don't go further in the code for this input line)
            next
            }
    
         # every line from other files (only foo here)
         {
            # every odd line, replace content with corresponding array content
            # NR = record line and is odd so (NR + 1) / 2 -> half the line number uprounded
            if (NR % 2 == 1) $0 = bar [ ( ( NR + 1 ) / 2)]
    
            # print the line (modified or not)
            print
         }
        ' bar foo
    

    【讨论】:

      【解决方案6】:

      在一次调用中运行所有sed 命令,并且您只重写一次foo,而不是每行bar 重写一次。

      x=1
      sed_exprs=( )
      while IFS= read -r i; do
        sed_exprs+=( -e "$x s/^.*$/$i/" )
        x=$(( x + 2 ))
      done < bar
      
      sed "${sed_exprs[@]}" -i foo
      

      【讨论】:

      • 启动带有数万个参数的sed 会失败还是会引入意外的性能瓶颈?
      • 如果组合长度超过 ARG_MAX,它将失败。也可能很慢,具体取决于实施细节。
      • 您知道 ARG_MAX 的典型值是多少吗?
      • 老实说,我会在这个问题上使用 codeforester 的解决方案。
      • 需要明确的是,我保留它是因为它可能对其他试图将一组更小的更改合并到单个 sed 命令的人有用,而许多其他答案非常专门针对 OP 的确切情况,但不一定可能帮助其他人。
      【解决方案7】:

      这是一个流式处理解决方案,它可以使用小的常量内存来工作,以防您在 RAM 很少的机器上拥有非常大的文件。

      #!/bin/bash
      
      # duplicate lines in bar to standard output
      paste -d '\n' bar bar |
      
      # pair line-by-line foo with lines from previous command
      paste -d '|' foo - |
      
      # now the stream is like:
      #  foo line 1|bar line 1
      #  foo line 2|bar line 1
      #  foo line 3|bar line 2
      #  foo line 4|bar line 2
      #  foo line 5|bar line 3
      #  ...
      {
        # set field separator to correspond with previous paste delimiter
        IFS='|'
        # read pairs of lines, discarding the second
        while read -r foo bar && read -r junk
        do
          # print the odd lines from foo
          printf "%s\n" "$foo"
          # interleaved with the lines from bar
          printf "%s\n" "$bar"
        done
      }
      

      您必须选择foo 中没有的分隔符(此处为|)。测试:

      paste (GNU coreutils) 8.26
      

      【讨论】:

        【解决方案8】:

        这是我的第一个答案的重大修改版本,我将按照提交的基准单独发布。

        #!/bin/bash
        exec 3< foo
        exec 4< bar
        eof=0
        IFS=
        n=$'\n'
        while :
        do
           readarray -n 2 -u 3 fl && read -r -u 4 bl || break
           echo "${fl[1]}$bl"
        done
        # Add remaining data
        [[ -n ${fl[1]} ]] || echo "$fl"
        [[ -n $bl ]] || echo "$bl"
        # Cat the rest of the lines from foo (if any), if bar did not
        # have enough lines compared to foo
        cat <&3
        # Close file descriptors
        exec 3>&-
        exec 4>&-
        

        事实证明,我的“手动优化”解决方案比我的第一个版本更简单、更易读,这表明考虑速度有时会带来简化,这总是好的。

        在我的机器上,我的第一个答案的测试与基准测试的运行时间大致相同,并且这个新答案的运行时间不到 7 秒,这要快得多,但没有@987654322 快@ 解决方案,当然。

        编辑

        我用一个 readarray 替换了“foo”中的两个读取,这将运行时间(在我的机器上)从大约 9 秒减少到 7 秒以下,比我想象的要多。这使我认为可以通过读取数组中的两个文件(但不是整个文件以避免达到内存限制的风险)来进行重大改进,但显然会增加代码复杂性。

        【讨论】:

          猜你喜欢
          • 2017-11-14
          • 1970-01-01
          • 2018-05-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-10-09
          • 1970-01-01
          • 2020-03-13
          相关资源
          最近更新 更多