Perl 入门

出差两周,无心画画,编码环境又恶劣,恰逢一些需要进行文本处理的需求(csv->sql),用 java 处理起来痛苦至极(早该把 node 环境装上的),幻想能否有更好的工具来做文本处理。某日看到一篇 文章,声称两个半小时学会 perl,猛然意识到 git for windows 自带 perl,遂起了一些念头。回家又问 GPT,发现 Perl 对函数式编程有部分支持,语法简洁,系统调用方便,因此开始幻想着能否熟练 perl,将其当作主力的脚本语言。

其实在上一家公司里,编码环境恶劣的情况下也尝试学习过 perl,但当时应该是能力问题且没找到正确的资料,始终没能理解印记($@%)的使用,遂放弃,这次跟随这篇文章把这个坎越过去了(心智模型是,根据印记后的东西的类型决定用何种印记,比如我们知道数组,哈希均是只能存储标量的,因此从其中取出的东西必然是标量,因此访问数组和访问哈希时,均使用$作为印记,但该心智模型对函数的返回值,对&印记似乎行不通),这里记录一下学习过程中敲的代码。

正则表达式,模块/包,OOP 还没有开始学习,后两者没啥必要,前者之后专门了解。

Hello World

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
use strict;
use warnings;
use 5.010; # 指定最低兼容版本,目的是为了开启 say 函数
# print 不会在末尾添加换行符,使用 say 保证好看

# 函数调用不需要括号,不一定需要
# 行末分号必须!
say 'Hello';
say 'World';

# 三种类型:标量,数组,哈希,相应变量相关的符号分别是 $,@,%

# 标量,包含:undef(即 null),数值(即 number),字符串,变量引用(指针)
my $undef = undef;
my $someScalar = 'hello';
my $someNumber = 12.3 + 12.4;
say $someNumber;

# . 连接字符串
say 'Hello, ' . 'World!';

say "双引号字符串允许插值,就像 shell:$someScalar";
say '同时,只有在双引号下形如、n 的转义符才会作用';

# "bool" 类型,undef,0,'',"0" 为 false,其它为 true
# 一般来说,true===1,false===""
if (undef) {
say 'undef is true!';
} else {
say 'undef is false!';
}

# 很难判断 0 和 '0' 的区别,但它们确实是不同的,去 json 序列化一下就知道了

my $str1 = '4A';
my $str2 = '4B';

# 为什么很难?看看这个:
say ($str1 + $str2), "\n"; # 8

# 数字和字符串使用不同的两套运算符——
# 数字 : < > <= >= == != <=> + *
# 字符串: lt gt le ge eq ne cmp . x
# <=>返回-1,0,1;字符串的 x 需要一个操作符为数字

say ('hello' x '10'), "\n"; # 或者 10 也行,hellohellohello..

# Array,使用@,() 来声明
my @array = (
'Hello',
'World',
'Me', # 末尾可以有,好文明
);

# !!! 使用$来存取 array 中的值,因为取到的是标量
say $array[1]; # World
say $array[-2]; # World
say $array[-4]; # "",返回警告

say "数组长度:". scalar @array;
say "数组最后下标:". $#array;
say "数组内容:@array";
say "\$和、@如果出现在双引号字符串中需要转义";
say '$$$@@@';
# 数组“解构”
my ($a, $b) = @array;
say $a, $b; # HelloWorld

# hash,使用%符号
# 注意,$@%变量处在不同的命名空间下,因此可以重名
my %idol = (
name => 'haruka',
age => 16,
3 => 4,
);
# =>和,完全等价,决定是 Array 还是 Hash 的看是@还是%
my @ohMeOneArray = (
name => '11',
age =>
);
# 好吧,其实不一样,“key”可以不加引号
# 取字段要使用{},前面仍然是$,因为结果是标量
# 为什么 Array 和 Hash 要使用不同的操作符来取元素?因为它们能重名,可以有歧义……wqnmd
say $idol{'name'}; # haruka

# 能同时取多个项,显然这时候的结果是数组,因此是@
say @idol{'name', 'age'}; # haruka16

# 哈希"解构"
my ($name, $age) = @idol{'name', 'age'};
say $name, $age; # haruka16

# Array 和 Hash 可以互转,只要 Array 元素数量是偶数就行
# 但是键值对的位置可能改变,因为 Hash 不是顺序存储的
my @idol = %idol;

# 上面,Array 和 Hash 的初始化语法中,=右边的形式是完全一样的
# 这样的字面量称为列表 list
# !!! 因为它们形式完全一样,所以列表不能嵌套,因为 perl 无法知晓嵌套的列表的类型
# !!! perl 会直接放弃治疗,把这玩意扁平化再处理
my @array = (
'hello',
'world',
(
'earth',
'is',
('flat!')
)
);
say @array; # helloworldearthisflat!
my %whatTheFuck = (
'hello',
'world',
(
'earth',
'is',
('flat!')
)
);
say %whatTheFuck; # 警告 Odd number of elements in hash,打印:earth is flat! hello world

# 但这个行为让连接 Array 和合并 Hash 变得简单:
my @arrayA = (1, 2, 3);
my @arrayB = (0, 0, @arrayA, 2, 3, 4);
say @arrayB; # 00123234

my %hashA = (name => 'haruka', age => 16);
my %hashB = (%hashA, class => 765);
say %hashB; # name haruka age 16 class 765

# !!! perl 代码对上下文敏感——表达式要么在 scalar 上下文中求值,要么在列表上下文求值,取决于此处期待产生一个 scalar 还是列表

# 首先,scalar 和 array,hash 初始化时,分别为 scalar 上下文和列表上下文
# 这意味着……
my @array = 'hello'; # 等价于 my @array = ('hello');,虽然'hello'是 scalar,但是因为上下文,是作为列表被看待的
my $scalar = ('hello', 'world'); # 等价于 my #scalar = 'world';
say $scalar; # world
my $scalar = @array; # 等价于 my $scalar = scalar @array;,即返回 array 的长度
say $scalar; # 1
# 如何理解这个不是返回最后一个元素?因为这是 Array 在 scalar 上下文中求值,而不是 list 在 scalar 上下文中求值
say scalar @array; # 1
say scalar ('1', '2'); # 1

say scalar %hashB; # 3,hash 在 scalar 上下文中求值也是大小
say "%hashB \n";
say 'say 函数', '本身在 list 上下文中求值', '它接受任意个参数';

# 列表只能包含标量,到了引用出场的时候了
my @outer = (1, 2, 3);
my @inner = (100, 200);
# 如何让 outer 的头部存储 inner 呢?
$outer[0] = @inner; # 这样?
say @outer; # 223,因为@inner 在标量上下文中求值得到 2
# 使用反斜杠创建引用
my $someValue = 'hello';
my $someValueRef = \$someValue; # 这 tmd 是指针啊草
say $someValueRef; # SCALAR(0x7fa5a9073d40)
${$someValueRef} = 'world'; # 解引用,等价于 $$someValueRef = 'world';
say $someValue;

# array 和 hash 的引用可以使用->运算符来取元素
my @color = ('red', 'blue', 'green');
my $colorRef = \@color;
# 三者等价
say $color[0];
say ${$colorRef}[0];
say $colorRef->[0];

my %idol = ( name => 'haruka', age => 16, 3 => 4, );
my $idolRef = \%idol;
# 三者等价
say $idol{'name'};
say ${$idolRef}{'name'}; # 可以理解为 $$idolRef = @idol,左结合 lol
say $idolRef->{'name'};

# [],{} 生成匿名 array 和 hash,它们以引用的形式返回,因此是标量
my $office = {
name => '765',
description => '...',
idols => [
{name => 'haruka', age => 16},
{name => 'chihaya', age => 16},
{name => 'miki', age => 14},
],
};
say $office->{'name'}; # 765
# 如何获取 haruka 的 age 呢?
# 首先要获取 idols 数组引用,即
my $idolsRef = $office->{'idols'}; # ARRAY(0x7fb094829ed0)
# 然后,获取第一项元素的引用
my $harukaRef = $idolsRef->[0];
say $harukaRef; # HASH(0x7f7ac18d7430)
# 然后,获取 age
say $harukaRef->{'age'}; # 16
# 还好,能够链式调用
say $office->{'idols'}->[0]->{'age'};
# perl 很贴心,允许连续的调用时省略后面的->
say $office->{'idols'}[1]{'name'}; # chihaya
my %office = (
idols => [
{name => 'hibiki'}
]
);
# 更贴心的是,如果最外层是 Hash,第一个->也可以省略,这下原汁原味了
# 显然,这就是定义复杂数据类型的最佳实践,没有之一,最外层使用列表,里层使用匿名 array 和匿名 hash
say $office{'idols'}[0]{'name'}; # hibiki
say $office{'idols'}->[0]->{'name'}; # hibiki
# 理解这个语法的心智模型可以是,首先处理$office{'idols'} 得到 Array 引用,然后 perl 在后面每个间隔处添加一个->

# !!! 如何整蛊——这个 array 只有一个元素,即这个匿名 array 的引用
my @itsMyFaultNow = [1,2,3,4,5];
say scalar @itsMyFaultNow; # 1
say @{$itsMyFaultNow[0]}; # 12345

# 类似 ${$SOME_REF},@{$SOME_REF},可以认为是解引用$SOME_REF 为 scalar,array...

# 这语言指定有点大病
my $magic = \\\\\\1;
say $$$$$$$magic; # 1

# 所以,嵌套数组是——
my @matrix = ([1, 2], [3, 4]);
say $matrix[0][1]; # 2
say @{$matrix[0]}; # 12
# 记住,最左边,最外面的$@%符号,始终指示着返回值的类型

say "\n".'分隔符=================================';

# 变量终于结束了……开始控制结构

# if,elsif,else,典型的,注意拼写
# !!! if 语句在 scalar 上下文中求值,这符合逻辑
my $cash = 30;
# 括号必须……
if ($cash < 10) {
say 'you pool' # 注意最后一个语句可以不用加分号
} elsif ($cash >= 10 && $cash < 20) {
say 'just soso'
} else {
say 'worth a headshot'
}
# 注意 || 和 &&,行为同 js
say '' || 1; # 1
say 0 && 1; # 0

my $tempo = 120;
# 类似 ruby 的语法
say 'It\'s too slow!' if $tempo <= 222; # It's to slow!
say 'It\'s too slow!' unless $tempo > 222;

# 有 c 风格的三目,但我不给例子

say "\n".'分隔符=================================';

# 循环

# 最基本的 while, until, for
my @arr = (1,2,3,4,5);
my $i = 0;
while ($i < scalar @arr) {
say $arr[$i++];
}
my $i = 0;
until ($i >= scalar @arr) {
say $arr[$i++];
}
say "\n";
for (my $i = 0; $i < scalar @arr; $i++) {
say $i . ':' . $arr[$i] . "\n";
}
# 这里就离开了$i 的作用域了

# 但是若没有必要,谁愿意写这种 for 呢?
# 迭代器语法 foreach(和 for 可以互相替换,用可读性高的)
# 想象 python 的 for i in arr
foreach my $v (@arr) {
say $v;
}

# range 操作符。.,返回一个匿名的整型列表,前闭后闭
my @range = (1 .. 10);
say @range; # 1234512345678910

# 从 0 到最后一个下标
foreach my $i (0 .. $#arr) {
say $i . ':' . $arr[$i] . "\n";
}

# keys 函数返回 Hash 的所有 key,用来迭代 hash
foreach my $key (keys %idol) {
say $key . ':' . $idol{$key} . "\n"; # name:haruka...
}

# 开始魔法/约定——如果不给定迭代的变量(这玩意似乎叫迭代器),使用$_作为迭代变量:
foreach (@arr) {
say $_;
} # 12345
# 如果循环里只有一条语句……可以使用下面的语法……omg
say $_ foreach @arr;

# next 和 last,相当于 continue 和 break

# 比如,找到数组中特定的数,并执行操作
my @arr = (1,2,3,4,5,6,7);
my $toFind = 4;
foreach (0 .. $#arr) {
my $value = $arr[$_];
next if $value != $toFind;
say $value. '...got it! index: '. $_;
last
}

数组操作

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
use strict;
use warnings;
use 5.010; # 指定最低兼容版本,目的是为了开启 say 函数

# 和 js 的假设一样,数组头部是队列头部,尾部是栈尾部
my @stack = (1, 2, 3);
say @stack; # 123
push @stack, 4, 5, 6;
say @stack; # 123456

# 注意函数调用的优先级,不是像 haskell 一样左结合,而是某种右结合
say pop @stack; # 6

say @stack; # 12345

unshift @stack, -1, 0;
say @stack; # -1012345

say shift @stack; # -1
say @stack; # 012345

# 它们都是 splice 的特例,splice 将数组中特定范围的切片捞出来,替换为另一个数组,并返回捞出来的数组
my @arr = (1, 2, 3, 4);
say splice @arr; # 只有 1 个参数,清空数组,并返回清空前的值即 (1,2,3,4)

my @arr = (1, 2, 3, 4);
say splice @arr, 2; # 2 是要删除的范围的开头,这里会删除原数组的 (3, 4),并返回 (3, 4)
say @arr; # 12

my @arr = (1, 2, 3, 4);
# 1,2 是要删除的范围的开头和删除的数量
say splice @arr, 1, 2; # 23
say @arr; # 14

# 隔壁 substr 入参形式也是一样的,从下标 2 开始,取 2 个字符
say substr 'hello, world', 2, 2; # ll

my @arr = (1, 2, 3, 4);
# 第四个以后的参数为要用来替换的数组,为什么不需要扩起来?hint:函数参数也是列表
say splice @arr, 2, 1, 200, 300; # 3
say @arr; # 122003004

# 另外一些列表操作。..join, reverse, map, grep
my @arr = ('hello', 'the', 'world');
say @arr; # hellotheworld
say "@arr";
say join ', ', @arr; # hello, the, world

# reverse,列表上下文下反转数组,标量上下文下反转字符串
say reverse 'hello'; # hello(默认就是列表上下文,只有一个元素也是列表上下文,
say reverse 'hello', 'world'; # worldhello
say reverse @arr; # worldthehello
say scalar reverse @arr; # dlrowehtolleh,注意 scalar 不能当成函数来看待,行为和函数不一样,它直接影响了 reverse 的行为

# map,不用解释,当前元素为 $_
# 注意 map 里函数和数组之间没有逗号,不知为何,不知这是如何定义的
my @arr = qw(hello the world); # qw,quote word, 等价于 ('hello', 'the', 'world')
say @arr; # hellotheworld
say join ', ', @arr; # hello, the, world
say join ', ', map {uc $_} @arr; # HELLO, THE, WORLD,uc 是 upperCase,$_是当前 map 的元素,可以省略
say join ', ', @arr; # hello, the, world,无副作用

# grep,filter
my @lst = qw/1 2 3 4 5 6 7 8 9/;
say grep {$_ % 2 == 0} @lst; # 2468

# 结合 grep,eq/== 和 scalar 可以用来判定数组中是否包含特定元素
my @fruits = qw/apple orange banana/;
say 'love banana' if scalar grep {$_ eq 'banana'} @fruits; # love banana
say @fruits; # appleorangebanana,grep 无副作用
# sort,默认是使用字符串的比较方式,这很多时候不是我们想要的
# sort 无副作用
my @elevations = (19, 1, 7, 23, -2, 0, 100, 29666);
say join ', ', sort @elevations; # -2, 0, 1, 100, 19, 23, 29666, 7
# sort 第二个参数可以传递一个代码块作为 comparator,其中比较的两个元素分别为 $a,$b,这是常见的约定
say join ', ', sort {$a <=> $b} @elevations; # -2, 0, 1, 7, 19, 23, 100, 29666,这里的$a,$b 不可省略

say map uc $_, qw(1 2 3 4 5 6 hello);

子例程

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
use strict;
use warnings;
use 5.010; # 指定最低兼容版本,目的是为了开启 say 函数

# 子例程,用户定义的子例程总是接受列表,但内置的函数各有各的行为。
# !!!!!!!!!!! 子例程是引用传递,引用传递!不是值传递,不是引用的值传递,是引用传递,这和所有编程语言都不一样
sub inc {
# 函数参数保存在 @_
$_[0]++
}
my $v = 1;
inc $v;
say $v; # 2
# inc 2; # error, Modification of a read-only value

# 取参数可以使用 shift 函数
sub pureInc {
my $v = shift; # 等价于 shift @_
return $v + 1;
}
say pureInc 2; # 3

# 多个参数可以使用某种“解构”语法
sub add3Num {
my ($a, $b, $c) = @_;
return $a + $b + $c;
}
say add3Num 1, 2, 3; # 6
# 更多的参数?传 hash 吧

# 能够使用 wantarray 函数来检查当前的上下文是否是列表上下文
sub contextualSubroutine {
return (1, 2, 3) if wantarray;
return 1; # scalar 上下文
}
say contextualSubroutine; # 123,say 和 print 是列表上下文
say scalar contextualSubroutine; # 1

系统调用

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
use strict;
use warnings;
use 5.010; # 指定最低兼容版本,目的是为了开启 say 函数

# 定义一个 trim,后面用
sub trim {
my $string = shift;
$string =~ s/^\s+//; # 去除开头的空白符
$string =~ s/\s+$//; # 去除结尾的空白符
return $string;
}

# tip:程序返回给系统的返回码是 16 位,高 8 位是我们认知中的返回码,0 表示正常退出,低 8 位不用关心

# perl 能够通过 exit 函数来退出,接受参数为返回给系统的 0-255 的返回码
exit 0 if 0; # 别!

# system 函数允许进行系统调用,参数列表为所有参数,返回值为 16 位返回码,标准输出会打印到控制台
# 返回码同时也会保存在$? 中,为 16 位,需要右移 8 位
system 'ls', '-alh', '/';
$? >>= 8;
say "ls return code: $?";

# 使用反引号也可以执行命令,返回值在标量上下文下为命令标准输出的字符串形式,列表上下文则按行分割
# 若报错,会打印到错误流(应该
my $path = '/a/b/c';
my @rootFiles = `ls $path`;
$? >>= 8;
say "return code: $?, res: ", join ', ', map {trim $_} @rootFiles;

# 文件句柄也是标量
# 打开文件,读模式,文件句柄保存在$fn,open 若打开成功返回 true,打开失败返回 false,并将错误信息置于 $!
my $file = '/Users/yuuki/code/tmp/a.js';
open my $fn, '<', $file or die "cannot open $file, reason: $!";
# readline 函数能读取文件的每一行(显然当前读取位置保存在句柄里面
while (1) {
my $line = readline $fn;
last unless defined $line; # 读取完成后返回 undef,也可以使用 eof 函数检查是否到末尾
chomp $line; # 移除末尾可能的换行符,注意这个方法有副作用
}
# 文件句柄会在离开作用域后自己关闭

open my $fn, '<', $file or die "cannot open $file, reason: $!";
# while (my $line = readline $fn) 看上去很诱人,但它遇到"0",空字符串也会停止!不要这么做!
# <>运算符可以避免该问题,可以认为<>在底层读取一行后会保存到$_,并使用 defined 检查当前是否是 undef,可以认为底层是 while (defined(my $_ = readline $fn))
while (<$fn>) {
chomp $_; # 移除末尾换行符
say $_;
}
# <>运算符可以更酷,它赋值到标量会读一行,赋值给数组,可以读取全部内容到数组中,比如下面统计行号
open my $fn, '<', $file or die "cannot open $file, reason: $!";
my $firstLine = <$fn>;
print $firstLine; # 它自带换行符
my @lastLines = <$fn>;
chomp foreach @lastLines; # 为每一行都去掉末尾的换行符
say @lastLines; # 会发现它们全扭在一起了
say scalar @lastLines; # 剩下的行数,22 行

# <> 如果没有参数,会使用标准输入流。
# 更更更酷的是,unix 管道也可以当作文件句柄——
open my $rootLs, "ls / |" or die "execute ls / failed, $!";
my @rootFiles = <$rootLs>;
chomp foreach @rootFiles;
say "total count: " . scalar @rootFiles . ", files: @rootFiles";

# 如何写入文件呢?print 打印内容到输出流,默认是标准输出流,so……
# <, >, >>, 和 linux 的习惯一致
open my $writeFile, '>>', '/Users/yuuki/code/tmp/helloworld.txt' or die $!;
say $writeFile 'Hello'; # 注意不能逗号分隔,一逗号分隔 say 就选择打印该文件句柄了
say $writeFile 'World'; # 用户不能自定义这样的函数,只能暂且认为这里 say $writeFile 是一个整体

# 且慢!在函数参数里定义变量是什么操作?因为 perl 是引用传递,所以定义的变量能直接被函数所修改,这里就像 C#的 out

sub lastElem {
my $pointer = \$_[0];
my ($ignore, @last) = @_;
$$pointer = $last[-1];
}
lastElem my $last, (1, 2, 3);
say $last; # 3

# 数组解构
my ($a, $b, @c) = (1, 2, 3, 4, 5);
say $a; # 1
say $b; # 2
say @c; # 345

# 咳咳,回到正题
# 检查文件是否存在的一些函数是和 shell 习惯一致的,
# -e 文件是否存在
# -d 文件是目录,ef 文件是文件
# 查询这些函数的文档需要搜 “perl file test”,因为-X 是不搜索 X
sub fileExist { # 屁话
return -e shift;
}