Perl / Unix 单行脚本对决,第二部分

第一部分中,我比较了Perl的正则表达式功能与sed和Awk。在本节的最后一部分,我将涵盖利用Perl丰富的内置功能和第三方模块的示例。

更大的库

与Awk相比,Perl拥有更多的内置函数。对于命令行使用,我经常需要trjoinmapgrep。我喜欢Perl中的数组与哈希是区分开的,并且对这些数据类型应用sort要简单得多。

向列表中添加项目

这个问题希望向列数不足的行添加列,如bcd

a,10,12,13
b,20,22
c,30
d,33

这个例子通过再次使用/e将零添加到列表中。这次,Perl在替换中计算逗号的数量,并将其从3中减去,以找出还需要多少列。其中x是字符串复制运算符

$ perl -pe 's|$|",0" x (3 - tr/,//)|e' ip.txt
a,10,12,13
b,20,22,0
c,30,0,0
d,33,0,0

颠倒事物

针对特定字段的DNA序列的反向互补中,我需要选择字符串的一部分,进行互补,并将其颠倒。我想处理第三列

ABC DEF GATTAG GHK
ABC DEF GGCGTC GHK
ABC DEF AATTCC GHK

我在替换的一侧使用了trreverse(再次使用/e

$ perl -pe 's/^(\H+\h+){2}\K\H+/reverse $&=~tr|ATGC|TACG|r/e' test.txt
ABC DEF CTAATC GHK
ABC DEF GACGCC GHK
ABC DEF GGAATT GHK

或者,我可以使用-a,它会自动根据空白分割结果,并将结果放入@F中。然后我处理第三元素,再次输出@F

$ perl -lane '$F[2]=reverse $F[2]=~tr/ATGC/TACG/r; print "@F"' test.txt
ABC DEF CTAATC GHK
ABC DEF GACGCC GHK
ABC DEF GGAATT GHK

对CSV行进行排序

关于在不包含标题和第一列的情况下对csv文件中的行进行排序?这里有一些简单的以逗号分隔的值

id,h1,h2,h3,h4,h5,h6,h7
101,zebra,1,papa,4,dog,3,apple
102,2,yahoo,5,kangaroo,7,ape

我又使用了-a,但还使用了-F,,使逗号成为字段分隔符

$ perl -F, -lane 'print join ",", $.==1 ? @F : (shift @F, sort @F)' ip.txt
id,h1,h2,h3,h4,h5,h6,h7
101,1,3,4,apple,dog,papa,zebra
102,2,5,7,ape,kangaroo,yahoo

$.变量跟踪输入行号。我使用这个变量跳过第一行(标题)。在其他所有行中,我制作一个包含@F第一个元素的列表和其余元素的排序列表。注意,本例中要排序的数字位数相同,否则不会起作用。

插入增量行和列标签

在矩阵中插入行和列需要添加固定间隔的数值标签

2 3 4 1 2 3
3 4 5 2 4 6
2 4 0 5 0 7
0 0 5 6 3 8

在这里,我使用map生成标题

$ perl -lane 'print join "\t", "", map {20.00+$_*0.33} 0..$#F if $.==1;
              print join "\t", 100+(0.33*$i++), @F' matrix.txt
        20      20.33   20.66   20.99   21.32   21.65
100     2       3       4       1       2       3
100.33  3       4       5       2       4       6
100.66  2       4       0       5       0       7
100.99  0       0       5       6       3       8

# with formatting and alternate way to join print arguments
$ perl -lane 'BEGIN{$,="\t"; $st=0.33}
              print "", map {sprintf "%.2f", 20+$_*$st} 0..$#F if $.==1;
              print sprintf("%.2f", 100+($st*$i++)), @F' matrix.txt
        20.00   20.33   20.66   20.99   21.32   21.65
100.00  2       3       4       1       2       3
100.33  3       4       5       2       4       6
100.66  2       4       0       5       0       7
100.99  0       0       5       6       3       8

使用Perl模块

除了内置函数外,标准或CPAN模块也非常有用。使用-M加载它们,并在=之后放置导入列表

# randomize word list after filtering
$ s='floor bat to dubious four pact feed'
$ echo "$s" | perl -MList::Util=shuffle -lanE '
                    say join ":", shuffle grep {/[au]/} @F'
bat:four:pact:dubious

# remove duplicate elements while retaining input order
$ s='3,b,a,3,c,d,1,d,c,2,2,2,3,1,b'
$ echo "$s" | perl -MList::Util=uniq -F, -lanE 'say join ",", uniq @F'
3,b,a,c,d,1,2

# apply base64 decoding only for a portion of the string
$ s='123 aGVsbG8gd29ybGQK'
$ echo "$s" | perl -MMIME::Base64 -ane 'print decode_base64 $F[1]'
hello world

CPAN

综合Perl档案网络(CPAN)有各种用例的模块的大量集合。这里有一些示例。

提取IPv4地址

Regexp::Common有匹配常见事物的配方。这里有一些包含点分十进制IP地址的文本

3.5.52.243 555.4.3.1 34242534.23.42.42
foo 234.233.54.123 baz 4.4.4.3123

提取IPv4地址非常简单

$ perl -MRegexp::Common=net -nE 'say $& while /$RE{net}{IPv4}/g' ipv4.txt
3.5.52.243
55.4.3.1
34.23.42.42
234.233.54.123
4.4.4.31

只有当IPv4地址不被数字字符包围时才能匹配,因此我不会在34242534.23.42.42的中间匹配

$ perl -MRegexp::Common=net -nE '
        say $& while /(?<!\d)$RE{net}{IPv4}(?!\d)/g' ipv4.txt
3.5.52.243
234.233.54.123

真正的CSV处理

之前我进行了一些简单的CSV处理,但如果你想真正进行CSV处理,可以使用Text::CSV_XS来确保一切正确发生。这个处理了引用字段fox,42

$ s='eagle,"fox,42",bee,frog\n1,2,3,4'

# note that neither -n nor -p are used here
$ printf '%b' "$s" | perl -MText::CSV_XS -E 'say $row->[1]
                     while $row = Text::CSV_XS->new->getline(*ARGV)'
fox,42
2

处理XML

处理XML文件是另一种容易出错的格式。许多人尝试使用正则表达式来做这件事,但那很容易出错。这里是一个示例文件

<doc>
    <greeting type="ask">Hi there. How are you?</greeting>
    <greeting type="reply">I am good.</greeting>
    <color>
        <blue>flower</blue>
        <blue>sand stone</blue>
        <light-blue>sky</light-blue>
        <light-blue>water</light-blue>
    </color>
</doc>

可以使用xpath(一个Perl程序)和xmllint来处理XML文件

$ xpath -e '//blue/text()' sample.xml
Found 2 nodes in sample.xml:
-- NODE --
flower
-- NODE --
sand stone
$ xpath -q -e '//blue/text()' sample.xml
flower
sand stone

$ xmllint --xpath '//blue/text()' sample.xml
flower
sand stone

如果你需要Perl的强大功能,使用XML::LibXML模块会有所帮助

$ perl -MXML::LibXML -E '
    $ip = XML::LibXML->load_xml(location => $ARGV[0]);
    say $_->to_literal() for $ip->findnodes("//blue")' sample.xml
flower
sand stone

$ perl -MXML::LibXML -E '
    $ip = XML::LibXML->load_xml(location => $ARGV[0]);
    say uc $_->to_literal() for $ip->findnodes("//blue")' sample.xml
FLOWER
SAND STONE

处理 JSON

JSON 文件也存在同样的问题。您不希望对此进行正则表达式处理

$ s='{"greeting":"hi","marks":[78,62,93],"fruit":"apple"}'

各种 JSON 模块,例如 Cpanel::JSON::XS 可以处理这个问题。例如,格式化打印

$ echo "$s" | cpanel_json_xs
{
   "fruit" : "apple",
   "greeting" : "hi",
   "marks" : [
      78,
      62,
      93
   ]
}

这里是特别选择的内容

$ echo "$s" | perl -MCpanel::JSON::XS -0777 -E '$ip=decode_json <>;
              say join ":", @{$ip->{marks}}'
78:62:93

有时将其放在脚本中会更简单(尽管这不再是真正的单行脚本)。我使用 Bash 函数作为快捷方式

$ pj() { perl -MCpanel::JSON::XS -0777 -E '$ip=decode_json <>;'"$@" ; }

$ echo "$s" | pj 'say $ip->{fruit}'
apple
$ echo "$s" | pj 'say join ":", @{$ip->{marks}}'
78:62:93

相同的非 Perl 示例是 jq,但这是需要单独安装的东西,可能不可用

$ echo "$s" | jq '.fruit'
"apple"
$ echo "$s" | jq '.marks | join(":")'
"78:62:93"

速度

与专用工具相比,Perl 通常较慢,但正则表达式引擎在某些回溯和量词的情况下表现更好。

$ time LC_ALL=C grep -xE '([a-z]..)\1' /usr/share/dict/words > f1
real    0m0.074s

$ time perl -ne 'print if /^([a-z]..)\1$/' /usr/share/dict/words > f2
real    0m0.024s

$ time LC_ALL=C grep -xP '([a-z]..)\1' /usr/share/dict/words > f3
real    0m0.010s

与 Awk 的关联数组相比,Perl 的哈希实现对于大量键的性能更好。下面的 SCOWL-wl.txt 文件是使用 app.aspell.net 创建的。 words.txt 来自 /usr/share/dict/words。Mawk 通常更快,但 GNU Awk 在这个特定情况下做得更好。

$ wc -l words.txt SCOWL-wl.txt
  99171 words.txt
 662349 SCOWL-wl.txt
 761520 total

# finding common lines between two files
# Case 1: shorter file passed as the first argument
$ time mawk 'NR==FNR{a[$0]; next} $0 in a' words.txt SCOWL-wl.txt > t1
real    0m0.296s
$ time gawk 'NR==FNR{a[$0]; next} $0 in a' words.txt SCOWL-wl.txt > t2
real    0m0.210s
$ time perl -ne 'if(!$#ARGV){$h{$_}=1; next}
                 print if exists $h{$_}' words.txt SCOWL-wl.txt > t3
real    0m0.189s

# Case 2: longer file passed as the first argument
$ time mawk 'NR==FNR{a[$0]; next} $0 in a' SCOWL-wl.txt words.txt > f1
real    0m0.539s
$ time gawk 'NR==FNR{a[$0]; next} $0 in a' SCOWL-wl.txt words.txt > f2
real    0m0.380s
$ time perl -ne 'if(!$#ARGV){$h{$_}=1; next}
                 print if exists $h{$_}' SCOWL-wl.txt words.txt > f3
real    0m0.351s

其他阅读内容


[图片来自 Riccardo Maria Mantero 在 Flickr 上的照片,(CC BY-NC-ND 2.0)]

标签

Sundeep Agarwal

Sundeep Agarwal 沉迷于写 书籍 和阅读小说(主要是奇幻和科幻)。

浏览他们的文章

反馈

这篇文章有什么问题?请在 GitHub 上打开一个问题或拉取请求,帮助我们