颜林林的个人网站

使用Perl实现部分字符替换

2019-12-16 20:09

今天在微信群里见到一个需求,需要把类似如下文本中,偶数行(即fasta的序列)的左右两端的“-”字符替换成“?”:

>seq-1
---ACTGACTG---
>seq-2
--AC--AC--AC--
>seq-3
--ACG---ACG----

当即有群友给出了awk的解决方案:

1
2
cat in.fa \
  | awk '{if(NR%2==1){print$0}else{gsub("-","?",$0);print$0}}'

这个方案很简洁,且有效。awk对于这种需要指定特定行做操作的需求,确实很方便。

然而,这种方案会把序列中间的“-”也进行替换,这不符合需求,提问者希望保留中间的“-”不替换,而只替换两个末端的“-”。

正则表达式很容易提取左右两个末端的一个或多个“-”(/(^-*|-*$/),然而要指定替换结果为另一个字符组成的同长度字符串,却有些困难了。

于是,只好祭出字符处理的终极武器Perl,让正则替换过程,使用一个代码块(更详细的用法可参见“perldoc perlre”命令列出的详细文档)来进行:

1
2
$ echo -e ">a-a\n--ha-ha---" \
  | perl -ne 'if($i++%2){$_=~s{^(-*)(.*?)(-*)$}{("?"x(length($1))).$2.("?"x(length($3)))}ex};print'

由于原输入文件来自Windows系统,其行结束符("\r\n")导致了右侧替换失败,于是进一步改进了这段“咒语”:

1
2
3
$ echo -e ">a-a\n--ha-ha---" \
  | unix2dos \
  | perl -ne 'if($i++%2){$_=~s{^(-*)(.*?)(-*)\r?$}{("?"x(length($1))).$2.("?"x(length($3)))}ex};print'

问题解决。

小结:Perl虽然早已不是最好的语言了,但在字符串处理上,却的确是“终极”的,因为现代的各种正则处理,基本都是直接或间接从Perl的各种支持逐步演变过来的。只可惜,Perl这门语言太过古典,很多“咒语”吟唱起来太过拗口冗长,被淘汰恐怕是迟早的事了。或许,未来这种吟唱,可以被机器代替,以另一种更友好的操作界面来进行呢?