Perl 子集
一、前言
由于种种特殊符号和正则表达式的存在,Perl 不易读几乎是天然的,但只要稍微学一下相关的知识,立刻就能够感受到它在文件、字符串处理上的强悍。
Perl 能够涉及的领域特别的广,例如有人会拿它做后端语言写网站,但实际上,Perl 只是在文本处理领域表现的足够优秀,而在其它领域,与其它编程语言相比,它并没有什么优势。
我个人也只推荐大家用它来做文本信息处理以及本地自动化运维,不推荐使用这门语言其它的功能。
那么,要做到文本信息处理和自动化运维,到底需要哪些 Perl 的知识呢?
为了回答这个问题,于是就有了这片文章。写这篇文章的目的就是罗列出为达到这样的效果应该掌握哪些必要语法,而对于其它非必要语法,很抱歉,本文一概不予讨论。
也正是因此,我给这篇文章的取名 《Perl 子集》。
二、开发工具
推荐使用 PyCharm CE 或 IntelliJ IDEA CE 作为开发环境。对的,你没看错,就是 Jetbrains 家的 IDE !!!
没必要使用专业版,社区版足够了!
用 PyCharm 或 IDEA 打开 Perl 脚本后,一般会推荐你安装三个插件,其实我们只需要安装其中名为 Perl 的插件,图标看起来像个洋葱。
其余的插件是 Prolog 语言的插件,由于 Prolog 语言与 Perl 语言都是以 .pl 作为文件后缀,所以 IDE 也分不清你打开的 .pl 文件是 Perl 脚本还是 Prolog 源码,因此会推荐多余的插件给你。
请你一定要将多余的插件卸载掉,否则会影响 Perl 脚本的语法高亮和代码检测。
三、个人代码风格
本人既做过 Python 开发,又做过 Linux 运维,所以写 Perl 代码的风格偏向于向 Python 和 Shell 靠拢。
我希望能够将 Perl 代码写的工整且简洁,并充分发挥 Perl 胶水语言的特点。
胶水语言和跨平台,在某些情况下是有一定的矛盾的。如果过你用 Perl 调用了一个 Linux 系统特有的命令,这时 Perl 提现了胶水语言的优点,但却让整个程序无法实现跨平台。
因此,靠 Linux 吃饭的我,写 Perl 时不会关注跨平台的问题,甚至会刻意不去使用一些跨平台的方法(因为调用系统命令实现起来可能会更简单)。
我在使用 Perl 内置函数和子程序时,一般不会写括号。
至于原因,一是因为这样会使 Perl 的内置函数看起来更像 Shell 命令,二是因为 Perl 语言本身没有严格意义上的函数签名。
抬杠的话,Perl 甚至没有函数,只有“子程序”。虽然“子程序”和函数很像,但它们之间还是有区别的。
连 Perl 自身都刻意区别子程序和函数,我们干嘛要那么要像调用函数一样去调用子程序呢?所以,能不写括号就不写括号。
当然,这只是一般情况,并不绝对。在某些情况下,如果真的需要像调用函数一样调用子程序时,我也可能会写括号。关键是看哪种表意更加简洁直白。
三、基础语法
[重要] 一行式:
perl -ne 'print "Hello World"'
这是 Perl 黑客的必备技能。 -e 参数可以在命令行执行 Perl 代码, -n 参数可以在读取文件。
上面这行例子中没有给出特定要读取的文件,默认从管道读取内容。按回车的话,可以看到 shell 中会显示 ‘Hello World’, 按 Ctrl + C 可以中断程序。
[重要] Here 文档:
Perl 的 Here 文档和 Shell 的 Here 文档有相似之处,但也许多不同。下面的例子是我从菜鸟教程上拔下来的,注意观察包裹 EOF 的引号。
#!/usr/bin/perl
$a = 10;
$var = <<EOF;
这是一个 Here 文档实例,使用双引号。
可以在这输如字符串和变量。
例如:a = $a
EOF
print "$var\n";
#!/usr/bin/perl
$var = <<'EOF';
这是一个 Here 文档实例,使用单引号。
例如:a = $a
EOF
print "$var\n";
以上程序输出的结果分别为:
这是一个 Here 文档实例,使用双引号。
可以在这输如字符串和变量。
例如:a = 10
这是一个 Here 文档实例,使用单引号。
例如:a = $a
[一般] 注释:
Perl 使用 # 开头做注释
[一般] 脚本:
Perl 脚本的后缀可以是 .pl 或 .PL,当然,这是在 Windows 平台上;
如果在 *nix 平台上,是没有后缀之分的,只需使用 #!/usr/bin/perl 指名解释器并赋予可执行权限即可,但为了保证平台兼容性,依旧建议使用 .pl 做 Perl 脚本后缀。
[重要] 引号:
与 Shell 一样,Perl 中的:
- 单引号不会解析引号的变量;
- 双引号会解析字符串中的变量;
- 反引号会把括住的内容当做命令进行执行
[一般] 标量:
与 Shell 不一样的是,Perl 的标量必须永远都要带着 $ 符号,哪怕是在声明时。
[重要] 数组:
数组的声明与其它语言有很大差别,使用 @ 符号来指名是数组类型,例如 @tmparray
数组操作(有副作用,会改变原数组):
- 末尾添加:push @array
- 末尾删除:pop @array
- 首位添加:unshift @array
- 首位删除:shift @array
- 元素替换:splice @array, <首位偏移量> [, <替换元素个数> [, <替换元素列表>]]
创建新数组:
- 数组切片:@array[1..5] 截取数组的标号为 1 的元素到标号为 5 的元素(包含)
- 合并数组:@array3 = (@array1, @array2)
- 数组排序:sort [<排序规则>] <列表或数组>
- 字符串转换为数组:split [<分隔符> [, <指定字符串> [, <返回数组的元素个数>]]]
数组与字符串类型互转:
- 字符串转换为数组:split [<分隔符> [, <指定字符串> [, <返回数组的元素个数>]]]
- 将数组转换为字符串:join <连接符>, <列表或数组>
数组映射:
- grep
- map
- reverse
[重要] 哈希:
使用 % 表示哈希类型,例如 %tmphash。
将哈希值提取到数组的语法格式: @tmphash{key1, key2}
特殊函数:
- keys:读取哈希的所有键
- values:读取哈希的所有值
- exists:判断 key 是否存在
- delete:删除哈希中的某个元素
- each: 函数能以随机顺序返回一个双元素数组 , 该数组的一个元素是散列表项的键 , 另一个元素则是该键所对应的值 。
[一般] 条件语句:
比起使用大段的条件代码块,我个人更倾向于使用条件语句倒装。
注意:语句的执行顺序是先判断条件是否成立,然后再决定是否执行前面被装饰的表达式
print 'true' if $arg == 1;
print $x unless $x == 6;
[重要] 循环语句:
我非常不推荐使用循环语句!个人实现循环的方法是 Label + redo + 条件语句倒装。
给 Label 起名要像给变量起名一样,能够说明代码块的含义,这样能极大的增加代码的可读性。
循环控制语句
-
next
效果类似于 C 语言中的 continue 语句,但不完全一样。
由于代码块等效于只执行一次的循环体, 因此 next 语句亦可用于退出代码块,或者在 continue 代码块存在的时候跳转到 continue 代码块。
-
last
相当于是 C 语言中的 break。由于代码块等效于只执行一次的循环体,因此 last 语句亦可用于退出代码块。
-
continue
通常在条件语句再次判断前执行。注意:它和其它语言的 continue 很不一样。
个人对它的理解:为 Label 代码块提供了切换上下文的方式。
-
redo
直接转到循环体的第一行开始重复执行本次循环,redo 语句之后的语句不再执行,continue 语句块也不再执行。
redo 存在的核心意义:在某些条件下重新定义循环变量,并可以有悔地重新进行本轮循环。
范式如下:
#!/usr/bin/perl -w
Loop_Label: {
last if eof DATA; # 检测文件是否已经读取完毕;
my $var = <DATA>;
next if $var =~ /match_regex/g; # 跳转到 continue 代码段,往往用于扫尾工作
redo Loop_Label unless condition; # 不满足 condition 就开始循环
} continue {
# 扫尾代码场景举例:
# 例如安装程序中途执行失败,需要清空已经安装的文件
}
循环语句也可以像判断语句那样后置,但循环体能够容纳的代码有限。
个人认为,循环语句能够后置这种设计更多的是为了给 perl -ne 提供支持,用于强化命令行。
所以它不应该写在脚本中,因为脚本的执行环境往往要比命令行环境要来的复杂,而且涉及到代码维护的问题。
[一般] 运算符:
这里值得注意的是,比较运算符与 Shell 有些差异:
-
Shell:数字比较使用字符比较运算符,字符串比较使用数字比较运算符;
-
Perl:数字比较使用数字比较运算符,字符串比较使用字符比较运算符。
字符串连接运算符与 Lua 一样,使用 “.” 来连接两个字符串。
[一般] 时间:
默认状态下时间函数返回的是毫秒数,但在不同的上下中,为变量赋予的值也不尽相同,这一点要注意。
特殊变量
有人说,使用英文名可以让程序变得更易读,但实际上提升效果不大。
全局数组特殊变量、全局哈希特殊变量、全局特殊文件句柄 和 全局特殊常量 本身就使用英文变量名,不需要引用 English 照样可以使用,而全局标量特殊变量、正则表达式特殊变量和文件句柄特殊变量的英文名可以说是又臭又长。
所以,English 这个包显得很鸡肋。
我这里只挑一些常见的特殊变量来进行说明。
全局标量特殊变量
| 变量名 | 功能 |
|---|---|
| $_ | 默认输入和模式匹配内容(不建议使用,因为这个变量会让程序很难读) |
| $/ | 输入记录分隔符,默认是新行字符。如用 undef 这个变量,将读到文件结尾(配合 STDIN 来使用) |
| $\ | 输出记录分隔符(配合 STDOUT 来使用,比较常用) |
| $? | 返回上一个外部命令的状态(配合调用 Shell 使用) |
| $$ | 运行当前 Perl 脚本程序的进程号 |
| $0 | 包含正在执行的脚本的文件名 |
| $! | 这个变量的数字值是 errno 的值,字符串值是对应的系统错误字符串(异常处理打印报错信息时比较常用) |
全局哈希特殊变量
| 变量名 | 功能 |
|---|---|
| @ARGV | 传给脚本的命令行参数列表 |
| @INC | 在导入模块时需要搜索的目录列表 |
| @F | 命令行的数组输入 |
全局特殊文件句柄
| 变量名 | 功能 |
|---|---|
| ARGV | 遍历数组变量 @ARGV 中的所有文件名的特殊文件句柄 |
| STDERR | 标准错误输出句柄 |
| STDIN | 标准输入句柄 |
| STDOUT | 标准输出句柄 |
| DATA | 特殊文件句柄引用了在文件中 __END__ 标志后的任何内容,包含脚本内容。 或者引用一个包含文件中__DATA__ 标志后的所有内容,只要你在同一个包有读取数据,__DATA__ 就存在 |
| _ (下划线) | 特殊的文件句柄用于缓存文件信息(fstat、stat和lstat) |
全局特殊常量
| 变量名 | 功能 |
|---|---|
| __END__ | 脚本的逻辑结束,忽略后面的文本。 |
| __FILE__ | 当前文件名 |
| __LINE__ | 当前行号 |
| __PACKAGE__ | 当前包名,默认的包名是main |
正则表达式特殊变量
| 变量名 | 功能 |
|---|---|
| $n | 包含上次模式匹配的第n个子串 |
| $& | 前一次成功模式匹配的字符串 |
| $` | 前次匹配成功的子串之前的内容 |
| $' | 前次匹配成功的子串之后的内容 |
| $+ | 与上个正则表达式搜索格式匹配的最后一个括号 |
文件句柄特殊变量
这部分变量,我用的非常少,所以也不是很理解。
| 变量名 | 功能 |
|---|---|
| $| | 如果设置为零,在每次调用函数 write 或 print 后,自动调用函数 fflush,将所写内容写回文件 |
| $% | 当前输出页号 |
| $= | 当前每页长度。默认为 60。 |
| $- | 当前页剩余的行数 |
| $~ | 当前报表输出格式的名称。默认值是文件句柄名。 |
| $^ | 当前报表输出表头格式的名称。默认值是带后缀"_TOP"的文件句柄名 |
子程序
承接参数有没有一种优雅的写法呢?
这里给大家提供一种参考:
sub test {
my $arg3 = shift;
my ($arg2, $arg3) = (shift, shift);
……
}
在参数数量较少的情况下,这种写法相对来说比较优雅,给那些不愿意写 @_ 的同学提供一个参考。
shift 还可以换成 pop,二者结合的话,还可以给特定位置参数赋予特殊的意义,例如:第一个参数、最后一个参数等。
如何像 Perl 一样在不同的上下文中返回不同的值呢?
可以使用 wantarray 函数来检测子程序处于什么上下文当中,并返回一个合适的结果到上下文:
sub contextualSubroutine {
# 调用者如果想要一个数组
return ("Everest", "K2", "Etna") if wantarray;
# 调用者如果想要一个标量
return 3;
}
my @array = contextualSubroutine();
print @array; # "EverestK2Etna"
my $scalar = contextualSubroutine();
print $scalar; # "3"
变量访问控制
[一般] my
作用:声明一个词法变量
注意:
-
仅在当前作用域有效,在子程序中或外部域中无效
-
如果 my 与 local 不在同一作用域,那么 my 声明的变量无法被 local 重新修饰。
因为 my 声明的是局部变量,而 local 无法创造变量。
-
全局变量应该尽量避免使用 my
-
临时变量应该尽量多用 my
[重要] local
作用:为全局变量提供一个临时值
注意:
-
local 不能创造变量
-
作用范围是当前子程序以及在当前子程序中被调用的子程序;
-
它无法修饰全局中使用 my 修饰过的变量,这也是全局变量不该使用 my 的原因
-
如果一个全局变量是空值(没有默认值),可以用它来给全局变量做局部初始化
local 是一个很独特的访问控制关键字,很明显是配合 Perl 子程序设计出来的
[一般] our
作用:声明一个全局变量。
注意:
- 全局变量要在被赋予初值后才能使用
[一般] state
作用:声明一个静态变量。
注意:
- 使用 state 前,要在脚本开头通过 use feature 'state' 导入
异常处理
[一般] 捕获异常:
Perl 使用 if 语句和 unless 语句来捕获异常。
这导致的一个问题是:你必须精确的知道,你的哪一行代码可能会出问题。
毕竟 if 和 unless 语句的括号之间空间很小,不能容纳太多语句。
如果每次调用命令都用 if 捕获以下异常,会导致写出来的代码与 Go 语言很像。(明白的人自然会明白,如果不明白,你也不用去纠结这句话的意思)
[一般] 抛出异常:
print 函数用于像标准输出中打印信息,一般用作信息提示。
warn 函数用于出发一条警告信息,不会有其他操作,输出到 STDERR;
die 函数与 warn 类似,但它会执行退出。一般用作错误信息的输出。
类似于 Python 日志的级别,print、warn 和 die 构成了 Perl 脚本输出信息的三个级别。
正则表达式
如果不是因为正则表达式,我想你也不会用 Perl。无需多言,超级重要!!!
- 匹配:m//(m 可以省略)
- 替换:s///
- 转换:tr///,别名 y///
正则表达式默认从 $_ 中读取待匹配内容(所以可以在 while 循环内部使用)
这三种形式中,/ 是默认的分隔符,如果表达式中同样含有 /,可以考虑换用其它字符做分隔符,例如 s@@@,y$$$
这三种形似都和 =~ 或 !~ 搭配使用,=~ 表示匹配,!~ 表示不匹配。
匹配:m//
m 是单词 match 的缩写,意思是“匹配”。
m 是可以省略的,即可以写作 //。
模式匹配修饰符
| 修饰符 | 描述 |
|---|---|
| i | 忽略模式中的大小写 |
| m | 多行模式。 1. 以换行或字面量(\n)作为行尾,对 $ 符进行匹配。 2. 一般要配合 g 修饰符使用 3. 使用 m 模式时,应放在循环语句的循环范围语句中使用,或者使用数组 @array=m// 来保存结果,否则 $& 会只返回最后一次匹配到的值 |
| o | 仅赋值一次 |
| s | 单行模式,让 . 可以匹配 \n。 PS:一般情况下,如果无需保留字符串原格式,我更推荐你先把换行符删掉再匹配。 |
| x | 忽略模式中的空白。 主要目的是用来增加复杂正则表达式的可读性,可以通过添加空格、Tab 或回车来从视觉上分隔正则表达式,且不用担心空白字符会成为正则表达式的一部分。 |
| g | 全局匹配,因为 Perl 默认只替换第一个找到的目标字符串 |
| cg | 全局匹配失败后,允许再次查找匹配串 |
m 模式举例:
# 准备工作
# 声明变量
my $str = "line123\nline456\nline789";
# 变量也可以声明成下面这种形式
my $str = "line123
line456
line789";
# 不使用 m 模式:
for my $i ($str =~ /\d+$/g) {
print $i;
}
# 打印信息如下:
789 # 由此可见,不使用 m 时,正则表达式只匹配了最后一个多位数字
# 使用 m 模式:
foreach my $i ($str =~ /\d+$/mg) {
print $i;
}
# 打印信息如下:
123
456
789 # 由此可见,使用 m 后,正则表达式匹配了每行的多位数字
正则表达式变量
- $`: 匹配部分的前一部分字符串
- $&: 匹配的字符串。
- $': 还没有匹配的剩余字符串
替换:s///
s 是单词 substitute 的缩写,意思是“替换”
替换操作有副作用,会修改原字符串。
替换操作修饰符
| 修饰符 | 描述 |
|---|---|
| i | 类似于匹配的 i 修饰符 |
| m | 类似于匹配的 m 修饰符 |
| o | 表达式只执行一次。 |
| s | 类似于匹配的 s 修饰符 |
| x | 类似于匹配的 x 修饰符 |
| g | 类似于匹配的 g 修饰符 |
| e | 替换字符串作为表达式(简单来说,就是 s///e 的第二部分中可以使用子程序) |
转换:y///
首先声明:y/// 是 tr/// 的别名,二者是同一个东西。
其实对于这个表达式,大家所看到的各类教程中,使用 tr/// 的似乎更多一些。
我之所以更愿意使用 y/// ,是觉得 y/// 对内部机制表达的更清楚。
y 应该是英语单词 yank 的缩写,有 “抽出、提起、抽屉” 的意思。
会用 vim 的小伙伴应该都知道 yy 这个快捷键,可将整行复制,其原理是将选中的内容保存到了寄存器(这个寄存器和 CPU 寄存器不是同一个东西,只是名字一样而已)
Perl 的 y/// 也用到了类似于 vim 的 y 命令的机制。
$string =~ y/SEARCH/REPLACEMENT/
使用 y/// 时,SEARCH 和 REPLACEMENT 都是不是严格意义上的正则表达式;
- 注意:这里是与 s/// 不同的,s/// 的 SEARCH 是正则表达式,而 REPLACEMENT 是替换字符串。
- SEARCH 所代表的含义,就像 [SEARCH],。举个例子 y/abc// 中,abc 表示匹配到 abc 三个字母中某个字母,就像 s/[abc]//。
- SEARCH 和 REPLACEMENT 都支持正则表达式的范围语法,即 a-zA-Z0-9
- SEARCH 和 REPLACEMENT 中没有通配符
SEARCH 和 REPLACEMENT 的匹配规则都是有顺序的。在没有任何转换修饰符时,SEARCH 和 REPLACEMENT 二者的匹配规则应该一一对应。
- 注意:上面我只说了长度相同的情况;
- 举个例子:y/abc/xyz/ 中,a -> x,b -> y,c -> z,这三者会一一对应。
- 当长度不同时,y/// 往往要配合修饰符来使用,具体细节见转换修饰符
SEARCH 是用来匹配字符串 $string 的,匹配到的内容会被存放到一个临时存储区,然后 REPLACEMENT 会去按照替换规则,一一替换临时存储区的内容。
转换操作修饰符
| 修饰符 | 描述 |
|---|---|
| c | 转化所有未指定字符 在 $string 中取 SEARCH 的补集,并将其全部替换为 REPLACEMENT 的最后一个字符(所以使用 c 时,REPLACEMENT 只需指定一个字符即可) |
| d | 删除所有指定字符。优先级比 c 修饰符低,所以 cd 连用时,会删掉所有未指定 |
| s | 把 SEARCH 规则匹配到的多个相同的输出字符缩成一个(没有被 SEARCH 规则匹配到的字符,即使由多个相同的字符,也不会被替换) |
| r | 原字符串不会被更改,但匹配表达式的返回值变更为 SEARCH 匹配到的所有字符经过修改后连接成的字符串。主要用来规避正则表达式会修改原字符这个副作用。 |
贪婪匹配与非贪婪匹配
Perl 默认是贪婪匹配,也支持非贪婪匹配。如需关闭贪婪性,用户只需在贪婪限定符后面加上问号 “?” 便可。这样就能让搜索操作在碰到第一个匹配时就告一段落 , 而不是直到最后一个匹配 。
往返断言
| 元字符 | 匹配项 |
|---|---|
| /PATTERN(?=pattern)/ | 正向前查找 |
| /PATTERN(?!pattern)/ | 负向前查找 |
| (?<=pattern)/PATTERN/ | 正向后查找 |
| (?<!pattern)/PATTERN/ | 负向后查找 |
当使用正的向前查找模式时,Perl 会在字符串中向前查找所需模式 ( ?=pattern )。如果找到了该模式,则继续执行正则表达式的模式匹配动作。负的向前查找模式则会向前查看模式 ( ?!pattern )。
是否存在,如果不存在,这才完成模式匹配。当使用正的向后查找模式时,Perl 会在字符串中向后查找所需模式 ( ?<=pattern )。如果找到了该模式,则继续执行正则表达式的模式匹配动作。负的向后查找模式则会向后查看模式 ( ?<!pattern )是否存在,如果不存在,这才完成模式匹配 。
其余基础的正则表达式规则,请自行搜索
文件系统树操作
Linux 系统实际上有 “两棵树”,一棵“文件系统树”,一棵“进程树”。
本节罗列的是操作文件系统树所需掌握的内容。
[重要] 目录操作:
-
创建新目录: mkdir
-
删除目录: rmdir
-
切换目录:chdir
-
目录下搜索:glob
这个函数比较有意思的是,它是使用 通配符(注意不是正则表达式,而是 Shell 通配符)来查找指定路径下的能够被匹配的文件,返回文件路径列表
至于 opendir 等函数,我觉得非常啰嗦。通过 Perl + Shell 完全可以解决这方面的问题,而且代码逻辑更清晰。
opendir 等其它工具,其存在的最大意义是其有利于实现跨平台。如果对跨平台有刚需,推荐你仔细学一下。
[重要] 文件操作:
进程树操作
Linux 系统实际上有 “两棵树”,一棵“文件系统树”,一棵“进程树”。
本节罗列的是操作进程树所需掌握的内容。

浙公网安备 33010602011771号