跟我学Perl
2009-05-21 21:13:52 Administrator

Perl 是一种动态解释型的脚本语言。最初的设计者为拉里・沃尔(Larry Wall)于 1987 年 12 月 18 日发表。它借取了 C、sed、awk、shell scripting 以及很多其他编程语言的特性,其中最重要的特性是他内部集成了正则表达式的功能,以及巨大的第三方代码库 CPAN。它应用广泛,涵盖 CGI、图形编程、系统管理、网络编程、金融、生物等领域。由于其灵活性,Perl 被称为脚本语言中的瑞士军刀;鉴于 Perl 在实际工程应用中广泛使用,MacOS,Linux,FreeBSD 等现代化操作系统默认安装 Perl。

2000 年开始,拉里・沃尔着手开发 Perl 6 来作为 Perl 的后继,Perl 6 语言的语法有很多转变,也被视为 Perl 家族中的另一个语言(Perl 6 于 2019 年更名为 Raku)。目前 Perl 语言家族包含两个分支 Perl 5 以及 Perl 6。

其原名 Pearl,但是在这个语言正式发表前,拉里・沃尔发现已经有个语言 “pearl”,便将这个语言的名字改成 Perl。对于 Perl 这四个字母有一些解释,包括充满幽默感的 “Pathologically Eclectic Rubbish Lister”。 “Practical Extraction and Report Language” 曾经出现在很多有关 Perl 的资料里,包括官方的主页。不过事实上这个长名字是后来凑出来的。它的名字第一个字母大写(Perl)时就指这个编程语言,无大写字母(perl)时就指它的编译器。将 Perl 写成 “PERL” 是不适当的,因为它并非一个缩写词。

Perl 语言的中心思想可以集成为一句话「TMTOWTDI」:
There's More Than One Way To Do It.(不只一种方法來做这件事。)

Perl 的作者 Larry Wall 建议可以把这个缩写词念成「Tim Toady」。

另一个 Perl 程序员常常想起的俗语是:
Easy things should be easy, and hard things should be possible.

Perl 骆驼是 O'Reilly 设计给 Perl 语言的图标。它也是一种黑客的象征,出现在某些 T-shirt 和其他衣服标签上。O'Reilly 拥有此图标的商标,并且宣称,唯有在捍卫「符号之完整性」时,才会行使其法律上的权力。O'Reilly 允许此商标在非商业目的之前提下被使用,并同时供了 Programming Republic of Perl 的图像以及 Powered by Perl 的按钮图。然而,骆驼其实从未被考虑成为 Perl 的官方识别符号,真要说有的话,其识别符号其实是洋葱。

为了实现这样的目标,并且又因为 Larry Wall 本人也是一个语言学家,他设计 Perl 语言时使用了很多语言学的思维。相比 C、Pascal 这样的 “高级” 语言而言,Perl 直接提供泛型变量、动态数组、Hash 表等更加便捷的编程元素。Perl 具有动态语言的强大灵活的特性,并且还从 C/C++、Basic、Pascal 等语言中分别借鉴了语法规则,从而提供了许多冗余语法。使得程序员可以忽略计算机内部数据存储、类型、处理方法、运算规则、甚至内存越界等等的细节,而将思考中心放在所需要的程序逻辑上。就这一点而言,很多 Perl 程序员认为目前只有 Perl、Python 等泛型语言才能称为 “高级” 语言,而 C、Pascal 甚至 C++ 这些只能称为 “中高级” 语言而已。可以说,在统一变量类型和掩盖运算细节方面,Perl 做得比 Python 更为出色。

由于从其他语言大量借鉴了语法,使得从其他编程语言转到 Perl 语言的程序员可以迅速上手写程序并完成任务,这使得 Perl 语言是一门容易用的语言。但问题是这样写出来的 “类 xx 语言” 的 Perl 程序对不熟悉这种语言的程序员却如同天书,这使得 Perl 语言易写不易读。也正是因为 Perl 的灵活性和 “过度” 的冗余语法,也因此获得了 write-only 的 “美誉”,因为许多 Perl 程序的代码令人难以阅读,实现相同功能的程序代码长度可以相差十倍百倍。 但 Perl 同样可以将代码书写得像 Python 或 Ruby 等语言一样优雅。


如果你曾经使用过linux,无论时间长短,都必定听说过perl;甚至可能在不知道的情况下运行了不少perl的脚本。很多服务程序,象 “inews”、“mirror”、“debconf”、“majordomo”、“sirc”等等,都是纯粹用perl写的。在Debian区域的 “Packages.gz”文件里做一个简单的的“zgrep”,你就可以知道有382个包需要perl的支持(也就是意味着这些包都至少有一部分是用 perl写的),其它28个软件包也建议或推荐使用perl支持。

但是Perl语言有什么出色之处呢?

"Perl语言在文本处理方面非常突出,它把不同的内容联成一个整体。对于这种脚本语言来说所有的那些不同的元素,看起来都是一样的。”——John Ousterhout,Tcl脚本语言的作者。Perl即“Practical Extraction and Report Language”(实用析取报表语言)。是的,比较粗,但是我想那就是你所得到的——如果你打算确信$HUNN NGOUS CORP并且使用它的话。事实上,Larry Wall(Perl的创立者)在Perl的帮助手册页里提出:“Perl实际上代表的是Pathologically Edectic Rubbish Lister,但不要告诉任何人我这样说过。”唔,他是Larry,对此我又能再说些什么呢?

Perl已经被广泛的认为是“一种拥有各种语言功能的梦幻脚本语言”、“Unix 中的王牌工具”。以及其他的一些类似的称呼,这些都是赞誉之辞,Perl被用来写单行脚本、快速执行程序、大的规划项目(Amazon.com的所有评论产品和控制系统、Netscape的内容策划管理和传送系统、人类整组基因工程的DNA排序以及计划管理等等)。还有数以百万计的令我们惊讶的各种各样的事情的高速程序。Perl还能够实现许多UNIX的公共系统工具的功能

提示:如果你正在学或必须学awk, sed, grep和 tr,我建议以Perl来代替它们。所有的功能,更快的执行效率,相信你将永远不会因为它的性能发展太快而不适应它。

就像你对所有的现代语言所期望的那样,Perl允许你建立面向对象的程序。它也可以进行网络操作(例如socket等等),并且有良好的可移植性(一个写得好的脚本可以在Linux、BSD、Solaries、Dos、Win9x、NT、MacOS、OS/2、AnugaOS、VMS等操作系统中不需要任何修改的运行),编写和调试周期很短——由于没有编译的要求,你只需将变化的部分写出,就可以运行脚本。还有数目庞大的可适用于执行任何一项任务的模块(即预建立 Perl的例程),Comprehensive Perl Archiue Network(CPAN)就是每一个Perl程序员所能拥有的最好的模块库之一。哦,但那是真的吗?

问得好,我希望在你使用Perl大约一年之后,就能告诉我答案。一样东西的描述就像一个容器……,我仍旧在试图寻找一个适合Perl的足够大的容器(要是带有能锁的盖子就更好了)。

成为系统管理员的利刃


对于SysAdmins来说,Perl达到了一个“最佳点”:
1.它适用于所有Linux发行版(这很重要,因为在大型公司的安全部门通常禁止安装其他语言);
2.它在Shell环境中集成得非常好(可以说非常好);
3.它很容易替换多个实用程序(gas(grep、awk、sed),bash较长的脚本等),并且在不同风格的Unix解决方案之间是统一的;
4.存在与包括数据库在内的整个应用程序堆栈交互的模块。

Perl是一门需要认真学习的通用语言,学习它将收获了很多Perl和非Perl的东西,感谢Perl和那些好书。下面是一些个人在学习Perl过程中曾读过完整或部分章节且觉得好的书籍:
入门级别1:《Learning Perl》即小骆驼,中文版《Perl语言入门》,完全Perl 零基础最佳最快入门书籍
入门级别2:《Intermediate Perl》即羊驼,中文版《Perl进阶》,系《Perl语言入门》的进阶书籍
入门后复习:《Beginning Perl》,中文版《Perl入门经典》,似乎网上没什么评价,但是系统性学习Perl的非常好的资料
系统性学习和进阶:《Pro Perl》,中文版《Perl高级编程》(完善Perl的最佳书籍)
Perl编码技巧:《Effective Perl Programming》,中文版《Perl高效编程》

关于《Mastering Perl》(中文版《精通Perl》)和《Programming Perl》(中文版《Perl语言编程》),即羊驼一家和大骆驼在网上评价很高:
《精通Perl》是作者(brian d foy)以第一人称来描述他怎么理解Perl的。
《Perl语言编程》是Larry Wall自己编写的书籍,也许他智商太高,书中很多地方的跳跃性都非常大,不过这本书确实好。

然后是某个方向的书籍,比如:
http客户端《perl lwp》(循序渐进,作者的写作方式非常友好)
数据库操作《Programming the Perl DBI》(有中文版《Perl DBI编程》,如有数据库基础和文本处理经验,将有很多意外收获,可能是对数据库的一些顿悟)
网络编程《Perl网络编程》(虽然非常老的书,但仍然强烈推荐)

那Perl不适用的方面呢?
我不会用Perl去写一个GUI文字处理程序,一个图形游戏,或者一个图形浏览器。Perl能通过与许多其他语言的接口真正的交互,所以你可以实现刚才所说的全部程序。但在我看来,在其它编程语言里有许多更有效的方法去做那些事情。“对一个拿着锤子的人来说,所有的问题看上去都像是钉子”——程序员们小心了。

值得注意的是,Perl本身并不是用Perl写成的,也不是Linalx的内核。那些底层的事务用C/C++来处理会更好的。“用最适合的工具去做最适合的工作”应成为每个程序员的座佑铭。在拉开跳伞索之前的最后一次告诫。如果你对Perl有一点了解,或者看过《Aint The Way I Learned It》系列内容的话,一定记得Perl的口号:“做事总有不止一种办法(There's More Than One Way To Do It)”。这常被缩写为TMTOMDI,并称为“tim-today”,是Perl的核心观点之一。当然任何对明显的错误的订正都非常欢迎。

那些看过我早些时间写的关于Shell脚本的丛书的人也许记得,一个脚本开始都有被称作hash-bang或shebang的行:
#!/bin/ bash
这行告诉解析器(shell)派生一个subshell(子shell),随后的代码将被这个特定的subshell程序解释执行。Perl脚本也一样,第一行必须是:
#!/usr/bin/perl
或者任何一个正确的Perl解释器的路径,注意到Hash-bang的必要条件:
1)它必须是在脚本中的第一行。

2)#号必须是本行的第一个字母,并且在#号和!号之间不能有任何字母或符号。

3)必须使用绝对的路径,而不是只有可执行程序的名称。

下面让我们试着写出我们的第一个 Perl脚本:
#!/usr/bin/perl
# "goodbye" - a modern, high-angst replacement for "Hello World"
print "Goodbye, cruel world!";
unlink $0;

嗯,至少在离开以前说了“再见”;礼貌夫人都会为此骄傲的,我们在这个脚本里做了什么呢?有几件事情是相当明显的:
第一,“hash-bang”。

第二,一行告诉我们脚本要做什么的说明—从shell脚本发展出来的另一个东东,并且这是个不错的想法(在代码中并没有过多的解释!)。

第三,用 “print”函数打印出所要的信息。

注意:在一串字符末尾的“ ”:Perl不会自动为你提供一个换行,所以你必须自己决定要还是不要。还要注意,在每一行代码末尾的分号:就像C语言,Perl也有同样的要求,不幸的是这常被编程者所忘记。实际上,由于有着相对易懂的信息,Perl的错误检查十分容易,作为代码语句的分隔标志,分号常被认为是下一行的前导符。如果你注意到这个问题,并不麻烦,最好记住使用分号。

最后一行的意思是删除 “goodbye cruel world”文件,符号“$0”仅仅是被运行堵塞的脚本的名字的引用,“unlink”做了和“rm”一样的事。注意,“$0”比“goodbye”甚至是“/goodbye”有用的多,不管文件是什么名字,“$0”将返回这个文件名。

顺便说说一些指示代码
写完美无缺的代码对我来说有一些不切实际。在过去的一些场合,我写了些“write-only”代码,这将使任何试着读它的变成不同的颜色。我经常尝试着提高水平,我真的希望看到这个想法得以实现。

空白——tab和space键——在Perl中受到了忽略,也就是说,它是无所谓有无所谓无的。正因为这一点,你能格式化你的Perl代码来表达你正想要表达的思想,举个比较简单的例子:
@boats=(“Aloa”,“Cheoy Lee”,“Pearson”,“Mason”,Swem,“Westsail”,“S2”,“Petersen”, “Hereshoff”);#海船的名单列表。

这个例子中,我们用一些海船的名字填入数组“@boats”中,下面的例子也许更加容易理解:
@array=(“Aloa”,#法国OSTAR/IOR的船
“Cheoy Lee”,#舒适但是昂贵
“Pearson”,#结实但是笨重
“Mason”,#设计良好,但有些雍肿
“Swan”,#上等船只,如果你是大款
“Westsail”,Westsail在双尾船里算是相当不错了
“S2”,#漂亮的海湾船只——但不适宜远洋
“Petersen”, #钢铁巡洋舰,宽敞但援慢
“Hereshott”,#快速而华丽,狭窄而昂贵

这个习惯不仅仅在Perl中得到应用。绝大多数的现代语言允许附加空白,这样可以使代码更加易懂。当我写这个系列文章的时候,我将尽最大努力示范至少我自己认为好的代码样式的版本。同时也希望每一个人在创建他们自己的代码时认真考虑这件事。

变量
在Perl里,易用性非常重要的。它是一种被称作“格式宽松”的语言,其中变量的定义并没有严格要求。实际上没有什么方法可以定义32位的浮点数变量。Perl中有三种变量,它们分别是标量型变量(Scalars),数组型变量(arranys)和散列表(hashes),除了这些颇令人头疼的名字,他们其实都很简单:仅仅包含一些数据的不同排列而已。标量型变量:数字、字符串和引用
一个标量型变量由$标志来表示,例如$nuru,$joe,$pointer。例:
“0.0421”,“Joe’s gloue”,内存地址“0Xa000”
数组型变量:联系数字标识的标量型变量的列表或者说集合,一个数组型变量由@标志来表示,例如@v,@list,@variable,例:
0--"Sundary"
1--"Mondeny"
2--"Tuesday"
3--"Wednesday"

散列表:标量型列表的引用键
一个散列表变量由%标志来表示,例如%people,%x,%this_is_a_hash。例:
ridcnt -> "Sherlock Holmes"
addr -> "221B Backer Street"
code -> "NW1"
city -> "Londen"
country -> "Steuth"
……

注意,虽然数组型变量是按数字顺序存储的,但散列表却不是——恢复散列表的第一个元素的往往和你载入的第一个元素毫无关联。散列的元素通过文本键代替它们在结构中的位置来进行索引。

利用这三种数据类型,你可以表示你想要的任何事,并且会很轻松的实现。另外很重要的一点:$a、@a 和%a 三者之间完全毫无关联,他们处在不同的名字空间。我在设计程序时,尽量小心不使用这些在视觉容易产生混淆的名字。特别是像$a[0](数组@a的第一个元素的引用)这种变量已经存在的情况下——这的确是你需要注意的。

可以给变量赋不同类型的值——数字型和字符串——我们下一步将进行有关两种类型的共同的操作。Perl为你提供这些,但应该记住哪些类型的用哪些操作符。
Operator Num Str
等于 == eql
不等于 != ne
小于 < Lt
大于 > gt
小于等于 <= le
大于等于 >= ge

当通过比较这些字母或字符串之后会觉得他们异常容易记忆和使用——比较字符的时候,就使用字符。以前我总是给出具体的例子,但这次给你们出一道足以使你们头发变白,身心疲惫的难题:
#!/usr/bin/perl
# A Political evaluation script
$a=“A1”;
$b=“George”;
if($a>$b)print $a wordinake a better Presideut
If($a<$b) print $b word make a betler Presiclent;
If($a=$b)Print $a or $b, tlere’s no Diflereue;

输出结果表明其中并没有什么不同,这也许只反映出一个政治性的现实,但我们比较出的结果又是什么呢?对了,我们应该使用字符串操作,不对吗?
#!/usr/bin/perl
# A political evaluation script
$a = "Al";
$b = "George";
if ( $a > $b) { print "$a would make a better President."; }
if ( $a < $b) { print "$b would make a better President."; }
if ( $a==$b) { print "$a or $b, there's no difference..."; }

呵呵,输出表明它们俩没有什么不同的地方。这也许是政治上的真实反映,但对我们期望的比较输出却……哦,对了,我们应该使用字符串操作符,嗯?
#!/usr/bin/perl
# A political evaluation script
$a = "Al";
$b = "George";
if ( $a gt $b) { print "$a would make a better President."; }
if ( $a lt $b) { print "$b would make a better President."; }
if ( $a eq $b) { print "$a or $b, there's no difference..."; }

现在,比较操作符才能正确的工作(现实世界的逻辑却并非如此……我离题了)。顺便提一下,在第一个例子中为什么Perl会认为“Al”和“George” 是等值的呢?什么时候程序中参进了政治观点?很重要的一点原因是,它要依赖Perl分辨“真(true)”与“假(false)”的方法。可以进行一些试验,如“if”,“while”,“wntil”等,根据实验的结果,我们可以理解这一点。“0”意味着false,不管它是个数字还是字符串。所有没有定义的变量(那些没有赋于任何值的量)都是false。一个空的字符串——“”或‘’——也是false。其余的都是true。好了,这儿有一些较麻烦的例子,看看这些量,判断他们是true还是false:
“00” “-1” “ ” “5-5”

请在这篇文章的注1中寻找答案,这里先卖一个关子。另外一个比较重要的问题是变量的输出,这是决定引号中的变量是否需要解释说明的方法,举例如下:
$name = ‘Bessie’;
print ‘Our cow is named $name.’;

输出为:
Our cow is named $name.

我并不认为如果那样叫的话,会有自我意识强烈的母牛到来(我已不打算再提发音的困难)。但是,我们怎样才能使Bessie显示出来呢?
#注意过去是单引号而现在是双引号的地方。
$name =‘Bessie’;
print “Our cow is named $name.”;

通过Perl达到了我们的目的,我说过你可以做任何事情。如果我们想打印变量的名字该怎么做呢?对Perl来说那太简单了:
$joe = “Joe”;
print “The variable $joe contains the value $joe.”;

我们可以打印出任何转义字符——也就是说字符在 Perl中有特殊的含义——但需在他们前面加一个反斜杠。看看下面的例子:
$joe = "Joe";
print "The variable "$joe" contains the value "$joe."";
唔…… TMTOWDI:
print 'The variable "$joe" contains the value "', $joe, '".';

按照你自己的选择,一定要理解他们之间的不同之处,注意在“print”语句中使用逗点作为分隔符的重要性,没有这个逗点,语义将截然不同,这一点我们将在以后的文章中讨论。在结束这篇文章之前,还有一点应重点考虑,为创建脚本文件时,通常使用“-W”参数作为hash-bang的一部分:
#!/usr/bin/perl -w

这样会对脚本中的问题发出警告,并指出这些问题的所在,如果你是一个Perl的初学者一定要记住这个用法,但如果你是一个Perl的专家那就更会记住使用这个开关。那些错误不会随着你的进步而消失,他们会很兴奋的增多。

尾声
这段时间,我们进行了一段短途旅行,伴随着一段轻快的爵士乐,我们轻盈的跳过巨岩和戈滩,下个月我们将向更深的层次进展;也许研究数组或散列表,也许向拥有不可思议的强大功能的“常规表达”或Perl的regexes发起冲刺,此时,我建议尝试一些我们已经讨论过的东西,或许自己做一些实践,我发现了最好的学习语言的方法,勇往直前直到极限,然后把我所遇到的困难向比我了解的更多的人请教,如果你不知道问题的话你就不会得到理想的答案。

注释[1];全是true,他们都不符合“假”的范围:“00”与“0”不同;“-1”也与“0”不同,空格“ ”,并不是空的字符串(),“5-5”,没有核定,并不是“0”,参考书:“Perl:The Complete Reference”,Martin C. Brown

有关Perl的专有名词
Perl(纵览)
Perltoc(TOC文档)
Perlsyn(语法)
Perlrun(执行)
Perltrap(容易犯错之处)
Perlfaq(常见问题)
Perldate(数据结构)
Perlop(操作和优先权)
Perlfunc(内建函数)
Perlkstyle(格式向导)
Perldoc 和 Perldoc-f

通过脚本学Perl语句
作为一名程序员我得应付各种五花八门的客户问题。最近做一个活的时候,编程中要求从某种应用程序中导出用符号分隔的普通文件并且对其进行格式化,这样才能把它导入另一种应用程序。这个文件中的数据包含了3000个数据项,而且项目描述采用了变长字段,我得设法把这种格式转换成3个30字符长的字段。方法有二:使用文本编辑器编辑3000个条目或者写一个Perl脚本来完成这一任务。当然得选择第2种方案。

脚本
清单A 就是我编写的脚本。这里我也不拿什么算法和程序设计说事,也就直接带你阅读这个脚本程序来掌握Perl 5的主要语句。这个脚本是在Windows 98计算机上用ActiveState开发的ActivePerl编写的。编写和测试脚本用了三个小时,确定基本算法用了大约一个小时,另两个小时是程序的反复精练。这是一个“快餐型”的脚本程序,不过话又说回来,就说明Perl的关键的功能和构造而言还是足够了。
$input = “Inventory.txt”; #设置$input为符号分隔文件名
$output = “>inv.txt”; #设置$output为输出文件名

以上的代码显示了Perl的字符串变量定义方式(比如,$variable_name = “string”;)。Perl语句用分号结束(;)。用#符号表示注释。
open (INV, $input); #打开$openme 供读取数据
open (OUTFILE, $output); #打开Inv.txt 供写入数据

Open 语句用来打开文件以实现读、写或添加操作。open语句的第1个参数是将为其他语句作为参数使用的文件句柄(filehandle)的名字(文件句柄是一种数据结构,Perl脚本通过它可以访问文件)。第1个参数是包含文件名字符串的变量。在我定义 $output 时我用 > 告诉解释器打开Inv.txt写入数据。要是为添加数据而打开文件的话就要用到 >> 了。

while语句在执行花括号内的代码之前会对执行条件进行测试,当条件测试为false时即退出循环。在这种情况下,循环会连续执行直到到达文件末尾。
$offset =0; #最初的30字符字段偏移
$TheLine = $_;  #设置$TheLine为当前行

接下来的这两行代码声明和初始化输出字符串格式化所需要的两个整数变量。第二行把输入文件的当前行赋给$TheLine变量。Perl用$_当作默认的输入和模式匹配变量。在这个例子中,它的值是由while语句赋予的。
chomp($TheLine); #删除换行符
chomp语句用来删除行终止符,例如换行符等。
@cols = split(" ",$TheLine); #占位符分隔

以上语句介绍了Perl编程中两个不可或缺的方面。Perl语言中的数组以@打头而且被分配了一列元素。split语句有两个参数:将被分隔的字符和被分隔的字符串,Split返回元素列表。

$splitme = $cols[6];  #把第7列的字符串赋给$splitme(项目描述)
上句的作用是指向数组中的元素,数组按照从0到(N-1)的序列索引,其中的N是元素总数。

@splitup = split('',$splitme); #把字符串转换为字符数组
if ($#splitup >= 30) { #如果超过30个字符
以上代码的第二行介绍了第二种条件表达式:if语句。如果If语句括号内的条件得到满足则执行相应的代码,Perl用$#array_name在数组中存储最后一个元素的索引。

for($i = 0; $i <= 10; $i++) { #找出最后10个字符中的第1个空格
现在介绍for循环。Perl语言中的for循环结构类同C、C++或Java等编程语言中的for循环。For循环的语法是这样的:for(initial condition; exit condition; increment statement).

if ($splitup[(30 - $i)]=~ /W/) { #检查空格
这条 if语句中的条件又是一个新概念。这里我采用了所谓的正则表达式,具体地说就是模式匹配。如果字符串包含空格该语句将返回true。模式匹配条件的语法如下所示:$string =~ /pattern/.

清单B 中的最后一行代码演示了 join函数。Join差不多就是split的反面。Join也有两个参数:分隔符和元素数组。Join会返回一个字符串,这个字符串是通过连接数组元素并在每两个元素之间放置分隔符之后构成的。

在清单C结尾你还会发现一个 else语句。如果 if语句的条件无法满足就会执行else语句牵涉的代码。
if ($#splitup <=0 ){ $newguy = " ";} #如果没有说明就分配3个字段
$cols[6] = $newguy; #在数组中插入 3 个字段描述
$newline = join (" ",@cols); #创建输出字符串
print OUTFILE $newline . " "; #把输出字符串写入文件

现在我们到了写数据的阶段。Print函数用来打印字符串,如果没有给定文件句柄该函数会把结果打印到标准 I/O设备。在示例语句中,只要创建一行数据我就给输出文件写入一行,你可以在字符串之后加上句号。

}#结束while循环

close OUTFILE; #关闭输出文件

这里,循环执行到文件的末尾,于是我们关闭输出文件以便它从内存获取数据存储到磁盘。close语句只有一个参数,也就是它要关闭的文件句柄。

小结
下面就是我在编写以上脚本中用到的Perl知识:变量(数值变量和数组)、流程控制(while和for循环以及if/else语句)、正则表达式、函数(chomp、join和split)。

文件输入/输出

Perl是一种很容易在掌握的脚本编程语言,网上有很多学习资源可以帮助你获得有用的指导信息和相关资料,比如learn.perl.org 和O’Reilly的perl.com都是相当不错的站点。

Perl的经典用法
用open()函数打开文件
打开文件的常用方法是:
open(FH, "< $filename")
or die "Couldn't open $filename for reading: $!";  

open()函数通常带有两个参数,第一个为文件句柄,用于指向打开的文件,第二个参数是文件名及模式(文件的打开模式)的混合体,如果文件被成功打开,open()函数返回true,否则为false。我们用“or”来测试该条件。上述代码中的模式由小于字符(<)来表示。如果文件不存在,open()将返回 false。此时你可以读文件句柄,但不可以写,大于字符表示写。如果文件不存在,就会被创建。如果文件存在,文件被清除,以前的数据将会丢失。你可以写入文件句柄,但不可以读入。

# 如果文件不存在就创建它
open(FH, "> $filename")
or die "Couldn't open $filename for writing: $!";  
如果文件不存在,添加模式(用两个大于符号表示)可以用来创建新文件,如果文件存在,该模式并不会清除原来的数据。

同“<”或“读”模式一样,你只能对文件句柄进行写操作。 (所以的写入内容都添加到文件尾)。企图进行读操作,会产生运行错误。
open(FH, ">> $filename")
or die "Couldn't open $filename for appending: $!";  
通过“+”模式。
open(FH, "+> $filename")
or die "Couldn't open $filename for reading and writing: $!";  
注意“+”的区别,两者都可以可读可写。前者为非破坏性写,后者为破坏性写。

错误
错误是如何出现的?很多地方都会出现错误:如目录不存在,文件不可写入,你的程序丢失了文件句柄等等。你应该检查系统调用的结果 (如open() 和sysopen()),看看是否调用成功。为了帮助用户查错,通常使用“or die()”,你应记住这些用法。首先,应写出系统调用失败(“open”)的信息。其次,应写出文件名的信息,以便修正错误时更容易地定位。第三,要写出打开文件的方式,(“for writing,”“for appending”)。第四,输出操作系统的出错信息(包含在$!中)。这样,一旦出现文件不能打开的问题,使用你的程序的用户会大体上知道为什么不能打开。有时,我们把第一个和第三个合并在一起:
or die "unable to append to $filename: $!";  

如果在open() 和出错信息中都写了文件的全名,你会冒改变了open() 的风险,使得出错信息不合时宜或不正确。
# 下面会出现虚假的出错信息
open(FH, "
or die "Can't open /var/log/file.pod for writing : $!";  

用 Sysopen()进行更多的控制
为了更好的控制文件的打开方式,可以使用 sysopen() 函数:
use Fcntl;
sysopen(FH, $filename, O_RDWR|O_CREAT, 0666)
or die "Can't open $filename for reading/writing/creating : $!";

函数 sysopen() 带有四个参数,第一个是同open()函数类似的文件句柄参数,第二个参数是不带模式信息的文件名,第三个参数是模式参数,由Fcntl 模块提供的逻辑OR运算组合起来的常数构成,第四个参数(可选),为八进制属性值(0666表示数据文件,0777表示程序)。如果文件可以被打开,sysopen() 返回true,如果打开失败,则返回false。

不同于open()函数,sysopen()不提供模式说明的简写方式,而是把一些常数组合起来,而且,每个模式常数有唯一的含义,只有通过逻辑OR运算才能将它们组合起来,你可以设置多个行为的组合。
O_RDONLY Read-only
O_WRONLY Write-only
O_RDWR Reading and writing
O_APPEND Writes go to the end of the file
O_TRUNC Truncate the file if it existed
O_CREAT Create the file if it didn't exist
O_EXCLError if the file already existed (used with O_CREAT)

当你需要小心行事的时候,就使用sysopen() 函数,例如,如果你打算添加内容到文件中,如果文件不存在,不创建新文件,你可以这样写:
sysopen(LOG, "/var/log/myprog.log", O_APPEND, 0666)
or die "Can't open /var/log/myprog.log for appending: $!";  

读入单个记录
有一个容易的方法读入filehandles:用 操作符。在标量内容下,它返回文件中的下一个记录,或者返回未定义出错信息。我们可以使用它来把一行读入到一个变量中:
$line = ;
die "Unexpected end-of-file" unless defined $line;

在循环语句中,我们可以这样写:
while (defined ($record = )) { # long-winded
# $record is set to each record in the file, one at a time
}

因为要大量进行这样的工作,通常再进行一下简化,把记录放到$_ 中,而不是$record中:
while () {
# $_ 每次为文件中的一个记录
}
在Perl 5.004_04中,我们可以这样做:
while ($record = ) {
# $record 每次为文件中的一个记录
}  

defined() 将自动加上,在Perl 5.004_04以前的版本中,该命令给出一个警示。要了解所用的Perl版本,可在命令行下打入:perl -v
一旦我们读出了一个记录,通常打算去掉记录分隔符,(缺省值为换行符字符):
chomp($record);

Perl 4.0版本仅有chop()操作,去掉串的最后一个字符, 不管该字符是什么。chomp() 没有这么大的破坏性,如果有行分隔符存在,它仅去掉行分隔符。如果你打算去掉行分隔符,就用chomp() 来代替chop()。

读入多个记录
如果你调用,返回文件中剩余的记录。如果你处于文件尾,则返回空表:
@records = ;
if (@records) {
print "There were ", scalar(@records), " records read. ";
}   

在下面的一步中,进行赋值和测试两项工作:
if (@records = ) {
print "There were ", scalar(@records), " records read. ";
}

chomp() 也可适用对数组操作:
@records = ;
chomp(@records);
对于任何表达式,都可以进行chomp操作,故你可以在下面的一步中这样写:
chomp(@records = );  

什么是记录?
记录的缺省定义为:“行”。
记录的定义由$/ 变量控制的,该变量存放所输入的记录的分隔符,因为换行符字符(根据定义!)是用来分隔行的,故其缺省值为串“ ”。例如,你可以用任何你想要替换的符号来代替“ ”。
$/ = ";";
$record = ;  # 读入下一个用分号分隔的记录
$/可以取其它两个有趣的值:空串("") 和undef。

读入段落
$/ =""的写法是用来指示Perl读入段落的,段落是由两个或两个以上的换行符构成的文本块。这不同于设置为" ",后者仅读入由两行组成的文本块。在这种情况下,将出现这样一个问题:如果有连续的空行存在,例如“text ”,你既可以把它解释为一个段落 ("text"),也可以解释为两个段落 ("text", 后面跟两个换行符,以及一个空段落,后面跟两个空行。)

在读入文本时,第二个解释用途不大。如果你正在读的段落出现上述情况,你不必过滤出“空”段落。
$/ = " ";
while () {
chomp;
next unless length;     # 跳过空段
# ...
}  

你可以把 $/设置为undef,它用于读入后面跟着两个或多个换行符组成的段落: undef $/;
while () {
chomp;
# ...
}  

读入整个文件
$/ 的其它有趣的值为undef。如果设置为该值,就将告诉Perl,读命令将把文件的剩余部分作为一个串返回:
undef $/;
$file = ;

因为改变了 $/的值,将会影响以后的每次读操作,而不仅是下一个读操作。通常你需要将该操作限制在局部。通过下面的例子,可以把文件句柄的内容读入到一个串中:
{
local $/ = undef;
$file = ;
}

记住:Perl变量可读入很长的串。尽管你的文件大小不可以超出你的虚拟内存容量的限度,你仍可以读入尽可能多的数据。

用正则表达式对文件进行操作
一旦你有个包含了整个串的变量,你可以使用正则表达式,对整个文件进行操作,而不是对文件中的某个块进行操作。有两个有用的正则表达式标记/s和/m。一般,Perl的正则表达式对行进行处理,你可以这样写:
undef $/;
$line = ;
if ($line =~ /(b.*grass)$/) {
print "found ";
}  

如果把我们的文件填入如下内容:
browngrass
bluegrass

则输出为:
found bluegrass

它没有找到“browngrass”,这是因为$ 仅在串尾寻找其匹配, (或者在串结束前的一行)。如果在包含很多行的串中,用"^" 和"$" 来匹配,, 我们可以使用 /m ("multiline") 选项:
if ($line =~ /(b.*grass)$/m) {}
现在程序会把如下的信息输出:
found browngrass

类似地,句点可以匹配除了换行符之外的所有字符:
while () {
if (/19(.*)$/) {
if ( < 20) {
$year = 2000+;
} else {
$year = 1900+;
}
}
}

如果我们从文件中读入“1981”,$_ 将包含“1981 ”。正则表达式中的句点匹配“8”和“1”, 而不匹配“ ”。这里正需要这样做,因为换行符不是日期的组成部分。

对于一个包含很多行的串,我们也许要提取其中的大的块,这些块可能会跨越行分隔符。在这种情况下,我们可以使用 /s 选项,并用句点来匹配除了换行符以外的所有字符。
if (ms) {
print "Found bold text: ";
}

此处,我用了{}来表示正则表达式的起始和结束,而不用斜杠,所以我就可以告诉 Perl我正在匹配,起始字符为"m",结束字符为"s"。你可以把/s 和/m 选项组合使用:
if (m{^(.*?)}sm) {
# ...
}  

总结
有两种方法打开文件:open()函数的特点是快速简捷,而sysopen()函数功能强大而复杂。通过操作符,可以读入一个记录,$/ 变量可以让你控制记录是什么。如果你打算把很多行的内容读入到一个串中,不要使用忘记/s和/m 这两个正则表达式标记。

Perl语言编程特殊技巧
Perl因其能够用较少的空间表达复杂的命令而出名,本文将探讨perl语言是如何实现这些功能。将从一个最简单的小程序开始,它的功能是从键盘输入字符,然后将其送到控制台。采用perl语言实现,可以会这样写:
while ($line = <>) {
print $line;
}

一开始这一程序已十分简洁,但是它的每条语句是什么意思呢?简单地说,是一个专用的文件句柄,在本例中它属于标准输入缓存(称为 STDIN),通常是连接到键盘。每次我们将的值赋给变量$line时,即为将STDIN缓存中的首行取出,然后放入$ line。当到达缓存最后一行时,就会取出while语句判断条件为假的(EOF)文件结束标识符。程序的其它部分很简单可以这样解释。现在我们已经将输入信息放入$line变量,接着我们就可以用print函数将其打印到屏幕。或者更准确地说,打印到通常连接显示器的标准输入缓存。标准输入和输出缓存都可以重定向,例如可重定向到保存程序结果的某个文件中,如果是在处理文本,那么通常可以认为这与键盘及显示器是等价的。你可能会认为这一程序已经是最短的了,但是通过使用perl的特殊变量,它还可以更短。

默认数据变量:$_
Perl 有很多特殊变量,它们在程序运行过程中被自动分配,可用于访问程序本身的一些信息,诸如程序名或者进程ID号、命令行参数、或最后一个表达式的运算结果。这些变量中最经常使用也可能是最有用的是$_这一默认变量。如果你在程序中未指明,默认变量作为某个未分配的功能变量,用于保存一些perl结构体和函数的运算结果。在你熟悉perl之前这可能听起来令人费解,但是它的功能的确很强大。我们可以用变量$_来取消程序中的$line变量:
while () {
print $_;
}

这一程序和前一个是等价的,即使在测试条件while语句中我们仅使用到一个文件句柄。程序将输入存入到默认变量,所以当我们要输出时就可以用变量$_访问输入的信息。但是我们还可以把程序变得更短,记住我说过,$_是一个用于未定义的实现某个功能的默认变量,这样打印是它其中的一个功能,所以我们现在可以把程序这样来写:
while () {
print;
}

现在我们得到实现同样功能的程序,但是该程序消除了所有外部变量。因为我们仅仅是将STDIN和STDOUT连在一起。如果我们能够去掉while循环则会更好,这一循环无非是在重复的将数据从一个缓存移到另一个。这一过程也可以用下面的语句实现:
print ;

这个程序的工作过程有点复杂。当采用$_实现打印时,我们是将一个变量放在另一个称为标量变量的情况下。简单地说就是它将这一变量看作一个对象,比如可以是一个数或者一个字符,但并不是一个集合。打印函数也可以采用在一组标量的方式,即将变量看作一系列对象,当我们使用打印函数时,它将轮流打印每一个。当我们使用句柄进行打印时,它将把标准输入看作一个字符序列,依次打印。这和while循环实现一样的功能。

这可能是一个极端的例子,但是使用一些perl编程技巧,可以将我们的程序长度减少一半。如果我们仅仅想把STDIN影射到STNOUT,这样做很好。但是如果我们想使我们的程序像 unix中的可以打开和打印文件的cat一样时,应该怎么做?我们可能要检查命令行参数,以确定它们是不是有效文件,然后依次打开和打印它们。但是这么一个很平常的事情,perl语言可以使用更简单更短的方法实现。

特殊的文件句柄:
像其它的默认变量一样,perl语言的这一写为的特殊句柄可以使程序编写更容易。这个文件句柄将所有命令行参数都看作文件名,依次打开它们。如果没有命令行参数,那么就从STDIN读入。像unix中的习惯用法per一样,如果将“-”用作命令行参数,它将会打开STDIN代替一个文件。因此如果我们想编写一个像上面提到的可以支持命令行中的文件的程序,这很简单,可以这样写:print ;

当你考虑到perl可以仅用8条语句写一个实现cat功能的程序时,你就会明白为什么perl被认为功能强大。但是如果我们想在输入上面实现一些更重要的功能而不是仅仅将其写回显示器上,这该怎样实现?

记录行数
如果我们想单个处理每一行输入,这时仅将文件句柄连到打印是不够的。让我们看一个在每行输入上加上行号的一个小程序。
$num = 0;
while () {
$num = $num + 1;
print "$numt$_";
}

在这一例子中我们使用$num变量来记录行号,每输入一行,我们将其加一,然后将行号和输入内容一起输出。当我们在字符串中使用由双引号括起的变量时,这一变量名将会用它的内容来代替,这使得perl中的格式化输出变得轻而易举。仅这几个简单的程序,我们就可以看出使用特殊变量可让你编写更小更快的程序。如果你感兴趣,perl所有特殊变量的用法可以在perl手册的变量部分得到。

养成良好的Perl OOP习惯:如果你曾经用 Perl开发过应用程序,那你可能用过很多程序模块。而且你还可能天天用到这些程序包的面向对象接口。但如果你像那些普通的Perl程序员一样,在开发自己的代码时都不试着使用面向对象技术。那么这段文章可以帮助你成为一位习惯面向对象编程的Perl开发者。

OOP速成
面向对象编程(OOP)的基本思想非常简单,说不定你可能已在有限场合下用过这种技术了。所谓对象不过是数据的集合,而这些数据又千丝万缕地联系在一起。不妨考虑下面的例子:假设某个哈希散列装载的是说明球类特性的数据,这样哈希散列的键(key)就可能牵扯到球的颜色、直径以及制造材料等。而OOP的方法 (method)只是处理每个球类数据的哈希散列子程序,这种子程序在操作每个球有关数据时会采用相应的定义字段。

什么是哈希散列?
小型的哈希散列基本上不会占用什么内存,对系统的性能不会有大的影响,你完全可以放心地用它们把对象数据归拢到一起来。如果你不知道Per语言中的l哈希数组(也叫作联合数组),那你可没好好利用Perl。普通的数组结构按照数据的位置命名单个条目;而在哈希数组里,你能给条目命名(叫作键:key)。其实不妨把哈希散列想象为一个小数据库。给出一个名字就可以通过它查出一条记录。Perl编程时在命令行上运行“perldoc  perldata”可以查阅Perl数据类型和结构的有关文档。

OOP不仅仅只把你的数据和子程序组织为互相关联的集合,它还保证了对象及其方法实际连接的编译和运行(runtime)时。那样你就不必直接调用哈希数组的ball_volume值了。OOP语言还进一步的提供了莫大的好处,OOP语言简化了编程句法而且为声明复杂的、对象-对象关系提供了大量的其它特性。在最简单的情况下,每个对象都会告诉Perl它是什么种类的对象,这样只有知道如何安全地操作该对象的子程序才会被调用。开发人员则只需使用方法的名字即可。如果你调用book对象的volume值,它会告诉你目前该数值该图书数据中的位置而不会告诉你它真正占用了多少体积空间。

习惯 #1 :使用哈希散列
请把相关的数据通过小哈希散列收集起来。以前你可能创建过诸如$red、$green以及$blue之类的一整套变量,但不久你就认识到你需要2或3个集合把这些变量修改为$a_red、$b_red、$c_red来区别不同对象的属性,对绿色(green)和蓝色(blue)变量也同样如此。所以请把这9 个变量修改为% a_color、% b_color和%c_color的3个哈希散列,而把r、g和b选作参数。

在CPAN内,几乎每一个模块都封装成了哈希散列。把成组关联的数据包含在哈希散列内是一种优秀的面向对象编程技术。这样做的话你声明的变量就少多了,数据也组织得更为合理。同时还提高了可靠性,因为诸如$boy之类的变量实际上自身就包含了明显的含义,清单A即是一例。

习惯 #2 :使用子程序
下一个好习惯是使用子程序来围绕那些数据集合组织你的代码。尽量地把哈希散列上成块代码分组到子程序里。保证你传递的是集合而非单个的值。调用 invert_new ( %color )显然比调用invert_old ($reda , $greena , $bluea );更合理。至少你不需要记得参数的顺序。
子程序把代码组织得更有逻辑了;程序宛如从子程序文件的头顶略过而底下的工作全然不知。可以把所有的变量都传递给子程序,重用代码就轻松多了,新的程序使用不同的全局变量,而其中某些全局变量还用于其他完全不同的用途。至少小型的子程序在调试的时候也方便多了,在这种情况下,所有你需要了解的代码刚好占一屏(参看程序清单B)。看,这有多方便!

习惯 #3 :重用
第3 个良好的OOP习惯是把子程序收集起来放进一个单独的文件供以后再次使用他们。你还应该精制它们使之能用于一般用途。此外还应该在多个脚本中使用这些子程序而且对它们不断地进行调试。现在你不妨把采用某个特定哈希散列的所有的子程序都组合进一个文件,这就算走出了重用代码的第1步。

采取以上的做法只需把子程序放进某个文件,再使用的时候采用“require”关键词即可。已经不用的子程序就别这么做了;那纯粹是浪费时间。保证它们能工作就行了,清单C是一个比较不错的例子。

习惯 #4 :保护哈希散列
保护你的哈希散列。不要直接创建哈希,最好是对用来创建哈希的数据进行再次检查之后添加一个返回哈希散列索引的子程序。然后再添加一些微小的子程序返回哈希散列中的各个值以便安全地修改它们,这些代码示例如上所示。

这样处理小子程序,可以让你在改变规则的时候可以更容易找出错误所在。所以不要在程序中编写那些修改哈希散列的代码,你应该把这些代码移到函数库中去。

益处
以上这些建议归根结底就是一句话:哈希散列把数据关联了起来,编写采用整个哈希散列的子程序,把联系某一哈希散列的子程序收集到一个简单的库内,保护你的哈希散列库。这些小技巧几乎没有增加什么工作量但却具有极好的组织性。但这些工作在你创建面向对象的模块时占了90%以上的比重。

如果你保证哈希索引首先发送给了每个子程序。而且你只通过库函数同哈希散列内部打交道,那么你基本上只需要为你的模块起个名字即可。这样就让 Perl 知道它应该把你的哈希散列当作一个对象,你的代码库要变成模块甚至很可能不需要20行代码。

利用HTTP::Daemon模块构件简单的Web服务器
当你的服务器需要一个简单的接口,你会考虑利用这个接口构造一个Web服务器到你的应用程序中。在Perl中使用HTTP::Daemon模版构造一个基本的Web服务器是非常简单的。这个模版将完成大量和获得请求和发送回复的工作。下面的代码是完整的Web服务器。它发送了一个请求类型的响应指示,被请求路径,请求头文件和当前时间:
use HTTP::Daemon;
use HTTP::Status;
use HTTP::Response;

my $daemon = new HTTP::Daemon LocalAddr => 'localhost', LocalPort => 1234;
print "Server open at ", $daemon->url, " ";
while (my $connection = $daemon->accept){
print Identify($connection) . " Connected ";
while (my $request
= $connection->get_request){
print Identify($connection) . " Requested $ $ ";
if ($request->method eq 'GET'){
$response = HTTP::Response->new(200);
$response->content(<

Simple Web server

You requested path $
using protocol $
via method $

Your header information was:

${join '
', split(/ /, $request->headers_as_string())}

I'm a simple server so that's all you are going to get!

Generated ${scalar localtime}

EOT
$connection->force_last_request; # allows us to service multiple clients
$connection->send_response($response);
}
else{
$connection->send_error(RC_FORBIDDEN)
}
}
print "$:$ Closed ";
$connection->close;
undef($connection);
}

sub Identify{
my ($conn) = @_;
return "$:$";
}

在循环的时候通过简单的替换最里层的代码,可以让你的Web服务器完成任何你想要完成的事情。比如可以返回你程序的位置或者从Web照相机中获得一个图片,当使用HTTP::Daemon模版的时候添加一个Web接口到你的Perl脚本中是简单的。

该文章最后由 阿炯 于 2024-02-04 17:31:25 更新,目前是第 4 版。