【上集回顾】

上次说到了-p-n参数,其实再加上之前学的-e参数已经可以做很多事情了,但是为了方便,Perl还有这样一对搭档组合的参数,就是-a-F

【参数解释】

-a : 将读入的$_进行分割,保存到@F列表之中,类似于split /分隔符/ , $_; 而这个分隔符是由-F参数指定的,其实这个功能与awk工具相似

-F : 在添加-a参数时候,指定分隔符(可以是正则表达式),如果不加好像是由空格作为分隔符,一般对其进行设置

实例说明

为了更加清晰的说明,还是举例子吧,打开终端或者git for windows

输入

1
2
3
4
5
6
7
8
9
10
11
12
# 例如我需要把来自管道的数据按照空格分隔成一个一个单元,存到列表里面
echo "qwe asd zxc" | perl -n -a -F"\s+"-e '
$" = "\n";
foreach my $item (@F){
print "$item\n";
}
'
--------------------------------------
# 输出
qwe
asd
zxc

这次的例子要比之前的例子复杂一点,我来一一说明

  1. echo在屏幕上打印出qwe asd zxc这个字符串,这字符串中间由空格分成三个部分,分别是qwe、asd、zxc
  2. echo的输出进入管道 |
  3. perl逐行读取(因为只有一行,所以直接读完了)
  4. 读取的字符串赋值给$_
  5. $_被分割为三份(应-a的要求,根据-F(注意双引号是贴着-F参数的,中间没有空格隔开)的指定的\s+(意思是按照一个或者多个空格或者制表符分隔,这里也可以改为" +"),将$_分割为三份)
1
2
3
4
5
6
7
8
9
10
11
qwe asd zxc
^ ^
| |
空格

# 根据空格来划分 \s+ 表示如果有多个空格相连也一并视为一个整体
# 切割之后,空格都消失

/ /
qwe/asd/zxc 成为 @F中的元素 ('qwe','asd','zxc')
/ /
  1. 分隔的三份按照顺序存在@F列表中
  2. 遍历@F列表,将其中的内容打印出来

其实你可能会说,用之前之前学的参数就够了啊!比如

1
2
3
4
5
echo "qwe asd zxc" | perl -n -e '
my @F = split /\s+/,$_;
$" = "\n";
print "@F\n";
'

那为什么要这样做呢?其实在平常的文本中比没有感觉到,在linux或者mac系统下面,有很多信息就是以文本的形式给出来的,而且中间一般都是用空格或者制表符分隔的,就比如使用df命令查看磁盘使用情况

1
2
3
4
5
6
7
df
------------------------------------------
# 输出
Filesystem 1K-blocks Used Available Use% Mounted on
C:/Program Files/Git 104857596 63822260 41035336 61% /
D: 318168060 313664144 125465657 40% /d
E: 41942012 21699268 20242744 52% /e

可以看到很鲜明的由空格或者制表符分隔的信息形式。 问题来了,利用这两个参数,我们可以试着做一下事情

问题1:我需要将所有的盘符提取并打印出来?

来试一下:

1
2
3
4
5
6
7
8
9
df | perl -n -a -F"\s+" -e '
print $F[0],"\n";
'
------------------------------------------
# 输出
Filesystem
C:/Program
D:
E:

可是标题行不是我想要的,怎么除去呢? 有多种方法

  1. 使用特殊变量$.

$.意为当前读取的行数

1
2
3
4
5
6
7
8
9
10
11
12
13
df | perl -n -a -F"\s+" -e '
# 第一行就是标题行了,直接跳过它
if($. == 1){
next;
}else{
print $F[0],"\n";
}
'
------------------------------------------
# 输出
C:/Program
D:
E:
  1. 借助Linux命令
1
2
3
4
5
6
7
8
9
# awk中NR为内置变量,与上面的perl中的 $. 变量意义相同,就是当前读取的行数
df | perl -n -a -F"\s+" -e '
print $F[0],"\n";
' | awk 'NR>1{print $0}'
------------------------------------------
# 输出
C:/Program
D:
E:

注意:你发现在现实盘符的时候C:/Program Files/Git显示的是不完整的,只显示了C:/Program,也就是它被分隔了!!这里要说明一下,文件夹是可以使用空格的(特别是像windows下面的系统文件夹C:/Program Files,的确是很烦人),这个时候使用空格分隔则要小心,一般在linux和mac下面碰不到这种情况。这里为了演示更加方便,然后便于初次的讲解,我把C盘排除掉。但是要是的确有需要加入C盘来进行处理也是可以进行的,只是有点复杂,这里我不叙述,在文章末尾我进行一下探讨。

问题2:我要计算D盘和E盘总共已经使用的磁盘的内存(排除了C盘,原因见上述说明)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 与上面一样,还是将df命令的结果读取进来,然后分隔成各个元素存到@F中去
df | perl -n -a -F"\s+" -e '
BEGIN{
$total = 0;
}
chomp;
# 排除C盘
if(m/^C:/){
next;
}
if($. == 1){
next;
}else{
$total = $total + $F[2];
}
END{
print "total use : $total\n";
}
'
----------------------------------------------------
# 输出
total use : 335363412

上面用到了两个特殊的代码块BEGIN{}END{},这两个代码块在perl的单行程序中会经常用到,说明一下它们两个的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 单行程序中的结构          # 流程解释
_________________________________________________________
BEGIN{ | +++++++++ 读取文件之前
代码1; | + 代码1 + 就运行代码1
} | +++++++++ 只运行一次
|
| ---> ++++
| ---> +代+ 然后每次读取一行
代码2; | ---> +码+ 运行一下代码2
| ---> +2 +
| .... ++++
|
END{ | +++++++++ 最后文件读取完毕
代码3; | + 代码3 + 运行代码3
} | +++++++++ 只运行一次
_________________________________________________________

其实BEGIN{}END{}块放的顺序和位置并不重要,也就是说可以这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 形式1

BEGIN{
代码1;
}
END{
代码3;
}
代码2;
------------------
# 形式2
END{
代码3;
}
代码2;
BEGIN{
代码1;
}
------------------
# 形式3
代码2;
BEGIN{
代码1;
}
END{
代码3;
}

其实除了这些由空格分隔的,我们平常使用的excel中的两个格式也是由特定的字符分隔的:

  • CSV文件 : 由逗号分隔的文本文件
  • TSV文件 : 由制表符分割的文本文件

对于这种文件,使用这两个参数进行搭配,就省了很多事儿,是吧

1
2
3
4
5
# 例如一个文件 123.csv
# 新建一个txt文本文件,将后缀名改成csv就可以
# 内容为
name,apple,banana,orange,grape,strawberry
color,red,yellow,orange,purple,red
  • 示例1

目标:打印出第一列,也就是标题

1
2
3
4
5
6
7
cat 123.csv | perl -n -a -F"," -e '
print "$F[0]\n";
'

# 输出
name
color
  • 示例2

目标:计算出现了多少种颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cat 123.csv | perl -n -a -F"," -e '
# 如果第一列是color就执行代码
if($F[0] eq 'color'){
# 将第一个元素给扔掉
shift @F;

for my $color (@F){
# 利用哈希对重复的颜色的合并
# 而不是简单的记录这个列表中有多少元素
# 因为存在重复的颜色
# 红色是两份,它的值为2
$hash{$color}++;
}
}
END{
# 使用scalar方法得到哈西键的个数
print "Total number of color type : ",scalar(keys %hash),"\n";
}
'
---------------------------------------------
# 输出
Total number of color type : 4
  • 示例3 目标:按照下面那样的方式打印出来(之间是逗号相隔开),这个其实就是列表的翻转,这个例子稍微有点复杂,这个例子意义其实不大。但是结合了多个perl单行程序
1
2
3
4
5
6
name,color
apple,red
banana,yellow
orange,orange
grape,purple
strawberry,red
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 代码开始
cat 123.csv | perl -n -a -F"," -e '

# 因为没有去处换行符,所以每一个元素后面均会带有回车符和换行符
# 这里将其除去
$F[-1] =~ s/\r*\n//;

my $title = shift @F;
my @items = @F;
my $item_num = scalar(@items) unless defined $item_num;
$title_num++;

# 列表里面的原始是有序的
# 用它来记录有顺序的title
push @title_list,$title;

# 哈希里面的元素是无序的
# 用它来记录每个title对应的该行的元素
$hash{$title} = \@items;

END{
$" = ",";

# 先输出标题行
print "@title_list\n";

# 然后打印出各个元素
for my $row (0..$item_num-1){
for my $key (@title_list){
print $hash{$key}->[$row];

print ",";
}
print "\n";
}
}
' | perl -p -e 's/,$//'

-----------------------------------------------------
# 结果
name,color
apple,red
banana,yellow
orange,orange
grape,purple
strawberry,red

补充说明

  • -a与-F参数的顺序不重要,但是一定要放在-e参数之前
  • -F指定分隔符的时候后面的分隔符要贴着-F参数,中间不要有空格之类,否则会报错

探讨

上面说到有时候文件夹会出现空格的情况,像上面出现的C:/Program Files/Git被分隔的情况。那这样难道就没有办法来处理吗?再来看一下df的输出结果:

1
2
3
4
5
6
7
df
------------------------------------------
# 输出
Filesystem 1K-blocks Used Available Use% Mounted on
C:/Program Files/Git 104857596 63822260 41035336 61% /
D: 318168060 313664144 125465657 40% /d
E: 41942012 21699268 20242744 52% /e

虽然按照空格来分隔可能有些行不通了,但是能不能转换一下思维,按照字符串的数量来划分:

1
2
3
4
5
|-------------------|---------|---------|---------|----|----------|
Filesystem 1K-blocks Used Available Use% Mounted on
C:/Program Files/Git 104857596 63822260 41035336 61% /
D: 318168060 313664144 125465657 40% /d
E: 41942012 21699268 20242744 52% /e

可以看到每一列对应的字符串长度(空格也会被计算)是一致的 来!试一试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 这个时候不能直接用空格分隔了,得采用一些特殊的方法
# 设置行数
export col=1
df | perl -n -e '
BEGIN{
# 设置想要打印的列数
# 传入环境中变量
$col = $ENV{'col'};
}
if($. == 1){
@title_slice = (22,11,11,11,5,11);
next;
}
my $offset = 0;
map {$offset+= $_} @F[0..$col-2] if $col-2 > 0;
print substr($_,$offset,$title_slice[$col-1]-1) =~ s/\s*(.+?)\s*/$1/r,"\n";
'
# 但是这样需要人工去数,也不是个好办法