【问题标题】:Optimize shell script for multiple sed replacements优化多个 sed 替换的 shell 脚本
【发布时间】:2014-08-29 06:50:15
【问题描述】:

我有一个包含替换对列表的文件(大约 100 个),sed 使用这些替换对替换文件中的字符串。

这对是这样的:

old|new
tobereplaced|replacement
(stuffiwant).*(too)|\1\2

我当前的代码是:

cat replacement_list | while read i
do
    old=$(echo "$i" | awk -F'|' '{print $1}')    #due to the need for extended regex
    new=$(echo "$i" | awk -F'|' '{print $2}')
    sed -r "s/`echo "$old"`/`echo "$new"`/g" -i file
done

我不禁认为有一种更优化的方式来执行替换。我尝试转动循环以首先运行文件的行,但结果证明成本要高得多。

还有其他方法可以加快这个脚本的速度吗?

编辑

感谢所有快速回复。让我在选择答案之前尝试各种建议。

需要澄清的一点:我还需要子表达式/组功能。例如,我可能需要的一种替换是:

([0-9])U|\10  #the extra brackets and escapes were required for my original code

关于改进的一些细节(待更新):

  • 方法:处理时间
  • 原始脚本:0.85s
  • cut 而不是 awk:0.71s
  • anubhava的方法:0.18s
  • chthonicdaemon 的方法:0.01s

【问题讨论】:

  • 这个问题有答案here。是的,您正在寻找速度,但请回答两个问题。
  • 老实说,这个问题并没有真正提出速度的因素,也没有提出子表达式的因素。这里给出的答案更有帮助。
  • 好的,然后通过将子表达式放在数据中并提供输入和所需输出来澄清您对子表达式的问题,这将大大改善您的问题并清楚地将其与其他问题区分开来。
  • +1 用于运行所有基准测试。我自己学会了一些技巧。

标签: bash shell sed


【解决方案1】:

您可以使用sed 生成正确格式的sed 输入:

sed -e 's/^/s|/; s/$/|g/' replacement_list | sed -r -f - file

【讨论】:

  • sed: -e expression #1, char 17: unknown option to 's'。字符 17 恰好是 |我的替换文件中的分隔符
  • 话虽如此,我现在明白了这个概念并正在尝试对其进行测试。
  • 问题在于逗号(错字?)。但无论如何,绝对是极快的速度,也相当简约!谢谢!
  • 很抱歉 - 我正在编辑表达式并且没有测试最后一次迭代。很高兴你明白了。
  • @shrx 您可以使用 FIFO。把末尾的file改成<( whatever your command to generate input )
【解决方案2】:

我最近对各种字符串替换方法进行了基准测试,其中包括一个自定义程序 sed -eperl -lnpe 和一个可能不那么广为人知的 MySQL 命令行实用程序 replacereplace 针对字符串替换进行优化几乎比 sed 快一个数量级。结果看起来像这样(最慢的优先):

custom program > sed > LANG=C sed > perl > LANG=C perl > replace

如果您想要性能,请使用replace。不过,要让它在您的系统上可用,您需要安装一些 MySQL 发行版。

来自replace.c

替换文本文件中的字符串

这个程序替换文件中的字符串或从标准输入到标准输出。它接受 from-string/to-string 对的列表,并将每次出现的 from-string 替换为相应的 to-string。匹配找到的字符串的第一次出现。如果字符串替换的可能性不止一种,则在较短的匹配之前优先选择较长的匹配。

...

程序生成字符串的 DFA 状态机,速度不依赖于替换字符串的数量(仅依赖于替换的数量)。假设一行以 \n 或 \0 结尾。字符串长度没有内存限制。


关于 sed 的更多信息。您可以通过 sed 使用多个核心,将您的替代品拆分为 #cpus 组,然后通过sed 命令将它们通过管道传输,如下所示:

$ sed -e 's/A/B/g; ...' file.txt | \
  sed -e 's/B/C/g; ...' | \
  sed -e 's/C/D/g; ...' | \
  sed -e 's/D/E/g; ...' > out

此外,如果您使用 sedperl 并且您的系统具有 UTF-8 设置,那么在命令前面放置 LANG=C 也会提高性能:

$ LANG=C sed ...

【讨论】:

  • 关于那个话题,使用 N 个 -e 或 N 个单个 sed 命令,sed 运行得更快吗?当 N > 100 时。
  • IIRC,在单个sed 命令中使用N 替换比N number sed 命令要快一些。我记得当时有点惊讶,并行运行几百个进程并没有过多地降低性能。
  • mysql replace 只能替换固定字符串。 sd 是 rust 中的类似工具
【解决方案3】:

您可以减少不必要的 awk 调用并使用 BASH 来破坏名称-值对:

while IFS='|' read -r old new; do
   # echo "$old :: $new"
   sed -i "s~$old~$new~g" file
done < replacement_list

IFS='|'将启用读取以在 2 个不同的 shell 变量 oldnew 中填充名称-值。

这是假设 ~ 不存在于您的名称-值对中。如果不是这种情况,请随意使用备用 sed 分隔符。

【讨论】:

  • 这似乎真的很快,但我遇到了子表达式的问题。我没有返回存储在组中的值,而是按字面意思获取它们(例如 \1 \2 等......)。
  • 你能告诉我一些带有这些子表达式的示例行,以便我可以重现它并建议你修复。
  • 感谢您的回复,例如([0-9])U|\\10
  • 感谢您的回答和额外的帮助!可悲的是,我将不得不投票给 chthonicdaemon 的答案,因为它更快且更简约。
  • 毫无疑问 chthonicdaemon 回答的优点。我自己也为他的创新把戏投了赞成票。
【解决方案4】:

这是我会尝试的:

  1. 将您的 sed 搜索替换对存储在 Bash 数组中,例如 ;
  2. 使用parameter expansion 在此数组的基础上构建您的 sed 命令
  3. 运行命令。
patterns=(
  old new
  tobereplaced replacement
)
pattern_count=${#patterns[*]} # number of pattern
sedArgs=() # will hold the list of sed arguments

for (( i=0 ; i<$pattern_count ; i=i+2 )); do # don't need to loop on the replacement…
  search=${patterns[i]};
  replace=${patterns[i+1]}; # … here we got the replacement part
  sedArgs+=" -e s/$search/$replace/g"
done
sed ${sedArgs[@]} file

这导致这个命令:

sed -e s/old/new/g -e s/tobereplaced/replacement/g 文件

【讨论】:

    【解决方案5】:

    你可以试试这个。

    pattern=''
    cat replacement_list | while read i
    do
        old=$(echo "$i" | awk -F'|' '{print $1}')    #due to the need for extended regex
        new=$(echo "$i" | awk -F'|' '{print $2}')
        pattern=${pattern}"s/${old}/${new}/g;"
    done
    sed -r ${pattern} -i file
    

    这将只对包含所有替换的文件运行一次 sed 命令。您可能还想用cut 替换awkcut 可能比 awk 更优化,虽然我不确定。

    old=`echo $i | cut -d"|" -f1`
    new=`echo $i | cut -d"|" -f2`
    

    【讨论】:

    • 0.3s 改进。还不错。
    • 我错了,cut 确实加快了进程,但模式位实际上并没有起作用。由于某种原因,提供给sed 的文件名的第一个字符被删除了。试图找出原因。
    • useless use of cat 和多个 quoting errors 对于这个答案来说不是好兆头。谨慎行事。
    【解决方案6】:

    您可能想在 awk 中完成所有事情:

    awk -F\| 'NR==FNR{old[++n]=$1;new[n]=$2;next}{for(i=1;i<=n;++i)gsub(old[i],new[i])}1' replacement_list file
    

    从第一个文件中建立一个新旧单词列表。 next 确保脚本的其余部分不在第一个文件上运行。对于第二个文件,遍历替换列表并一个一个地执行它们。末尾的1 表示打印该行。

    【讨论】:

    • 我的一个问题是我在 sed 替换中使用了组(即 \1)。
    • 你在使用 gawk 吗?如果是这样,这可以适应使用gensub
    【解决方案7】:
    { cat replacement_list;echo "-End-"; cat YourFile; } | sed -n '1,/-End-/ s/$/³/;1h;1!H;$ {g
    t again
    :again
       /^-End-³\n/ {s///;b done
          }
       s/^\([^|]*\)|\([^³]*\)³\(\n\)\(.*\)\1/\1|\2³\3\4\2/
       t again
       s/^[^³]*³\n//
       t again
    :done
      p
      }'
    

    更多通过 sed 编码的乐趣。尝试一段时间的性能,因为这只会启动 1 个递归的 sed。

    对于 posix sed(所以 --posix 使用 GNU sed)

    解释

    • 在文件内容前复制替换列表,并带有分隔符(用于与³ 对齐的行和用于与-End- 的列表)以便于sed 处理(在posix sed 中很难在类字符中使用\n。
    • 将所有行放入缓冲区(替换列表的行分隔符和-End-之前)
    • 如果这是-End-³,请删除该行并转到最终打印
    • 将文本中找到的每个第一个模式(第 1 组)替换为第二种模式(第 2 组)
    • 如果找到,重启 (t again)
    • 删除第一行
    • 重新启动进程 (t again)。需要 T 是因为 b 不会重置测试,并且下一个 t 始终为真。

    【讨论】:

      【解决方案8】:

      感谢上面的@miku;

      我有一个 100MB 的文件,其中包含 80k 替换字符串的列表。

      我尝试了 sed 顺序或并行的各种组合,但没有发现吞吐量比大约 20 小时的运行时间更短。

      相反,我将我的列表放入一系列脚本中,例如“cat in | replace aold anew bold bnew cold cnew ... > out ; rm in ; mv out in”。

      我为每个文件随机选择了 1000 个替换,所以一切都是这样的:

      # first, split my replace-list into manageable chunks (89 files in this case)
      split -a 4 -l 1000 80kReplacePairs rep_
      
      # next, make a 'replace' script out of each chunk
      for F in rep_* ; do \
          echo "create and make executable a scriptfile" ; \
          echo '#!/bin/sh' > run_$F.sh ; chmod +x run_$F.sh ; \
          echo "for each chunk-file line, strip line-ends," ; \
          echo "then with sed, turn '{long list}' into 'cat in | {long list}' > out" ; \
          cat $F | tr '\n' ' ' | sed 's/^/cat in | replace /;s/$/ > out/' >> run_$F.sh ;
          echo "and append commands to switch in and out files, for next script" ; \
          echo -e " && \\\\ \nrm in && mv out in\n" >> run_$F.sh ; \
      done
      
      # put all the replace-scripts in sequence into a main script
      ls ./run_rep_aa* > allrun.sh
      
      # make it executable
      chmod +x allrun.sh 
      
      # run it
      nohup ./allrun.sh &
      

      .. 不到 5 分钟,不到 20 小时!

      回首往事,我本可以在每个脚本中使用更多对,方法是找出有多少行可以构成限制。

      xargs --show-limits </dev/null 2>&1 | grep --color=always "actually use:"
          Maximum length of command we could actually use: 2090490
      

      所以不到 2MB;我的脚本需要多少对?

      head -c 2090490 80kReplacePairs | wc -l
      
          76923
      

      看来我可以使用 2 * 40000 行的块

      【讨论】:

        【解决方案9】:

        扩展chthonicdaemon的解决方案

        live demo

        #! /bin/sh
        
        # build regex from text file
        
        REGEX_FILE=some-patch.regex.diff
        
        # test
        # set these with "export key=val"
        SOME_VAR_NAME=hello
        ANOTHER_VAR_NAME=world
        
        
        escape_b() {
          echo "$1" | sed 's,/,\\/,g'
        }
        
        
        regex="$(
          (echo; cat "$REGEX_FILE"; echo) \
          | perl -p -0 -e '
            s/\n#[^\n]*/\n/g;
            s/\(\(SOME_VAR_NAME\)\)/'"$(escape_b "$SOME_VAR_NAME")"'/g;
            s/\(\(ANOTHER_VAR_NAME\)\)/'"$(escape_b "$ANOTHER_VAR_NAME")"'/g;
            s/([^\n])\//\1\\\//g;
            s/\n-([^\n]+)\n\+([^\n]*)(?:\n\/([^\n]+))?\n/s\/\1\/\2\/\3;\n/g;
          '
        )"
        
        echo "regex:"; echo "$regex" # debug
        
        exec perl -00 -p -i -e "$regex" "$@"
        

        -+/ 为前缀的行允许空的“加号”值,并保护前导空格免受有问题的文本编辑器的影响

        样本输入:some-patch.regex.diff

        # file format is similar to diff/patch
        # this is a comment
        
        # replace all "a/a" with "b/b"
        -a/a
        +b/b
        /g
        
        -a1|a2
        +b1|b2
        /sg
        # this is another comment
        
        -(a1).*(a2)
        +b\1b\2b
        
        -a\na\na
        +b
        
        -a1-((SOME_VAR_NAME))-a2
        +b1-((ANOTHER_VAR_NAME))-b2
        

        样本输出

        s/a\/a/b\/b/g;
        
        s/a1|a2/b1|b2/;;
        
        s/(a1).*(a2)/b\1b\2b/;
        
        s/a\na\na/b/;
        
        s/a1-hello-a2/b1-world-b2/;
        

        此正则表达式格式与 sed 和 perl 兼容

        由于miku 提到了mysql replace: 用正则表达式替换固定字符串并非易事, 因为您必须转义所有正则表达式字符, 但您还必须处理反斜杠转义...

        天真的转义者:

        echo '\(\n' | perl -p -e 's/([.+*?()\[\]])/\\\1/g' 
        \\(\n
        

        【讨论】:

          猜你喜欢
          • 2012-09-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-07-02
          • 1970-01-01
          • 2013-12-23
          • 2021-11-27
          • 1970-01-01
          相关资源
          最近更新 更多