针对多个sed替换优化shell脚本

时间:2014-08-29 06:50:15

标签: bash shell sed

我有一个文件,其中包含替换对的列表(大约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

8 个答案:

答案 0 :(得分:8)

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

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

答案 1 :(得分:3)

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

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

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

来自replace.c

  

替换文本文件中的字符串

     

此程序替换文件中的字符串或stdin转换为stdout。它接受from-string / to-string对的列表,并用相应的to-string替换每个出现的from-string。匹配找到的字符串的第一次出现。如果要替换字符串的可能性不止一个,则在较短的匹配之前首选较长的匹配。

     

...

     

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


更多关于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 ...

答案 2 :(得分:1)

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

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

IFS =&#39; |&#39;将允许启用读取以在2个不同的shell变量oldnew中填充名称值。

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

答案 3 :(得分:1)

以下是我要尝试的内容:

  1. 将您的sed搜索替换对存储在Bash数组中,例如;
  2. 使用parameter expansion
  3. 基于此数组构建您的sed命令
  4. run command。
  5. 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 file

答案 4 :(得分:0)

你可以试试这个。

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命令一次。您可能还想将awk替换为cutcut可能比awk更优化,但我不确定。

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

答案 5 :(得分:0)

您可能想要在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表示该行已打印。

答案 6 :(得分:0)

{ 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,即recursif。

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

<强>的解释

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

答案 7 :(得分:0)

感谢上面的@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 行块