Perl编程24学时教程


下载 第一部分 Perl 基础 第1学时 Perl 入门 第2学时 Perl 的基本构件:数字和字符串 第3学时 控制程序流 第4学时 基本构件的堆栈:列表与数组 第5学时 进行文件操作 第6学时 模式匹配 第7学时 哈希结构 第8学时 函数下载 第1学时 P e r l入门 P e r l是一种通用编程语言。凡是其他编程语言能够使用的地方,都有它的用武之地。在各 行各业中,它已经被用于你能够想像到的各种各样的任务的处理。它已经用于股票市场、产 品制造、产品设计、客户支持、质量控制、千年虫测试、系统编程、工资处理和库存管理等 各个领域,当然还有We b。 P e r l的用途之所以如此广泛,原因是 P e r l被称为是一种“胶水语言”。所谓胶水语言,也 就是说它是可以用来将许多元素连接在一起的语言。你可能不想用 P e r l来编写一个文字处理程 序(尽管可以编写这样的程序),因为现在已经有许多非常出色的文字处理程序了。用 P e r l来 编写数据库、电子表格、操作系统或者特性完善的 We b服务程序也不是聪明之举,不过编写 这些程序是完全可以做到的。 P e r l真正擅长的是将这些程序连接在一起。 P e r l能够利用你的数据库,将它转换成一个具 有电子表格特性的文件,并且在你进行文件的处理时,根据需要对数据进行调整。 P e r l也能够 利用文字处理文档,将它们转换成 H T M L文档,以便在We b上显示。 由于P e r l是一种“胶水语言”,能够将许多元素连接在一起,因此它具有极强的适应性。 它至少能够在二十几种操作系统下运行,甚至能够在更多的操作系统下运行。 P e r l的编程样式 非常灵活,因此可以用许多不同的方法来做同一件事情。你编写的 P e r l程序看上去可能与我的 程序毫无共同之处,但是如果它们运行起来,却没有任何问题。必要的时候, P e r l可以是一种 非常严谨的语言,而对于编程新手来说,如果你愿意,它又可以是一种非常随意的语言,这 完全可以根据你的需要来定。 下面让我们来澄清一些基本概念。这个编程语言的名字是 P e r l。运行你的程序的程序(即 解释程序)的名字是 p e r l。对你来说它们之间的差别通常并不十分重要,不过当你试图启动你 的程序时,情况就不一样了,那时它的名字总是 p e r l。有时,你会看到 P e r l被写成了P E R L, 这可能是因为P e r l的名字是Practical Extraction and Report Language缩略而来的。现在已经没 有人再说P E R L,因为这个名字显得太一本正经了。 P e r l这个名字则比较随便。 P e r l的许多特性是从其他语言中借来的。这种借用曾经在早期导致 P e r l成 为另一个用语Pathologically Eclectic Rubbish Lister的缩写。 本学时介绍的内容包括: • 安装P e r l。 • 访问P e r l的内部文档。 • 编写你的第一个P e r l脚本程序。 1.1 安装Perl 若要使用P e r l,首先必须安装 P e r l。P e r l的安装是非常容易的,并且不会出错。实际上, 作为安装步骤的一部分, P e r l应该进行自我测试,以确保它安装成功。安装的操作过程可以有很大的差别,这要根据你运行的操作系统而定。因此,为了使安装操作能够进行下去,首先 要确定你运行的是什么操作系统,然后再进行下一步工作。 1.1.1 等一等,也许你已经安装了Perl 当你着手在你的系统上安装 P e r l之前,应该检查一下是否已经安装了 P e r l。有些U N I X的 供应商已经为操作系统配备了 P e r l。Windows NT也将P e r l作为Windows NT的资源工具包 (Resource Kit)的组成部分提供给客户。若要查看你的操作系统上是否已经安装了 P e r l,你需 要获得一个命令提示。 在U N I X系统下,只需要登录到该系统中。如果你拥有一个图形操作环境,需要打开终端 窗口。当已经登录或者打开终端窗口后,你会看到下面这个提示符: $ 这个提示符也可能是%,也可能是b a s h %,无论什么提示符,它都称为 s h e l l提示符或命令 提示符。在本书的头几个学时中,你将需要使用这个提示符与 P e r l进行交互操作。 若要了解你的操作系统上是否已经安装了 P e r l,请键入下面这行命令(不要键入$提示符): $ perl -v 这时系统可能显示一个出错消息,如 command not found(命令没有找到),也可能P e r l作 出响应,输出它的版本号。如果 P e r l输出它的版本号,那么就表示它已经安装好了,你就不需 要重新进行安装。 报告的版本号至少应该是 5,也许是5 . 0 0 4、5 . 0 0 5、5 . 6等,不能小于这 些数字。如果 P e r l的版本号是 4 . x,那么你必须安装一个新拷贝。 Perl 4这个 版本太老,错误很多,而且不再能够得到维护,本书中只有很少的示例程 序能在Perl 4下运行。在撰写本书时, 5 . 0 0 5是P e r l的当前版本, 5 . 6在1 9 9 9 年底推出。 如果你拥有一台运行Wi n d o w s操作系统的计算机,要想查看是否安装了 P e r l,必须显示图 1 - 1所示的M S - D O S提示符。 4使用第一部分 Perl 基础 下载 图1-1 可以在这个 D O S 提 示符下查看P e r l的版 本在这个提示符下,键入下面这个命令(不要重复键入提示符): C:\> perl -v 如果P e r l已经安装,它就会显示版本号。正如上面的警告中所说的那样,它的版本号至少必须 是5。如果D O S回答说Bad command or file name(命令或文件名不正确),那么你就应该安装P e r l。 在M a c i n t o s h计算机上,你可以像图 1 - 2所示的那样,运行 File Find命令(C o m m a m d - f), 在F i n d的命令框中键入‘M a c P e r l’,来查看是否已经安装了 P e r l。如果找到了该应用程序,那 么将它打开,观察 A p p l e菜单下面的“ About MacPerl”选项。你至少应该拥有 Version 5.2.0 Patchlevel 5.004这个版本号,否则就应该安装 M a c P e r l的新版本。 1.1.2 在Windows 95/98/NT上安装Perl 若要在Wi n d o w s下安装P e r l,请记住,你可以像许多其他情况中那样,既可以使用捷径, 也可以使用比较笨的办法。如果你对开发环境中需要的 C编译器和各种工具(比如 M a k e f i l e、 s h e l l等)非常熟悉,那么可以从头开始安装你自己的 P e r l。可以随意查看、修改和改变 P e r l解 释程序的源代码,使之适合你的需要。详细情况请参见第 1 6学时(P e r l程序的开发界)的内容。 想要在Wi n d o w s下从头安装P e r l并不容易,对于大多数人来说,这样做并不值得。 安装P e r l时使用的捷径确实是非常容易的。 ActiveState To o l公司提供了一个自行安装 P e r l 的工具,安装工作就像安装其他任何 Wi n d o w s应用程序一样,如图 1 - 3所示。这个 P e r l是在 ActiveState Community License(A c t i v e S t a t e团体许可证)下提供的,你应该阅读该许可证的 有关说明。该公司的U R L是h t t p : / / w w w. A c t i v e S t a t e . c o m。 这个P e r l与你自己建立的P e r l是完全相同的东西。 A c t i v e S t a t e公司只是为你做了最困难的 那部分工作,并且用有关安装程序将它包装了起来。如果你需要的话, A c t i v e S t a t e公司还为 P e r l提供了商业上的支持,并且提供了一些附加产品,如调试程序和其他开发工具及文件库。 本书的光盘上包含了A c t i v e S t a t e公司的P e r l产品的拷贝,你可以直接使用 这个光盘来安装P e r l,也可以通过A c t i v e S t a t e公司的We b站点来获取最新版本 的P e r l。 第1学时 Perl 入门使用5下载 图1-2 查看M a c i n t o s h计算 机上的P e r l 图1-3 用A c t i v e S t a t e提供 的工具在 Wi n d o w s 下安装P e r l1.1.3 在UNIX上安装Perl 若要在 U N I X上安装P e r l,需要具备两个条件。首先,需要一个 P e r l的源模块包的拷贝。 你始终都可以从h t t p : / / w w w. p e r l . c o m的D o w n l o a d s区域下载它的最新版本。可以从那里找到多 个版本,不过你需要的版本总是带有“ S t a b l e”或“P r o d u c t i o n”标号。还需要一个 ANSI C编 译器。如果不知道这个编译器的作用,也不必担心。 P e r l的配置程序能够选定一个,如果没有 这个编译器,你可以安装一个预安装版本,这在本书的结尾处将要介绍。 如果你的U N I X配有一个用于安装预安装软件包的系统,你就能够安装一 个预安装的P e r l版本。L i n u x、S o l a r i s、A I X和其他U N I X系统均配有已经捆绑 的预安装P e r l版本,它们的安装非常容易。请查看有关资料,以了解何处能 够得到这些软件包。 当你拥有P e r l的源模块包后(它的文件名类似 S t a b l e . t a r. g z),必须对它进行拆包,然后进 行安装。若要进行操作,请输入下面的命令: $ gunzip stable.tar.gz $ tar xf stable.tar 这两个命令的运行需要花费一定的时间。如果没有 g u n z i p 解压缩程序,可以从 h t t p : / / w w w. g n u . o rg下载一个拷贝。该程序包称为 g z i p。当你完成所有的拆包操作后,就会看 到一个提示符,然后键入下面的命令: $ sh Configure 这时C o n f i g u r e程序就开始运行,并且问你一系列的问题。如果大部分问题你不知道如何 回答,这没有关系,你只需要按 E n t e r键即可。默认的答案通常是最好的答案。 P e r l几乎能够 在任何U N I X系统上安装而不会出现任何问题。当所有这些操作完成时,键入下面的命令: $ m a k e P e r l的安装需要花费相当长的时间,你可以乘此机会喝点儿咖啡。如果你的系统运行速度 比较慢,你可以利用这个时间用午餐。当安装完成时,再键入下面这两个命令: $ make test # make install make test这个命令用于确保 P e r l的安装百分之百正确并且使之可以准备运行。若要运行 make install命令,你必须以一个根用户的身份进行登录(正因为这个原因,所以使用提示符 #, 这是根用户的提示符),因为它需要将P e r l安装到系统目录中去。 当make install运行正确时,你可以测试P e r l的安装情况,方法是再次键入下面的命令: $ perl -v 如果这个命令运行正确,那么祝贺你安装成功了! 在U N I X下安装P e r l时使用的源模块拷贝位于本书所附的光盘上。可以直 接从这里拷贝这个安装模块包,也可以从 h t t p : / / w w w.perl .com那里获得P e r l的 最新版本。 6使用第一部分 Perl 基础 下载1.1.4 在Macintosh系统上安装Perl Macintosh Perl的最新版本称为M a c P e r l,可以从C PA N端口目录下获得该版本。你必须访 问h t t p : / / w w w. p e r l . c o m / C PA N / p o r t s / m a c站点,从那里下载安装文件。你应该从该目录下下载 MacPerl appl.bin的最新版本,安装时,请使用 S t u ffIt Expander,从下载文件中取出M a c P e r l安 装程序,然后运行该安装程序。 当完成安装后,你可能想要为 P e r l文档的阅读者安装一个帮助程序 S h u c k,它是与M a c P e r l 一道安装的。 MacOS 8的用户可以通过 I n t e r n e t控制面板进行安装,方法是打开 A d v a n c e d - >File Mapping,给S h u c k应用程序添加文件扩展名映像 . p o d。这样就可以更加容易地访问该文 档了。还可以给M a c P e r l应用程序建立. p h、. p l、. p l x、. p m、. c g i和. x s(这些都是P e r l使用的扩 展名)等文件映像,请务必将文件类型设置为‘ T E X T’。 MacOS 7的用 户 必 须 使用 I n t e r n e t C o n f i g实用 程 序 来 进行 类 似 的映 射 操 作 。在 I n t e r n e t C o n f i g中,选定H e l p e r s,为p o d添加新的帮助应用程序 s h u c k。另外,还要将前面提到 的其他扩展名的帮助文件添加给 M a c P e r l应用程序。 本书所附的光盘上包含了一个 M a c P e r l的安装软件包的拷贝。你可以直接 用光盘进行安装,也可以访问 h t t p : / / w w w. p e r l . c o m / C PA N / p o r t s / m a c站点,从 那里获得一个最新的拷贝。 1.2 文档资料 这个问题非常重要,因此你应该格外注意。每安装一个 P e r l,你就会得到一份完整的 P e r l 语言和解释程序的当前文档资料的拷贝。 是的,安装软件包配有用于 P e r l的整套文档资料,你可以免费获得这套资料。 Perl 5.005 版包含的资料超过 1 7 0 0页。这些资料包括参考资料、培训资料、 FA Q、历史资料,甚至是关 于P e r l内部情况的说明。 可以使用各种不同的方法来访问这些文档资料。在 Wi n d o w s和U N I X系统上,与Pe r l一道 安装了一个称为 p e r l d o c的实用程序。你可以使用 p e r l d o c程序来搜索这些文档资料,为手册提 供格式化输出。若要运行 p e r l d o c程序,你必须处在一个命令提示符下。下面这个例子使用的 是U N I X提示符,不过在D O S命令提示符下也可以: 第1学时 Perl 入门使用7下载手册的各个部分可以分成不同的节,其名字可以是 p e r l f u n c(P e r l函数)、p e r l o p(P e r l运 算符)和p e r l f a q(Perl FA Q)等。若要访问 p e r l f u n c手册页,可以输入命令 perldoc perlfunc。 手册的所有部分的名字都在 perldoc perl手册页中列出。 若要搜索手册,查找某个函数名,可以运行带有 - t f开关的p e r l d o c实用程序。下面这个例 子用于查找P e r l的p r i n t函数的手册页: $ perldoc -tf print FA Q是指关于P e r l的常见问题。这些问题是学习 P e r l的人一再提出的问题。为了节省人们 的时间,减少一些麻烦,这些问题集中起来放入称为 FA Q的文件中。若要搜索 FA Q文件,找 出某个关键字,你应该使用 - q开关,后随FA Q标题中可能出现的一个单词。例如,如果想要 知道有关P e r l的支持信息,可以使用下面这个命令: $ perldoc -q support 这时,就会显示FA Q问题的条目“Who Supports Perl ? Who develops it? Why is it free ?” (谁支持P e r l?谁开发了P e r l?它为什么是免费的?) 1.2.1 某些特殊的文档资料举例 当在U N I X系统上安装P e r l时,安装人员会看到一个选项提示,让他按传统的“ m a n”格 式来安装手册页。如果安装人员选择 y e s(是),那么标准P e r l文档就转换成m a n格式,并且存 放在一个相应的位置。若要访问 P e r l文档,既可以使用 p e r l d o c程序,也可以像通常在 U N I X中 那样使用m a n程序: $ man perl 当在Microsoft Wi n d o w s系统上安装A c t i v e S t a t e公司的P e r l产品时,手册页被转换成 H T M L 文档格式,并且可以用We b浏览器进行访问。如果想要阅读手册,请将你的 We b浏览器指向本 地目录C : \ P e r l \ h t m l,假如你使用标准安装目录的话;如果你不使用标准目录,则使用你选定 的目录。 如果是M a c i n t o s h系统,M a c P e r l配有一个称为 S h u c k的实用程序,它位于 M a c P e r l文件夹 中。你可以用它来阅读和搜索 P e r l文档,如图1 - 4所示。 8使用第一部分 Perl 基础 下载 图1-4 M a c P e r l的S h u c k文 档阅读器1.2.2 如果无法找到文档该怎么办 如果无法找到你要的文档,那么可能在两个地方出现了错误。首先是没有查找正确的文 档位置。也许p e r l d o c实用程序安装到的目录不在你的 s h e l l搜索路径上,或者你的搜索路径具 有很强的限制性。请反复搜寻 p e r l d o c实用程序,将该目录添加到你的 s h e l l的PAT H环境变量 中。 第二个原因是文档被删除了。也许是不小心删除的,也可能是恶意删除的。 P e r l的安装包 括了文档的安装。你不能只安装 P e r l而不安装文档。如果文档没有了,那么完全可以认为 P e r l 的安装不正确,或者安装以后遭到了破坏。也许你或者系统管理员应该考虑重新安装 P e r l。文 档是P e r l开发环境的不可分割的组成部分,如果没有文档,那么 P e r l的某些部分将无法运行。 如果其他方法都失败了,而你仍然无法得到文档的本地拷贝,那么你可以求助于 We b。 在P e r l的主要销售站点( h t t p : / / w w w. p e r l . c o m)上,可以访问一组标准文档。拥有你的 P e r l版 本配备的实际文档更好,因为它是根据你的特定 P e r l版本和安装情况裁剪的文档,不过这个在 线文档只有在紧急情况下才能使用。 1.3 编写你的第一个Perl程序 若要编写你的P e r l程序,需要一个称为文本编辑器的实用程序。使用文本编辑器,可以将 不带任何格式的纯文本输入文件中。 Microsoft Windows 的N o t e p a d和DOS 的E D I T. E X E均属文 本编辑器。在U N I X中,v i、e m a c s和p i c o等,都是文本编辑器。你的系统上至少存在其中的一 个文本编辑器。在 M a c系统下,M a c P e r l应用程序包含一个基本的文本编辑器,若要打开一个 新程序,请在F i l e菜单下选定N e w。 不应该使用文字处理程序来键入你的 P e r l程序。文字处理程序,比如 M i c r o s o f t的Wo r d、 Wo r d p a d和Wo r d P e r f e c t等,在保存文档时,会将格式化代码嵌入文档之中,即使文档并不包 含任何格式。这些格式化代码会使 P e r l感到莫名其妙,你的程序将无法正确运行。如果需要使 用文字处理程序,那么务必将你的程序保存为纯文本文件。 1.3.1 键入程序 打开文本编辑器,正确无误地键入下面这个 P e r l程序: #!这个程序行应该是文件中的第一行。 当将该程序键入你的文本编辑器后,将它保存在名字为 h e l l o的文件中。不需要在这个文 件名上使用扩展名,但是你加不加扩展名, P e r l都无所谓。有些 Wi n d o w s和M a c i n t o s h实用程 序使用扩展名来指明该文件是什么类型的文件。如果需要或者想要使用扩展名,那么常用的 扩展名是. p l或. p l x,因此你可以使用h e l l o . p l这样的文件名。 1.3.2 运行程序 这时,若要运行该程序,必须进入到一个命令提示符下。在 U N I X中,请打开一个终端窗 口,或者登录到系统中。在 Microsoft Wi n d o w s计算机上,显示一个 M S - D O S提示符。还应该 第1学时 Perl 入门使用9下载使用你的s h e l l的c d命令,转到你存放h e l l o程序的目录中。 当显示该提示符时,键入下面这个命令。(下面显示的是 D O S提示符,U N I X提示符稍有 不同。) C:\PROGRAMS> perl hello 如果一切进行正常,P e r l应该显示下面这行消息: Hello, world! 如果你键入的这个命令运行正确,那么祝贺你的程序运行成功了!请记住如何运行这个 程序,因为这也是你启动本书其余章节中的程序的方法。(也可以使用其他一些方法,下面很 快就要介绍这些方法。) 如果该命令不能运行,请进行下列检查: • 如果看到出错消息Bad command or file name或者perl : command not found,那么表示 p e r l程序不在你的执行路径上。必须确定 p e r l程序究竟安装在什么地方,并将该目录添 加给你的s h e l l中的PAT H变量。 • 如果看到出错消息C a n ’t open perl script hello : A file or directory does not exist(无法打 开P e r l脚本h e l l o:文件或目录不存在),那么你可能没有进入早些时候保存 h e l l o文件所 在的这个目录,也许你将该文件保存到另一个目录中了。 • 如果看到syntax error这样的错误,那么 P e r l能够正常启动运行,但是无法确定 h e l l o文件 中究竟有什么。也许键入的文件内容有错误,也可能使用了一个文字处理程序,它将格 式应用到保存的文件中了。可以使用 U N I X的c a t命令,或者D O S的t y p e命令,来核定文 件中的内容。如果键入的内容有误,你应该对所有内容进行检查,引号和所有标点符号 都很重要。 如果使用M a c P e r l,只需要从S c r i p t菜单中选定R u n “ h e l l o ”,运行你的第一个 p e r l程序。如 果没有使用M a c P e r l的内置编辑器来编写你的程序,那么请从 F i l e菜单中选定O p e n命令,打开 M a c P e r l中你的程序,然后选定R u n。 1.3.3 程序正确将会发生什么情况 当键入命令perl hello时,你的计算机上一个名叫 p e r l的程序就启动运行。该程序称为 p e r l 解释程序。这个 perl 解释程序是P e r l的核心和灵魂。它的作用是取出对它赋予的文件(这里是 文件h e l l o ),找出里面的程序,并运行该程序。 所谓“运行该程序”,是指首先要检查构成 P e r l程序的语句、函数、运算符、数学算法和 其他所有元素,以确保句法正确,然后每次运行一个语句。 当perl 解释程序完成了从磁盘中读取你的程序的全部操作后,它即开始运行该程序,并 且继续运行,直到整个程序运行完成。当它完成程序的运行时, perl 解释程序退出,将控制 权重新交给你的操作系统。 现在让我们来看一看h e l l o程序是如何“运行”的。 1.3.4 Perl程序的具体运行过程 H e l l o程序的第一行是: 10使用第一部分 Perl 基础 下载# ! / u s r / b i n / p e rl 对于P e r l来说,代码行上的 # 符号后面的一切均被视为注释行。注释是指 P e r l将对它加以 忽略的一些东西。在某些情况下,程序的第一行上的 #!是不同的。它后面的路径名 / u s r / b i n / p e r l是到达perl 解释程序的路径。如果 U N I X程序有一行是以 #!开头,后随一个解释 程序的路径,那么U N I X就知道这是个程序,并且可以按照名字来运行。关于如何运行程序的 说明,请参见本学时结尾处的“专家答疑”。 有些能够运行P e r l程序的We b服务器,比如A p a c h e,也对 #!这个程序行非常注意,并且 能够在没有p e r l命令的显式说明下运行该程序。 现在我们正好是将 #!视为一个注释行。 下一个程序行是: print "Hello,World!\n"; 这里包含了许多内容。这一行构成了一个 P e r l语句,它为 P e r l标明了一项需要执行的操 作。 首先,这一行包含一个函数,称为 p r i n t。这个 p r i n t函数取出它后面的所有内容,并默认 显示在屏幕上。p r i n t函数的输出结果,是直到分号(;)前的那部分内容。 P e r l中的分号是个语句分隔符。你应该在 P e r l程序中的各个语句之间放一个分隔符,以便 显示一个语句的结束和另一个语句的开始。 在这个例子中, p r i n t函数显示了短语 H e l l o , Wo r l d !。程序行结尾处的 \ n告诉P e r l将一个新 的空行插入到它输出的短语的后面。短语前后的引号和 \ n告诉P e r l,这是个文字串,不是另一 个函数。下一个学时将要非常详细地介绍字符串的内容。 1.3.5 必须知道的一些情况 P e r l被人们称为是一种自由格式的编程语言。这意味着 P e r l语句在编写的时候并不是非常 严格。可以将空格、制表符,甚至回车符(它们称为白空间)插入 P e r l语句中的任何位置,这 实际上都没有关系。 但是有些位置不能随意插入白空间,这些位置是应该加以限制的位置。例如,不能在函 数名的中间插入空格,pr int是个无效函数。另外不能将空格插入数字中,比如 25 61这个数字 是不行的。像“Hello Wo r l d !”这样的文字串中的白空间当然可以显示为白空间。几乎任何其 他地方它都是有效的。你可以编写类似下面这样的 P e r l程序示例: 这个程序在功能上与原先的程序是相同的。 P e r l语言的这种自由格式特性使你的 P e r l程序 可以具有非常丰富的“样式”。在程序的格式上可以有很大的随意性。不过请记住,总有一天 其他用户会查看你的程序的,因此你不能让他们看不懂。 本书中的程序所用的样式是相当保守的。有时为了清楚起见,或者为了节省空间,语句 被分成了若干行,因为 Pe r l的语句可能非常长。 P e r l的文档资料甚至提供了一个建议性的样式 指南,可以浏览该文档,以便了解有关的建议。可以搜索名字为 p e r l s t y l e的文档。 第1学时 Perl 入门使用11下载P e r l程序中的样式有时可能非常特殊。有的 P e r l程序可以写成诗歌,甚 至俳句,这些都是有效的。有些令人难忘的 Pe r l程序看上去就像是图画,不 过它们仍然能够做一些有用的工作。《Perl Journal》(网址是 h t t p : / / w w w. tPj . c o m)每年都要举行一次编写样式特殊的 Pe r l程序的比赛,比赛的名字 是Obfuscated Perl Contest。你不应该采取这些程序项目的样式。 1.4 课时小结 在本学时中,我们学习了一些关于 P e r l和P e r l是如何运行的知识。随着你阅读本书其他章 节的内容,你会不断增加对它的了解。还学习了如何在你的系统上安装 P e r l,并且如何来检验 它是否运行正确以及它的所有文档资料是否已经安装到位。最后,键入并且运行了你的第一 个P e r l程序。接着分析了这个程序,并且进一步学习了 P e r l如何运行的一些知识。 1.5 课外作业 1.5.1 专家答疑 问题:P e r l运行的那些东西是称为P e r l脚本还是P e r l程序? 解答:名字实际上并不重要。传统上,程序被编译成机器语言并且按照机器语言的形式 来存放,机器语言是可以多次运行的。而脚本可以被放入一个外部程序中,每当脚本运行时, 外部程序就将脚本转换成一些操作。 P e r l的发明人Larry Wa l l曾经说过:“脚本是你为操作人员 提供的东西,而程序则是你提供给用户的东西。”你可以随意称呼它们。在本书的其余章节中, 将它们称为P e r l程序,如果你学习成绩很好的话,那么你就可以称为一名 P e r l程序员。 问题:是否必须键入本书中的一些程序清单?有些程序清单非常长。 解答:本书中的所有程序清单和程序示例,以及这些程序需要的数据文件等,都在本书 所附的光盘上。 问题:在“运行程序”这一节中,讲到在 U N I X下有一种比较简便的方法可以用来运行 P e r l程序。究竟如何运行呢? 解答:首先,必须确保程序的 #!行正确无误,同时路径名确实指向一个 p e r l解释程序; / u s r / b i n / p e r l是它的通常位置,在有些机器上它的位置是 / u s r / l o c a l / b i n / p e r l。接着,必须使用 c h m o d命令,使该程序能够执行。如果是 h e l l o程序,那么U N I X的s h e l l命令是chmod 755 hello。 这项操作完成后,你可以键入 h e l l o或 . / h e l l o,运行该P e r l程序。应该说明的是,在 U N I X下, 不要将你的程序命名为“ t e s t”。U N I X的s h e l l有一个命令的名字是t e s t,当错误地运行t e s t命令 时,就会带来很大的麻烦。关于不能使用的其他程序名,请参见你的 s h e l l的文档资料。 另外,如果使用光盘上的程序清单,必须修改 #!行,使之与你的系统上的 P e r l位置相一 致,否则,你将必须从命令提示符下键入 perl programname运行你的程序。 1.5.2 思考题 1) Perl是编程语言的名字;p e r l则 a. 也是该语言的名字。 12使用第一部分 Perl 基础 下载b. 是解释程序的名字。 c. 是一个D O S命令的名字。 2) 在何处总是可以找到P e r l文档资料的拷贝? a. http://www. m i c r o s o f t . c o m b. http://www. p e r l . c o m c. http://www. p e r l . n e t 3) 在哪个手册页中能够找到P e r l句法的描述? a. perlsyn b. perlop c. perlfaq 1.5.3 解答 1) 答案是b。不过,P e r l安装后,它也是DOS shell中的一个有效的命令。因此 c也是对的。 2) 答案是b。它也可以安装在你的系统上。 3) 答案是a。除非你运行perldoc perl,否则无法明确知道能否在那里找到该描述。 1.5.4 实习 • 请浏览FA Q (常见问题)。即使你不能理解它里面的所有内容,也能够大致了解FA Q中可以 得到哪些类型的信息。 如果你喜欢通过浏览器来阅读它的内容,请搜索 h t t p : / / w w w. p e r l . c o m,并在那里阅读它的 内容,不过应该仔细阅读才行。 第1学时 Perl 入门使用13下载下载 第2学时 P e r l的基本构件:数字和字符串 每种编程语言,以及人类的每种语言,都有一个相似的出发点,那就是必须要有谈话的 要素。在P e r l中,数字和字符串就是谈话的基本单位,这些基本单位称为标量。 标量是P e r l的基本谈话单位。本书中的每个学时都要涉及到标量,对标量可以进行增加、 减少、查询、测试、集中、清除、分隔、折叠、排序、保存、加载、输出和删除等操作。标量 是P e r l的单个名词,它们可以代表一个单词、一个记录、一个文档、一行文本或者一个字符。 P e r l中的标量能够代表直接量数据,它在程序的生命期内是不变的。有些编程语言将这些 值称为常量或直接量。直接量数据可以用于表示没有变化的值,比如 p的值,物体落地的加速 度和美国第1 5届总统的名字等。如果一个 P e r l程序需要这些值,那么在程序的某个位置上可以 用一个标量直接量来代表它们。 P e r l中还有另一些类型的标量是变化的,它们称为标量变量。变量可以在你对它进行操作时用 来存放数据。可以改变变量的内容,因为它们只是作为它们代表的数据的句柄而存在的。变量要 被赋予相应的名字,这些名字比较简单,而且很容易记住,它们可以帮助你引用你要操作的数据。 本学时还要介绍P e r l的运算符。运算符是 P e r l语言中的一种动词,运算符取出 P e r l的名词, 负责从事你在编写执行特定任务的程序时需要进行的实际操作。 在本学时中,将学习下列内容: • 直接量数字和字符串。 • 标量变量。 • 运算符。 2.1 直接量 P e r l拥有两种不同类型的标量常量,它们都称为直接量。一种是数字直接量,一种是字符 串直接量。 2.1.1 数字 数字直接量都是一些数字, P e r l可以接受若干种不同的数字写法。表 2 - 1显示的所有例子 在P e r l中都是有效的数字直接量。 表2-1 数字直接量示例 数 字 直接量的类型 6 整型数 1 2 . 5 浮点数 1 5 . 0 另一个浮点数 0 . 7 3 2 0 5 0 8 也是一个浮点数 1 e 1 0 科学记数法 6 . 6 7 E - 3 3 科学记数法( e或E均可以) 4 _ 2 9 4 _ 2 9 6 带有下划线而不是逗号的大数字数字可以根据你设想的样子来加以表示。整数是一些连续的数字。至于浮点(十进制) 数,可以按照通常的形式使用小数点。科学记数法用一个指数字母 e和一个尾数(对数的十进 制部分)来表示。至于大整数,可以在通常使用逗号的地方换上下划线,以便于阅读。当使 用数字值的时候,P e r l会删除这些下划线。 在数字前面不要使用前导 0,比如0 1 0。对于P e r l来说,这个数字代表一 个八进制数字,它的基数是 8。P e r l还允许使用十六进制直接量数字(基数是 1 6)和二进制数字(基数是 2)。关于这些数字的详细信息请参见 p e r l d a t a部分 的在线文档。 2.1.2 字符串 P e r l中的字符串直接量是指原义字符构成的串。它们能够包含你所想要的那么多数据。字 符串的长度实际上是没有限制的,不过不能超出计算机中的虚拟内存的容量。字符串也可以 包含任何种类的数据,比如简单的 A S C I I文本,最高位为 1的A S C I I码,甚至二进制数据。字 符串也可以是空的。 应该用引号将字符串直接量括起来,不过在 P e r l中也有很少的一些例外。这个过程称为给 字符串“加引号”。给字符串加引号有两种主要方法,一种是使用单引号( ' '),一种是使用双 引号(" ")。下面是字符串直接量的一些示例: 在双引号中,如果需要插入另一个引号,则必须使用反斜杠转义符。字符串直接量中的 反斜杠(\)用于告诉P e r l,它后面的字符不应该按通常的情况来处理,在这种情况下,它应 该被忽略。例如,下面这个字符串直接量对 P e r l来说就没有任何意义: 在这个字符串中,单词 Go前面的引号与第一个引号是一对,从而将 Go ahead,make my d a y这个短语留在引号的外面,因此这不是一个有效的 P e r l。为了防止这种情况的发生,请在 你想要让P e r l不当成引号对待的引号的前面放一个反斜杠,如下所示: 反斜杠使得P e r l能够知道后面的引号与第一个引号不是匹配的一对,因此它应该不被作为 引号处理。这条规则既适用于单引号,也适用于双引号,请看下面的例子: 给字符串加双引号和单引号的主要差别是:使用单引号的字符串含义是非常直观的,单 引号字符串中的每个字符就是表示它自己的含义。在双引号中的字符串中, P e r l要查看是否存 在变量名或转义序列。转义序列是一些特殊字符串,你可以将难以键入和以后难以识别的字 符嵌入字符串。表2 - 2显示了一个P e r l的转义序列的简短列表。 第2学时 Perl 的基本构件:数字和字符串使用15下载表2-2 示例字符串的转义序列 序 列 含 义 \ n 换行 \ r 回车 \ t 制表符 \ b 退格 \ u 将下一个字符改为大写 \ l 将下一个字符改为小写 \ \ 直接量反斜杠字符 \ ' 用单引号(' ')括起来的字符串中的直接量 ' \ " 用引号括起来的字符串中的直接量 " 可以从在线手册中找到转义序列的完整列表。正如在第一学时中介绍的 那样,可以使用P e r l中包含的p e r l d o c实用程序,找到整个Pe r l语言的文档资料。 转义序列位于“p e r l o p”手册中的“Quote and Quote-like Operators(引号与 引号式的运算符)”这个标题下。 如果在字符串中包含许多个引号,那么当每个嵌入的引号必须转义时,键入字符串就会 非常困难,而且很容易发生错误。请看下面这个例子: P e r l还提供了另一个引号机制,即 q q和q运算符。使用q q运算符,就可以用 q q()而不是 引号将字符串括起来: q q取代了双引号。这个机制的作用几乎在所有方面都与双引号完全一样。 也可以用q运算符来代替单引号将文本括起来: q q和q运算符可以使用任何非字母、非数字字符来标记字符串的开始和结束。这些标记称 为界限符。在前面这个例子中使用了括号,不过也可以使用任何其他的非字母或非数字字符 作为界限符: 你想用作界限符的字符必须出现在紧靠运算符 q q或q的后面。使用任何一个成对的界限符, 如(),< > , { } , [ ],它们都能够正确地将字符串括起来。这就是说,如果在 q q或q运算符中以偶 数对的形式使用这些界限符,就不必使用反斜杠转义符: 但是,加上这些界限符后, P e r l程序的可读性就会有所降低。通常来说,只选择字符串中 不出现的界限符,读起来就比较容易。 2.2 标量变量 若要将标量数据存放在 P e r l中,必须使用标量变量。在 P e r l中,如果要写一个标量变量, 16使用第一部分 Perl 基础 下载可以写一个美元符号,后跟变量的名字。下面是一些标量变量的例子: 美元符号称为类型标识符,用于告诉 P e r l该变量包含标量数据。其他变量类型(哈希变量 和数组)则使用不同的标识符,甚至根本没有标识符(文件句柄)。P e r l中的变量名,比如哈 希变量、数组、文件句柄和标量,必须符合下列规则: • 变量名可以包含字母(a至z , A至Z)字符、数字或类型标识符后面的一个下划线字符(_)。 不过,变量名的第一个字符不能是数字。 • 变量名是区分大小写字母的。这意味着变量名中的大写和小写字母都是有特定意义的, 因此下面这些变量均代表不同的标量变量: $ v a l u e $ V A L U E $ V a l u e $ v a l u E P e r l只使用单字符变量名,它们不是以字母字符或下划线开始的。以 $ _、$”、$ /、$ 2和$ $ 开始的变量均属于特殊变量,在 P e r l程序中不能用作普通变量。这些特殊变量的作用将在下面 介绍。 P e r l与某些其他编程语言不同,P e r l中的标量变量在你使用它们之前,不必预先进行声明或 初始化。若要创建一个标量变量,只要使用它就行了。当使用一个未经初始化的变量时,P e r l将 使用它的默认值。如果它被用作数字时(如数学运算中的数字),P e r l将使用0(零)这个值;如 果它被用作字符串(几乎其他任何地方都使用),那么Perl将使用“ ”这个值,即空字符串。 在用一个值对变量进行初始化之前就使用该变量,这被认为是一种不好 的编程做法。当你在命令行上使用 - w开关,或者在程序开头的 # !行上使用- w 来调用P e r l程序时,P e r l就会向你发出警告。如果你试图使用的变量值预先没 有进行设定,那么当你的程序运行时以及试图使用该值时, P e r l就会报一条出 错消息Use of uninitialized value(使用未经初始化的值)。 特殊变量$_ P e r l拥有一个特殊变量,它的值可以用作“默认值”。对于许多运算符和函数来说,该变 量称为$变量。例如,如果你只是使用输出函数本身,而不设定要输出什么,那么它将输出 $ 的当前值: 像这样来使用 $ _,肯定会造成某些混乱。因为它确实没有指明究竟输出的是什么,尤其 是给$ _的赋值出现在程序中较高的位置上时更是如此。 有些运算符和函数实际上可以比较容易地用于 $ _变量,尤其是第6学时中介绍的模式匹配 运算符。不过在本书中将尽可能少用 $ _,这样学习起来就比较容易一些。 第2学时 Perl 的基本构件:数字和字符串使用17下载2.3 表达式和运算符 既然你已经学习了什么是标量数据并且知道如何使用标量变量,那么现在就可以开始用 P e r l来执行一些有用的操作了。 P e r l程序实际上是一些表达式和语句的集合,它们从 P e r l程序 的顶部向底部顺序执行,不过你也可以设定流控制语句的执行顺序,这将在第 3学时“控制程 序流”中介绍。程序清单2 - 1显示了一个有效的P e r l程序。 程序清单2-1 一个简单的P e r l程序 第1行:这一行是到达 P e r l解释程序的路径,这在第1学时中已经做了介绍。开关 - w告诉 P e r l,只要遇到警告就通知你。 第3行:这一行是个赋值行。数字标量数据 5 0存放在标量变量$ r a d i u s中。 第5行:这一行是另一个赋值行。在赋值运算符的右边是个表达式,该表达式包含了标量 变量$ r a d i u s、运算符(*和* *,下面将介绍这两个运算符)和数字标量( 2)。该表达式的值经 过计算后被赋予$ a r e a。 第6行:这一行负责输出存放在 $ a r e a中的计算结果。 表达式是具有值的简单元素。例如, 2是个有效的表达式。 5 4 * $ r、“J a v a”、s i n($ p i * 8) 和$ t = 6等,也都是表达式。表达式的值是在程序运行时进行计算的。程序对表达式中的函数、 运算符和标量常量进行计算,然后得出一个值。可以将这些表达式用于赋值,或者作为其他 表达式的一个组成部分,也可以作为其他 P e r l语句的组成部分。 2.3.1 基本运算符 正如你在程序清单 2 - 1中看到的那样,若要将标量数据赋予一个标量变量,可以使用赋值 运算符=。这个赋值运算符取出位于右边的值,再将它放入左边的变量中: 赋值运算符左边的操作数必须是能够被赋予变量的一个值。右边的操作数可以是任何种 类的表达式。赋值的本身就是一个表达式,它的值是右边表达式的值。这就是说,在下面这 个代码段中,$ a、$ b和$ c都被设置为4 2。 这个代码段中,$ c首先被设置为4 2。$ b被设置为表达式$ c = 4 2(它就是4 2)的值,然后$ a 被设置为表达式$b = 42的值。被赋值的变量甚至可以出现在赋值运算符的右边,如下所示: 赋值运算符的右边使用 $ a或$ c o u n t的老的值进行计算,然后,它的结果作为新值被赋予 左边。第二个例子在 P e r l中有一个特殊的名字,它称为递增。在后面我们还要更加详细地介 绍递增。 18使用第一部分 Perl 基础 下载2.3.2 数字运算符 P e r l中有许多运算符可以用来对数字表达式进行操作。这些运算符中,有些你已经非常熟 悉,有些是第一次遇到。已经知道的第一种运算符是数学运算符。表 2 - 3显示了这些运算符的 一个列表。 表2-3 数学运算符 举 例 运 算 符 名 表达式的值 5 + $t 加 5与$ t相加的和 $y - $x 减 $ y与$ x的差 $e * $pi 乘 $ e与$ p i的积 $f / 6 除 $ f除以6得出的商 24 % 5 求余数 2 4除以5得出的余数( 4) 4 * * 2 取幂 取4的二次方 数学运算符是按照我们通常的规则进行计算的,先取幂,再乘除,然后求余数,最后进 行加减。如果你不清楚表达式中各个元素的计算顺序,可以使用括号将元素括起来,以确保 计算顺序的正确。嵌套的括号总是从里向外进行计算: 2.3.3 字符串运算符 在P e r l中,数字值并不是可能受运算符影响的惟一值,字符串也可以进行相应的操作。第 一个字符串运算符是并置运算符,用圆点( .)来代表。并置运算符取出左边的字符串和右边 的字符串,返回一个将两个字符串合并在一起的字符串: 在这个例子中,$ a和$ b被赋予一些简单的字符串值。在最后一行上, $ a和$ b并置在一起, 变成H e l l o , World! Nice to meet you,然后存放在$ c中。$ a与$ b没有被并置运算符修改。 并置式的操作特性可以用另一种方式来执行。在前面,你了解到 P e r l在双引号字符串中查 找变量。如果P e r l在双引号字符串中找到了一个变量,那么它将被内插替换。这就是说,双引 号字符串中的变量名将被它的实际值代替: 在这个例子中,P e r l查看双引号字符串,发现 $ n a m e,并对字符串J o h n进行替换。这个过 程称为变量内插替换。为了防止变量查找的字符串被内插替换,可以使用单引号(它不进行 任何形式的内插替换),也可以在变量标识符的前面加上一个反斜杠: 上面这个例子中的两个p r i n t语句均用于输出I used the variable $name,字符串$ n a m e没有 被内插替换。因此,若要在不使用并置运算符的情况下执行并置操作,只要使用双引号字符 串即可,如下所示: 第2学时 Perl 的基本构件:数字和字符串使用19下载如果P e r l不能清楚地指明变量名在何处结束和字符串的其余部分从何处开始,那么可以使 用花括号将变量名括起来。使用这个句法, P e r l就能够找到可能模糊的变量名: 如果没有花括号,P e r l将不知道是要对双引号中的 $ d a t e还是对变量$ d a t e d a y进行内插替换。 加上花括号后,内插替换的对象就清楚了。 这里出现的另一个字符串运算符是重复运算符 x。运算符x配有两个参数,一个是要重复 的字符串,另一个是该字符串重复的次数,请看下面这个例子: 在上面这个例子中,字符-被运算符x重复7 0次。其结果存放在$ l i n e中。 2.4 其他运算符 P e r l的运算符非常多,由于本书的篇幅有限,因此无法一一列举。在本学时剩下的时间里, 将要介绍P e r l中最常用的一些运算符和函数。 2.4.1 单参数运算符 到现在为止,我们介绍的所有运算符都带有两个参数。除法( 6 / 3)需要一个被除数( 6) 和一个除数(3),乘法(5 * 2)需要一个被乘数( 5)和一个乘数( 2),如此等等。另一种运 算符只有一个参数。你可能已经熟悉这种运算符的一个例子,即一目减运算符( -)。一目减 运算返回变成负数的参数值: P e r l的许多运算符实际上是带名字的运算符,也就是说,它们不是一个符号,比如一目减操 作中的-符号,它们是一个单词。带名字的一目减运算符的操作数前后的括号是可有可无的,但 是为了清楚起见,表2 - 4中都将括号显示了出来。由于P e r l中带名字的运算符和函数看上去非常相 似,因此带名字的运算符的操作数有时也称为变元,这是P e r l函数用于它们的参数的一个术语。 表2 - 4显示了某些带名字的运算符的一个简单列表。 表2-4 一些带名字的运算符 运 算 符 用 法 举 例 结 果 i n t i n t(5 . 6 2 3 4) 返回它的参数的整数部分( 5)。 l e n g t h l e n g t h ( " n o s e " ) 返回它的字符串参数的长度( 4)。 l c l c("ME TO O") 返回它的转换成小写字母的参数(" me too ") u c u c("hal 9000") 返回与l c相反的参数值("HAL 9000") c o s c o s(5 0) 返回弧度5 0的余弦值(. 9 6 4 9 6 6) r a n d r a n d(5) 返回从0到小于该参数值之间的一个随机数 字。如果该参数被省略,则返回 0至1之间的 一个数字 20使用第一部分 Perl 基础 下载可以从在线手册中找到带名字的运算符的完整列表。我们在第 1学时中讲过,可以使用 P e r l产品中包含的p e r l d o c实用程序找到P e r l语言的完整文档。运算符的完整列表位于“ p e r l o p” 和“p e r l f u n c”手册中。其他运算符将根据需要在后面的一些学时中介绍。 2.4.2 递增和递减 在“数字运算符”这一节中,我们讲到了一种特殊的赋值类型,称为递增,它类似下面 的形式: 递增通常用于计数,比如读取的记录数,或者生成序列号,比如给列表中的项目编号。 在P e r l中,它是个非常常用的术语,因此可以使用一个特殊运算符,称为自动递增运算符 (+ +)。自动递增运算符能够给它的操作数递增 1: 在执行这个代码后,$ c o u n t e r递增1。 P e r l还提供了一种快捷运算符,用于递减一个变量的值,这个运算符称为自动递减运算符 (- -)。自动递减运算符的使用方法与自动递增运算符的使用方法完全相同: 关于自动递增运算符还有最后一个问题需要加以说明,那就是当你将它用于一个文本字 符串,而该文本字符串是以字母字符开始,后随字母字符或数字,那么这个运算符就具有一 种非常特殊的作用。字符串的最后一个(最右边的)字符被递增。如果它是个字母字符,它 将成为序列中的下一个字母;如果它是个数字,那么该数字将递增 1。你可以像下面这样通过 各个字母列和数字列: 自动递减运算符并不像上面那样对字符串进行递减。 2.4.3 尖括号运算符 尖括号运算符(< >),有时也称为菱形运算符,主要用于读写文件。我们将在第 5 学时详 细介绍这个运算符。不过现在要对它做一个简单的介绍,这会使我们的练习更加有趣。当你 开始学习第5学时的内容时,就会对该运算符有所了解。 到那时,你将能够用它的最简单的形式,即 < S T D I N >来使用尖括号运算符。这种形式告 诉P e r l,应该从标准输入设备(通常是键盘)读取一行输入信息。 < S T D I N >表达式返回从键 盘读取的这行信息: 第2学时 Perl 的基本构件:数字和字符串使用21下载上面这个代码在执行的时候(假设某人键入 9 . 5作为它的鞋子的尺寸),将类似下面的形 式: < S T D I N >表达式从键盘读取信息,直到用户按下 E n t e r键为止。整个输入行返回,并被存 放在$ s i z e中。由< S T D I N >返回的文本行也包含用户键入的换行符(因为按下了 E n t e r键)。在 大多数情况下,你不希望在字符串的结尾处出现换行符。若要删除换行符,可以像下面这样 使用c h o m p运算符: c h o m p运算符能够删除它的参数结尾处的任何换行符。它返回被删除的字符数,这个数字 通常是1,但是,有时如果没有字符需要删除,那么返回的是 0。 2.4.4 其他赋值运算符 前面我们曾经讲过,可以使用赋值运算符( =),将一个值赋予一个标量变量。实际上 P e r l 拥有一组完整的运算符,可以用于进行赋值操作。 P e r l的每个数学运算符和相当多的其他运算 符可以组合起来同时进行赋值和运算操作。下面是建立一个带有运算符的赋值语句的一般规则: 变量 运算符=表达式 这个规则与下面这个规则相同: 变量=变量 运算符 表达式 使用赋值语句通常不会使程序更加容易阅读,但是它能够使程序更加简洁。例如,按照 这个规则,语句$ a = $ a + 3;可以简化为 $ a += 3。 下面是另一些赋值语句的例子: 2.4.5 关于字符串和数字的一些说明 总的来说,P e r l允许你对数字和字符串进行互换使用。它使用的表示法取决于在这种情况 下P e r l查找的是什么。 • 如果某个元素看上去是个数字,那么 P e r l在需要数字时可以将它用作数字: • 如果某个元素看上去是个数字,那么当 P e r l需要一个字符串时,它可以使用数字的字符 串表示法: 22使用第一部分 Perl 基础 下载• 如果某个元素看上去不像一个数字,但是你将它用在需要数字的地方,那么 P e r l在它的 位置上使用0这个值: 但是,如果你激活了警告特性,那么如果你这样操作的话, P e r l就会发出警告消息。 所有这些用法都与P e r l的“尽可能随意自然”的原则是一致的。即使毫无意义,就像最后 一个例子中的情况那样, P e r l也设法用它来做一些有意义的事情。如果在 P e r l程序中激活了警 告特性,在 #!行上加上了一个- w开关,或者运行带有- w选项的P e r l程序,P e r l就会发出警告, 说明你的操作毫无意义,并给出下面这条消息: A rgument x isn’t numeric(参数x不是个数 字)。 2.5 练习:利息计算程序 这个练习是进行复利的计算。这个程序将根据利率、存款和时间等信息来计算储蓄帐户 的利息。下面是你要使用的计算公式: 使用文本编辑器,键入程序清单 2 - 2中的程序,并将它保存为 I n t e r e s t。不要键入行号。根 据你在第1学时中学习到的方法,使该程序成为可执行程序。 程序清单2-2 利息计算程序的完整源代码 第1行:这一行包含了到达解释程序的路径(可以对它进行修改,使它适合你的系统的需 要)和- w开关。请始终使警告特性处于激活状态。 第3行:这一行提示用户输入一个金额。 第4行:从标准输入设备(键盘)读取 $ p m t。 第5行:从$ p m t的结尾处删除换行符。 第2学时 Perl 的基本构件:数字和字符串使用23下载第7至9行:从键盘读取$ i n t e r e s t,同时删除换行符。 第11至1 3行:从键盘读取$ m o n s,同时删除换行符。 第1 6行:$ i n t e r e s t除以1 2,并且重新存入$ i n t e r e s t。 第1 8行:执行利息计算,将结果存入 $ t o t a l。 第2 0至2 1行:输出计算结果。 当你完成操作时,在一个命令行上键入下面的命令,设法运行该程序: perl Interest 程序清单2 - 3显示了一个利息计算程序输出信息的例子。 程序清单2-3 利息计算程序的输出 2.6 课时小结 在本学时中,我们了解到 P e r l的最基本的数据类型是标量。标量几乎可以是任何类型的数 据,并且可以存放在标量变量中。数字直接量可以用许多不同的格式来表示,比如整数、浮 点数等。字符串直接量用加引号的字符串来表示,字符串的前后既可以使用双引号,也可以 使用单引号。另外,P e r l提供了许多运算符,用于进行字符串的操作和基本的算术运算。 2.7 课外作业 2.7.1 专家答疑 问题:利息计算程序的输出显得有些杂乱,我如何才能控制它显示几位小数点? 解答:控制小数点位数的最简单的方法是使用 p r i n t f函数。我们将在第 9学时中介绍这个 函数。 问题:P e r l是否有一个用于数字舍入的函数呢? 解答:当显示数字时, p r i n t f函数通常可以用来进行你想要做的舍入操作。如果你确实需 要r o u n d函数,请查看P O S I X模块,它包含了该函数和其他许多函数。 问题:P e r l允许我操作的数字究竟可以有多大(或者多小)? 解答:答案取决于你使用的是什么操作系统。典型的 Intel UNIX系统的双精度浮点数的指 数幂可以超过3 0 0位。这就是说你操作的数字小数点的右边(或左边)可以有 3 0 0个0。不过大 数字的精度通常只有1 4位数。 2.7.2 思考题 1) 变量可以在q q引号中进行内插替换。 a. 是 b. 否 24使用第一部分 Perl 基础 下载2) 运行下面这个代码后,什么值将存放在 $ c中? $ a = 6 ; $ a + + ; $ b = $ a ; $ b - - ; $ c = $ b ; a. 6 b. 7 c. 8 3) 并置操作只能使用并置运算符( .)进行。 a. 是 b. 否 2.7.3 解答 1) 答案是a。q q的作用与一对双引号完全相同。这意味着它能够对变量进行内插替换。 2) 答案是a。$ a被设置为6,然后$ a递增为7,并被赋值给$ b。$ b递减为6,并被赋值给$ c。 3) 答案是b。在第1学时中我们讲过,在 P e r l中,一项操作可以使用多种方法来进行(这句 英文可以缩写为 T I M TO W T D I)。并置操作可以在双引号字符串中包含两个(或多个)标量, 如下面所示: q q($ a $ b $ c) : 2.7.4 实习 • 请编写一个短程序,提示用户输入一个华氏温度值,并输出摄氏温度值。若要将华氏变 成摄氏,你可以将华氏温度减去 3 2,然后乘以5 / 9。例如,华氏7 5度等于摄氏2 1 . 1度。 • 修改程序清单 2 - 3中的利息计算程序,输出的金额小数点不要超过两位。你可以不使用 p r i n t f函数,而巧妙地使用i n t运算符、乘法和除法来进行计算。 第2学时 Perl 的基本构件:数字和字符串使用25下载下载 第3学时 控制程序流 在第2学时中,我们学习了程序的语句、运算符和表达式等内容。该学时中的所有例子有 一个共同点,那就是所有的语句都是从上向下执行的,并且只执行一次。 我们之所以使用计算机,原因之一是计算机非常擅长执行重复任务,一次又一次地重复, 永不疲倦,也不会诱发手腕综合症。迄今为止,还没有任何办法来告诉 P e r l,让它“执行该任 务X次”,或者“重复执行该任务,直到任务完成为止”。在本学时中,我们要介绍 P e r l的控制 结构。使用这些控制结构,就能够将语句组合成所谓的“语句块”,并且重复执行这些语句组, 直到它们完成你想要完成的工作。 计算机擅长的另一项工作是能够迅速作出决策。如果计算机每次作出决策时都要询问你, 那么你一定会感到非常讨厌,而且也说明计算机太笨了。检索和读取电子邮件,可使你的计算 机作出上百万个决策,而这些决策并不是你想要处理的。比如,如何组合网络信息,什么颜色 构成屏幕上的每个像素,你收到的电子邮件应该如何分类和显示,当鼠标光标稍稍移动了一点 儿时应该怎么办,如此等等,这些都需要迅速作出决策。所有这些决策又是由其他决策构成的, 而且其中的一些决策需要每秒钟作出数千次。在本学时中,我们将要介绍条件语句。使用这些 语句,你就能够编写代码块,这些代码块是根据你的 P e r l程序中作出的决策来执行的。 在本学时中,我们将要介绍下列基本概念: • 语句块。 • 运算符。 • 循环。 • 标号。 • 程序执行后退出P e r l。 3.1 语句块 P e r l中最简单的语句组合称为块。若要将语句组合在一个块中,只需要用一组匹配的花括 号将语句括起来即可,如下所示: 在语句块中,语句像前面已经介绍过的情况那样,从上向下执行。在语句块中,你可以 拥有另一些语句块,请看下面的例子:语句块的格式与P e r l的其他程序格式一样,是自由随意的格式。语句与花括号可以如下面 所示放在同一行上,也可以放在若干不同行上,并且可以根据你的需要,采用任何一种对齐 方式,只要使用匹配的一组花括号即可: 虽然你可以按照你的喜好来安排语句块,但是,如果仅仅将各个语句凑合在一起,那么 程序就很难阅读。采用好的缩进方式虽然并不是必须的,但是却能建立便于阅读的 P e r l程序。 程序中出现的孤立语句块称为裸语句块。不过大多数情况下,你遇到的语句块是附属在 其他P e r l语句后面的。 3.2 if语句 若要根据P e r l程序中的某个条件来控制语句是否执行,通常可以使用 i f语句。下面显示了 i f语句的句法: 该语句的工作原理是:如果表达式计算的结果是真( T R U E),那么该代码块运行;如果 该表达式的计算结果是假( FA L S E),那么代码块不运行。请记住,该代码块包含了花括号。 请看下面这个例子: 该代码中被测试的表达式是 $r == 5。符号= =是个等式运算符。如果等式两边的两个操作 数($ r和5)的数值是相等的,那么该表达式被视为真,同时 p r i n t语句执行。如果$ r不等于5, 那么p r i n t语句不执行。 如果一个条件是真,那么 i f语句也能运行代码,否则,如果该条件不是真,则运行另一个 代码。该结构称为i f - e l s e语句,它的句法类似下面的样子: 只有当表达式是真的时候,表达式后面的语句块才运行;如果表达式不是真,那么 e l s e后 面的语句块运行。现在请看下面这个例子: 请注意,在上面这个例子中,为了将一个值赋予 $ r,使用一个赋值运算 符=。为了测试$ r的值,使用等式运算符 = =。这两个运算符有着很大的差别, 其作用是不同的。在你的程序中不要混淆它们的用法,因为调试非常困难。 请记住,运算符=用于赋值,而= =则用于测试一个等式。 第3学时 控制程序流使用27下载建立i f语句的另一种方法是使用多个表达式,然后根据哪个表达式是真,来运行代码: 可以像下面这样来读取上面这个语句块:如果标号为 e x p r e s s i o n 1的表达式是真,那么语 句块B L O C K 1就运行。否则,控制权转给 e l s i f,对e x p r e s s i o n 2进行测试,如果该表达式是真, 则运行B L O C K 2。如果e x p r e s s i o n 1和e x p r e s s i o n 2都不是真,那么B L O C K 3运行。下面是一个实 际的P e r l代码的例子,用于显示这个句法的实际情况: 3.2.1 其他关系运算符 到现在为止,我们都是使用等式运算符 = =来比较i f语句中的数字的量值。实际上 P e r l还有 一些运算符可以用来进行数字值的测试,表 3 - 1列出了这种运算符的大部分。 表3-1 数字关系运算符 运 算 符 举 例 说 明 = = $x == $y 如果$ x等于$ y,则为真 > $x > $y 如果$ x大于$ y,则为真 < $x < $y 如果$ x小于$ y,则为真 >= $x >= $y 如果$ x大于或者等于 $ y,则为真 < = $x <= $y 如果$ x小于或者等于 $ y,则为真 ! = $x != $y 如果$ x不等于$ y,则为真 若要使用这些运算符,只需要将它们放在你的程序中需要测试数字值之间的关系的任何 位置,就像程序清单3 - 1中所示的i f语句那样。 程序清单3-1 一个小型猜数字游戏 28使用第一部分 Perl 基础 下载第1行:这一行是P e r l程序第一行的标准格式,它指明你想要运行的解释程序和用于激活警 告特性的- w开关。与第1学时的内容比较,你会发现那里的第一行与这里的第一行稍有不同。 第3行:函数(rand 10)取出0至10 之间的一个数字,i n t()对该数字范围进行舍位,这 样只有0到9的整数被赋予$ i m _ t h i n k i n g _ o f。 第4 ~ 6行:这一行让用户进行猜测,将它赋予 $ g u e s s,并删除结尾处的换行符。 第8 ~ 9行:如果$ g u e s s大于$ i m _ t h i n k i n g _ o f中的数字,那么这些行将输出一个相应的消息。 第1 0 ~ 11行:否则,如果$ g u e s s小于$ i m _ t h i n k i n g _ o f中的数字,那么这些行便输出该消息。 第1 2 ~ 1 3行:剩下的惟一选择是用户猜测该数字。 表3 - 1中的运算符只能用于测试数字值。使用这些运算符可以测试你可能不想要的运行特 性中的非字母数据的结果。请看下面这个例子: $ f i r s t和$ l a s t中存放的两个值实际上是要测试它们之间是否相等。对它们进行测试的原因 在第1学时中已经做了说明。当 P e r l期望数字值的时候,如果使用了非数字字符串,那么这些 字符串的计算结果将是 0。因此,上面这个 i f表达式在P e r l看来就像是:i f( 0 == 0)。这个表 达式的计算结果是真,这可能不是你想要的结果。 如果程序中的警告特性被打开,那么当程序运行时,用 = =运算符来测试 两个字母字符值(上面代码段中的 s i m p l e和S i m o n),就会产生一个警告消息, 以提醒你存在这个问题。 如果你想要测试非数字值,你可以使用另一组 P e r l运算符,表3 - 2列出了这些运算符。 表3-2 字母关系运算符 运 算 符 举 例 说 明 e q $s eq $t 如果$ s等于$ t,则为真 g t $s gt $t 如果$ s大于$ t,则为真 l t $s lt $t 如果$ s小于$ t,则为真 g e $s ge $t 如果$ s大于或者等于 $ t,则为真 l e $s le $t 如果$ s小于或者等于 $ t,则为真 n e $s ne $t 如果$ s不等于$ t,则为真 这些运算符通过从左到右观察每个字符,然后按照 A S C I I的顺序对它们进行比较,来确定 “大于”和“小于”。这意味着字符串按照升序进行排序,大多数标点符号放在最前面,然后 是数字,接着是大写字母,最后是小写字母。例如, 1 5 0 6大于H a p p y,而H a p p y又大于h a p p y。 3.2.2 “真”对于Perl意味着什么 到现在为止,我们已经介绍了“如果该表达式是真 . . . . . .”或者“. . . . . .计算的结果为真. . . . . .,” 等情况,但是尚未介绍 P e r l认为的“真”的正式定义。关于什么是真,什么不是真, P e r l有几 个简短的规则,当你认真思考一下这些规则之后,就会认识到这些规则的意义。下面就是这 些规则: 第3学时 控制程序流使用29下载• 数字0为假。 • 空的字符串(“”)和字符串“0”为假。 • 未定义值u n d e f为假。 • 其他东西均为真。 明白了吗?需要注意的另一个问题是:当你测试一个表达式,看它是真还是假时,该简 化表达式,调用函数,使用运算符,数学算式简化为表达式,如此等等,然后转换成标量值, 以便进行计算,确定它是真还是假。 请考虑这些规则,然后看一下表 3 - 3。在查看答案之前,猜测一下该表达式是真还是假。 表3-3 真还是假的例子 表 达 式 真 还 是 假 0 假。数字0为假 10 真。这是个非 0数字,因此是真 9>8 真。关系运算符将根据你的期望返回真或假 - 5 + 5 假。这个表达式计算后得出的结果是 0,而0是假 0.00 假。这个数字是0的另一种表示法,0 x 0,0 0,0 b 0和0 e 0 0也是一样 " " 假。在上面介绍的规则中明确讲到这个表达式是假 " " 真。引号中有一个空格,这意味着它们并不是完全空的 " 0 . 0 0 " 真。真奇怪!它已经是个字符串了,而不是“ 0”或“”。因此它是真 " 0 0 " 也是真,原因与“0 . 0 0”相同 "0.00" + 0 假。在这个表达式中,对 0 . 0 0 + 0进行了计算,结果是0,因此这是假 到现在为止,我们只介绍了 i f语句的关系运算符。实际上你可以使用任何表达式,按照你 想要使用的方法来计算它是真或假: u n d e f这个值在P e r l中是个特殊值。尚未设置的变量均拥有 u n d e f这个值,并且有些函数在 运行失败时也返回u n d e f。它不是0,也不是一个普通的标量值,它有几分特殊。当在一个 i f语 句中测试为真时,u n d e f总是计算为假。如果你试图使用数学表达式中的 u n d e f值时,它将被视 为0。 使用尚未设置的变量,往往是程序出错的标志。如果你运行的程序激活了警告特性,那 么在一个表达式中使用 u n d e f值或者将未定义的值传递给某个函数,就会使 P e r l发出一个警告, 即Use of uninitialized value(使用了未经初始化的值)。 3.2.3 逻辑运算符 当你编写程序时,有时可能需要类似下面这样的某种代码,即如果 $ x是真,并且$ y是真, 那么执行这项操作,但是,如果 $ z是真,则不要执行该操作。可以将这个例子的代码编写成 一系列的i f语句,不过这并不是个出色的代码: 30使用第一部分 Perl 基础 下载P e r l拥有一套完整的运算符,可以将真和假的语句组合在一起,这些运算符称为逻辑运算 符。表3 - 4显示了各个逻辑运算符。 表3-4 逻辑运算符一览表 运 算 符 替 代 名 举 例 分 析 & & a n d $s && $t 只有当$ s和$ t都是真时,才是真 $q and $p 只有当$ q和$ p都是真时,才是真 | | o r $a || $b 如果$ a或$ b是真,则为真 $c or $d 如果$ c或$ d是真,则为真 ! n o t ! $m 如果$ m不是真,则为真 not $m 如果$ m不是真,则为真 使用表3 - 4中的运算符,可以将前面这个代码段改写得更加简洁,如下所示: 用逻辑运算符连接起来的表达式将自左向右进行计算,直到能够为整个表达式确定一个 真或假的值。请看下面的代码段: 第1行:这一行代码为变量赋予一个默认值。 第3行:$ a首先被计算。它的结果是假,因此 a n d表达式不可能是真。 $ b从来不计算,它 也不必计算,因为表达式的真是在计算 $ a之后知道的。P r i n t没有执行。 第4行:$ d首先被计算。它的计算结果是假。即使 $ d是假,该表达式仍然可能是真,因为 它包含一个逻辑 o r,因此下一步要观察 $ b。$ b的结果是真,因此该表达式是真,同时 p r i n t开 始运行。 第5行:$ d首先被计算。它的结果是假。即使 $ d是假,该表达式可能仍然是真,正如第 4 行中的情况一样,因为它包含一个逻辑 o r。接着,$ b的真(为1,因此是真)被求反,因此该 表达式变成假。 o r语句的真尚不能确定,因此 $ c被计算。$ c的计算结果是真,因此整个表达 式是真,p r i n t开始运行。 这个运行特性(即一旦确定表达式是真,立即停止逻辑表达式的计算)称为短路。整个 特性可供P e r l程序员用来借助逻辑运算符创建简单的流控制语句,而完全不使用 i f语句: 在上面这个例子中,如果 $ a或$ b中有一个是假,那么 o r右边的表达式必须计算,并且消 息被改变。如果 $ a和$ b都是真,那么 o r必须是真,并且不必计算右边的表达式。整个表达式 的真值根本不使用。这个例子使用 a n d和o r运算符的短路副作用来操作 $ m e s s a g e。 第3学时 控制程序流使用31下载运算符 || 和o r并不完全相同。它们的差别在于 || 的运行优先级要高于 o r。 这意味着在一个表达式中, || 要比o r更早地进行计算。这与数学表达式中乘 法的计算要先于加法的情况是一样的。这个规则也适用于 & & / a n d和!/ n o t。 如果你对计算顺序没有把握,可以使用括号来确保表达式的计算顺序的正 确。 P e r l的逻辑运算符有一个有趣的属性,那就是它们不仅仅返回真或假,它们实际上返回计 算得出的最后值。例如,表达式 5 && 7的计算结果并不只是返回真,而是返回 7。这样你就可 以创建下面这个代码: 这比下面这个代码更加简洁: 3.3 循环 在本学时的开头我们说过,仅仅根据条件来进行决策和运行代码是不够的。在许多情况 下,需要一次又一次重复运行一段代码。程序清单 3 - 1中显示的程序练习并没有太大的趣味, 因为只是进行一次猜测,它是个毫无意义的游戏。如果你希望能够进行多次猜测,那么必须 按照一定的条件重复执行一些代码段,这就是循环所要达到的目的。 3.3.1 用while进行循环 最简单的一种循环是 w h i l e循环。只要表达式是真的, w h i l e循环就会重复执行该代码段。 w h i l e循环的句法类似下面的形式: 当P e r l遇到w h i l e语句时,它就计算该条件。如果条件计算的结果是真,代码块就运行。 当运行到代码块的结尾时,表达式被重新计算,如果结果仍然是真,代码块重复执行,如程 序清单3 - 2所示: 程序清单3-2 while循环示例 第1行:$ c o u n t e r被初始化为0。 第2行:表达式$counter < 10被计算。如果计算的结果是真,该语句块中的代码就运行。 第4行:$ c o u n t e r的值递增1。 第5行:花括号}给第2行上以 {为开始的代码块做上结束标号。这时, P e r l返回到w h i l e循 32使用第一部分 Perl 基础 下载环的顶部,并重新计算该条件表达式。 3.3.2 使用for循环 f o r语句是P e r l循环结构中最复杂和最有用的语句。它的句法类似下面的形式: f o r语句分为3个部分,即i n i t i a l i z a t i o n、t e s t和i n c r e m e n t,它们之间用分号隔开。当 P e r l遇 到一个f o r循环时,便出现下面这个操作顺序: • 初始化表达式被计算。 • 测试表达式被计算。如果它的计算结果的真,代码块就运行。 • 当该代码块执行结束后,便执行递增操作,并再次计算测试表达式。如果该测试表达式 的计算结果仍然是真,那么代码块再次运行。这个进程将继续下去,直到测试表达式的 计算结果变为假为止。 下面是f o r循环的一个例子: 在上面这个代码段中, $ a设置为0,执行测试表达式 $ a < 1 0,发现其结果为真。循环的本 身输出了一条消息。然后递增语句 $ a = $ a + 2开始运行,它将$ a的值递增2。测试语句再次执行, 循环重复运行。这个特殊的循环将重复运行,直到 $ a的值达到1 0为止。这时测试语句变为假, f o r循环后面的程序将继续运行。 不必使用f o r语句来进行计数,它只是进行重复操作,直到测试表达式变为假为止。要记 住,f o r语句的3个组成部分中的每一个都是可有可无的,但是两个分号是必不可少的。下面这 个f o r语句漏掉了某些元素,不过它仍然是有效的: 3.4 其他流控制工具 用循环和条件语句来控制你的程序运行方式是不错的,但是还需要其他的流控制语句, 以便提高程序的可读性。例如, P e r l有一些语句可以用来提前退出 w h i l e循环,跳过f o r循环的 某些部分,在代码块结束之前退出 i f语句,或者甚至在不到结束的时候就退出程序。使用本节 中介绍的某些结构,就能够使你的 P e r l程序变得更加简洁和便于阅读。 3.4.1 奇特的执行顺序 i f语句还可以使用另一种句法。如果在 i f语句块中只有一个表达式,那么该表达式实际上 可以放在i f语句的前面。因此不要写成下面这个语句: 第3学时 控制程序流使用33下载可以写成: 下面是该语句变形的两个例子: 在P e r l代码中使用该句法通常是为了清楚起见。有时,如果在条件之前看到它的作用,那 么阅读代码就会更加容易。 i f前面的表达式必须是个单一表达式。 i f语句也必须后跟一个分 号。 3.4.2 明细控制 除了使用语句块、f o r、w h i l e、i f以及其他流控制语句来控制代码块以外,还可以使用 P e r l 语句来控制语句块中的流程。 为你提供这种控制能力的最简单的语句是 l a s t。l a s t语句能够使当前正在运行的最里面的 循环块退出。请看下面这个例子: l a s t语句能够在$ i的值是5时使w h i l e循环退出,而不是在通常w h i l e测试的结果是假时退出。 当你拥有多个嵌套的循环语句时, l a s t将退出当前正在运行的循环。 程序清单3 - 3用于找出其乘积等于1 4 0的所有小于1 0 0的两个数,比如2与7 0、4与3 5等,不 过查找的效率不太高。这里需要注意的是 l a s t语句。当找到一个乘积时,其结果被输出,里面 的循环(在$ i上重复运行的循环)退出,外面的循环继续运行(通过递增 $ i),并返回里面的 循环。 程序清单3 - 3 n e x t语句使得控制权被重新传递给循环的顶部,同时下一个循环的重复运行则开始进行, 如果该循环尚未结束的话: 该循环将输出从 0到9 8之间的所有偶数。如果 $ i不是偶数,那么n e x t语句将使该循环通过 它的下一个迭代运行过程。表达式 $i % 2是$ i除以2的余数。在这个例子中, p r i n t语句被跳过 了。编写这个循环时使用的一种更加有效的方法是按 2这个值来递增 $ i,但是这将无法展示 n e x t的作用,是不是? 34使用第一部分 Perl 基础 下载3.4.3 标号 P e r l允许你给语句块和某些循环语句( f o r、w h i l e)加上标号。也就是说,可以在语句块 或语句的前面放置一个标识符: 上面这个语句块的标号是M Y B L O C K。标号名使用的约定与变量名基本相同,不过有一个 很小的差别,那就是标号名不像变量,它不带 %、$和@之类的标识符。应该确保标号名与P e r l 的内置关键字不能冲突。就样式而言,如果标号名全部使用大写字母,那么这是最好的。不应 该使它与目前和将来的P e r l关键字的形式发生任何冲突。f o r和w h i l e语句也都可以带有标号。 l a s t、r e d o和n e x t语句都可以带有一个标号,作为参数。这样就可以退出一个特殊的语句 块。在程序清单3 - 4中,使用一对嵌套的 f o r循环,找到两个1 4 0的因子。如果想在找到一个因 子后立即退出该循环,需要在两个循环之间对标志变量和 i f语句进行复杂的安排。问题是不能 从内循环中退出外循环。 现在,最后一个语句可以用来设定它想要退出哪个循环,在这个例子中,要退出的是 O U T E R循环。这个代码段只输出它找到的 1 4 0的第一个因子。 3.4.4 退出Perl e x i t语句是最后的一个流控制工具。当 P e r l遇到e x i t语句时,程序就停止执行, P e r l将一个 退出状态返回给操作系统。这个退出状态通常用来指明程序已经成功地完成运行。第 11学时 我们将要更加详细地介绍各种退出状态。现在,我们看到退出状态 0意味着一切运行正常。下 面是e x i t的一个例子: e x i t语句具有某些对你的操作系统非常重要的副作用。当一个 e x i t执行时, 所有打开的文件均被关闭,文件锁被解开, P e r l分配的内存被释放给系统, P e r l解释程序执行清楚的关闭操作。 3.5 练习:查找质数 在这个练习中,我们将要观察一个小程序,它用来查找和输出质数。质数是只能被 1和它 第3学时 控制程序流使用35下载本身整除的数。例如,2是个质数,3也是个质数,而4不是质数(因为它可以被1、4和2整除), 如此等等。质数的数量是无限的,它们需要占用大量的计算机功能来查找。 使用文本编辑器,键入程序清单 3 - 4的程序,并将它保存为 P r i m e s。不要键入行号。根据 你在第1学时学习到的方法,使该程序成为可执行程序。 当你完成上面的操作时,键入下面这个命令行,设法使该程序启动运行: Perl Primes 程序清单3-4 用于查找质数的完整源代码 第1行:这一行包含到达解释程序的路径(可以修改该路径,使它适合你的系统的需要) 和开关- w。请始终使警告特性处于激活状态。 第3行:$ m a x p r i m e s是你想要查找的质数的最大数量。 第4行:$ v a l u e是你将要测试其质数特性的值。 第5行:$ c o u n t是迄今为止找到的质数的数量。 第6行:只要程序没有找到足够数量的质数, w h i l e循环就继续运行。 第7行:$ v a l u e被递增,因此,经过检查符合质数要求的第一个数是 2。 第8行:$ c o m p o s i t e是f o r循环中使用的一个标志,用于指明找到的数是合数,不是质数。 第9 ~ 1 0行:f o r循环重复运行通过 $ v a l u e的所有可能的因子。如果 $ v a l u e是4,那么这些循 环将产生2和2、2和3、3和3。 第11 ~ 1 4行:$ i与$ j的值相乘;如果乘积是 $ v a l u e,那么$ v a l u e是个合数。$ c o m p o s i t e标志 被设置,同时,各个f o r循环均退出。 第1 7 ~ 2 0行:在f o r循环之后,检查$ c o m p o s i t e标志。如果它是假,那么这数是质数。然后 这些行输出一个消息,同时计数器的数字递增。 这里用来查找质数的算法其运行的速度并不特别快,效率也不高,但是 它很好地展示了循环的运行情况。在更好的关于数值算法的著作中,你会找 到更好的方法。 36使用第一部分 Perl 基础 下载3.6 课时小结 在本学时中,我们介绍了 P e r l的许多流控制结构。有些结构,比如 i f和逻辑运算符,可以 用于根据真或假的值来控制程序的各个部分是否运行。其他的结构,如 w h i l e、u n t i l和f o r,用 于根据需要的次数循环运行代码段。我们还介绍了 P e r l的“真”究竟是什么概念, P e r l中的所 有测试条件实际上都使用这个概念。 3.7 课外作业 3.7.1 专家答疑 问题:我熟悉另一种编程语言C,它有一个s w i t c h(或c a s e)语句。P e r l的s w i t c h语句在哪里? 解答:P e r l没有这个语句。 P e r l提供了各种各样的测试方法,它确定 s w i t c h语句的最佳句 法是非常可怕的。下面是仿真 s w i t c h语句的最简单的方法: 如果在命令行提示符后面键入 perldoc perlsyn,你会看到一个在线句法手册页,它包含了 许多关于如何在P e r l中仿真s w i t c h语句的出色方法,其中有些配有很像 s w i t c h的句法。 问题:在f o r(w h i l e、i f)语句块中我能够嵌套多少个这样的语句块? 解答:如果你的系统的内存允许的话,嵌套的语句数目是不受限制的。但是,通常情况 下,如果循环中嵌套的语句太多,那么就要使用不同的方法来处理这个问题。 问题:P e r l向我显示了一条消息,即Unmatched right bracket或Missing right bracket(括号 不匹配,或者括号遗漏)。报告的行号是文件的结尾。我该怎么办? 解答:在你的程序的某个位置,使用了左花括号( { ),但是没有右括号,或者有了右括号, 没有左括号。有时P e r l能够猜到你的程序中的某个位置出现了键入错误,但是有时无法猜到这 样的错误。由于控制结构可以人工嵌套许多层,因此,直到 P e r l到达文件的结尾但没有找到对 应的括号时,它才知道你产生了键入错误。好的程序编辑器(如 v i、E m a c s或U l t r a E d i t)配有 一个特性,可以帮助你查找不匹配的括号,你可以选择使用。 3.7.2 思考题 1) 只要条件为真,w h i l e语句就始终循环运行。当条件是假时,什么语句将循环运行? a. if (not) {} b. while (! condition ) {} 2) 下面的表达式是真还是假? (0 and 5) || ( (" 0 " or 0 or " ") and (6 and "H e l l o")) or 1 a. 真 b. 假 第3学时 控制程序流使用37下载3) 下面这个循环运行之后,$i的值是什么? for($i=0; $i<=10; $i++) { } a. 10 b. 9 c. 11 3.7.3 解答 1) 答案是b。while (! condition ) {}语句将循环运行,直到条件变为假为止。 2) 答案是a。该表达式可以使用下面的步骤加以简化: (假)| |((假)a n d(真))o r真 假| |真o r真 真 3) 答案是c。测试表达式是$ i < = 1 0,因此当测试的条件最后变为假时, $ i必须是11。如果 你在这个问题上出错,请不要担心。这是个非常常见的错误,在程序员中甚至有一个专门的 名字来表示它,即篱笆桩错误或一步之差的错误。 3.7.4 实习 • 修改程序清单3 - 1,使游戏继续进行,直到做出一次正确的猜测。 • 程序清单3 - 4在查找质数时的效率实际上是非常低的。例如,它要分析 2以上的所有偶数, 而这些偶数不可能是质数。请对这种方法进行修改,使这个程序运行起来更加有效。 38使用第一部分 Perl 基础 下载下载 第4学时 基本构件的堆栈:列表与数组 标量是P e r l的单数名词。它们可以代表任何一种元素,如单词、记录、文档、一行文本或 者一个字符。但是,有时需要一些元素的集合,比如许多个单词、几个记录、两个文档、 5 0 行文本或者十几个字符等。 当需要谈论P e r l中的许多东西时,可以使用列表数据。可以用 3种方法来表示列表数据, 它们是列表、数组和哈希结构。 列表是列表数据最简单的表示方法,它们只是一个标量的组合。有时它们使用一组括号 将标量括起来,各个标量之间用逗号隔开。例如,(2,5,$ a,“B o b”)是两个数字,一个标 量$ a和单词“B o b”的列表。列表中的每个项目称为列表元素。为了不违背自然随意的原则, P e r l的列表可以根据你的需要包含任意多个元素。由于列表是标量的集合,并且标量也可以任 意大,因此列表能够存放相当多的数据。 若要将一个列表存放在一个变量中,需要一个数组变量。在 P e r l中,数组变量用一个符号 (@)后随一个有效的变量名(第1学时中的“数字与字符串”这一节做了介绍)来表示。例 如,@ F O O就是P e r l中的一个有效的数组变量。数组变量可以与标量变量使用相同的名字,例 如,$ n a m e s与@ n a m e s可以指不同的东西,$ n a m e s指一个标量变量,而@ n a m e s则指一个数组。 这两个变量之间毫无关系。 数组中的各个项目称为数组元素。各个数组元素按它们在数组中的位置来引用,这个位 置称为索引(比如说,数组 @ F O O的第三个元素,或者数组@ n a m e s的第五个元素等等)。 另一种列表类型是哈希结构,它类似数组。哈希结构将在第 7学时中详细介绍。 在本学时中,我们将要介绍: • 如何填充和清空数组。 • 如果逐个元素查看数组。 • 如何对数组进行排序和输出。 • 如何将标量分割成数组,以及如何将数组重新合成为标量。 4.1 将数据放入列表和数组 将数据放入一个列表是非常容易的。正如你刚刚看到的那样,列表的语法是用一组括号 将一些标量值括起来。下面就是列表的一个例子: (5, 'apple', $x. 3.14159) 这个例子用于创建一个由 4个元素组成的列表,它包含数字 5、单词a p p l e、标量变量 $ x 和p值。如果列表只包含简单的字符串,而用单引号将每个字符串括起来对你来说又太麻烦, 那么P e r l提供了一个快捷方式,即q w运算符。下面是使用q w的一个例子: qw (apples oranges 45.6$x) 这个例子创建了一个由 4个元素组成的列表。列表的每个元素之间用一个白空间(空格、 制表符或换行符)隔开。 $ x是个直接量$和x,它没有内插到它的值中去。如果有一些嵌入了白空间的列表元素,那么就不能使用 q w运算符。在这种情况下,上面这个代码的作用就像编 写的是下面这个代码一样: ('apples', 'oranges','45.6' '$x') 请注意,$ x是用单引号括起来的。 q w没有对看起来像变量的元素进行变量值内插,它们 是作为常规的形式来处理的,因此 ‘ $ x ’没有被转换成标量变量$ x的任何值,它只是留下了一个 美元符号和字母x。 P e r l有一个非常有用的能够对列表进行操作的运算符,称为范围运算符。范围运算符由一 对圆点(. .)来表示。下面是该运算符的用法的例子: ( 1 . . 1 0 ) 范围运算符用一个左边的操作数( 1)和右边的操作数( 1 0)构成了一个包含1到1 0(含1 与1 0)之间的所有数的列表。如果需要在列表中使用若干个范围,那么只要使用多个范围运 算符即可: (1..10, 20..30); 上面这个例子创建了一个包含 2 1个元素的列表,即包含 1到1 0和2 0到3 0(含1、1 0、2 0和 3 0)之间的数。如果范围运算符的右边的操作数小于左边的操作数,比如( 1 0 . . 1),那么将产 生一个空列表。范围运算符既可以用于字符串,也可以用于数字。范围 ( a . . z )可以产生一个包 含所有2 6个小写字母的列表。范围( a a . . z z )可以生成一个大得多的列表,它由6 7 5个字母对组成, 从a a、a b、a c、a d开始,到z x、z y、z z结束。 数组 直接量列表通常用于对某些其他结构进行初始化,比如数组或哈希结构。若要在 P e r l中创 建一个数组,只需要将某些数据放入数组即可。 P e r l与其他编程语言不同,不必预先告诉 P e r l, 你要创建一个数组,或者该数组将有多大。若要创建一个新数组,并用一个项目列表填入该 数组,只要编写下面这个代码即可: @boy=qw(Greg Peter Bobby); 这个例子称为数组赋值,它使用数组赋值运算符— 等号,这与标量中的情况一样。当 运行该代码后,数组 @ b o y s将包含3个元素,即G r e g、P e t e r和B o b b y。请注意,该代码也使用 了q w运算符。使用该运算符后,你就不必键入 6个引号和2个逗号。数组赋值也可以包含其他 数组甚至空列表,如下面的例子所示: @c o p y =@o r i g i n a ; @c l e a n = ( ) ; 在这里,@ o r i g i n a l数组的所有元素都被拷贝到新数组 @ c o p y中。如果@ c o p y中原先已经 拥有元素,那么这些元素就会丢失。这时 @ c l e a n就变成空数组。将一个空列表(或者空数组) 赋予一个数组变量,就会从该数组中删除所有的元素。 如果直接量列表中包含了其他列表、数组或哈希结构,那么这些列表将全部合并成一个 大列表。请看下面这个代码段: 40使用第一部分 Perl 基础 下载在将有关的值赋予 @ k i d s数组之前,列表( @ g i r l s,@ b o y s)被P e r l合并成一个由所有小 孩名字( G r e g,P e t e r等等)组成的简单列表。在下一行上,数组 @ k i d s被合并,同时列表 (‘ M i k e ’,‘C a r o l ’)被合并成一个长列表,然后该列表被赋予 @ f a m i l y。@ b o y s、@ g i r l s、 @ k i d s的原始结构和列表 (‘M i k e ’,‘ C a r o l’)本身则没有保留在 @ f a m i l y中,保留的只是各个元 素,即M i k e和C a r o l。 这意味着上面这个用于建立 @ f a m i l y的代码段与下面这个赋值语句是等价的: @family=qw(Greg peter Bobby Marcia Jan Cindy Mike Carol Alice); 如果赋值数组左边的列表只包含变量名,那么该列表可以用来对其元素进行初始化。请 看下面这个例子: ($a, $b, $c)=qw (apples oranges bananas); 在这个例子中, $ a被初始化为 ‘ a p p l e s ’,$ b被初始化为 ‘ o r a n g e s ’,而 $ c则被初始化为 ‘ b a n a n a s ’。如果左边的列表包含一个数组,那么该数组就接受来自右边列表的剩余值。现在 请看下面的例子: 在这个例子中,$ a被设置为‘p e a c h e s’。右边列表中的其余水果被赋予左边的 @ f r u i t。$ c 没有留下任何元素来接受一个值(因为赋值语句左边的数组吸收了来自右边的所有剩余值), 因此$ c被设置为u n d e f。 另一个值得注意的问题是:如果左边包含的变量比它拥有的元素多,那么多余的变量将 接受u n d e f这个值。如果右边的变量比左边的元素少,那么右边多余的元素将被忽略。下面让 我们观察一个代码,以便理解这个概念: 在第一行中,$ t、$ u和$ v均接受来自右边的值。右边多余的元素( ‘ q u a i l ’)将不用于该表 达式。在第二行中, $ a、$ b和$ c均接受来自右边的值。但是 $ d没有从右边得到任何值( $ c得 到了最后一个值‘ g o p h e r’),因此$ c被设置为u n d e f。 4.2 从数组中取出元素 到现在为止,我们在本学时中一直是在介绍整个数组和列表方面的内容。但是在许多情 况下,需要获得数组中的各个元素,需要搜索数组,改变元素的值,或者将元素添加给数组 或从数组中删除各个元素。 若要获得整个数组的内容,最简单的方法是使用双引号中的数组: print "@array"; 这个例子用于输出 @ a r r a y的元素,每个元素之间用空格隔开。数组中的各个元素可以按 索引来访问,如下面这个代码所示。数组元素的索引从数字 0开始,每增加一个元素,索引便 递增1。数组的每个元素都有一个索引值,如下所示: 第4学时 基本构件的堆栈:列表与数组使用41下载数组中元素的数量只受系统内存的限制。若要访问一个元素,可以使用句法 $ a r r a y [ i n d e x ], 其中a r r a y是数组的名字,i n d e x是你想要的元素的索引。在引用各个元素之前,数组不一定存 在。数组会魔术般地自动弹出来。下面是访问数组元素的一些例子: 请注意,如果要指 @ t r e e s中的某个元素,该代码可以使用一个 $。你可能会问:“$标记通 常是保留供标量使用的,这里是怎么回事呢?”在 $ t r e e s [ 3 ]中的$是指@ t r e e s中的一个标量值。 标量也可以用美元符号来表示,因为它们也是单数。你应该注意这里的一个模式。 在本学时的开头,我们讲过标量和数组可以拥有相同的变量名,但是它们互不相关。 P e r l 能够说明$ t r e e s与@ t r e e s [ 0 ]之间的差别,因为 $ t r e e s [ 0 ]中有一个方括号。 P e r l知道你指的是 @ t r e e s的第一个元素,而根本不是指 $ t r e e s。 还可以将数组划分成分组,称为片。若要使用数组的一个片,可以使用 @标号,以指明 你说的是一组元素,也可以使用方括号,以指明你说的是数组的各个元素,如下所示: 4.2.1 寻找结尾 有时需要寻找数组的结尾,例如,要查看 @ t r e e s数组中究竟有多少树状结构,或者从 @ t r e e s数组中砍下一些树枝来。 P e r l提供了两个机制,可以用来查找数组的结尾。第一个方法 是个特殊变量,其形式是 $ # a r r a y n a m e。它能够返回数组的最后一个有效索引的号码。请看下 面这个例子: 这个例子包含 8个元素,但是你必须记住,数组是从 0开始编号的。因此,上面这个例子 输出的号码是7。如果修改$ # t r e e s的值,就会改变数组的长度。如果要缩小数组,请在你设定 的某个索引处截断数组;如果要扩大数组,那么就给它增加更多的元素。新增加的元素的值 将全部设置为u n d e f。 寻找数组大小的另一种方法是在期望存在标量的位置上使用数组变量: $ s i z e = @ a r r a y ; 这将把@ a r r a y中的元素数量放入 $ s i z e中。它利用了P e r l的一个概念,称为上下文(环境), 这将在下一节中介绍。 也可以为数组设定负索引。负索引号从数组的结尾开始计数,然后反向 递增。例如,$ a r r a y [ - 1 ]是@ a r r a y的最后一个元素,$ a r r a y [ - 2 ]是倒数第二个元 素,依次类推。 42使用第一部分 Perl 基础 下载4.2.2 关于上下文的详细说明 什么是上下文呢?上下文是指有关项目周围的一些事物,这些事物可以用来帮助定义这 个项目的含义。例如,你看到一个人穿着外科消毒服,根据他所在的地方,这可能表示不同 的含义。如果是在医院,那么穿着这样的衣服的人是一名医生。如果是在万圣节上,那么他 只是一位化装了的宾客。 人类的语言使用上下文来帮助确定词汇的含义。例如,单词 l e v e l可以拥有若干不同的含 义,这要根据如何使用这个单词以及在什么上下文中使用它: • 木工使用水平仪(l e v e l)使安装的门能够达到水平。 • 调解人说话的语调平和(l e v e l)。 • 池塘里的水只有齐腰深(l e v e l)。 虽然每次使用的单词都是 l e v e l,但是它的含义在不同的场合却发生了变化。根据它在句 子中的不同用法,它可以是个名词或形容词,也可以是另一种类型的名词。 P e r l能够对上下文作出敏感的反应。在 P e r l中,函数和运算符能够根据使用时所在的上下 文而具有不同的运行特性。 P e r l中的两个最重要的上下文是列表上下文和标量上下文。 如你所见,可以将赋值运算符(等号)用于数组和标量。赋值运算符左边的表达式类型 (列表或标量)用于决定右边的表达式计算时所在的上下文。现在请看下面这个代码段: 该代码段的最后一行很有意思,因为在标量上下文中计算时,数组将返回该数组中元素 的数量。 观察下面这几行代码中的$ a和$ b,它们执行的操作几乎相同: 在该代码的结尾, $ a包含数字4,$ b包含数字3。为什么会出现这个差别呢?因为 @ f o o是 在标量上下文中运行的,它返回 $ a的元素的数量。 $ b设置为最后一个元素的索引号,而索引 是从0开始计数的。由于标量上下文中的数组返回数组中的元素的数量,因此测试数组中是否 包含元素就变得如此简单: 在这里,数组 @ m y d a t a被计算为一个标量,它返回元素的数量,在这个例子中,这个数 量是4。在i f语句中,数量4计算的结果是真,i f语句块的本身就开始运行。 实际上,@ m y d a t a在这里是用在一个特殊的标量上下文中,称为布尔上 下文,不过它的运行特性是相同的。当 P e r l希望得到一个真或假的值时,就 会出现布尔上下文,比如在一个 i f语句的测试表达式中。另一个上下文称为 无效( v o i d )上下文,我们将在第9学时中对该上下文进行介绍。 第4学时 基本构件的堆栈:列表与数组使用43下载4.2.3 回顾以前的几个功能 P e r l的许多运算符和函数能够强制它们的参数成为标量上下文或列表上下文。有时这些运 算符和函数的运行特性将根据它们所在的上下文而各不相同。你已经遇到的有些函数具备这 些属性,但是以前这个情况却并不重要。 P r i n t函数希望有一个列表作为其参数,不过该列表在什么上下文中计算却并不特别重要。 因此,用类似这个 p r i n t的函数来输出数组将会导致该数组在列表上下文中被计算,从而产生 @ f o o的各个元素: print @foo; 可以使用一个称为s c a l a r的特殊伪函数来强制将某个东西放入标量上下文: print acalar (@foo); 这个例子用于输出 @ f o o中的元素的数量。 S c a l a r函数强制@ f o o在一个标量上下文中进行 计算,因此@ f o o返回@ f o o中的元素的数量。然后p r i n t函数就输出返回的数量。 你在第2学时中了解到的 c h o m p函数既能够将数组作为参数,也能够将标量作为其参数。 如果c h o m p函数获得一个标量,那么它就从标量的结尾处删除记录分隔符。如果它获得一个 数组,它将从数组中的每个标量的结尾处删除记录分隔符。 我们在第2学时中介绍c h o m p时,还讲述了如何使用 < S T D I N >读取键盘输入的一行数据。 尖括号实际上是P e r l中的一个运算符,它们的运行特性随着上下文的不同而各有差异。在标量 上下文中,该运算符能够读取来自终端的一行输入。但是在列表上下文中,它能够读取来自 终端的所有输入,直到读到文件的结尾,并将数据放入列表。请看下面这个代码: 在第三个例子中, $ a将收到什么呢?本学时的开头我们讲过,在列表赋值语句中,如果 左边没有足够的变量来存放右边的全部元素,那么右边的多余元素将被放弃。这里,来自终 端的所有输入均被读取,但是 $ a只接收第一行。 什么是文件结尾呢?当 P e r l读取来自终端的全部输入且你完成 P e r l数据的 输入时,你必须发出通知。为此通常键入一个 End of File(文件结束)字符 (E O F)。该字符随着你使用的操作系统的不同而各有差别。在 U N I X下,该字 符通常是在一行的开头使用C t r l + D。在M S _ D O S或者Wi n d o w s系统上,该字符 是在输入的任何位置两次使用C t r l + Z。 我们在第1学时中介绍的重复运算符在列表上下文中有一个特殊的运行特性。如果左边的 操作数放在括号中,那么运算符本身将用在列表上下文中,它返回一个重复的左边操作数的 列表。下面的例子用于建立一个 1 0 0个星号的数组: x左边的操作数“ *”放在括号中,如果将它赋予一个数组,那么就使它进入一个列表上 下文中。该句法可以用于将数组的元素初始化为一个特定的值。 另一个运算符你一直在使用,但是可能不知道它是个运算符,这就是逗号(,)。到现在 为止,你一直使用逗号来分隔列表中的各个元素,比如下面这个例子: 44使用第一部分 Perl 基础 下载上面这个代码段是在通常的列表上下文中对列表进行计算操作的。但是,在标量上下文 中使用的逗号是个运算符,它用于从左至右对每个元素进行计算,再返回最右边的元素的值: 在这个代码段中,赋值运算符右边的那些宠物名字实际上并不是一个列表。它们只是一 组字符串直接量,从左到右进行计算,因为表达式的右边是在标量上下文中计算的(因为等 号的右边有一个$ l a s t _ p e t)。结果是该$ l a s t _ p e t被设置为等于‘ iguana’ 。 另一个例子是l o c a l t i m e函数,根据它所在的上下文,可以用两种完全不同的方法来运行。 在标量上下文中, l o c a l t i m e函数返回一个格式化很好的当前时间字符串。例如, p r i n t s c a l a r ( l o c a l t i m e )这个代码,它输出的结果将类似于 Thu Sep 16 23:00:06 1999。在列表上下文 中,l o c a l t i m e将返回能够描述当前时间的一个元素列表: 表4-1 显示了这些值代表的含义。 表4-1 列表上下文中l o c a l t i m e返回的值 字 段 值 $ s e c 秒,0 ~ 59 $ m i n 分,0 ~ 59 $ h o u r 时,0 ~ 23 $ m d a y 月份中的日期, 1 ~ 2 8、2 9、3 0或3 1 $ m o n 年份中的月份, 0 ~ 11(这里请特别要小心) $ y e a r _ o ff 1 9 0 0年以来的年份。将1 9 0 0加上这个数字,得出正确的 4位数年份 $ w d a y 星期几,0 ~ 6 $ y d a y 一年中的第几天,0 ~ 3 6 4或3 6 5 $ i s d s t 如果夏令时有效,则为真 不要将‘ 1 9 ’附加给l o c a l t i m e返回的年份。它返回的年份是 1 9 0 0的偏移量。 比如,在1 9 9 9年,年份是 ‘ 9 9 ’;在2 0 0 0年中,它是 ‘ 1 0 0 ’。将1 9 9 9与该值相 加,可以在 2 0 0 0年以后正确地产生年份。 P e r l不存在 2 0 0 0年问题,但是, 如果简单地将 ‘ 1 9 ’(或 ‘ 2 0 ’)附加给该年份,就会导致程序中产生 2 0 0 0年问 题。 怎么能够知道函数或运算符使它的参数在什么上下文中进行计算呢?又怎么知道它在标 量上下文或列表上下文中运行的情况呢?很简单,你无法知道,你也没有什么好办法来猜测 这个问题的答案。如果你不清楚的话,在线文档列出了每个函数和运算符,并且说明了它们 的各个因子。在本书的其他章节中,如果一个函数或运算符强制让它的参数在某个上下文中 运行,或者它们在自己运行的上下文中出现了完全不同的运行特性,那么我们将在首次介绍 该函数时指明这些情况。这种情况不会经常发生,不过当本书中发生这种情况时,一定有特 别说明。 4.3 对数组进行操作 既然你已经了解到创建数组的基本规则,那么现在我们就可以介绍一些工具来帮助你对 第4学时 基本构件的堆栈:列表与数组使用45下载这些数组进行操作,以便执行一些有用的任务。 4.3.1 遍历数组 在第3学时中,我们介绍了如何使用 w h i l e、f o r和其他结构进行循环的方法。你想使用数 组执行的许多操作都涉及到查看数组的每个元素,这个进程称为数组的迭代。迭代的方法之 一是使用f o r循环,如下所示: 第一行代码用于以冰淇淋的风味对该数组进行初始化,为了清楚起见,它使用了 q w运算 符。如果使用两个单词组成的冰淇淋风味,比如 Rocky Road,那么将需要一个普通的单引号 列表。第二行进行了代码的大部分工作。 $ i n d e x被初始化为0,并且按1进行递增,直到到达 @ f l a v o r s。请记住,@ f l a v o r s是在标量上下文中计算的,计算的结果是 5,即@ f l a v o r s中的元 素的数量。 上面这个例子似乎要对数组的迭代进行大量的工作。在 P e r l中,如果需要进行大量的工作, 那么通常可以找到一种比较简单的方法来进行这项操作,这没有例外。 P e r l还有另一个循环语 句,称为f o r e a c h语句,我们在第3学时没有介绍。f o r e a c h语句设置一个索引变量,称为迭代器, 它相当于列表的每个元素。请看下面这个例子: 在这个代码中,变量$ c o n e设置为@ f l a v o r s中的各个值。当$ c o n e被设置为@ f l a v o r s中的各 个值时,循环体就开始执行,为 @ f l a v o r s中的每个值输出消息。请注意,在一个 f o r e a c h循环 中,迭代器并不只是设置为列表中的每个元素的值,它实际上是对列表的元素的引用。因此, 在上面这个f o r e a c h循环中,如果修改该循环中的 $ c o n e,就能修改@ f l a v o r s中的对应元素。下 面让我们来观察一下这个例子: 第2行修改了$ f l a v o r,将ice cream附加给了结尾处,因此第3行负责输出I’d like a bowl of chocolate ice cream,然后继续输出 v a n i l l a,s t r a w b e r r y等等。当该循环最后运行结束时, @ f l a v o r s改为在每个元素的结尾处包含 ice cream。 在P e r l中,f o r e a c h和f o r循环语句实际上是同义语句,它们可以互换使用。 为了清楚起见,在本书的整个篇幅中,你将发现使用 f o r e a c h ( )循环语句在数 组上进行迭代运行,并将 f o r ( )循环语句用于像第 3学时中那样的普通 f o r循环。 请记住它们是可以互换的。 4.3.2 在数组与标量之间进行转换 一般来说,P e r l并没有关于在标量与数组之间进行转换的规则。我们提供了许多函数和运 46使用第一部分 Perl 基础 下载算符,以便进行它们之间的转换。将标量转换成数组的方法之一是使用 s p l i t函数。S p l i t函数 拥有一个模式和一个标量,并且使用该模式来分割该标量。第一个参数是该模式(这里用斜 杠括起来),第二个参数是要分割的标量: 当运行该代码时, @ w o r d s包含了各个单词,即 T h e、q u i c k、b r o w n和f o x,单词之间没有 空格。如果要设定一个字符串,请使用变量 $ _。如果没有设定一个模式或字符串,那么使用白 空间来分割变量$ _。一个特殊模式’’(即空模式)用于将标量分割成各个字符,如下面所示: 第一行用于读取来自终端的数据,每次读一行,并将 $ _设置为等于该行。第二行使用空 模式来分割$ _。S p l i t函数返回来自$ _中的这个行的每个字符的列表。该列表被赋予左边的列 表,而该列表的第一个元素则被赋予 $ f i r s t c h a r,其余均放弃。 s p l i t使用的模式实际上是一些正则表达式。正则表达式是一种复杂的模 式匹配语言,我们将在第 6学时介绍这方面的内容。而现在,我们的例子 将使用一些简单的模式,如空格、冒号、逗号等等。当你学习了正则表达 式的内容之后,我们将举例说明如何使用比较复杂的模式来用 s p l i t分割标 量。 在P e r l中使用这种方法来分割标量变量的列表是非常常见的。当分割一个标量,而标量中 的每个元素都是与众不同的元素(比如记录中的域)时,就能够比较容易地确定哪个元素是 什么。请看下面这个例子: 当你直接分割带有命名标量的列表时,能够清楚地看到哪个域代表什么。第一个域是记 录名,第二个域是艺术家。如果将代码分割成一个数组,各个域之间的区别也许不会那样清 楚。 若要用数组来创建一个标量,也就是进行 s p l i t的反向操作,可以使用P e r l的j o i n函数。j o i n 函数取出一个字符串和一个列表,使用该字符串将列表的各个元素组合在一起,然后返回产 生的字符串。请看下面这个例子: 这个例子将字符串1,2,3,4,5,6,7,8,9,10 赋予$ n u m b e r s。然后可以使用s p l i t和 j o i n将字符串分割后又将它们重新组合在一起。一个函数的输出(返回值)可以用作另一个函 数的输入值,请看下面的代码: 第4学时 基本构件的堆栈:列表与数组使用47下载在这个例子中,$ m e s s a g e被s p l i t分割成一个列表。该列表被 j o i n函数使用,并用逗号重新 组合在一起。产生的结果是下面这个消息: 4.3.3 给数组重新排序 当你创建一个数组时,常常想让它们用不同于创建时的顺序来显示。例如,如果你的 P e r l 程序从文件中读取一个客户列表,那么用字母顺序来输出该客户列表是可行的。若要给数据 排序,P e r l提供了s o r t函数。S o r t函数将一个列表作为它的参数,并且大体上按照字母顺序对 列表进行排序,然后该函数返回一个排定顺序的新列表。原始数组保持不变,如下面这个例 子所示: 这个例子用于输出Bush Carter Clinton Ford Nixon Reagan。应该预先注意的是,它的默认 排序次序是A S C I I顺序。这意味着以大写字母开头的所有单词均排在以小写字母开头的单词的 前面。用A S C I I顺序对数字进行排序的方式与你期望的不一样,它们不按值来排序。例如, 11 排在1 0 0的前面。在这种情况下,必须按非默认顺序来进行排序。 使用s o r t函数,你可以使用代码块(或者子例程名)作为第二个参数,按照你想要的任何 顺序进行排序。在代码块(或者子例程)中,两个变量 $ a和$ b被设置为列表的两个元素。代 码块的任务是:如果 $ b小于、等于或大于 $ a,则分别返回 - 1、0或1。下面是进行数字排序的 硬办法,假设@ n u m b e r s是包含了许多数字值的话: 上面这个例子当然按照数字顺序给 @ n u m b e r s进行排序。但是,该代码从事这样一个普 通的排序任务看起来太复杂了。由于你可能认为这个方法太麻烦,所以 P e r l有一个捷径可供 使用,你可以使用飞船运算符 < = >。飞船运算符因为从侧面看它像一个飞行的碟子而得名。 如果它左边的操作数小于右边的操作数,那么它返回 - 1,如果左边的操作数大于右边的操作 数,则返回0: 这个代码看上去比较清楚,并且更加直观。飞船运算符只应该用来比较数字值。 若要比较字母字符串,请使用 c m p运算符,它的运行方式与飞船运算符完全相同。只需要 编写一个更加复杂的排序例程,就可以将更加复杂的排序参数放在一起。如果你需要了解更 多的情况,本学时第7节中的P e r l常见问题给出了一些比较复杂的例子。 本学时要介绍的最后一个函数是个非常容易使用的函数,即 r e v e r s e。当在标量上下文中 被赋予一个标量值时, r e v e r s e函数能够对字符串的字符进行倒序操作,返回倒序后的字符串。 例如,在标量上下之中调用 r e v e r s e ( " P e r l ")将返回l r e p。当在列表上下文中被赋予一个列表时, r e v e r s e函数能够返回倒序后的列表元素,如下面的例子所示: 这个代码段用于输出ham and eggs green like not do I。若要继续进行这个试验,充分展示 该函数堆栈的功能,可以给混合列表添加更多的奇怪内容: 48使用第一部分 Perl 基础 下载print join(' ', reverse sort @ l i n e s ) ; 该代码首先运行 s o r t函数,产生一个莫名其妙的列表( I,a n d,d o,e g g s,g r e e n,h a m, l i k e,n o t)。该列表被倒序后再被传递给 j o i n函数,以便将这些元素连接起来,并且加上一个 空格。结果是not like ham green egg do and I,真不敢恭维这样的句子。 4.4 练习:做一个小游戏 本学时充满了许多新鲜的内容,比如,你熟悉的运算符在不同的上下文中产生不同的运 行特性,一些新的函数和运算符,还有一些需要记住的新的句法规则。为了使你不至于编写 出难以运行的代码,所以增加了这个练习,让你做一个游戏,使你具备的关于数组和列表的 知识能够得到很好的应用。 使用文本编辑器,将程序清单 4 - 1中的程序键入编辑器,并将它保存为 H a n g m a n。务必按 照第1学时中的说明使该程序成为可执行程序。 当完成上面的操作后,键入下面这个命令,使该程序运行: perl Hangman 程序清单4-1 完整的H a n g m a n程序清单 第4学时 基本构件的堆栈:列表与数组使用49下载第1行:包含到达解释程序的路径(可以修改这个路径,使它适合你的系统的需要)和开 关- w。请始终使警告特性处于激活状态! 第3行:数组@ w o r d s使用游戏能够使用的单词列表进行初始化。 第4 ~ 5行:有些变量被初始化。@ g u e s s e s用于存放游戏的玩主过去猜测的所有单词的列表。 @ w r o n g用于存放迄今为止猜错的单词的数量。 第7行:从数组@ w o r d s中随机选择的一个单词,并将它赋予 $ c h o i c e。r a n d()函数期望 得到一个标量参数,由于 @ w o r d s被视为一个标量,因此它返回元素的数量(在这里返回 4)。 r a n d函数返回一个 0至3之间的一个数字,但是不包括 0或4。结果,当将一个十进制数字用作 数组索引时,小数部分将被放弃。 第8行:h a n g m a n被定义。他很难看,但是通过了。 第1 0行:$ c h o i c e中的m y s t e r y单词在@ l e t t e r s中被分割成各个字母。 第11 行: h a n g m a n标量在 @ h a n g m a n中被分割成小块。头是 $ h a n g m a n [ 0 ],颈部是 $ h a n g m a n [ 1 ],等等。 第1 2行:数组@ b l a n k w o r d用于标明游戏的玩主猜对了哪些字母。(0) x scalar(@ h a n g m a n) 创建了一个列表,其长度与 @ h a n g m a n中的元素的数量相同,它被存放在 @ b l a n k w o r d中。当 猜字母时,这些0改变为第3 5行中的字母,这将标出猜对的字母的位置。 第1 3 ~ 1 4行:建立一个包含大部分程序的循环。它有一个标识符 O U T E R,这样,在循环 的内部,就能够对它进行某些具体的控制。它不断进行循环,直到猜错的字母数量与 h a n g m a n 的长度相同为止。 第1 5 ~ 2 1行:这个f o r e a c h循环为猜测的每个字母在数组 @ b l a n k w o r d上迭代运行。如果 @ b l a n k w o r d不包含该特定元素中的一个字母,那么就输出一个破折号,否则,输出该字母。 第2 3 ~ 2 5行:$ w r o n g包含猜错的字母的数量。如果这个数量至少是 1,那么第2 4行就使用 一个程序片来输出h a n g m a n数组,从位置0开始,直到猜错的字母的数量(减 1)。 第2 6 ~ 2 7行:这两行用于获得游戏玩主的猜测。 c h o m p()删除结尾处的换行符。 第2 8 ~ 3 0行:这几行用于搜索@ g u e s s e s,以了解玩主是否已经猜到该字母。如果他已经猜 到,就可以重新启动第1 3行的循环。如果玩主多次猜错,他不会受到处罚。 第3 2 ~ 3 8行:这是该程序的核心!搜索包含猜字母游戏的 @ l e t t e r s数组。如果在游戏中找 到了猜测的字母,那么对应的 @ b l a n k w o r d元素就被设置为该字母。数组 @ b l a n k w o r d既包含猜 对的字母,也包含任何特定元素中的 u n d e f。称为$ r i g h t的标志被设置为1,表示至少有一个字 母已经被找到。 第3 9行:除非游戏的玩主猜到了一个字母,否则 $ w r o n g被递增。 第4 0 ~ 4 3行:数组@ b l a n k w o r d的各个元素被连接在一起,构成一个字符串,并与原始的 要猜测的字符串相比较。如果它们完全相同,表示游戏的玩主猜到了所有的字母。 50使用第一部分 Perl 基础 下载第4 5行:玩主没有能够猜对字母,解释程序放弃了从第 1 3行开始的循环。这一行输出一 条安慰玩主的消息,然后退出游戏。 程序清单4 - 2显示了H a n g m a n程序的输出的一个示例。 程序清单4-2 Hangman程序的输出示例 这个游戏对本学时中介绍的大部分概念进行了操作练习,这些概念包括直接量列表、数 组、 s p l i t、j o i n、上下文和 f o r e a c h循环等。你可以使用各种各样的方法来实现这个小型 H a n g m a n游戏程序,不过希望你能够掌握数组具备的一些基本功能。 4.5 课时小结 数组和列表是P e r l的集合变量。你可以使用它们来存放数量不受限制的标量,并且既可以 对它们进行整体操作,也可以将它们作为单个元素来进行操作。 P e r l提供了许多使用非常方便 的机制,可以用来对数组进行拷贝、排序、组合,以及在标量与数组的数据之间来回进行转 换。另外,P e r l的许多运算符和函数对它们所在的上下文是非常敏感的,它们的运行特性将根 据它们是在标量上下文中还是列表上下文中而各有不同。 4.6 课外作业 4.6.1 专家答疑 问题:你能告诉我一种在数组元素中快速找到特定字符串的方法吗? 解答:迭代运行该数组,并查看每个元素,这是进行这项操作的常用方法。如果你经常 发现你正在搜索一个数组,以了解某个元素是否在该数组中,那么不要首先将数据存储在一 个数组中。随机访问元素的一种更加有效的结构是哈希结构,这将在第 7学时中介绍。 问题:如何消除数组中的重复元素?如何计算数组中的各个元素的数目?如何来了解两 个数组是包含相同的还是不同的元素? 解答:这几个问题的答案都是一样的,那就是使用哈希结构。使用哈希结构,你就能够 非常迅速而有效地对数组进行某些有意思的操作。所有这些问题将在第 7学时中回答。 4.6.2 思考题 1) 如果要将$ a和$ b这两个标量变量中包含的值进行交换,哪个方法最有效? a. $a=$b; b. ($ a,$ b)=($ b,$ a); c. $c=$a;$ a = $ b;$ b = $ c; 第4学时 基本构件的堆栈:列表与数组使用51下载2) 语句$ a = s c a l a r(@ a r r a y);将什么赋值给变量$ a? a. @array中的元素的数量; b. @array的最后一个元素的索引; c. 该语句无效。 4.6.3 解答 1) 答案是b。第一个选择显然不能成立,因为 $ a中包含的值将被撤消。 c这个选择虽然回 答了这个问题,但是它要求数据交换时用第三个变量来存储数据。 b这个选择能够正确地进行 数据的交换,它不使用额外的变量,并且是个非常清楚的代码。 2) 答案是a。使用标量上下文中的数组能够返回数组中的元素数量。 $ # a r r a y将返回数组的 最后一个索引。在这个例子中使用 s c a l a r()是不必要的,在赋值运算符的左边有一个标量, 就足以将@ a r r a y放入一个标量上下文中了。 4.6.4 实习 • 修改H a n g m a n游戏程序,以便用竖向位置来输出 h a n g m a n。 52使用第一部分 Perl 基础 下载下载 第5学时 进行文件操作 到现在为止,我们介绍的 P e r l程序都是独立的程序。除了向用户提供消息和接收来自键盘 的输入信息外,它们无法与外界进行通信。这种状况将要改变。 P e r l是一种能够进行文件输入和输出(文件 I / O)的非常出色的语言。P e r l的标量能够延伸, 以便将尽可能长的记录存放在文件中,另外,P e r l的数组能够扩展,以便存放文件的全部内容, 当然这必须是在内存允许的情况下才能做到。当数据包含在 P e r l的标量和数组中时,可以对该 数据进行不受限制的操作,并且可以编写新的文件。 当你读取数据或者将数据写入文件时, P e r l总是尽量设法不妨碍你的操作。在某些地方, P e r l的内置语句甚至进行了优化,以便执行常用类型的 I / O操作。 在本学时中,我们将要介绍 P e r l怎样使你能够访问文件中你可以使用的所有数据。 在本学时中,你将要学习: • 如何打开和关闭文件。 • 如果将数据写入文件。 • 如何从文件中读取数据。 • 如何使你编写的P e r l程序具备保护功能,从而使其更加强大。 5.1 打开文件 若要在P e r l中读取文件或写文件,必须打开一个文件句柄。 P e r l中的文件句柄实际上是另 一种类型的变量,它们可以作为在你的程序与操作系统之间对某个特定文件使用的非常方便 的一个引用(即句柄,如果你愿意这样说的话)。句柄包含了关于如何打开文件和你在文件中 读(或写)到了什么位置等信息。它们还包含了用户定义的关于如何读写文件的属性。 在前面的课程中,你已经熟悉了一个句柄,即 S T D I N。该句柄是在启动程序时 P e r l自动赋 予你的,它通常与键盘设备相连接(后面还要更加详细地介绍 S T D I N)。文件句柄名字的格式 与第2学时介绍的变量名基本相同,不同之处是句柄的名字前面没有类型标识符( $、@)。由 于这个原因,句柄名字最好使用大写字母,这样就不会与 P e r l的当前和将来的保留字 f o r e a c h、 e l s e和i f等发生冲突。 也可以将字符串标量或能够返回字符串的任何东西(如函数)用作文件 句柄名。这种类型的句柄名称为间接句柄。描述它们的用法会给 P e r l的初学 者造成一些混乱。关于间接句柄的详细说明,请参见 p e r l f u n c手册页中关于 o p e n的在线文档。 每当需要访问磁盘上的文件时,必须创建一个新的文件句柄,并且打开该文件句柄,进 行相应的准备。当然必须使用 o p e n函数来打开文件句柄。O p e n的句法如下: open (filehandle, pathname) o p e n函数将文件句柄作为它的第一个参数,将路径名作为第二个参数。路径名用于指明要打开哪个文件,因此,如果没有设定完整的路径名,比如 c : / w i n d o w s / s y s t e m /,那么o p e n函 数将设法打开当前目录中的文件。如果 o p e n函数运行成功,它将返回一个非 0值。如果o p e n函 数运行失败,它返回u n d e f(假): 在上面这个代码段中,如果 o p e n运行成功,它计算得出的值是真,而 i f代码块则用打开的 文件句柄M Y F I L E来运行。否则,文件不能打开,代码的 e l s e部分开始运行,这表示出现了错 误。在许多P e r l程序中,这个“打开或失败”语句是使用 d i e函数来编写的。 d i e函数用于停止 P e r l程序的执行,并且输出下面这个出错消息: 在这个消息中,s c r i p t n a m e是P e r l程序的名字,x x x是遇到d i e的行号。d i e和o p e n这两个函 数常常以下面的形式同时出现: 这一行代码可以读作“打开或撤消”,它有时表示你想要让程序如何处理没有打开的文件。 如果o p e n运行没有成功,也就是说它返回 FA L S E,那么逻辑 O R(| |)必须计算右边的参数 (d i e);如果o p e n运行成功了,也就是说它返回 T R U E,那么就不要计算d i e的值。这个习惯用 语也可以用逻辑O R的另一个符号o r来书写。 当你完成文件句柄的操作后,将文件句柄关闭,这是个很好的编程习惯。关闭文件句柄 的操作,将通知操作系统说,该文件句柄可以重复使用,同时,尚未为文件句柄写入的数据 现在可以写入磁盘。另外,你的操作系统只允许打开规定数量的文件句柄,如果超过这个数 量,你就不能打开更多的文件句柄,除非关闭某些句柄。若要关闭文件句柄,可以使用下面 这个c l o s e函数: 如果文件句柄名字重复使用,即另一个文件用相同的文件句柄名字打开,那么原始文件 句柄将先被关闭,然后重新打开。 5.1.1 路径名 到现在为止,我们只是用类似 n o v e l . t x t的名字来打开文件。当试图打开没有设定目录名的 文件名时,P e r l假定该文件是在当前目录中。若要打开位于另一个目录中的文件,必须使用路 径名。路径名用于描述P e r l为了打开系统中的文件而必须使用的路径。 若要设定路径名,可以使用你的操作系统期望的方式来设定,如下面这个例子所示: 在Wi n d o w s和M S - D O S系统下,设定P e r l中的路径名时可以使用反斜杠作为路径名分隔符, 比如\ Wi n d o w s \ u s e r \ p i e r c e \ n o v e l . t x t。需要注意的惟一的问题是:当在带有双引号字符串中使 用反斜杠分隔符路径名时,反斜杠字符序列将被转换成一个特殊的字符。请看下面的例子: 54使用第一部分 Perl 基础 下载这个例子可能运行失败,因为双引号字符串中的 \ n是个换行符,而不是字母 n,而且其他 的所有反斜杠将被P e r l悄悄地删除。下面是打开文件的正确方法: 为了使这行代码看是去更好一些,也可以使用 Wi n d o w s和M S - D O S中的正斜杠( /)来分 隔路径中的各个元素。Wi n d o w s和D O S能够正确地对它们进行转换,请看下面的代码: 你设定的路径名可以是绝对路径名,例如 U N I X中的/ h o m e / f o o或Wi n d o w s中的c : / w i n d o w s / w i n . i n i,也可以是相对路径名,如 U N I X中的. . / j u n k f i l e或Wi n d o w s中的. . / b o b d i r / b o b s f i l e . t x t。 o p e n函数也能够接受Microsoft Wi n d o w s下的通用命名约定(U N C)路径名。U N C路径名的格 式如下: P e r l能够接受使用反斜杠或正斜杠的 U N C路径名,如果你的操作系统的网络和文件共享特 性的设置正确的话,它能打开远程系统上的文件,请看下面的例子: 在M a c i n t o s h计算机上,路径名是按卷、文件夹,然后文件这样的顺序来设定的,各个元 素之间用冒号分开,如表5 - 1所示。 表5-1 MacPerl路径名的说明符 M a c i n t o s h路径 含 义 S y s t e m : U t i l s : c o n f i g 系统驱动器,文件夹实用程序,文件名 c o n f i g M y S t u ff : f r i e n d s 从该文件夹向下到文件夹 M y S t u ff,文件名f r i e n d s S h o p p i n g L i s t 该驱动器,该文件夹,文件名 S h o p p i n g L i s t 5.1.2 出色的防错措施 在计算机上进行编程肯定会产生一种自我陶醉的感觉。程序员会说:“这次程序一定能够 运行,”或者说:“我已经找到了所有的错误。”你在工作中产生这种自豪感在某种意义上讲当 然是很好的,对程序的改进往往是出于这样一种信念,即你能够做到似乎不可能的事情。不 过,你在编程过程中要切忌盲目自信。 自从计算机最初使用程序以来,人们就产生了这种看法。 F r e d r i c k P. B r o o k s曾经在他的经典著作“ Mythical Man-Month”中说:“所有程序员都 是乐观主义者。但是,由于我们的思路常常出错,因此在编程的时候总是会 犯错误,所以我们的乐观主义是没有理由的。” 到现在为止,你看到的所有代码段和程序练习都是用来处理内部数据(对数字进行因子 分解,对数据进行排序等等)的,用户输入的信息很简单。当你进行文件的处理时,你的程 序就要与外部信息源进行交流,而这些程序是无法控制外部信息源的。当你与不是在你的计 算机上的数据源(比如网络上的数据)进行通信时,情况也是如此。请记住,如果某个地方 可能出现错误,那么它就可能出错,因此你应该根据情况来编写程序。用这种方法来编写程 序称为防错性编程,如果你进行防错编程,那么从长远来说你会感到更加乐观。 第5学时 进行文件操作使用55下载每当程序要与外界进行交互操作时,比如打开一个文件句柄,始终必须确保操作成功之 后才能进行下一步操作。有人调试了上百个程序,在这些程序中,程序员要求操作系统执行 某项操作,但是却不检查结果,因此就导致程序的错误。甚至当你的程序只是个“程序示例”, 或者是个“简易程序”时,也应该进行程序检查,以确保程序能够产生你期望的结果。 5.1.3 以适当的方式运行die函数 在P e r l中,d i e函数可以用来在出现错误的时候停止解释程序的运行,并输出一条有意义的 出错消息。正如你在前面已经看到的那样,只要调用 d i e函数,就能够输出类似下面的消息: d i e函数也可以带有一系列的参数,这些参数将取代默认消息而被输出。如果消息的后面 没有换行符,那么消息的结尾就附有 at scriptname line xxx字样: P e r l中有一个特殊的变量 $ !,它总是设置为系统需要的最后一个操作(比如磁盘输入或输 出)的出错消息。当 $ !用于数字上下文时,它返回一个错误号,这个号可能对任何人都没有 什么用处。在字符串上下文中, $ !返回来自你的操作系统的相应的出错消息: 如果由于文件不存在,上面这个代码段中的代码运行失败,那么输出的消息将类似 c a n n o t open myfile : a file or directory in the path does not exist,这个出错消息很好。在你的程序中, 好的出错消息应该能够指明什么错了,为什么出错,你想怎么办。如果程序的某个方面出错 了,那么好的诊断程序能够帮助你找出问题的所在。 不要使用$ !的值来检查系统函数的运行是失败还是成功。只有当系统执 行一项操作(比如文件输入或输出)之后, $ !才有意义,并且只有在该操作 运行失败后, $ !才被设置。在其他时间中, $ !的值几乎可以是任何东西,并 且是毫无意义的。 不过有时并不想使程序停止运行,只是想要发出一个警告。若要创建这样的警告, P e r l有 一个w a r n函数可供使用。 w a r n的运行方式与 d i e完全一样,你可以从下面这个代码中看出来, 不过差别是它的程序将保持运行状态: 5.2 读取文件 可以使用两种不同的方法读取 P e r l的文件句柄。最常用的方法是使用文件输入运算符,也 叫做尖括号运算符(< >)。若要读取文件句柄,只需要将文件句柄放入尖括号运算符中,并将 该值赋予一个变量: 56使用第一部分 Perl 基础 下载标量变量中的尖括号运算符能够读取来自文件的一行输入。当该文件被读完时,尖括号 返回值u n d e f。 “一行输入”通常是指发现第一个行尾序列之前的文本流。在 U N I X中, 行尾是一个换行符( ASCII 10);在D O S和Wi n d o w s中,它是回车符与换行 符的序列(ASCII 13、1 0)。这个默认行尾值可以被 P e r l进行操作,产生某些 有趣的结果。这个问题将在第 1 2学时中介绍。 若要读取和输出整个文件,那么如果 M Y F I L E是个开放的文件句柄,你可以使用下面的代 码: 结果表明,读取文件句柄的快捷方式是使用 w h i l e循环。如果尖括号运算符是 w h i l e循环的 条件表达式中的唯一元素,那么 P e r l将自动把输入行赋予该特殊变量 $ _,并重复运行该循环, 直到输入耗尽为止: w h i l e循环将负责把输入行赋予 $ _,并确保文件句柄尚未耗尽(称为文件结束)。这个奇 特的运行特性只有在 w h i l e循环中才能发生,并且只有在尖括号运算符是条件表达式中的唯一 表达式时才能发生。 请记住,在 P e r l中用文件句柄读取的所有数据中,除了包含文件行中的 文本外,还包含行尾字符。如果只需要文本,请在输入行上使用 c h o m p,以 便舍弃行尾字符。 在列表上下文中,尖括号运算符能够读取整个文件,并将它赋予该列表。文件的每一行 被赋予列表或数组的每个元素,如下所示: 在上面这个代码段中,文件句柄 M Y F I L E中的剩余数据也被读取并赋予 @ c o n t e n t s。文件 n o v e l . t x t的第一行被赋予 @contents:$contents [0] 的第一个元素。第二行被赋予 $content [1], 如此等等。 在大多数情况下,将整个文件读入一个数组(如果文件不是太大),是处理文件数据的一 种非常容易的方法。你可以来回通过该数组,对数组元素进行操作,并可使用数组和标量的 所有运算符来处理该数组的内容,而不必担心会出现什么问题,因为你实际上只是对数组中 的一个文件拷贝进行操作。程序清单 5 - 1显示了对内存中的文件可能进行的某些操作。 程序清单5-1 对文件进行倒序 第5学时 进行文件操作使用57下载如果该文件的测试文件包含文本 I am the very model of a modern major-general, 那么程序 清单5 - 1中的程序将产生下面的输出: 第1行:这一行包含到达解释程序的路径(可以修改它,使之适合你的系统的需要)和开 关- w。请始终使警告特性处于激活状态! 第3行:使用文件句柄 F H打开文件的测试文件。如果该文件没有正确地打开, d i e函数便 开始运行,并产生一个出错消息。 第4行:测试文件的整个内容被读入数组 @ s t u ff。 第7行:数组@ s t u ff被倒序,第1行变成最后一行,依次类推。 f o r e a c h语句遍历产生的列 表。倒序后的列表的每一行被赋予 $ _,同时f o r e a c h循环体被执行。 第8行:列表的每一行(现在位于 $ _中)本身也进行倒序,由从左到右,变为从右到左, 并且输出倒序后的每一行。它需要使用 s c a l a r函数,因为p r i n t期望一个列表。另外,在列表上 下文中, r e v e r s e用于对列表进行倒序,这样, $ _就不会发生任何问题。 s c a l a r函数将强制 r e v e r s e进入标量上下文中,同时它按逐个字符对 $ _进行倒序。 只有对小型文件,才能将整个文件读入数组变量,以便进行操作处理。如果要将非常大 的文件读入内存,虽然是允许的,但是可能导致 P e r l占用系统中的全部可用内存。 如果将太大的文件读入内存,或者进行其他的一些操作,从而使你占用的内存超出了 P e r l 能够使用的内存,P e r l就会显示下面这条出错消息: 这时你的程序就会终止运行。如果当一次性地将整个文件读入内存时出现了这种情况, 你应该考虑每次读入文件的一行。 5.3 写入文件 若要将数据写入文件,首先必须有一个打开的文件句柄,以便进行写入操作。打开一个 文件以便进行写入操作的句法如下: 第一个语句行你应该是熟悉的,不过在路径名的前面有一个 >。>符号用于告诉 P e r l, p a t h n a m e设定的文件应该用新数据改写,而现有的任何数据都应该删除,同时 f i l e h a n d l e是打 开的,可以用于写入。在第二个例子中, > >告诉P e r l打开该文件,以便进行写入操作,但是, 如果文件存在,那么将数据附加到该文件的结尾处。现在请看下面这些例子: 58使用第一部分 Perl 基础 下载到现在为止,你的 Pe r l程序几乎还不可能对任何东西造成损害。现在, 你已经知道如何将数据写入文件,那么必须非常注意只能对你想要修改的文 件进行写入操作。在操作系统文件非常容易受到损坏的系统(Windows 95/98、 M a c)上,如果不小心将数据写入文件,那么就会损坏你的操作系统。你应 该非常清楚要将数据写入什么文件。要想对不小心用 >打开的文件中的数据 进行还原,那几乎是不可能的。想要清除你不小心用 > >打开的文件中的数据, 那也是困难的,因此你要格外小心。 当完成了对打开的文件句柄的写入操作后,关闭该文件句柄是特别重要的。当你对文件 进行写入操作时,操作系统并不将数据存放到磁盘,它只是将数据放入缓存,然后随时进行 写入操作。c l o s e函数通知操作系统说,你已经完成写入操作,数据应该移到磁盘上的永久性 存储器中: 当你打开了用于写入操作的文件句柄时,将数据写入文件实际上是非常容易的,因为你 已经熟悉p r i n t函数。到现在为止,你一直使用 p r i n t函数将数据显示在屏幕上。 p r i n t函数实际 上可以用于将数据写入任何文件句柄。将数据写入文件句柄的句法如下: f i l e h a n d l e是要将数据写入到的文件句柄, L I S T是要写入的数据的列表。 在p r i n t语句中,请注意文件句柄名与列表之间没有逗号,这一点很重要。在列表中,逗 号用于分隔各个项目,迄今为止一直是这样做的。如果在文件句柄与列表之间没有逗号,那 么P e r l就会知道p r i n t后面的标记是个文件句柄,而不是列表中的文件元素。如果忘记使用这个 逗号,但是打开了 P e r l的警告特性,那么 P e r l就会向你发出下面这条警告消息: No comma allowed after filehandle(文件句柄后面不允许有逗号)。 现在请看下面这个代码: 在上面这个代码段中,名字为 l o g f i l e的文件被打开,以便进行附加操作。 p r i n t语句将一条 消息写入L O G F文件句柄。p r i n t的返回值被检查核实,如果记录项无法输出,便发出警告消息。 然后文件句柄被关闭。 可以同时打开多个文件句柄,以便进行读取和写入操作,正如下面这个代码段显示的那 样: 上面这个代码段实现了一个简单的文件拷贝。实际上同时进行读取和写入操作可以将例 程缩短一些: 第5学时 进行文件操作使用59下载由于p r i n t函数希望有一个列表作为其参数,因此 < S O U R C E >是在列表上下文中计算的。 当尖括号运算符在列表上下文中进行计算时,整个文件将被读取,然后输出到文件句柄 D E S T。 5.4 自由文件、测试文件和二进制数据 文件和文件系统不一定只包含你写入文件的数据。有时文件句柄代表的不仅仅是个简单 文件。例如,文件句柄可以被附加给键盘设备、网络套接字和大容量存储设备,如磁带驱动 器。 另外,文件系统也可以包含所谓的关于你的文件的元数据。 P e r l可以用来查询文件系统, 以便确定你的文件究竟有多大,上次修改文件的时间,谁修改了这个文件,以及文件中究竟 有些什么信息等。在有些操作系统中,文件的元数据甚至能够确定文件是作为文本文件还是 二进制文件来处理。 5.4.1 自由文件句柄 最初,P e r l是个U N I X实用程序,有时,甚至在非U N I X平台上也会出现它的许多蛛丝马迹。 当你的P e r l程序启动运行时,它会得到3个自动打开的文件句柄。它们是S T D O U T(标准输出)、 S T D I N(标准输入)和S T D E R R(标准错误)。按照默认设置,它们均与你的终端相连接。 当你键入数据时,P e r l能够读取来自S T D I N文件句柄的输入: 当想要显示输出信息时,可以使用 p r i n t函数。按照默认设置, p r i n t使用S T D O U T文件句 柄: 在第1 2学时中,我们将要介绍如何改变 p r i n t的默认文件句柄。 S T D E R R通常也可以设置为你的终端,但是它用于显示出错消息。在 U N I X中,出错消息 和正常的输出可以发送到不同的显示设备,并且,将出错消息写入 S T D E R R文件句柄是一种 传统的做法。d i e和w a r n函数都能够将它们的消息写入 S T D E R R。如果你的操作系统没有单独 的报告错误的文件句柄,比如像 Wi n d o w s或D O S那样,那么S T D E R R输出将被送往 S T D O U T 设备。 在U N I X中将出错消息和输出信息送往其他设备的问题不在本书讲解的范 围之内,而且这方面的技术将根据你使用的 s h e l l程序而各有差别。凡是介绍 U N I X的好的著作都会全面介绍这方面的内容。 5.4.2 二进制文件 有些操作系统、比如V M S、Atari ST,尤其是Wi n d o w s和D O S,都对二进制文件(原始文 件)与文本文件进行了区分。这种区分产生了许多的问题,因为P e r l无法知道它们之间的差别, 并且你也不希望它进行这样的区分。文本文件只不过是以行尾字符(称为记录分隔符)结束的记 60使用第一部分 Perl 基础 下载录。二进制文件是指需要进行内部转换的许多信息位的集合,如映像、程序和数据文件等。 当你将数据写入一个文本文件时, P e r l将\ n字符序列转换成你的操作系统使用的记录分隔 符。在U N I X中,\ n变成一个ASCII 10(L F);在M a c i n t o s h上,\ n转换成ASCII 13(C R); 在D O S和Wi n d o w s系统上,它变成序列 ASCII 13和ASCII 10(C R L F)。当你写入文本时,这 个运行特性是合适的。 当写入二进制数据即 G I F文件、E X E文件或MS word文档时,这种转换是你所不想要的。 每当你真的想要写入二进制数据并且不希望 P e r l或操作系统对它进行转换时,你必须使用 b i n m o d e函数,将文件句柄标记为二进制文件。应该在文件句柄打开之后和对它进行输入输出 之前使用b i n m o d e: 只能对文件句柄使用一次 b i n m o d e函数,除非将它关闭后再重新打开。如果在不能区分二 进制文件和文本文件的系统( U N I X或M a c i n t o s h)上使用b i n m o d e函数,不会造成任何危害。 5.4.3 文件测试运算符 在打开文件之前,有时应该了解一下该文件是否存在,或者该文件是否是个目录,或者 打开文件是否会产生一个 permission denied(不允许访问)的错误。对于这些情况, P e r l提供 了文件测试运算符。这个文件测试运算符的句法如下: 这里的x是指你想进行的特定测试,而 f i l e h a n d l e是你想要测试的文件句柄。也可以在不打 开文件句柄的情况下测试一个 p a t h n a m e。表5 - 2列出了一些运算符。 表5-2 部分文件测试运算符一览表 运 算 符 举 例 结 果 - r - r‘f i l e’ 如果可以读取‘f i l e’,则返回真 - w -w $a 如果$ a中包含的文件名是可以写入的文件名,则返回真 - e - e‘m y f i l e’ 如果‘m y f i l e’存在,则返回真 - z - z‘d a t a’ 如果‘d a t a’存在,但是它是空的,则返回真 - s - s‘d a t a’ 如果‘d a t a’存在,则返回‘d a t a’的大小,以字节为计 量单位 - f - f‘n o v e l . t x t’ 如果‘n o v e l . t x t’是个普通文件,则返回真 - d - d‘/ t m p’ 如果‘/ t m p’是个目录,则返回真 - T - T‘u n k n o w n’ 如果‘u n k n o w n’显示为一个文本文件,则返回真 - B - B‘u n k n o w n’ 如果‘u n k n o w n’显示为一个二进制文件,则返回真 - M - M‘f o o’ 返回程序启动运行以来‘ f o o’文件被修改后经过的时间 (以天数计算) 可以通过在线文档来查看文件测试运算符的完整列表。在命令提示符处键入 p e r l d o c p e r l f u n c,查看“Alphabetical List of Perl Functions”(按字母顺序排列的P e r l函数列表)这一 节的内容。 文件测试运算符可以像下面这样在改写文件之前用于检查该文件是否存在,或者用于核 第5学时 进行文件操作使用61下载实用户的输入信息,也可以用于确定需要的目录是否存在以及是否可以进行写入操作: 5.5 课时小结 本学时我们介绍了如何在 p e r l中打开和关闭文件句柄。可以使用 o p e n函数来打开文件,使 用c l o s e函数来关闭文件。当文件句柄打开时,它们可以使用 < >或r e a d函数进行读取,使用 p r i n t进行写入操作。另外,我们还介绍了你的操作系统处理文件时使用的一些特殊方法,以 及如何使用b i n m o d e来处理文件。 此外,希望你也了解到了一些关于防错编程的方法。 5.6 课外作业 5.6.1 专家答疑 问题:我的o p e n语句经常出错,我不知道究竟问题何在。请问问题究竟在哪里? 解答:首先,请检查 o p e n语句的句法是否正确。应该确保打开的是正确的文件名。如果 希望更加有把握的话,请在 o p e n的前面放上文件名。如果打算写入文件,请务必在文件名的 前面加上一个>,你必须这样做。最重要的是,你有没有使用 o p e n()|| die “$ !”;语句来检 查o p e n的退出状态呢?d i e消息在帮助你查找错误方面是非常重要的。 问题:我对文件进行写入操作,可是什么也写不进去。我的输出数据到哪里去了呢? 解答:你的文件句柄正确打开了吗?如果使用了错误的文件名,你的数据就会送往别的 文件。常见的错误是在路径名中使用反斜杠并且用双引号将路径名括起来,以便打开文件进 行写入操作,如下所示: 这行代码创建了一个文件,称为 c :(t a b)e m p ( n e w l i n e ) o t e s . t x t,也许这不是你想要创建的 文件。另外,你应该确保 o p e n函数运行成功。除非你激活了 P e r l的警告特性,否则,如果你将 数据写入一个尚未正确打开的文件, P e r l就会悄悄地删除输出数据。 问题:当我试图打开一个文件时, o p e n运行失败了,p e r l报告说permission denied(不允 许访问)。为什么? 解答:P e r l要遵守你的操作系统的文件安全性规则。如果你不拥有对文件、目录或者文件 所在驱动器的访问权,那么 P e r l也无权访问它们。 问题:我如何才能一次读取一个字符? 解答:P e r l的函数g e t c能够从文件读取单字符输入。从键盘读取单字符就比较困难,因为 一次输入一个字符要取决于你的操作系统。当你学习了第 1 5学时中关于模块的内容后,并且 读到第1 6学时中的专家答疑时,请查看里面的有关说明。在那里的专家答疑中,详细介绍了 如何从各种不同的平台读取单个字符的方法,并且配有许多代码举例。大多数代码不属于本 62使用第一部分 Perl 基础 下载书讲解的范围。 问题:如何才能使其他程序能够同时将数据写入同一个文件? 解答:你所说的问题称为文件锁定。文件锁定将在第 1 5学时中介绍。应该说明的是,这 项操作并不太容易,也不是太难。 5.6.2 思考题 1) 为了打开一个名叫d a t a的文件,以便写入数据,你应该使用下面的哪个代码: a. open(F H,“d a t a”,w r i t e); b. open(F H,“d a t a”);并且只是输出到F H c. open(F H,“> d a t a”)|| die “Cannot open data: $!”; 2) (-M $file > 1 and -s $file)是真,如果 a. $file已经在几天以前被修改,并且里面有数据。 b. 该表达式不可能是真。 c. $file可以写入,并且里面没有数据。 5.6.3 解答 1) 答案是c。选择a是不能成立的,因为这不是 o p e n如何打开一个文件以便进行写入操作 的问题;选择b也是不行的,因为它打开的文件句柄只能读取。 c 是正确的,因为它才符合你 的要求,并且使用正确的形式来进行错误检查。 2) 答案是a。- M返回文件已经存在的天数(> 1是指一天以上),如果文件里面有数据,则- s 返回真。 5.6.4 实习 • 修改第4学时中的H a n g m a n程序,以便从数据文件中取出可能的单词的列表。 第5学时 进行文件操作使用63下载下载 第6学时 模 式 匹 配 在上个学时中,我们介绍了如何从文件中读取数据的方法。懂得这个方法后,再加上标 量、数组和运算符方面的知识,就可以准备对该数据进行操作,以便做你想要做的任何事情。 有时,文件中的数据没有采用便于使用的格式进行格式化,它不能使用简单的 s p l i t函数对数 据进行分割,或者有的文件行包含了你不感兴趣的数据,你想通过编辑将它删除。 你必须具备一种能力,来识别输入数据流中的模式。根据这些模式来选择数据,并且对 数据进行编辑,使之变成比较容易使用的格式。 P e r l的工具中有一个工具可以用来执行这项任 务,即正则表达式。在本学时的课文中,正则表达式与模式几乎可以互换使用。 正则表达式本身几乎是一种语言,它是用于描述要匹配模式的正式方法。在本学时中, 我们将要介绍一些关于这种模式匹配的语言。 在线文档对 P e r l使用的完整的正则表达式语言进行了更加深入的(但是 比较扼要的)描述。你可以查看 P e r l中包含的p e r l r e文档。这个问题涉及的内 容非常广泛,因此有人出版了整整一本书来介绍正则表达式。 P e r l界大力推 荐的这本书名叫“ Mastering Regular Expressions”,它是由 J e ffery E.F. F r i e d l撰写的, 1 9 9 7年出版。它全面介绍了正则表达式,并且对 P e r l给予了 很大的关注。 正则表达式也可以用于其他编程语言,如 T C L、J a v a S c r i p t、P a t h o n和C语言。U N I X操作 系统的许多实用程序也使用正则表达式。 P e r l正好配有一组非常丰富的正则表达式,这与其他 操作系统的情况非常相似,学习这些正则表达式不仅有助于你在 P e r l中的应用,而且可以用于 其他语言。在本学时中,你将学习: • 如何创建简单的正则表达式。 • 如何使用正则表达式进行模式匹配。 • 如何使用正则表达式来编辑字符串。 6.1 简单的模式 在P e r l中,模式被括在模式匹配运算符中间,有时该运算符采用 m / /的形式。下面就是一 种简单的模式: m / s i m o n / 上面这个模式依次与字母 S - i - m - o - n相匹配。但是究竟在什么地方才能找到 S i m o n呢?以前 我们讲过,P e r l变量$ _常常用于P e r l需要默认值的时候。模式匹配是根据 $ _来进行的,除非你 告诉P e r l用别的方式来进行匹配(后面我们将要介绍)。因此前面的模式可以在标量变量 $ _中 寻找S - i - m - o - n。 如果由m / /规定的模式可以在变量 $ _中的任何地方找到,那么匹配运算符返回真。这样,能够看到匹配模式的正常位置是在条件表达式中,如下所示: 在这个模式中,除非字符是个元字符,否则每个字符均与自己相匹配。大多数“标准” 字符均与自己相匹配,这些字符包括 A至Z、a至z和数字。元字符是指改变了模式匹配运行特 性的那些字符。下面是元字符的列表: 下面我们很快就要介绍元字符能够做些什么。在你的模式中,如果想要匹配元字符的原 义值,只需要在元字符的前面加上一个反斜杠即可,如下所示: 前面我们已经讲过模式匹配运算符通常用 m / /来表示。实际上,可以用你想要的任何其他 字符来代替斜杠,如下面这个例子所示: 在许多情况下,当模式中包含斜杠( /)时且模式的结尾则可能与模式内的斜杠相混淆, 可用另一个字符来代替它,因此括号里面的斜杠的前面必须加上反斜杠,如下所示: 可以编写下面这个代码,使上面的代码更加容易阅读: 如果将模式括起来的字符(称为界限符)是斜杠,那么编写模式匹配代码时也可以不带 m。 因此,也可以将 m / C h e e t o s写成/ C h e e t o s /。通常情况下,除非需要使用不是斜杠( / /)的其他 界限符,否则,可以只使用斜杠而不使用 m来编写模式匹配代码。 变量也可以用在正则表达式中。如果在正则表达式中看到一个标量变量, P e r l首先计算该 标量,然后查看正则表达式。这个功能使你能够动态地创建正则表达式。下面这个 i f语句中的 正则表达式是根据用户输入创建的: 联机手册页和其他文档中的正则表达式有时称为 R E或r e g e x p。为了清楚 起见,在本书中将继续将它们称为正则表达式。 匹配的规则 当你开始在P e r l中编写正则表达式时,应该知道它必须遵循几条规则。不过,规则并不多, 大多数规则在你理解它们之后才具有更大的意义。这些规则是: • 通常情况下,模式匹配从目标字符串的左边开始,然后逐步向右边进行匹配。 • 如果并且只有当整个模式能够用于与目标字符串相匹配时,模式匹配才返回真(在任何 第6学时 模 式 匹 配使用65下载上下文中均如此)。 • 目标字符串中第一个能够匹配的字符串首先进行匹配。正则表达式不会漏掉某一个能够 匹配的字符串,而去寻找另一个更远的字符串来进行匹配。 • 进行第一次最大字符数量的匹配。你的正则表达式可能迅速找到一个匹配的模式,然后 设法尽可能延伸能够匹配的字符范围。正则表达式是“贪婪的”,也就是说,它会尽可 能多地寻找能够匹配的字符。 6.2 元字符 在下面的所有例子中,与模式相匹配的文本部分用下划线标出。请记住,即使目标字符 串中只有一部分与正则表达式相匹配,整个目标字符串也可以说是匹配的。下划线标记用于 帮助说明究竟哪些部分是匹配的。 阅读下列各节内容是非常重要的,但是,如果有些内容你无法立即理解,请不要担心。 你很快就会理解的。这些元字符的应用将会有所明确。 6.2.1 一个简单的元字符 第一个元字符是圆点(.)。在正则表达式中,圆点用于匹配除了换行符外的任何单个字符。 例如,在模式/ p . t /中,‘.’用于匹配任何单个字符。这个模式用于匹配p o t、p a t、p i t、c a rp e t、 p yt h o n和p u p _ t e n t。‘.’要求存在一个字符,但是不能有更多的字符。因此,该模式不能与 a p t 相匹配(p与t之间没有任何字符),也不能与e x p e c t相匹配(p t之间的字符太多)。 6.2.2 非输出字符 前面我们讲过,若要将元字符纳入正则表达式,应该在字符前面加上一个反斜杠,使它 失去“元”的含义,如下所示: 当普通字符的前面加上反斜杠后,它就 变成了元字符。正如你在第 2学时中看到的 字符串那样,有些字符的前面加上反斜杠后, 它在字符串中就具备了特殊的含义。在正则 表达式中,所有这些字符几乎代表相同的值, 如表6 - 1所示: 6.2.3 通配符 到现在为止,模式中的所有字符与它们要匹配的目标字符串之间存在一种一对一的关系。 例如,在/ S i m o n /中,s与S匹配,i与i匹配,m与m匹配,等等。通配符是一种元字符,它告诉 正则表达式有多少字符需要匹配。通配符可以放在任何单个字符或一组字符的后面(后面我 们将要详细介绍这方面的内容)。 最简单的通配符是 +元字符。+用于使前面的字符与后面的字符至少匹配一次,也可以任 意次地进行匹配,并且仍然拥有匹配的表达式。因此, / d o + g /将能够与下面的字符串匹配: 66使用第一部分 Perl 基础 下载 表6-1 特殊字符 字 符 匹配的字符 \ n 换行符 \ r 回车符 \ t 制表符 \ f 换页符h o u n dd o g h o td o g d o o gie howser d o o o o o og d o o g 但是不能与下面的字符串匹配: b a d g e (因为没有o ) d o o f u s (因为没有g ) D o o g i e (因为D与d不同) pagoda (因为d、o和g的顺序不对) 与元字符+的作用类似的是 *。元字符*使得前面的字符可以进行 0次或多次匹配。换句话 说,模式/ t * /可以进行任意次的匹配,但是,如果没有匹配的字符存在,这也没有问题。因此, / c a r * t /将能够与下面的字符匹配: c a r te d c a t c a r r r t 但是不能与下面的字符匹配: c a r r o t (多了一个字符o ) c a r l (模式中的t不是可有可无的) c a a r t (多出来的a不能匹配) 下一个元字符是?。元字符?用于使前面的字符进行 0次或一次匹配(但是不能超过一次)。 因此,模式/ c ? o l a /用于对c进行匹配,如果 c存在的话。然后对 o、l和a进行匹配。实际上,该 模式可以对带有 o l a在内的任何字符串进行匹配。如果 o l a的前面是一个c,那么该字符串也是 匹配的。 元字符?与*之间的区别是:模式/ c ? o l a /可以匹配c o l a和o l a,但是不能与c c o l a匹配。多出 来的c需要进行两次匹配。模式 / c * o l a /可以匹配c o l a、o l a和c c o l a,因为c可以根据需要重复匹 配任意次,而不只是0次或一次。 如果对一个模式进行 0次、一次或许多次匹配不能满足你的需要,那么 P e r l允许根据你需 要的具体次数为你进行匹配,方法是使用花括号 { }。花括号的格式如下: p a t{n, m} 这里的n是匹配的最小次数,m是匹配的最大次数,p a t是你试图量化匹配的字符或字符组。 可以省略n,也可以省略m,但是不能同时省略n和m。请看下面这些例子: / x { 5,10}/ x至少出现5次,但是不超过1 0次。 / x { 9,} / x至少出现9次,也可能出现更多次。 / x { 0,4 } / x最多出现4次,也可能根本不出现。 / x { 8 } / x必须正好出现8次,不能多,也不能少。 正则表达式中常用的一个通配符是 . *。可以用它来匹配任何东西,通常是你感兴趣的其他 两样东西之间的任何东西。例如 / f i r s t . * l a s t /。这个模式设法匹配单词 f i r s t,再匹配它后面的任 何东西,然后匹配单词l a s t。请观察/ f i r s t . * l a s t /是如何匹配下面的字符串的: first then last 第6学时 模 式 匹 配使用67下载The good players get picked first, the bad last. T h e first shall be last, and the last shall be first. 请仔细观察上面第3行中的匹配过程。这个匹配过程首先从单词 f i r s t开始。接着对单词l a s t 进行匹配,然后继续进行匹配,直到第二次出现单词 l a s t。这时,通配符*遵循“匹配规则” 这一节中列出的第4条规则,即它要匹配数量最大的字符串,并且仍然要完成该匹配过程。许 多情况下,匹配数量最大的字符串并不是你想要进行的操作,因此 P e r l提供了另一个解决方案, 称为最小数量匹配,这在p e r l r e手册页中有详细的描述。 6.2.4 字符类 正则表达式中的另一个常用做法是要求匹配“这些字符中的任何字符”。如果你想要匹配 数字,那么能够编写一个匹配“ 0 ~ 9的任何数字”的模式将是非常好的。或者,如果你要搜索 一个名字列表,想要匹配Von Beethoven与von Beethoven,那么使用能够匹配“v或者V”的模 式,将对你是有帮助的。 P e r l的正则表达式拥有这样一个工具,它称为字符类。若要编写一个字符类,可以用方括 号[ ]将这些字符括起来。进行匹配时,字符类中的所有字符被视为单个字符。在一个字符类 中,可以设定字符的范围(在范围有意义的时候),方法是在上限与下限之间加一个连字符。 下面是一些例子: 字 符 类 说 明 [ a b c d e ] 用于匹配a、b、c、d或e中的任何一个字符 [ a - e ] 与上面相同。用于匹配a、b、c、d或e中的任何一个字符 G 用于匹配大写字母G或小写字母g [ 0 - 9 ] 用于匹配一个数字 [ 0 - 9 ] + 用于顺序匹配一个或多个数字 [ A - Z a - z ] { 5 } 用于匹配任何一组5个字母字符 [ *!@ # $ % & ( ) ] 用于匹配这些符号中的任何一个 最后一个例子非常有意思,因为在字符类中,大多数通配符会失去它们的“通配符性质”, 换句话说,它们的运行特性将类似其他任何一个普通字符。因此,* 实际上代表一个普通的* 字符。 如果插入记号(^)作为字符类中的第一个字符,该字符类将变为无效。也就是说,该字 符类可以匹配不在该字符类中的任何单个字符。如下面的例子所示: 由于]、^和-等字符都是字符类中的特殊字符,因此,如果按照字符的原义来匹配字符类中 的这些字符,就要使用某些规则。若要匹配字符类中的原义字符 ^,必须确保它不出现在字符 类中的第一个字符的位置上;若要匹配字符 ],你既必须将它放在字符类中的第一个字符位置 上,也可以在它的前面加上一个反斜杠(例如 /[abc\ ] ]/);若要将一个原义连字符( -)放在 字符类中,你只需要将它放在字符类中的第一个字符位置上,或者在它的前面放一个反斜杠。 P e r l包含了某些常用字符类的快捷方式。它们用反斜杠和通配符来表示,如表 6 - 2所示。 下面是一些例子: 68使用第一部分 Perl 基础 下载表6-2 特殊字符类 模 式 用 于 匹 配 \ w 一个单词字符,与[ a - z A - z 0 - 9 _ ]相同 \ W 一个非单词字符(与\ w相反) \ d 一个数字,与 [ 0 - 9 ]相同 \ D 一个非数字 \ s 一个白空间字符,与[ \ t \ f \ r \ n ]相同 \ S 一个非白空间字符 不过请注意,上面的最后一个例子不一定匹配一个单词,它也可以匹配一个前后是空格 的下划线。同时,并不是所有单词都可以被最后一个模式来匹配的,它们的前后必须加上白 空间,并且“ d o n’t”之类的单词不能匹配,因为它包含一个省字号。本学时的后面部分的 内容还要进一步介绍用于单词匹配的更好的模式。 6.2.5 分组和选择 有时在正则表达式中,你可能想要知道是否找到了一组模式中的任意一个模式。例如, 这个字符串包含d o g s还是c a t s?正则表达式对这个问题的解决办法称为选择。当可能的匹配项 之间用一个 | 字符隔开时,正则表达式中就出现了选择,如下例所示: 选择可能是非常有趣的,但是,当想要匹配许多类似的东西时,它也可能很麻烦。例如, 如果你想要匹配f r o g、b o g、l o g、f l o g或c l o g等单词时,可以试用/ f r o g | b o g | l o g | f l o g | c l o g /这个表 达式,不过它包含许多重复的选择操作。而你真正想要进行的操作只是比较字符串的第一部 分,如下所示: 上面这个例子不一定能够运行,因为 P e r l没有办法知道选择是你要进行匹配的一个对象, 而o g是要匹配的另一个对象。为了解决这个问题,可以使用 P e r l的正则表达式,用括号将模式 的各个部分组合起来,如下所示: 可以对括号进行嵌套,使一个组中包含另一个组。例如,也可以将上面的表达式写成 /(fr|b| (f|c) | ) og/。 在列表上下文中,匹配运算符返回括号中匹配的表达式的各个部分的一个列表。每个加 括号的值都是列表的返回值,如果模式不包含括号,则返回 1。请看下面这个例子: 在上面这个代码段中,该模式先对任意对象(作为一个组)进行匹配,然后对白空间进 行匹配,再对单词i s进行匹配,然后匹配更多的白空间,再对任意对象(也作为一个组)进行 匹配。这两个分组的表达式返回左边的列表,并赋予 $ f r u i t和$ c o l o r。 6.2.6 位置通配符 最后两个通配符(相信你可能认为通配符是没有止境的)是位置通配符。可以使用位置 第6学时 模 式 匹 配使用69下载通配符来告诉正则表达式,你要查找的模式的准确位置是在字符串的开头,还是在字符串的 结尾。 第一个位置通配符是插入记号( ^)。正则表达式开头的插入记号告诉正则表达式只匹配 一行开头的字符。例如,/ ^ v i d e o /只匹配单词v i d e o,如果它出现在一行的开头的话。 与它相对应的通配符是美元符号( $)。正则表达式结尾处的美元符号能够使模式只匹配 一行结尾的字符。例如/ e a r t h $ /用于匹配e a r t h ,不过它只能位于行尾。 模 式 作 用 / ^ H e l p / 只只匹配以H e l p开头的行 / ^ F r a n k l y. * d a r n $ / 只用于匹配以F r a n k l y开头和以d a r n结尾的行。它们中间的所有字符也进行匹配 / ^ h y s t e r i a $ / 只用于匹配只包含单词h y s t e r i a的行 / ^ $ / 只用于匹配一行的开头,紧接着匹配该行的结尾。它只用于匹配空行 / ^ / 只用于匹配带有开头字符的行(所有行)。/ $ /的作用也相同 6.3 替换 仅仅查找字符串中的模式和输入的信息行是不够的,有时也需要修改数据。方法之一 (当然不是惟一的方法)是使用替换运算符 s / / /。它的句法如下: 替换运算符用于默认搜索 $ _,找出searchpattern, 并且用r e p l a c e m e n t来替换整个匹配的正 则表达式。该运算符返回匹配的数量或进行替换的数量,如果没有进行任何匹配,则返回 0。 下面是一个例子: 在这个例子中,替换按照你希望的那样进行。单词 m i d d l e替换成e n d,i n替换成a t。但是i f 语句运行失败了,因为单词 a p a r t m e n t没有出现在$ _中,因此无法替换。 替换运算符也可以使用非斜杠( /)的界限符,使用的方法与匹配运算符相同。只需要直 接在s的后面加上你想要的界限符即可,如下所示: 6.4 练习:清除输入数据 当你试图更改数据的时候,常常会进行上面这个例子中的“盲目”替换,即替换是进行 了,但是不检查退出状态。更改数据是从用户或文件中取出没有完全按照你的要求进行格式 化的数据,然后对数据重新进行格式化。程序清单 6 - 1显示了一个例程,用于将你在地球上的 体重转换成月球上的体重,这可以展示数据操作时的情况。 使用文本编辑器,键入程序清单 6 - 2中的程序,并将它保存为 M o o n。务必按照第1学时中 的说明使该程序成为可执行程序。 当完成上述操作后,键入下面这个命令行,设法运行该程序: perl Moon 70使用第一部分 Perl 基础 下载程序清单6 - 1显示了某些输出示例。 程序清单6-1 月球体重转换程序的输出示例 程序清单6-2 你的月球体重转换程序 第1行:这一行包含到达解释程序的路径(可以修改该路径,使之适合你的系统的需要) 和开关- w。请始终使警告特性处于激活状态。 第3 ~ 4行:这几行用于提示用户输入他的体重,将输入的值赋予 $ _,并且用c h o m p命令删 除换行符。请记住,如果没有设定其他变量,那么 c h o m p将改成$ _。 第5行:模式/ ^ \ s + /对该行的开头的白空间进行匹配。它没有列出任何替换字符串,因此, 匹配该模式的$ _部分被删除了。 第7行:如果在用户的输入中发现了计量单位,那么该 i f代码块便删除该计量单位,并在 适当的情况下对它进行转换。 第8 ~ 9行:模式/ \s*(k g s ? | k i l o g r a m s ?)/ i对白空间进行匹配,然后对 k g或k i l o g r a m s(每个 元素的结尾都有一个选项 s)进行匹配。这意味着如果输入中包含 k g或k g(没有空格),那么 它就被删除。如果该模式被发现和删除了, $ _中留下的数据将与2 . 2相乘,或者转换成磅。 第11行:否则,从$ _中删除l b s或p o u n d s(并删除前导白空间)。 第1 4行:$ _中的重量(已经转换成磅)乘以 1 / 6,然后输出。 6.5 关于模式匹配的其他问题 现在你已经能够对 $ _进行模式匹配,并且懂得替换的基本概念,因此可以学习更多的功 能。为了使正则表达式的运行做到真正有效,必须对 $ _之外的变量进行匹配,并且进行更加 复杂的替换,还要使用适用于(但不是只能用于)正则表达式的 P e r l函数。 6.5.1 对其他变量进行操作 在程序清单6 - 2中,用户输入的重量存放在$ _中,并用替换运算符和匹配运算符进行操作。 第6学时 模 式 匹 配使用71下载不过该程序清单存在一个问题,那就是 $ _并不是用来存放“重量”的最佳变量名。对于初学 者来说,$ _并不十分直观,在不经意中, $ _也许被改变了。 一般来说,将数据长期存放在 $ _中是非常危险的,最终你会感到非常恼 火。P e r l的许多运算符都使用 $ _作为默认参数,其中有些运算符也会修改 $ _。 $ _是P e r l的通用变量,如果试图将一个值长时间存放在 $ _中(尤其是当你学 习了第8学时的内容后),最终将会导致某些错误。 在程序清单6 - 2中使用变量$ w e i g h t就比较好。如果要对非$ _的变量使用匹配运算符和替换 运算符,则必须将它们与该变量连接起来。为此可以使用连接运算符 = ~,如下所示: = ~运算符并不进行赋值,它只是取出右边的运算符,并使它对左边的变量进行操作。整 个表达式拥有的值与使用$ _时所拥有的值是相同的,正如你在下面这个例子中看到的那样: 6.5.2 修饰符与多次匹配 到现在为止,你看到的所有正则表达式都是区分大小写字母的。也就是说,在模式匹配 中,大写字符与小写字符是不一样的。如果在匹配单词时不考虑它们是大写字母还是小写字 母,那么需要使用下面的代码: 这个例子看上去不仅很笨,而且很容易出错,因为很容易在键入大写和小写字母对时发 生错误。替换运算符(s / / /)和匹配运算符(m / /)能够在匹配正则表达式时不考虑大小写字母, 如果匹配项的后面跟一个字母 i的话。 / m a c b e t h / i ; 上面这个例子可以对M a c b e t h进行匹配,无论它是使用大写字母、小写字母还是大小写混 合字母(M a C b E t H)。 用于匹配和替换的另一个修饰符是全局匹配修饰符 g。正则表达式(或替换)的匹配操作 不是一次完成的,它要重复通过整个字符串,第一次匹配后,立即进行下一次匹配(或替换)。 在列表上下文中,全局匹配修饰符可使匹配代码返回一个放在括号中的正则表达式的各 个部分的列表: 该模式首先匹配一个非单词字符,然后匹配字母 f,接着匹配4个单词字符。字母f和4个单 词用括号分组。该表达式被计算后,变量 @ F将包含4个元素,即f i s h、f r o g、f r e d和f o u l。 在标量上下文中,修饰符 g使得匹配操作迭代通过整个字符串,为每个匹配操作返回真, 当不再进行更多的匹配操作时,返回假。现在请看下面这个代码: 72使用第一部分 Perl 基础 下载上面这个代码段使用匹配运算符( / /),它带有标量上下文中的修饰符 g。w h i l e循环这个 条件提供了标量上下文。该模式用于匹配一个单词字符。 W h i l e循环将继续运行(并且letters 被 递增),直到匹配代码返回假为止。当该代码段运行完成后, $ l e t t e r s的结果将是11。 在第9学时中,我们将要展示更加有效的对字符进行计数的方法。 6.5.3 反向引用 当将括号用于P e r l的正则表达式中时,由每个带括号的表达式进行匹配的目标字符串的这 个部分将被记住。 P e r l将把这个匹配的文本记录在一些特殊的变量中,这些变量的名字是 $ 1 (用于第一组括号)、$ 2(用于第二组括号)、$ 3、和$ 4等等。现在请看下面这个例子: 上面这个模式用于匹配格式很好的美国 /加拿大电话号码,比如 8 0 0 - 5 5 5 - 1 2 1 2,同时将电 话号码的每个部分记录在$ 1、$ 2和$ 3中。这些变量可以用在下面的表达式的后面: 它们也可以用作替换操作中的替换文本的组成部分,如下所示: 不过应该注意,模式匹配运行成功时,变量 $ 1、$ 2和$ 3的值将被清除(不管它是否使用 括号),如果并且只有当模式匹配运行成功时,这些变量才被设置。基于这个情况,请看下面 这个例子: 在上面这个例子中,使用 $ 1时根本没有确定模式匹配是否可行。如果模式匹配运行失败, 将会带来麻烦。 6.5.4 一个新函数:grep P e r l中的一个常见操作是搜索数组,寻找某些模式。例如,如果将一个文件读入一个数组, 然后你想要知道哪一行包含某个单词。 P e r l有一个特殊的函数,可以用来进行这项操作,这个 函数称为g r e p。g r e p函数的句法如下: g r e p函数迭代运行通过 l i s t中的每个元素,然后执行 e x p r e s s i o n或b l o c k。在e x p r e s s i o n或 b l o c k中,$ _被设置为要计算的列表中的每个元素。如果该表达式返回真, g r e p就返回该元素。 请看下面这个例子: 在上面这个例子中, @ d o g s的每个元素被依次赋予 $ _。然后根据$ _对表达式/ h o u n d /进行 测试。返回真的每个元素被 g r e p返回,并存放在@ h o u n d s中。 第6学时 模 式 匹 配使用73下载这里你必须记住两点。首先,在表达式中, $ _是对列表中的实际值的引用。如果修改 $ _, 就会改变列表中的原始元素: 当运行这个代码后, @ h o u n d s将包含g r e y h o u n d s和b l o o d h o u n d s(请注意它们结尾处的字 母s)。通过修改 $ _,原始数组 @ d o g s也被修改了,同时,它现在包含了 g r e y h o u n d s、 b l o o d h o u n d s、t e r r i e r、m u t t和c h i h u a h u a。 需要记住的另一点( P e r l程序员有时忘记了这一点)是: g r e p不一定必须与模式匹配或替 换运算符一道使用,它可以与任何运算符一道使用。下面这个例子用于检索长度超过 8个字符 的犬名: g r e p函数与 U N I X的一个命令同名,该命令用于搜索文件中的模式。 U N I X的g r e p命令在U N I X中的用处是如此之大(因此在 P e r l中用处也很大), 以至于它已经变成了一个动词,即“ to grep”(进行模式搜索)。如果我们 说to grep through a book,那么这句话的意思是翻阅每一页,寻找某个模 式。 一个相关函数 m a p的句法与g r e p基本相同,不过它的表达式(或语句块)返回的值是从 m a p返回的,而不是$ _的值。可以使用m a p函数,根据第一个数组来产生第二个数组。下面是 该函数的一个例子: @ words= map {split ' ',$_} @ input; 在这个例子中,数组 @ i n p u t的每个元素(作为 $ _传递给语句块)均用空格隔开。这意味 着@ i n p u t的每个元素均产生一个单词列表。该列表存放在 @ w o r d s中。@ i n p u t的每个相邻行均 被分隔开来,并在@ w o r d s中进行累加。 6.6 课时小结 在本学时中,我们介绍了什么是正则表达式,它们采用什么样的结构,以及如何在 P e r l中 使用正则表达式。正则表达式是由标准字符和元字符构成的。标准字符通常是指字符本身的 原义,而元字符则用于改变标准字符的含义。正则表达式可以用于测试是否存在某些模式, 或者用于替换模式。 6.7 课外作业 6.7.1 专家答疑 问题:模式/ \w(\ w)+ W /似乎不能与文本行上的所有单词相匹配,它只能与中间的几个 单词进行匹配。为什么? 解答:你查找的是用非单词字符括起来的单词字符。文本行的第一个单词的前面没有非 单词字符。它的前面根本就没有任何字符。 问题:m / /与/ /之间有什么差别。我不明白。 74使用第一部分 Perl 基础 下载解答:它们之间几乎根本没有差别。当你决定设置一个不是 / 的模式界限符时,两者之 间就会显示出惟一的一个差别。如果你在该模式的前面放置一个 m,比如m ! p a t t e r n !,那么你 才能不使用 / 界限符。 问题:我想要检查用户键入的一个数字,但是/ \d * /似乎不起作用。它总是返回真! 解答:它返回真,因为仅仅使用通配符 *的模式总是能够运行成功的。它既能够对出现 0 次的\ d进行匹配,也能够对出现 2次、1 0 0次或1 0 0 0次的\ d进行匹配。使用/ \d + /,能够确保你 至少对一个数字进行匹配。 如果你已经开始学习正则表达式的模式,请设法做一下下面这些思考题,以了解学到了 哪些知识。 6.7.2 思考题 1) 如果你拥有一些格式化为“ x = y”的代码行,那么使用什么表达式可以将表达式的左边 与右边相交换? a. s/(. +)=(. +)/ $ 2 = $ 1 /; b. s/(*)=(*)/ $ 2 = $ 1 /; c. s/(.*)=(.*)/ $ 2 $ 1 /; 2) 下面这个代码运行后,$ 2中的值是什么? $ f o o =“Star Wars: The Phantom Menace”; $ f o o = ~ / S t a r \ s((W a r s):The Phantom Menace)/; a. 模式匹配后$ 2没有被设置,因为匹配失败。 b. Wa r s c. Wars:The Phantom Menace 3) 模式m / ^ [ - + ] ? [ 0 - 9 ] +(\ . [ 0 - 9 ] *) ? $/匹配的结果是什么? a. 日期,格式为0 4 - 0 3 - 1 9 6 9 b. 格式很好的数字,如4 5,1 5 . 3,- 0 . 6 1 c. 类似加法的模式,如4 + 1 2或8 9 + 2 6.7.3 解答 1) 答案是a。如果选择c,那么在替代字符串中将不包括符号 =,它在$ 1或$ 2中将不能被捕 获,因为=出现在括号的外面。选择 b是无效的,一个字符必须出现在*的前面。选择 a,就能 够正确地执行该操作。 2) 答案是a。匹配失败是因为s t a r没有使用大写,同时,匹配代码不包含区分大小写字母的 修饰符i。由于这个原因,你始终都应该在使用 $ 1、$ 2等之前测试匹配代码运行是否成功。(如 果模式匹配使用了i修饰符,或者s t a r已经变成大写字母,那么选择b就可以得到正确的结果。) 3) 答案是b。在匹配行的开头,该模式应该是一个选项 +或-,接着是一个或多个数字,然 后(可选项)在行的结尾是一个小数点并且可能是多个数字。该模式可以匹配简单的、格式 很好的数字。 6.7.4 实习 • 看一看你是否能够创建一个用于匹配标准时间格式的模式。下面的所有格式应该都是可 第6学时 模 式 匹 配使用75下载行的:1 2 : 0 0 a m、5 : 0 0 p m、8 : 3 0 A M。下面这些格式是不行的: 3 : 0 0、2 : 6 0 a m、9 9 : 0 0 a m、 3 : 0 p m。 • 编写一个短程序,使它能够执行下列操作: 1) 打开一个文件。 2) 将所有文件行读入一个数组。 3) 从每个行中取出所有单词。 4) 找出至少拥有4个连续辅音或非元音字母的所有单词(比如“ t h o ug h t s”或“y ar d s ti c k” 这样的单词)。 76使用第一部分 Perl 基础 下载下载 第7学时 哈 希 结 构 哈希是P e r l中的第三种基本数据类型。你学习的第一种数据类型是标量,它是一种简单的 数据类型,用于存放一个数据(任何一个大小任意的数据,但是只能存放一个数据)。第二种 数据类型是数组,它是标量的集合。数组可以根据你的需要存放任意多个标量,但是,如果 要在数组中搜索你需要的标量,通常必须顺序访问该数组,直到找到你需要的标量。 哈希是另一种集合型数据类型。与数组一样,哈希包含了许多个标量。数组与哈希的差 别是:哈希是按照名字来访问它们的标量的,而不是像数组那样使用数字标号进行访问。哈 希元素包含两个部分,即一个关键字和一个值。关键字用于标识哈希的每个元素,而值则是 与该关键字相关的数据。这种关系称为关键字值对。 许多应用程序非常适合于这种类型的数据结构。例如,如果想存放某个州的持照驾驶员 的信息,那么就可以使用这些驾驶员的执照号码作为关键字来存放执照信息。这些号码是独 一无二的(每个驾驶员只有一个号码)。与每个号码相关的数据就是驾驶员的信息(执照类型、 地址和年龄等)。每个驾驶员的执照代表哈希结构中的一个元素,其号码和信息就构成了关键 字值对的关系。具备哈希性质的其他数据结构有库存零件号、医院病历、电话付款记录、磁 盘文件系统、音乐光盘收藏、 R o l o d w x信息、国会图书馆、I S B N号码和其他许多数据结构。 P e r l中的哈希结构可以根据你的需要包含任意多个元素,至少可以包含系统内存允许存放 的最大数量的元素。当将元素添加到该哈希结构或者从哈希结构中删除元素时,哈希结构就 会改变其大小。访问哈希结构中的各个元素是非常快的,并且不会因为哈希结构变大而大幅 度降低访问速度。这意味着不管哈希结构拥有 1 0个元素还是1 0万个元素,P e r l都能够得心应手 并迅速地处理哈希结构。哈希结构的关键字的长度可以根据需要而定(它们只是标量而已), 而哈希结构的数据部分的长度也可以根据需要来确定。 传统上,在P e r l和其他语言中,哈希结构称为关联性数组。这是个冗长的术语,用于说明关 键字是与值相关的。由于Perl程序员不喜欢冗长的词语,因此关联性数组现在简称为哈希结构。 在P e r l中,哈希变量是以百分比符号( %)来标识的,它们与数组和标量不使用相同的名 字。例如,你可以拥有一个名字叫 % a的哈希变量,也可以有一个名字叫 @ a的数组,还可以有 一个名字叫$ a的标量。这些名字指的是3个互不相关的变量。 在本学时中,你将要学习如何进行下面的操作: • 创建哈希结构。 • 将元素插入哈希结构和从哈希结构中删除元素。 • 使用哈希结构对数组进行操作。 7.1 将数据填入哈希结构 若要创建哈希元素,只需要将值赋予这些元素即可,这与创建数组的元素很相似。例如, 可以使用类似下面的代码来创建各个哈希元素:在这个例子中,将 % A u t h o r s赋予哈希结构。该元素的关键字是单词 D u n e,数据是名字 Frank Herbert。这个赋值操作在哈希中创建了 D u n e与Frank Herbert之间的一种关系。 $ A u t h o r s {‘D u n e’}可以像任何其他标量那样来进行处理,它可以被传递给函数,被操作员 修改,可以输出,也可以重新赋值。当修改一个哈希元素时,请始终记住,你是修改存放在 哈希元素中的值,不是修改哈希本身的值。 为什么这个例子使用的是 $ A u t h o r s { },而不是% A u t h o r s { }呢?与数组一样,当哈希结构 作为一个整体来展示时,它们的变量名的前面有它自己的标记( %)。当你访问哈希结构的单 个元素,即一个标量值时,要在变量名的前面加上一个美元符号( $),表示它引用的是单个 值,同时使用花括号来指明该值。对于 P e r l来说,$ A u t h o r s {‘D u n e’}代表单个标量值,在这 个例子中,代表Frank Herbert。 只有一个关键字的哈希结构并不特别有用。若要将若干个值放入一个哈希结构,可以使 用一系列的赋值语句,如下面的代码所示: 若要使这个代码变得短一些,可以用一个列表对该哈希结构进行初始化。该列表应该包 含成对的关键字与值,如下所示: 这个例子看上去与第 4学时介绍的数组初始化的例子有些相似。实际上,当你学到本学时 后面部分的内容时,就会知道哈希结构可以在许多上下文中作为一种特殊类型的数组来处理。 当你对哈希结构进行初始化时,要想跟踪大型列表中的哪些项目是关键字,哪些项目是 值,这是很容易搞混的。 P e r l有一个特殊的运算符,称为逗号箭头运算符,即 = >。使用= >运 算符,同时利用P e r l忽略白空间的特性,就能够编写下面这样的哈希结构的初始化代码: 可以使用两个辅助的快捷方式来进行哈希结构的初始化 。= >运算符的左边将是个简单的 字符串,不需要用引号括起来。另外,花括号中的单个单词的哈希关键字会自动加上引号。 因此,前面显示的初始化代码将变成下面的形式: 逗号箭头运算符之所以叫做这个名字,原因是它的作用类似于逗号(当 它用于分隔列表的项目时),并且它看上去像个箭头。 7.2 从哈希结构中取出数据 若要从哈希结构中取出单个元素,只需要使用一个 $、哈希结构的名字和你想要检索的关 键字。请看下面这个例子: 78使用第一部分 Perl 基础 下载这些代码行用于输出哈希结构 % M o v i e s中的元素The Shining。这个例子将输出K u b r i c k。 有时查看哈希结构中的所有元素是非常有用的。如果哈希结构中的所有关键字都是已知 的,那么你可以像上面显示的那样按照关键字来访问它们。但是,大多数情况下,按照名字 来访问每个关键字很不方便。你可能不一定知道所有关键字的名字,也可能关键字的数量太 大,无法一一列举。 可以使用k e y s函数来检索作为列表返回的哈希结构的所有关键字,然后可以查看该列表, 找出哈希结构的所有元素。在哈希结构的内部,它的关键字并不按照特定的顺序进行存放, k e y s函数返回的关键字也不使用特定的顺序。若要输出该哈希结构中的所有电影名字,可以 使用下面的代码: 这里,$ f i l m使用keys %Movies返回的列表的每个元素的值。如果除了电影的名字外,还 想输出所有导演的名字,那么可以输入下面的代码: 这个代码段输出的结果如下: 由于$ f i l m包含一个哈希关键字的值,因此 $ M o v i e s { $ f i l m }将检索该关键字代表的哈希结 构的元素值。可以将它们同时输出,以便观察哈希结构中的关键字与值之间的关系。(请记住, 你的输出可能以不同的顺序出现,因为 k e y s函数返回的关键字不是按照特定的顺序排列的。) P e r l还提供了另一个函数 v a l u e s,用于检索哈希结构中存放的所有值。如果仅仅检索值, 通常是没有什么用处的,因为你不知道哪个关键字与哪个值相关联。返回的哈希结构的值的 顺序与k e y s函数返回的关键字的顺序是相同的。现在请观察下面这个例子: 在这个例子中, @ D i r e c t o r s和@ F i l m s的每个下标都包含了一个对来自 % M o v i e s的相同关 键字值对的引用,包含在 $ D i r e c t o r s [ 0 ]中的导演名字对应于存放在 $ F i l m s [ 0 ]中的电影名字,等 等。 有时,需要按值而不是按关键字从哈希结构中检索各个元素。按值来检索元素的最好方 法是对哈希结构进行切换,也就是说,所有关键字变成值,所有值变成新哈希结构的关键字。 下面就是一个例子: 这是什么呢?当你对哈希结构使用 r e v e r s e函数时,P e r l就将哈希结构转换成一个简单的列 表,也许类似于下面这个列表: 然后P e r l对该列表中的元素顺序进行倒序,得到下面这个输出: 第7学时 哈 希 结 构使用79下载请注意,现在所有的关键字值对的顺序都倒了过来(值放在了前面)。当你将这个列表赋 予% B y D i r e c t o r时,产生的哈希结构将与原始哈希结构相同,只不过现在所有的关键字变成了 值,而所有的值则变成了关键字。不过你应该知道,如果由于某个原因你的哈希结构拥有相 重复的值,如果该值(将要变成关键字)不是唯一的,那么你得到的哈希结构拥有的元素将 比原先要少。由于在新的哈希结构中,重复的值会发生冲突,因此老的关键字将被新的关键 字代替。 7.3 列表与哈希结构 当我们介绍如何对哈希结构进行初始化时,曾经提到哈希结构与数组之间有着一定的相 关性。每当哈希结构用于列表环境中时, P e r l会将哈希结构重新变为由关键字和值组成的普通 列表。该列表可以被赋予数组,这与其他任何列表的情况是一样的,如下所示: 这时,@ D a t a是个包含6个元素的数组,偶数元素(包含 0的元素)是导演的名字,奇数 元素是电影名字。可以对 @ D a t a进行任何通常的数组操作,然后将数组赋予 % M o v i e s,如下 所示: % M o v i e s = @ D a t a ; P e r l显然是以随机顺序来存放哈希关键字的,这个随机顺序只对Perl 有用。 P e r l并不设法记住放入哈希结构中的关键字的顺序,当检索关键字时,也不 按照任何特定的顺序来放置它们。如果要按照某个顺序来显示它们,那么就 必须对它们进行排序(参见本学时后面部分中的“用哈希结构进行的有用操 作”这一节的内容),否则你应该记住它们插入时的顺序(参见本学时结尾处 的“专家答疑”的内容)。 就其他方面来说,数组与哈希结构是相似的。若要拷贝一个哈希结构,只需要像下面这 样将这个哈希结构赋予另一个哈希结构即可: % N e w _ H a s h = % O l d _ H a s h ; 当你将% O l d _ H a s h置于哈希初始化代码的右边时( P e r l通常希望右边是个列表或数组), P e r l便将哈希结构转换成一个列表。然后该列表用于对 % N e w _ H a s h进行初始化。同样,可以 像处理列表那样,将几个哈希结构组合起来并对它进行操作,如你下面看到的那样: 上面代码中的第一行将两个哈希结构 % F i r s t和% S e c o n d组合成第三个哈希结构 % B o t h。对 于这个例子,你应该记住的是,如果 % F i r s t的有些关键字也出现在% S e c o n d中,那么第二次出 现的关键字值对就取代% B o t h中的第一个关键字值对。在第二行代码中, % B o t h显示为一个放 在括号中的关键字值对的列表,另外两个关键字值对也放在括号中。然后整个列表用于 对% A d d i t i o n a l进行初始化。 80使用第一部分 Perl 基础 下载7.4 关于哈希结构的补充说明 如果你刚刚开始学习P e r l,那么对哈希结构进行某些操作时可能会遇到困难。由于哈希结 构的特殊性质,有两个常用操作需要一些专门的函数,而这些函数对于标量和数组来说是不 必要的。 7.4.1 测试哈希结构中的关键字 若要测试哈希结构中是否存在某个关键字,需要使用下面的代码句法: 由于几方面的原因,这个代码是不能满足需要的。首先,这个代码段并不是用来测定 k e y v a l是否是哈希结构中的一个关键字,它实际上是测试哈希结构中的 k e y v a l关键字的值。如 果像下面这样定义了该关键字,那么它能否达到上面的测试要求呢? 同样,这个代码也是不行的。这个代码段仍然只是测试与关键字 k e y v a l相关的数据,而不 是测试是否存在该关键字。 u n d e f是个完全有效的值,用于与哈希关键字相关联,如下面的代 码所示: 这个已经定义的测试将返回假,因为它们并没有测试哈希结构中是否存在某个关键字, 它测试的是与关键字相关的数据。那么正确的方法应该是什么呢? P e r l有一个专门用于这个目 的的函数,称为e x i s t s。下面显示的e x i s t s函数可以用于测试哈希结构中是否存在哈希关键字, 如果存在,便返回真,否则返回假: 7.4.2 从哈希结构中删除关键字 你可以进行的另一个操作是从哈希结构中删除关键字。正如你在前面看到的那样,仅仅将 哈希元素设置为u n d e f是不行的。若要删除单个哈希关键字,可以使用 d e l e t e函数,如下所示: 若要从哈希结构中删除所有关键字和值,只需要将哈希结构初始化为一个空的列表即可, 如下所示: %Hash=( ); 7.5 用哈希结构进行的有用操作 由于许多方面的原因,在P e r l中使用哈希结构,其目的不仅仅是为了按关键字来存储记录, 供以后检索。使用哈希结构的优点是可以迅速访问各个关键字,并且哈希结构中的所有关键 字都是惟一的。由于具备这些特性,因此哈希结构对于数据操作是非常有用的。毫不奇怪, 由于数组和哈希结构非常相似,因此你用哈希结构进行的许多有趣的操作属于数组操作。 第7学时 哈 希 结 构使用81下载7.5.1 确定频率分布 在第6学时中,你学习了如何取出一行文本,然后将它分割成单词。请观察下面这个代码 段: 第一行代码每次读取一行标准输入,为每一行设置 $ _。 然后,第二行的 w h i l e()循环对$ _中的每个单词进行迭代操作。让我们回顾一下第 6学 时介绍的内容,在标量上下文中使用带有修饰符 g的模式匹配运算符(/ /),将返回匹配的每个 模式,直到不再剩下匹配的模式为止。寻找的模式是个单词字符 \ w,后随0个或多个单词字符 或者连字符[ \ w - ] *。在这个例子中使用了括号,以便记住特殊变量 $ 1中匹配的字符串。 下一行虽然很短,但是它是该代码段中令人感兴趣的部分。 $ 1依次设置为第二行上的模 式匹配的每个单词。该单词用作哈希结构 % Wo r d s的关键字,该关键字值对的值开始时没有定 义。通过对它进行递增,在第一次看到该单词时, P e r l将该值设置为1。第二次看到一个单词 时,哈希结构 % Wo r d s中已经存在关键字(该单词),同时它的值从 1递增为2。这个过程将继 续进行下去,直到不再有遗漏的输入为止。 当你完成操作后,哈希结构 % Wo r d s将包含读入的单词的频率分布情况。若要查看该频率 分布,可以使用下面这个代码: 7.5.2 在数组中寻找惟一的元素 上面这个代码中展示的方法也可以用来寻找数组中只出现一次的元素。假设已经将输入 的全部单词放入一个数组而不是哈希结构,同时没有专门采取措施来保证在将一个单词放入 列表之前,该列表中还没有这个单词。在这种情况下,列表中可能存在许多重复的单词。 如果输入的文本的开始行是 One Fish,Two Fish,那么该列表看上去如下所示: 如果你被赋予这个单词列表(在@ f i s h w o r d s中),同时你只需要该列表的独一无二的元素, 那么使用哈希结构就非常适合你的需要,如程序清单 7 - 1所示: 程序清单7-1 寻找数组中的惟一的元素 第1行:用于对临时哈希结构% s e e n进行初始化,该哈希结构用于存放你的所有单词。 第2行:对单词列表进行迭代操作,依次将 $ _设置为每个单词。 第3行:用于创建哈希结构 % s e e n中的关键字,以 $ _中的该单词作为关键字,并为该数据 创建一个名义上的值。 82使用第一部分 Perl 基础 下载第5行 :只是从哈希结构中取出所有关键字,并将它们存放在 @ u n i q u e w o r d s中。哈希结 构中的任何重复单词(例如 f i s h)将互相改写,然后只以一个关键字出现。 7.5.3 寻找两个数组之间的交汇部分和不同部分 对数组经常要进行的一项操作是寻找两个数组之间的交汇部分(即它们的重叠部分)和 两个数组之间的不同部分(它们不重叠的部分)。在这个例子中,你有两个列表,一个是包含 电影明星的列表,另一个是包含政治家的列表。你的任务是找出是电影明星的所有政治家。 下面是你的两个(非常不完整的)数组: 程序清单7 - 2显示了寻找交汇部分的代码。 程序清单7-2 寻找两个数组的交汇部分 第1行:用于对哈希结构 % s e e n进行初始化。这个临时哈希结构用于存放所有电影明星的 名字。 第2行:对电影明星的列表进行迭代操作,依次将 $ _设置为每个名字。 第3行:用电影明星的名字填入哈希结构 % s e e n的各个关键字,并将值设置为 1,这个值可 以是你想要的任何真值。 第5行:这一行看起来比它实际上更复杂一些。 @ p o l s中的G r e p函数对政治家的列表进行 迭代操作,依次将$ _设置给每个政治家。然后,在哈希结构 % s e e n中寻找该名字。如果该名字 返回真,那么它就位于哈希结构中,表达式 $ s e e n { $ _ }计算的结果为真。如果该表达式计算的 结果是真,那么g r e p返回$ _的值,然后该值被放入@ i n t e r s e c t i o n中。这个过程将重复进行,直 到@ p o l s被g r e p全部查看完毕。当该代码段运行结束时, @ i n t e r s e c t i o n便包含既是@ s t a r s又是 @ p o l s的所有成员的名字。 用于寻找两个数组之间的不同部分(即在一个数组中存在,而在另一个数组中不存在的 那些元素)的代码与上面这个代码几乎是相同的,可以使用程序清单 7 - 3来查找不是电影明星 的所有政治家。 程序清单7-3 寻找两个数组之间的不同部分 惟一有变化的一行是第 5行。它仍然用于查找哈希结构 % s e e n中的每个政治家的名字,但 是,现在如果它找到了政治家的名字,则返回假。相反,如果没有找到政治家的名字,则返 回真。出现在哈希结构 % s e e n中的所有政治家的名字并不返回给 @ d i ff e r e n c e。如果你想要找 到不是政治家的所有电影明星,使用的代码几乎完全一样,但是必须将 @ s t a r s切换成@ p o l s。 第7学时 哈 希 结 构使用83下载7.5.4 对哈希结构进行排序 许多情况下,仅仅按照默认顺序来检索哈希结构中的关键字是不够的(默认顺序是非常 随机的)。这是许多情况中的一种。可以用两种方法来输出你创建的频率分布,一种是按单词 的字母顺序,另一种是按频率的顺序。由于 k e y s函数能够返回一个简单的列表,因此可以像 下面这样使用s o r t函数对该列表进行排序: 按频率给列表排序并没有太大的差别。第 4学时中我们讲过,按照默认设置, s o r t函数只 是用A S C I I顺序给既定的列表排序。但是,如果需要进行比较复杂的排序,可以在代码块中调 用s o r t函数,以便设定排序顺序。下面这个代码显示了按照值该哈希结构进行排序的情况: 也许你还记得,与 s o r t一道使用的B L O C K被s o r t函数反复调用,$ a和$ b被设置为s o r t函数 需要进行排序的每一对值。在这种情况下, $ a和$ b被设置为哈希结构 % w o r d s中的各个不同关 键字。该代码不是直接比较 $ a与$ b,而是查看哈希结构% Wo r d s中的这些关键字的值,然后对 它们进行比较。 7.6 练习:用Perl创建一个简单的客户数据库 当你打电话给客户服务中心,并且最后接通对方的电话时,对方问你的第一个问题是你 的电话号码是什么。确实,每次几乎都是这样。有时,客户服务代表想要你的客户号码,甚 至你的社会保险号。对方需要的是计算机能够用来识别你的一个惟一标志。这些号码可以作 为在数据库中检索关于你的信息时使用的关键字,这就像 P e r l的哈希结构,是不是? 在这个练习中,你将要搜索一个客户数据库。这个程序假设数据库已经存在,并且尚无 更新数据库的任何手段。在这个数据库中,你将允许用户搜索一个或两个不同的域。 在开始做这个练习时,需要一些数据。请打开文本编辑器,键入文本(或类似的某些数 据),并将它保存为c u s t o m e r s . t e t。不必担心列与列之间的空格数量,也不必考虑使它们对齐, 你只需要在每一列之间留一个空格。 在同一个目录中,键入程序清单 7 - 5中的短程序,并将它保存为 c u s t o m e r。务必按照第1学 时中的说明使该程序成为可执行程序。 当完成上述操作后,键入下面这个命令行,设法运行该程序。 perl Customer 84使用第一部分 Perl 基础 下载程序清单7 - 4显示了C u s t o m e r程序的输出。 程序清单7-4 Customer程序的示例输出 程序清单7-5 Customer程序的完整清单 第1行:这一行包含到达解释程序的路径(可以修改这个路径,使之适合系统的需要)和 开关- w。请始终使警告特性处于激活状态。 第7学时 哈 希 结 构使用85下载第3行:文件句柄P H上的c u s t o m e r s . t x t文件被打开。当然,对它的错误进行了检查,并且 作了报告。 第4 ~ 5行:文件句柄P H被读取,每一行均被赋予 $ _。$ _则使用c h o m p命令删除结尾处的换 行符。 第6行:$ _中的这一行在白空间处( \ s +)被分割。s p i l t语句的前后加上一组括号,后面是 方括号。由于你只对每一行上的电话号码和电子邮件地址感兴趣,所以从分割的语句中取出 一部分返回值。这两个值被赋予 $ n u m b e r和$ e m a i l。 第7 ~ 8行:% E m a i l用于存放客户记录,关键字是电子邮件地址。 % P h o n e用于存放与客户 相关的电子邮件地址。 第1 0行:这一行用于关闭文件句柄。 第1 3行:该w h i l e循环包含了需要重复运行的这部分代码。语句 w h i l e(1)是个P e r l的习惯 用语,意思是“永远循环”。若要退出该循环,最终需要使用 l a s t语句。 第1 4 ~ 1 5行:电话号码被读取,换行符被删除。 第1 7 ~ 2 0行:如果没有电话号码,那么这些代码行提示你输入一个电子邮件地址。 第2 2 ~ 2 3行:如果没有输入任何信息,这一行便重复运行该循环。如果输入一个 g,则该 循环退出。 第2 5 ~ 2 8行:如果输入了一个号码,并且是个有效的号码,那么第 2 6行将输出该客户记录。 控制权重新传递给顶部带有 n e x t语句的代码块。 第3 0 ~ 3 3行:输入了一个地址,并且是个有效的地址,那么输出客户记录。控制权重新传 递给顶部带有n e x t的代码块。 第3 4 ~ 3 5行:如果输入了一个地址或电话号码,但是发现它是个无效的地址或号码,那么 这两行便输出一条消息,并重复运行带有 n e x t的代码块。 这个例子展示了 P e r l的几个特性。哈希结构可以用于根据关键字来迅速查找数据。由于 P e r l能够非常有效地实现哈希结构,即使该程序拥有哈希结构中的成千上万的记录,查询的响 应时间也不会变得很长。另外,这个程序也展示了使用简单 B L O C K的程序控制流,而不是其 他的控制结构(如w h i l e、d o和u n t i l等)。 7.7 课时小结 哈希结构为程序员提供了许多非常有用的工具。除了简单的记录存储和检索工具外,哈 希还提供了对数据进行转换和分析的有用机制。数组操作、记录存储和检索的公式将会给你 带来许多好处。在后面的几个学时中,哈希结构将为你提供一个进一步学习 D B M文件处理、 复杂数据结构的操作,以及与你的系统环境打交道等内容的途径。 7.8 课外作业 7.8.1 专家答疑 问题:如果需要用一个关键字来存储若干个数据(一个列表),我能否在哈希结构中存储 多个数据呢? 解答:可以。这需要使用两个基本方法。第一个方法(也是最笨的方法)是将哈希元素 86使用第一部分 Perl 基础 下载中的值的部分格式化为可以识别的东西,比如用一个用逗号分隔的列表。每当你存储哈希元 素时,可以使用 j o i n函数,将该列表组合到一个标量中,每当你从哈希结构中检索一个值时, 可以使用s p l i t函数,将标量重新分割成列表。这种方法比较麻烦,也容易出错。 另一种方法是使用一个引用项。使用引用项后,你就能够创建数组的哈希结构,哈希结 构中的哈希结构和其他复杂的数据类型。当你掌握了它的诀窍后,使用引用项来创建复杂数 据结构是非常容易的。关于这方面的内容,我们将在第 1 3学时中介绍。 问题:我应该如何按照将关键字赋予哈希结构时的顺序来保持它的顺序? 解答:同样你可以使用两种方法来保持它们的顺序。第一种方法比较难,你要跟踪你的 插入顺序。方法是使用一个对哈希结构进行镜像的数组。当将新元素放入哈希结构时,可以 使用p u s h将相同的关键字放入一个数组。当需要查看插入顺序时,只需要使用该数组而不是 k e y s函数。这种方法比较复杂,并且容易出错。 更好的方法是使用模块Ti e : : I x H a s h。这个模块可以根据你的需要,使 k e y s函数按照插入顺 序返回哈希关键字。在第1 4学时中,我们将要介绍如何使用这个模块的方法。 问题:你能介绍一种将哈希结构写入文件时可以使用的简便方法吗? 解答:当然可以。D a t a : : D u m p e r或S t o r a b l e之类的模块能够将哈希和数组等数据类型重新 格式化为便于存放的标量值,这些标量值可以写入文本文件。这些模块还拥有一些函数,它 们能够利用那些格式化的标量并重新创建你存储的原始结构。 在第1 5学时中,我们将要介绍一种将哈希写入文件的非常方便的方法,这就是使用 D B M 文件。使用D B M文件,你可以将你的哈希结构与一个磁盘文件关联起来。当你改变哈希结构 时,磁盘文件也会相应地变更。该磁盘文件能够使你的哈希结构一直保留着,只要文件保持 不变。 7.8.2 思考题 1) 对于本学时中你编写的 C u s t o m e r程序来说,尤其是如果客户列表非常长的话,为什么 n a m e是个不合适的搜索关键字? a. 姓和名字的组合超出了P e r l的哈希结构允许的长度。 b. 人的名字不是独一无二的关键字。 c. 没有人想要按照名字来搜索客户数据库。 2) 相关性数组与哈希结构之间的区别是什么? a. 没有区别。 b. 相关性数组用于更加正式的数据集,比如帐单记录。 c. 在P e r l中,哈希不是真正的相关性数组,因此它们拥有不同的名字。 3) 哪些种类的数据最适合哈希结构? a. 简单的项目列表。 b. 普通常用数据。 c. 关键字值对的列表。 7.8.3 解答 1) 答案是b。P e r l的哈希结构的大小实际上是不受限制的,用户必然要求按名字进行搜索。 第7学时 哈 希 结 构使用87下载但是按照人的名字来搜索是不合适的,因为名字不是独一无二的。电话簿中有许多的重复名 字,比如John Smith和Robert Jones等。 2) 答案是a。哈希结构与相关性数组是同一个东西。惟一的区别是哈希说起来顺口,拼写 容易。 3) 答案是c。选择c是正确的,不过组织得很好的普通数据哈希结构也是可以的。 7.8.4 实习 • 修改C u s t o m e r程序,使得它可以按照名字进行搜索。由于你不能将n a m e用作哈希关键字, 因此必须通过哈希中的值来进行搜索。 • 修改C u s t o m e r程序,使得它可以按照关键字的某个部分(比如电话号码的一部分或者电 子邮件地址的一部分)来进行搜索。可以使用正则表达式来搜索模式。请记住应该找到 多个结果,并且返回每个结果。 88使用第一部分 Perl 基础 下载下载 第8学时 函 数 几乎所有的计算机语言都支持函数。函数是一组代码,可以按名字对它进行调用,以便 执行某项工作,然后返回某个值。在本书中,你要使用许多函数,比如,你已经使用了 p r i n t、 r e v e r s e、s o r t、o p e n、c l o s e和s p l i t等函数。它们都是P e r l的内置函数。 P e r l还允许你编写自己的函数。在 Pe r l中,用户定义的函数称为子例程。与 P e r l的内置函 数一样,用户定义的函数也可以拥有参数,并且可以将值返回给调用者。 P e r l还支持作用域的概念。作用域用于确定某个时间内程序能够看到的一组变量。由于有 了P e r l的作用域,你就能够编写运行时不受你的程序的其余部分影响的函数。编写得非常出色 的函数可以在其他程序中重复使用。 在本学时中,你将要学习: • 如何定义你自己的函数和如何调用这些函数。 • 如何将值传递给函数,然后返回值。 • 如何使用use strict来编写程序,以便强制使用某种结构。 8.1 创建和调用子例程 可以使用下面的代码来创建用户定义的子例程: P e r l中的子例程名与第 2学时中介绍的标量、数组和哈希结构的命名约定是相同的。子例 程与现有的变量可以使用相同的名字。但是,你应该避免创建名字与 P e r l的内置函数和运算符 相同的子例程。如果在 P e r l中创建了名字相同的两个子例程,那么在报警特性激活的情况下, P e r l就会发出一条警告消息,否则第二个定义的名字会使第一个名字被忘记。 当子例程被调用时,子例程的代码启动运行,并且任何返回值均被重新传递到子例程被 调用时的位置。(调用子例程和返回值的内容将在后面介绍。)例如,下面这个短子例程将提 示用户输入一个答案: 若要调用一个子例程,可以使用下面两个语句行中的一个: & Y e s n o ( ) ; 或者 Y e s n o ( ) ; 如果代码中已经声明了子例程,那么可以使用第二个语句(不带 &);& y e s n o()语句 是任何位置上都能使用的。在本书中,我们将使用不带 &的语句形式,虽然两种语句形式都 可以使用。当子例程被调用时,P e r l能够记住它是在什么位置被调用的,并执行子例程的代码,然后, 当子例程运行完成时,返回它记住的程序中的位置,如下面这个例子所示: P e r l的子例程可以在程序中的任何位置进行调用,包括在其他子例程中进行调用,如下所示: 8.1.1 返回子例程的值 子例程并不只是用于按照一个便于使用的名字将代码组合在一起。子例程与 P e r l的函数、 运算符和表达式一样,它也有一个值。这个值称为子例程的返回值。子例程的返回值是子例 程中计算的最后一个表达式的值,或者是 r e t u r n语句显式返回的值。子例程的值是在子例程被 调用时计算的,然后该返回值将用于调用的任何子例程中。现在请看下面这个代码: 在上面这个代码段中,若要使 P e r l计算表达式8*t w o _ b y _ f o u r()的值,那么子例程 t w o _ b y _ f o u r()便开始运行,并返回值 8。然后计算表达式8*8,并输出6 4。 值也可以由子例程的 r e t u r n语句显式返回。当你的程序需要在子例程结束之前返回,或者 当你想要明确知道返回的是什么值,而不是“堕入”子例程的结尾并使用最后的表达式的值 时,就需要使用r e t u r n语句。下面这个代码段同时使用了两种方法: 子例程能够返回数组和哈希结构,也能返回标量,如下所示: 90使用第一部分 Perl 基础 下载8.1.2 参数 上面的所有子例程举例都有一个共同点,那就是它们都对硬编码的数据( 2*4)或者变量 进行操作,而这些变量里边恰好拥有正确的数据( x _ g r e a t e r t h a n 1 0 0()的$ x)。这个限制条件 产生了一个问题,因为如果函数依赖硬编码的数据,或者希望得到函数之外的值的数据,那 么这样的函数并不是真的能够移植的函数。当你调用函数时说:“取出这个数据并且用它进行 某些操作”,然后在以后又调用它并且说:“取出另一些数据并且用它进行某些操作。”这样, 函数的运行特性就可以根据传递给它的值来改变。 为了改变函数的运行特性而赋予函数的这些值称为参数,在本书中你都需要使用这些参 数。P e r l的内置函数(g r e p、s o r t、r e v e r s e和p r i n t等)都拥有一些参数,并且现在你的函数也 可以拥有参数。若要传递子例程的参数,可以使用下面任何一个语句: 只有当P e r l已经遇到子例程的定义时,才能使用上面不带括号的第二种参数形式。 在子例程中,被传递的参数可以通过 P e r l的特殊变量@ _来访问。下面这个代码段显示了 为函数传递参数(3个字符串直接量)和输出参数的情况: 若要像下面这个例子中那样,访问传递过来的各个参数,可以使用数组 @ _上的下标,就 像你对任何其他数组操作时那样。请记住, $ _ [ 0 ](@ _的一个下标)与标量变量$ _毫不相干: 对$ _ [ 3 ]这样的变量名进行操作并不是一种“明确的”编程风格。拥有多个参数的函数常 常为这些参数赋予一个名字,这样,就能够清楚地知道它们能够做些什么。为理解这些话的 含义,请看下面这个例子: 在上面这个子例程中,数组 @ _被拷贝到列表 ( $ h i t s,$ a t _ b a t s)中。@ _的第一个元素 $ _ [ 0 ]变成了$ h i t s,第二个元素变成了$ a t _ b a t s。这里使用的变量名只是为了增强可读性。 变量@ _实际上包含了传递给子例程的原始参数的别名。如果修改了 @ _ (或者修改了@ _的任何元素),就会修改参数列表中的元素变量。如果突然进 行这样的修改,将被视为一种不好的做法,你的函数不应该干扰来自函数调 用者的参数,除非函数的使用者要求这样做。 8.1.3 传递数组和哈希结构 传递给子例程的参数不一定是标量。你可以将数组和哈希结构传递给子例程,但是这样 第8学时 函 数使用91下载做需要三思而后行。将数组或哈希结构传递给子例程时使用的方法与传递标量的方法一样: 在该子例程中,整个数组@ i t e m s通过@ _进行引用: 当将数组和哈希结构传递给子例程时,会遇到一个小小的困难。将两个或多个哈希结构 (或数组)传递给子例程,通常并不执行你想要做的操作。请看下面这个代码段: @ f i r s t和@ s e c o n d两个数组一道被放入一个列表,各个元素则在调用子例程时放入 @ _中。 @ f i r s t的各个元素的结尾与 @ _中的@ s e c o n d的元素的开始是无法区分的,它只是一个大型平 面列表。在子例程中,赋值语句( @ a,@ b)= @ _取出@ _中的所有元素,并将它们赋予 @ a。 数组@ b没有得到任何元素。(其原因已经在第4学时中做了介绍。) 标量的混合体可以用单个数组或哈希结进行传递,只要标量在参数列表中被首先传递, 并且知道有多少个标量。这样,哈希结构或数组就包含了最后一个标量以外的所有值,正如 下面这个例子中的情况一样: 如果必须将多个数组和哈希结构传递到一个子例程中(并且能够在以后对它们进行区分), 则必须使用“引用”。我们将在第1 3学时中介绍如何传递引用的方法。 8.2 作用域 在本学时的开头,我们介绍了子例程被用来取出一些代码片,并将它们捆绑在一起,再 给它们赋予一个名字。然后就可以使用这个名字在需要的时候执行该代码。子例程还允许你 取出子例程中的代码,并使它独自运行。这就是说,你可以让它只使用它的参数、该语言的 内置函数和表达式来运行,以产生一个返回值。然后你可以在其他程序中重复使用该函数, 因为该函数不再依赖它被调用时所在的上下文,它只是取出它的参数,即内部数据,然后产 生一个返回值。该函数将变成一个黑匣子,数据可以进去,也可以出来,你不必从外面关心 发生了什么事情。这称为纯函数。 现在请看下面两个代码段: 92使用第一部分 Perl 基础 下载从长远来看,上面显示的第一个代码段在某种程度上说要好一些。它不要求设置任何外 部变量,即函数外边的变量。它使用它的参数(它拷贝到 $ w e i g h t的参数),然后进行计算。 第二个实现代码不容易在另一个程序中重复使用,必须确保 $ w e i g h t已经正确地进行设置,并 且没有用于某些其他值。如果它被用于其他某个值,你就必须编辑 m o o n w e i g h t()函数,以 便使用一个不同的变量。这样做并不非常有效 。 因此,上面的第一个例子是个比较好的函数,但是它仍然忽略了某些东西。变量 $ w e i g h t 可能与程序中的其他某个位置上的名字为 $ w e i g h t的变量发生冲突。 P e r l允许你在大型程序中为了不同的目的一次又一次地重复使用变量名。按照默认设置, 在你的程序的主体中和子例程中, P e r l的变量是可视的,这些类型的变量称为全局变量。 你要做的工作是使变量成为函数的专用变量。为此,必须使用 m y操作符: 在m o o n w e i g h t()中,$ w e i g h t现在是个专用变量。程序中的其他函数都不能访问 $ w e i g h t 的值。带有$ w e i g h t名字的任何其他变量均与m o o n w e i g h t()函数的$ w e i g h t完全隔开。这个子 例程现在已经完全是个独立的子例程。 可视变量的这部分程序称为变量的作用域。 可以使用m y操作符来声明标量、数组和哈希结构的变量是子例程的专用变量。例如,文 件句柄、子例程和 P e r l的特殊变量$ !、$ _和@ _都不能标记为子例程的专用变量。如果将括号 用于m y操作符,你就能够声明多个专用变量: 子例程的专用变量与全局变量的存储方式是完全不同的。全局变量和专用变量可以拥有 相同的名字,但是它们互相之间毫不相干,如下所示: 上面这个代码段将输出 1 0、2 0,然后输出1 0。m y f u n c()子例程中的 $ x与该子例程外面 的$ x是完全不同的。子例程有可能同时使用它的专用 $ x和全局$ x吗?答案是肯定的,不过答 案有些复杂,这个问题不在 P e r l入门书籍要讲解的范围之内。 大部分时候,P e r l子例程首先要将 @ _赋予一个变量名的列表,然后声明该列表是子例程 的专用列表: 第8学时 函 数使用93下载这种方法能够创建一个与程序员友好的函数,它的变量都是函数的专用变量,因此它们 不会影响其他的函数,或者受其他函数的影响(包括程序的主体)。当子例程运行结束时,所 有专用变量均被撤消。 my操作符的其他用法 你为变量声明的作用域也可以小于这个子例程。 M y操作符声明的变量的作用域实际上可 以包含一个属于子例程块的代码块。例如,在下面这个代码段中,专用变量 $ y(用m y声明的 这个变量)只有在该代码块中才能看到: 这个声明甚至可以在控制结构 f o r、f o r e a c h、w h i l e或i f中出现。实际上,在你拥有代码块 的任何地方,都可以为变量设定作用域,这样,它就只能在该代码块中才可以看到,如下面 这个例子中那样: 在上面这个代码段中,每次通过这个循环时,便会创建 m y的新变量$ s t u ff和% h a s h。 P e r l的5 . 0 0 4和更新的版本允许将 f o r和f o r e a c h循环中的迭代器以及 w h i l e和i f中的测试条件 声明为代码块的专用迭代器和测试条件: 同样,当包含的代码块运行结束时,代码块的任何专用变量及其值均被撤消。 8.3 练习:统计数字 既然你已经懂得了函数的基本概念,那么应该开始进一步了解将代码封装在独立函数中 的好处。函数提供了可以很容易重复使用的代码。在下面这个练习中,有 3个函数能够对几个 数字组进行分析。 让我们回顾一下在学校中学习过的一些知识。一组数字的平均值(也称为算术平均值) 只是所有数字的平均值。中项值是你给一组数字排序时位于中间的这个数字的值。如果元素 的数量是偶数,那么中项值是位于中间的两个数字的平均值。标准偏差的概念是指平均值附 94使用第一部分 Perl 基础 下载近的数字“聚合”有多密。大标准偏差意味着数字分布得很宽,而小标准偏差则意味着数字 在平均值附近聚合得很紧密。平均值加上或减去标准偏差用于代表大约 6 8 %的数字集,而平 均值加上或减去两个标准偏差则代表 9 5 %的数字集。 使用文本编辑器,键入程序清单 8 - 1的程序,将它保存为s t a t s。务必根据第1学时中的说明 使该程序成为可执行程序。 当完成上述操作后,键入下面的命令,以运行该程序: perl stats 程序清单8-1 Stats程序的完整清单 第1行:这一行包含了到达解释程序的路径(可以改变这个路径,使之适合系统的需要) 和开关- w。请使警告特性始终处于激活状态。 第3行:命令use strict意味着所有变量必须用m y来声明,裸单词必须用引号括起来。 第4 ~ 11行:使用f o r e a c h循环,使m e a n()函数运行,将$ s u m中的所有数字相加,然后除 第8学时 函 数使用95下载以数字的数量。 第1 2 ~ 2 1行:m e d i a n()函数以两种方式运行。如果元素是个奇数,它只是选择中间的元 素,方法是取出数组的长度,再除以 2,然后使用它的中间部分。如果元素的数量是偶数,它 进行相同的操作,但是它取出两个中间的数字,然后使用 m e a n()函数计算这两个在 $ u p p e r 和$ l o w e r中的数字的平均数,并将它作为中间值返回。 第2 3 ~ 3 2行:s t d _ d e v()函数非常简单,不过它主要是个数学运算函数。简单说来,从平 均数中减去@ d a t a中的每个元素值,再求它的平方值,然后将产生的结果在 $ s q _ d e v _ s u m中累 加。若要求得标准偏差,则用平方差的合计值除以元素的数量减 1,然后求它的平方根。 第3 3 ~ 3 5行:程序的主体中需要的变量被声明为词法单位(使用 m y进行声明),并提示用 户输入$ d a t a。然后使用模式/ [ \ s , ] + /将变量$ d a t a分割成数组@ d a t a s e t。该模式按照逗号和空格 对该行进行分割。额外的空格和逗号被忽略。 第3 8 ~ 4 0行:产生输出。请注意,这并不是调用函数 m e a n()、m e d i a n()和s t d _ d e v() 的惟一位置。这些函数也可以互相调用, s t d _ d e v()与m e d i a n()同时使用m e a n(),这是 重复使用代码的很好的例子。 程序清单8 - 2给出了一个S t a t s程序输出的示例。 程序清单8-2 Stats程序的输出示例 8.4 函数的脚注 现在你已经懂得了作用域的概念,有些操作只能用作用域才能有效地进行。可用函数之 一是递归函数,另一个是 P e r l语句use strict,它能够激活更严格的 P e r l,使你能够避免在编程 中出现错误。 8.4.1 声明local变量 Perl 的第4版并不配有真正意义上的“专用”变量。相反, Perl 4的变量只能称为“准” 专用变量。这个“准”专用变量的概念在 Perl 5中仍然存在。若要声明这些变量,可以像下面 这个例子中那样,使用l o c a l操作符: 在上面这个代码段中, $ f o o被声明为m y f u n c()子例程的局部变量。用 l o c a l声明的变量 的作用与使用m y声明的变量几乎相同,它的作用域可以局限于一个子例程,一个代码块,或 者e v a l,它的值将在退出子例程或代码块时被撤消。 它们之间的差别是:声明为局部变量的那些变量,可以在它的作用域范围内的代码块中看 到,也可以在从该代码块中调用的任何子例程中看到。表8 - 1显示了这两种变量之间的逐项比较。 表中显示的两个代码段基本相同,差别在于 m y f u n c()中对$ f o o的声明不一样。在左边, 它用m y进行声明,而在右边,它用 l o c a l来声明。 96使用第一部分 Perl 基础 下载表8-1 my变量与l o c a l变量的比较 当左边的代码运行时,创建的 $ f o o是m y f u n c()的专用变量。当 m e s s _ w i t h _ f o o()被调 用时,在m e s s _ w i t h _ f o o()中被修改的 $ f o o是全局变量$ f o o。当控制返回给 m y f u n c()时, 输出的值是2 0,因为m y f u n c()中的$ f o o从来没有改变。 当右边的代码运行时,$ f o o被创建并声明为m y f u n c()的局部变量。当m e s s _ w i t h _ f o o() 被调用时,$ f o o被设置为0。该$ f o o与m y f u n c()返回的$ f o o相同,“专用性”传递到了被调 用的子例程。当返回到m y f u n c()时,输出0这个值。 如果你对术语非常讲究,局部变量在技术上可以称为动态作用域变量, 因为它们的作用域可以随着被调用的子例程而变化。用 m y声明的变量可以称 为词法作用域变量,因为它们的作用域只需要通过读取代码并指明它们声明 时所在的代码块来决定,该作用域是不变的。 每当你的程序需要子例程专用的变量时,你几乎总是想要一个用 m y声明的变量。 8.4.2 使Perl变得更加严格 P e r l是一种比较随意的编程语言。它并不试图限制你的编程操作,它允许你在完成工作时 不会过多地抱怨代码的外在形式。你也可以使 P e r l对你的代码更加严格一些。例如,如果你在 命令行或者# !行上使用警告开关,那么 P e r l能够帮助你避免犯一些愚蠢的错误。当你使用未定 义的变量,或者仅仅使用一次变量名时, P e r l就会向你发出警告。 在较大的软件项目中,或者在你的程序变得越来越大的时候,就需要让 P e r l帮助你对程序 有所约束。除了使用- w开关外,也可以在编译时告诉 P e r l解释程序打开更多的警告消息。可以 使用use strict来进行这项操作: use strict语句实际上可以称为编译器命令。它能够告诉 P e r l,给下列情况做上指向当前代 码块或文件中的运行期错误的标志: 第8学时 函 数使用97下载• 试图使用不是用m y声明的变量名(不是特殊变量的名字)。 • 当函数定义尚未设置时,试图将裸单词用作函数名。 • 其他潜在的错误。 现在,use strict命令能够帮助你避免产生前面两个问题。让 P e r l给没有使用m y声明的变量 做上标志,就可以在你实际上打算使用专用变量时,避免使用全局变量。这是帮助你编写更 具独立性的代码并且不依赖全局变量的一种方法。 use strict解决的最后一个问题是裸关键字的问题。请看下面这个代码: $ v a r = v a l u e ; 在这个例子中,你打算将 v a l u e解释为一个函数调用还是一个字符串呢(但是你忘记了引 号)?P e r l的use strict命令会指出这个代码是个含糊的代码,并且不允许使用这种句法,除非 在到达该语句之前已经对子例程的值进行了声明。 从现在起,将把use strict命令纳入本书中的所有的练习和较长的程序清单中。 8.4.3 递归函数 你早晚都会遇到一个特殊的子例程类别。这些子例程实际上通过调用它们自己来执行它 们的操作。这些子例程称为递归子例程。 每当一些任务能够分割成较小的任务和较小的相同任务的时候,就可以使用递归子例程。 比如,一个递归子例程正在搜索一个目录树,以便寻找一个文件。当搜索了最上面的目录后, 找到了子目录,因此又必须搜索这些子目录。在这些子目录中,如果又找到了下一个层子目 录,那么又必须搜索下一个层子目录。在这里我们可以看到一种模式。 另一种递归任务是计算阶乘,它常常用于统计学。字母 A B C D E F排列方法的数量是 6的阶 乘。阶乘是一个数与所有较小的数(最小为 1)的乘积。因此,6的阶乘是6×5×4×3×2×1, 即7 2 0。若要计算6的阶乘,你必须计算 5的阶乘,再将它乘以6。若要计算5的阶乘,你必须计 算4的阶乘,再将它乘以5,等等。程序清单8 - 3显示了计算阶乘时使用的递归函数。 程序清单8-3 计算阶乘的递归函数 第2行:f a c t o r i a l()子例程的参数被拷贝到 $ n u m,它声明为该子例程的专用变量。 第3行:每个递归函数都需要有一个终止条件。也就是说,需要设定一个位置,在这个位 置上,该函数不再能够调用自己以便获得一个答案。对于 f a c t o r i a l()子例程来说,终止条件 是1的阶乘(或0的阶乘)。这两个阶乘的值都是 1。当使用1或0($ n u m < = 1)调用f a c t o r i a l() 子例程时,它就执行r e t u r n(1)。 第4行:否则,如果参数不是 0或1,那么必须计算下一个较小的数列的阶乘。如果 $ n u m 分别是:6、5、4、3、2、1,则第4行就分别计算下面的值:返回( 6×阶乘(5))、返回(5 ×阶乘(4))、返回(4×阶乘(3))、返回(3×阶乘(2))、返回(2×阶乘(1))、不能到达 第4行。f a c t o r i a l(1)返回1。 98使用第一部分 Perl 基础 下载当下一个较小的阶乘计算后(一直计算到 1),该函数开始返回上面各个函数调用序列的 值,直到最后能够计算6的阶乘为止。 递归函数并不是个常用的函数。大的递归函数创建起来非常复杂,而且很难调试。凡是 可以通过迭代(使用 f o r、w h i l e和 f o r e a c h)来执行的任务都可以使用递归函数来进行操作, 同时,任何递归任务也可以通过迭代来完成。递归函数通常保留用于执行少数任务,目的是 使它们执行起来容易一些。 8.5 课时小结 P e r l支持用户定义的函数,这种函数称为子例程,子例程的运行特性与内置函数相同,它 们可以带有参数,执行操作,然后在必要时将各个值返回给调用者。 P e r l的函数能够调用其他 函数,甚至能够调用它们自己。P e r l还允许声明对一个函数(或任何代码块)来说专用的变量, 并能创建可以重复使用的独立的代码块。 8.6 课外作业 8.6.1 专家答疑 问题:在调用函数时,使用和不使用 &有没有真正的区别? 解答:现在你还没有必要考虑这个区别。当使用函数的原型或者当你调用的函数没有括 号时,在$ f o o与f o o之间存在一个很小的差别。这些问题不在本书讲解的范围之内,但是为了 满足用户的好奇心,在p e r l s u b手册页中,对这个问题做了介绍。 问题:当我在程序中使用m y($ v a r)时,P e r l报了一条消息:syntax error,next 2 tokens m y(,这是什么意思? 解答: 可能出现了键入错误,也可能安装了 Perl 4这个版本。请在命令提示符后面键入 perl -v。如果P e r l报出的版本是第4版,那么你应该立即进行版本升级。 问题:应该如何将函数、文件句柄、多个数组或哈希结构传递(或返回)给子例程? 解答:若要传递函数、多个数组和哈希结构,必须使用引用,这个问题将在第 1 3学时中 介绍。若要将文件句柄传递给子例程,或者从子例程那里接收文件句柄,必须使用称为 t y p e g l o b的工具,或者使用I O : : H a n d l e模块。这两个内容都不属于本书讲解的范围。 问题:我使用的一个函数返回了许多值,但是我只对其中的一个值有兴趣,我应该如何 跳过其他的返回值? 解答:方法之一是用函数建立一个列表,方法是将整个函数调用放在括号中。当它是个 列表时,你就可以使用普通的列表块来获得该列表的各个部分的信息。 下面这个代码只是从 内置函数l o c a l t i m e(它实际上有9个返回值)中取出年份(当前年份减去 1 9 0 0): print "It is now ",1900+ (localtime [5] ) 另一种方法是将来自函数的返回值赋予一个列表,并且将不需要的值赋予 u n d e f或哑变量: (undef, undef, undef, undef, undef, $year_offset )= localtime; 8.6.2 思考题 观察下面这个代码段: 第8学时 函 数使用99下载1) 当你运行b a r($ a , $ b)后,$ b中的值是什么? a. 5 b. 100 c. 68 2) foo()的返回值是什么? a. 67 b. 68 c. undef 3) 在f o o()中,$ b是什么? a. 词法规定的作用域 b. 动态作用域 c. 全局作用域 8.6.3 解答 1) 答案是b。$ b在f o o()中声明为l o c a l,因此每次调用的子例程均共享同一个$ b值(除非 它们后来再次用l o c a l或m y声明了$ b)。调用b a r()后,在$ b被修改的地方,$ b被设置为1 0 0。 2) 答案是b。f o o()中的最后一个语句是 b a r($ a,$ b)。b a r()返回6 8。因为$ a的值被 传递给b a r,并且它被递增了。f o o()返回最后一个表达式的值,它是 6 8。 3) 答案是b。用l o c a l声明的变量是动态调用的作用域变量。 8.6.4 实习 • 使用本学时的统计练习中的函数和第 7学时中的单词统计代码来观察文档中单词的长度。 计算它们的平均值、中间值和标准偏差。 • 编写一个函数,输出斐波纳奇数列的一部分。这个数列的开始部分是 0,1,1,2,3,5, 8,它可以永远延续下去。斐波纳奇数列是数学和自然界中的一种递归模式。后面这个 数是前两个数的和(0和1是例外)。这些数可以用迭代方式和递归方式进行计算。 100使用第一部分 Perl 基础 下载下载 第二部分 高级特性 第9学时 其他函数和运算符 第1 0学时 文件与目录 第11学时 系统之间的互操作性 第1 2学时 使用P e r l的命令行工具 第1 3学时 引用与结构 第1 4学时 使用模块 第1 5学时 了解程序的运行性能 第1 6学时 Perl 语言开发界下载 第9学时 其他函数和运算符 P e r l遵循的传统原则是“一件事情可以使用许多方法来完成”。在本学时中,我们将要更 加深入地掌握这个原则。我们将要学习丰富多彩的新函数和运算符。 为了进行标量搜索和操作,到现在为止我们一直使用正则表达式。不过我们可以使用多 种方法来完成这项任务, P e r l提供了各种各样的函数,以便对标量进行搜索和编辑。在本学时 中,我们将要介绍其他的几种方法。 另外,我们介绍了作为项目的线性列表的数组,你可以使用 f o r e a c h迭代通过这些列表,或 者使用j o i n将它们组合起来,构成标量。在本学时中,我们将要介绍一种观察数组的全新方法。 最后,我们要重新介绍一下常用的 p r i n t函数,并且给它增加一点特性。使用新的改进后 的p r i n t函数,你就能够编写格式优美、适合向他人展示的报表。 在本学时中,你将要学习: • 如何对标量进行简单的字符串搜索。 • 如何进行字符替换。 • 如何使用p r i n t函数。 • 如何将数组用作堆栈和队列。 9.1 搜索标量 正则表达式非常适合对标量进行搜索,以便找出你要的模式,但是有时使用正则表达式 来搜索标量有点像杀鸡用牛刀的味道。在 p e r l中,对模式进行组装,然后在标量中搜索该模式, 需要花费一定的开销,不过这个开销并不大。另外,当你编写正则表达式时,很容易出错。 为此,p e r l提供了若干个函数,用于对标量进行搜索,或者从标量中取出简单的信息。 9.1.1 用index进行搜索 如果你只想在另一个标量中搜索单个字符串,Pert提供了index函数。index函数的句法如下: i n d e x函数从s t r i n g的左边开始运行,并搜索 s u b s t r i n g。i n d e x返回找到s u b s t r i n g时所在的位 置,o是指最左边的字符。如果没有找到 s u b s t r i n g,i n d e x便返回- 1。被搜索的字符串可以是字 符串直接量,可以是标量,也可以是能够返回字符串值的任何表达式。 s u b s t r i n g不是一个正 则表达式,它只是另一个标量。 请记住,你编写的P e r t函数和运算符可以带有包含参数的括号,也可以不带。下面是一些 例子:根据情况,可以给 i n d e x函数规定一个字符串中开始进行搜索的起始位置,如下面的例子 显示的那样。若要从左边开始搜索,使用的起始位置是 0: 也可以使用带有起始位置的 i n d e x函数,以便“遍历”一个字符串,找到出现一个较短字 符串的所有位置,如下所示: 上面这个代码滑动通过$s o u r c e,如下所示: 9.1.2 用rindex向后搜索 函数r i n d e x的作用与i n d e x基本相同,不过它是从右向左进行搜索。它的句法如下所示: 当搜索到结尾时,r i n d e x返回- 1。下面是一些例子: 用于i n d e x的遍历循环与使用r i n d e x进行向后搜索的循环略有不同。r i n d e x的起点必须从字符 的结尾开始,或者从结尾的后面开始,(在下例中,从l e n g t h ( $ s o u r c e )开始),但是,当返回- 1时, 它仍然应该结束运行。当找到每个字符串后,$s t a r t必须递减1,而不是像i n d e x那样递增1。 9.1.3 用substr分割标量 s u b s t r是个常常被忽略和很容易被遗忘的函数,不过它提供了一种从标量中取出信息并对 标量进行编辑的通用方法。 s u b s t r的句法如下: 104使用第二部分 高 级 特 性 下载s u b s t r函数取出s t r i n g,从位置o ff s e t开始运行,并返回从o ff s e t到结尾的字符串的剩余部分。 如果设定了l e n g t h,那么取出l e n g t h指明的字符,或者直到找出字符串的结尾,以先到者为准, 如下例所示: 如果o ff s e t设定为负值,s u b s t r函数将从右边开始计数。例如, s u b s t r($ a , - 5)返回$ a的最 后5个字符。如果l e n g t h设定为负值,则 s u b s t r返回从它的起点到字符串结尾的值,少于 l e n g t h 指明的字符,如下例所示: 在上面这个代码段中, s u b s t r从位置5开始运行,返回字符串的剩余部分,但不包含最后 1 0个字符。 你也可以使用赋值表达式左边的 s u b s t r函数。当用在左边时, s u b s t r用于指明标量中的什 么字符将被替换。当用在赋值表达式的左边时, s u b s t r的第一个参数必须是个可以赋值的值, 比如标量变量,而不应该是个字符串直接量。下面是使用 s u b s t r对字符串进行编辑的一个例 子: 9.2 转换而不是替换 下一个运算符是转换运算符(有时称为翻译运算符),它使我们想起正则表达式中的替换 的操作方式。替换操作符的形式是 s / p a t t e r n / r e p t a c e m e n t /,在第6学时中我们已经作了介绍。除 非你用连接运算符=~设定了另一个标量,否则该操作符将对$ _变量进行操作。转换操作符 的作用与它有些类似,不过它并不使用正则表达式,而且它的运行方式完全不同。转换操作 符的句法如下所示: 转换操作符t r / / /用于搜索一个字符串,找出 s e a r c h l i s t中的各个元素,并用r e p l a c e m e n t l i s t中 的对应元素对它们进行替换。按照默认设置,转换操作符用于对变量$ _进行搜索和修改。若 要搜索和修改其他变量,你可以像使用正则表达式进行匹配操作那样,使用连接运算符,如 下所示: 字符的逻辑分组之间可以使用连字符。例如 A-Z代表大写字母A到Z,这样你就不必将它 们全部写出来,请看下例: 如果r e p l a c e m e n t l i s t是空的,或者与s e a r c h l i s t相同,那么t r / / /将计算并返回匹配的字符。目 第9学时 其他函数和运算符使用105下载标字符串并不被修改,如下例所示: 最后要说明的是,由于历史的原因, t r / / /也可以写成 y / / /,其结果相同,因为 y与t r同义。 t r / / /运算符(和y / / /)也允许你为s e a r c h l i s t和r e p l a c e m e n t l i s t设定另一组界限符。这些界限符可 以是任何一组自然配对的字符,如括号或任何其他字符,请看下面的例子: t r / / /运算符实际上还具备另外一些功能,不过用得不多。若要了解 t r / / /能 够执行的所有其他任务,请查看 p e r l o p节中的在线文档。 9.3 功能更强的print函数 p r i n t函数是个非常简单的输出函数,它几乎不具备任何格式化功能。为了更具体地控制 输出操作,如左对齐和右对齐,十进制精度,以及固定宽度的输出,你可以使用 P e r l的p r i n t f 函数。p r i n t f函数是从C编程语言那里借用的(几乎是原原本本的借用),不过其他编程语言也 配有类似的函数,如B A S I C的print using函数。p r i n t f函数的句法如下: f o r m a t s t r i n g是一个描述输出格式的字符串,下面我们很快就要对它进行介绍。 l i s t是一个 你想让p r i n t f显示的值的列表,它类似 p r i n t语句中的l i s t。通常而言, p r i n t f将它的输出显示给 S T D O U T文件句柄,但与p r i n t一样,如果你设定了一个文件句柄,那么 p r i n t f就使用该文件句 柄。请注意,f i l e h a n d l e名与f o r m a t s t r i n g之间不使用逗号。 通常情况下f o r m a t s t r i n g是个字符串直接量,它也可以是一个用来描述输出格式的标量, f o r m a t s t r i n g中的每个字符均按其原义输出,但是以%开头的字符则属例外。%表示这是一个域 说明符的开始。域说明符的格式是% - w. d x, 其中w是域需要的总宽度,d是小数点左边的 位数(对于数字来说)和字符串域允许的总 宽度,x表示输出的是数据类型。 x说明符前 面的连字符表示该域在 w字符中左对齐,否 则它进行右对齐。只有%和x是不可少的。表 9 - 1列出了一些不同类型的域说明符。 106使用第二部分 高 级 特 性 减号(可 有可无) 域限定 符标记 域的总宽 度(必须有) 小数点后边的位 数(可有可无) 小数点 (可有可无) 域类型(必须有) 表9-1 Printf函数的部分域说明符列表 域 类 型 含 义 c 字符 s 字符串 d 十进制整数;截尾的小数 f 浮点数 下载完整的域说明符列表请参见在线手册。你可以在命令提示符后面键入 perldoc -f printf,以 查看该列表。 下面是使用p r i n t f的一些例子: 每个格式说明符均使用列表中的一个项目,如上所示。对于每个项目来说,都应该有一 个格式说明符;对于每个格式说明符来说,都有一个列表元素: 若要输出数字中的前导0,只需要在格式说明符中的宽度的前面设置 1个0,如下所示: s p r i n t f函数与p r i n t f几乎相同,不过它不是输出值,而是输出 s p r i n t f返回的格式化输出,你 可以将它赋予一个标量,或者用于另一个表达式,如下所示: 请记住,带有%f格式说明符的p r i n t f和s p r i n t f函数能够将计算结果圆整为你指定的小数点 位数。 9.4 练习:格式化报表 当你使用计算机时,必然要处理的一项任务是将原始数据格式化为一个报表。计算机程 序能够按照不同于人类阅读的格式对数据进行交换,常见的任务是使用该数据,并将它格式 化为人能够阅读的报表。 为了进行这个练习,我们为你提供了一组员工记录,它包含关于某些虚构员工的信息, 包括每小时的工资、工作的小时数、名字和员工号码。这个练习使用这些数据将它们重新格 式化为一个很好的报表。 你可以很容易地修改这种类型的程序,以便输出其他类的报表。这个练习的数据包含在 一个在程序开始初始化的数组中。在真实的报表中,数据可能来自磁盘上的一个文件。后面 我们还要修改这个练习,以便使用外部的文件。 使用文本编辑器,键入程序清单 9 - 2中的程序,并将它保存为 E m p l o y e e,不要键入行号。 按照第1学时中的说明,使该程序成为可执行程序。 当完成上述操作后,在命令行提示符后面键入下面的命令,设法运行该程序。 Perl Employee 程序清单9-1显示了E m p l o y e e程序的输出举例。 程序清单9-1 Employee程序的输出 第9学时 其他函数和运算符使用107下载程序清单9-2 Employee程序的完整清单 第1行:这一行包含到达解释程序的路径(可以更改这个路径,使之适合系统的需要)和 开关- w。请始终使警告特性处于激活状态。 第3行:use strict命令意味着所有变量都必须用 m y进行声明,裸单词必须用引号括起来。 第5 ~ 11行:员工列表被赋予 @ e m p l o y e e s。数组中的每个元素均包含名字、姓、员工号、 计时工资和工作的小时数。 第2 3 ~ 3 0行:@ e m p l o y e e s数组按姓和名字排序。 第2 4行:被排序的第 1个元素($a)分割为各个域。姓被赋予$ L 1,名字被赋于$ F 1。 两者都用m y声明为排序块的专用变量。 第2 5行:对另一个元素$b进行与上面相同的操作。姓名被赋于$ L 2和$L 1。 第2 6 ~ 2 9行:使用类似第 4学时中的程序清单 4 - 1介绍的顺序,按字母对各个名字进行比 较。 第3 2 ~ 3 4行:@ e m p l o y e e s中的已排序列表被传递给p r i n t - e m p ( ),每次传递一个元素。 108使用第二部分 高 级 特 性 下载第1 3 ~ 2 1行:p r i n t - e m p ( )函数输出格式化很好的员工记录。 第1 4 ~ 1 5行:传递来的记录 $ _ [ 0 ]被分割成各个域,并被赋于变量$ l a s t,$f i r s t等,它们 都是该子例程的专用变量。 第1 7行:名字和姓被合并为单个域,这样,两个域就可以放入某个宽度,并一道对齐。 第1 8 ~ 2 0行:记录被输出。$ h o u r s与$t i m e相乘,得出合计金额。金额 . 0 0 5与合计相加, 这样,当乘积被截尾而成为两位数时,它能正确地圆整。 9.5 堆栈形式的列表 到现在为止,列表(和数组)一直是作为线性数据数组来展示的,其索引用于指明每个 元素。 请使用你的想象力,将各个组数元素设想为一个纵向堆栈。 在计算机术语中,这种列表称为堆栈。堆栈可用于按顺序来处理的累计操作。 Klondike Solitaire游戏就是一个很好的例子。 7堆牌中的每一堆牌分别代表一个堆 栈;开始时,这些牌面向下放入堆栈。当需要这些牌时,它们被翻过来,并从堆 栈中取走,再将其他牌放在新翻过来的牌的上面。 P e r l中的堆栈通常用数组来实现。若要将各个项目放在堆栈的顶部,可以使用 p u s h函将项目压入堆栈;若要将项目从堆栈的顶部取出,可以使用 p o p函数。 另外,堆栈可以从底部进行修改,可以将它视为从一叠纸牌的底部对它进行处理一样。 S h i f t函数用于将元素添加到堆栈的底部, u n s h i f t用于从底部取出元素。每个函数的句法如下: p o p与s h i f t函数分别从t a rg e t _ a r r a y中删除一个元素。如果 t a rg e t _ a r r a y没有设定,那么元素 可以从@ _中删除,也可以从@ A R G V中删除。p o p和s h i f t函数返回被删除的元素,如果数组是 第9学时 其他函数和运算符使用109下载 0 苹果 桃 梨 李子 芒果 石榴 石榴 芒果 李子 梨 桃 苹果 1 2 3 4 5 推送 菠萝 石榴 弹出芒果 李子 梨 桃 转移转移 葡萄 苹果空的,则返回u n d e f。数组的大小将相应地缩小。 在子例程中,如果没有设定其他数组,那么 p o p、s h i f t、u n s h i f t和p u s h函 数将修改@ _。在子例程外面,你的程序主体中,如果没有设定其他数组,那 么这些函数将修改数组@ A R G V。 p u s h和u n s h i f t函数将n e w _ l i s t的元素添加给t a rg e t _ a r r a y,数组的大小将增大,以适应放置 新元素的需要。被放入 t a rg e r _ a r r a y的项目,或者从t a rg e t _ a r r a y取出的项目既可以是一个列表, 也可以是一个数组,如下例所示: 当你将元素添加给数组时,将元素推送(或移动)到数组中要比用手工 将元素添加到数组的结尾处更加有效。比如, p u s h ( @ l i s t,@ n e w i t e m s )比 @ l i s t = ( @ l i s t,@ n e w i t e m s )更加有效。P e r l的p u s h、s h i f t、u n s h i f t和p u p函数都 进行了优化,以适应这些操作的需要。 “堆栈”中的数组元素仍然属于标准的数组元素,可以用索引进行编址。堆栈的“底部” 是元素0,堆栈的“顶部”是数组中的最后一个元素。 拼接数组 迄今为止,我们介绍了数组可以按元素进行寻址、分行、移动、弹出、取消移动和推送。 数组操作的最后一个工具是 s p l i c e。s p l i c e函数的句法如下: s p l i c e函数用于删除数组中从 o ff s e t位置开始的元素,同时返回被删除的数组元素。如果 o ff s e t的值是负值,则从数组的结尾处开始计数。如果设定了 l e n g t h,那么只删除l e n g t h指定的 元素。如果设定了l i s t,则删除l e n g t h指定的元素,并用l i s t的元素取代之。通过这个处理过程, 数组就会根据需要扩大或缩小,如下面所示的那样: 9.6 课时小结 在本学时中,我们介绍了搜索其他字符串中的字符串时不需要使用正则表达式,你可以 110使用第二部分 高 级 特 性 下载使用i n d e x和r i n d e x进行简单的搜索,也可以使用 t r / / /运算符进行简单的替换。 s u b s t r函数既可 以用来从字符串中检索数据,也可以对它们进行编辑。你可以使用 p r i n t f和s p r i n t f语句,用P e r l 创建格式很好的输出。另外,我们介绍了用作项目堆栈而不是平面列表的数组,还学习了如 何对这些堆栈进行操作。 9.7 课外作业 9.7.1 专家答疑 问题:s u b s t r、i n d e x和r i n d e x等函数真的有必要使用吗?当正则表达式可以用来执行它们 的大多数操作时,为什么还要这几种函数? 解答: 首先,用于进行简单的字符串搜索的正则表达式的运行速度比 i n d e x和r i n d e x慢。 第二,为字符位置固定的正则表达式编写替换表达式会产生很大的混乱,而有时 s u b s t r则是比 较出色的解决方案。第三, P e r l是一种丰富多彩的语言,你可以使用你喜欢的解决方案,你可 以有多种多样的选择。 问题:如果我设定的索引位于标量的结尾之外,使用 s u b s t r(或i n d e x或r i n d e x)将会出现 什么情况? 解答:计算机的好处之一是:它具有很强的一致性,而且它有很强的耐心。对于“如果 ⋯将会出现什么情况”之类的问题,有时你只要试一试它的最容易实现的方法即可!什么是 可能出现的最坏情况呢? 在这种情况下,如果你激活了警告特性,那么访问并不存在的标量的某个部分,就会产 生一个“ use of undefined value”(使用了未定义的值)的错误。例如,如果你使用$ a = “F o o”;s u b s t r($a , 5);那么s u b s t r函数将返回u n d e f。 9.7.2 思考题 1) 假如有下面这个代码,那么在 @ A中将留下什么? a. oats peas beans b. deans barley c. peas beans barley 2) printf (“% 1 8 . 3 f”,$a )这个代码能够进行什么操作? a. 它输出一个浮点数,长度为 1 8个字符,小数点左边是 1 5个字符,小数点右边是 3个 字符。 b. 它输出一个浮点数,小数点左边是 1 8个字符,小数点右边是3个字符。 c. 它输出一个浮点数,长度为 1 8个字符,小数点左边是1 4个字符,右边3个字符。 3) 如果对一个字符串运行t r / a - z / A - Z,t r / A - Z / a - z能否使字符串恢复其原始形式? a. 是,当然能够。 b. 也许做不到。 第9学时 其他函数和运算符使用111下载9.7.3 解答 1) 答案是c。s h i f t删除了c a t s,而p u s h则将b a r l e y添加到结尾处。最后的 p o p是个假像,它 并没有设定任何数组,因此它从 array @_中弹出某些数据,但是它没有给 @ A带来任何变化。 2) 答案是c。如果你猜测的答案是 a,那么你没有将小数点计算在内,它在总数( 1 8=1 4 +1+3)中占有一个位置。 3) 答案是b。t r / a - z / A - Z /转换的“r o s e b u d”变成了“R O S E B U D”。如果试图用t r / A - Z / a - z / 将它变回原样,那么产生的是“ r o s e b u d”,而不是原始字符串。 9.7.4 实习 • 使用标量而不是数组,重新编写第 4学时中的H a n g m a n游戏。你可以使用s u b s t r,对标量 中的各个字符进行操作。 • 修改程序清单 9 - 2,从文件中读取数据,而不是从数组中获取数据。打开文件,将数据 读入一个数组,然后按正常情况继续操作。当然你必须在磁盘上创建该文件。 112使用第二部分 高 级 特 性 下载下载 第1 0学时 文件与目录 操作系统中的文件为数据提供了一个非常方便的存储方式。操作系统为数据提供了一个 名字(即文件名)和一个组织结构,这样你就可以在以后找到你要的数据。这个组织结构称 为文件系统。然后你的文件系统再将文件分成各个组,称为目录,有时也称为文件夹。这些 目录能够存放文件或其他目录。 在目录中嵌套目录的方法给计算机中的文件系统提供了一个树状结构。每个文件都是一 个目录的组成部分,每个目录又是父目录的组成部分。除为你的文件提供一个组织结构外, 操作系统还存放了关于文件的各种数据,比如上次读取文件是在什么时候,上次修改文件是 在什么时候,谁创建了文件,当前文件有多大等等。所有的现代计算机操作系统几乎都采用 这种组织结构。 在M a c i n t o s h系统中,仍然采用这种结构,不过它的高层目录称为卷,子目录称为文件 夹。 P e r l允许你访问这个组织结构,修改它的组织方法,并可查看关于文件的各种信息。 P e r l 用于这些操作的函数全部源自 u n i x操作系统,但是在P e r l运行的任何操作系统下,这些函数都 能够很好地运行。 P e r l的文件系统的操作函数是可以移植的,也就是说,如果你使用 P e r l的函 数对你的文件进行操作并查询你的文件,那么在 P e r l支持的任何操作系统下,运行你的代码都 是没有问题的,只要目录结构相类似。 在本学时中,你将要学习: • 如何获得目录列表。 • 如何创建和删除文件。 • 如何创建和删除目录。 • 如何获取关于文件的信息。 10.1 获得目录列表 从系统中获取目录信息的第一步是创建一个目录句柄。目录句柄与文件句柄相类似,不 同之处是:不是通过读取文件句柄来获得文件的内容,而是使用目录句柄来读取目录的内容。 若要打开目录句柄,可以使用 o p e n d i r函数: 在这个语句中,d i r h a n d l e是要打开的目录句柄, d i r e c t o r y是要读取的目录的名字。要是目 录句柄不能打开,你就无权读取该目录的内容,或者该目录根本不存在。 o p e n d i r函数将返回 假。目录句柄的结构应该与文件句柄相类似,它使用第 2学时介绍的变量名的创建规则,目录 句柄应该全部使用大写字母,以避免与 P e r l的关键字发生冲突。下面是目录句柄的一个例子: 本学时中介绍的所有例子都使用 U N I X样式中的正斜杠,因为与反斜杠相比,它不易产生 混乱,并且它可以同时用于 U N I X和Wi n d o w s操作系统。目录句柄打开后,可以使用 r e a d d i r函数来读取它的内容: 在标量上下文中, r e a d d i r函数返回目录中的下一项,如果目录中没有剩下任何项目,则 返回u n d e f。在列表上下文中, r e a d d i r返回所有的(剩余的)目录项。 r e a d d i r返回的名字包括 文件、目录的名字,而对于U N I X来说,则返回特殊文件的名字。它们返回时没有特定的次序。 r e a d d i r返回目录项.和. .。r e a d d i r返回的目录项不包含作为目录名的组成部分的路径名。 当完成目录句柄的操作后,应该使用 c l o s e d i r函数将它关闭: 下面这个例子说明如何读取一个目录: 在上面这个代码段中,整个目录被读入 @ F I L E S中。不过,在大多数时候,你对 .和. .文件 是不感兴趣的。若要读取文件句柄并清除这些文件,可以输入下面的代码: 正则表达式(/ ^ \ . \ . ? $ /)用于匹配也位于行尾的一个前导原义圆点(或两个圆点),而g r e p 则用于清除它们。若要获得带有特定扩展名的全部文件,可以使用下面的代码: r e a d d i r返回的文件名并不包含o p e n d i r使用的路径名。因此,下面的例子可能无法运行: 除非你在运行代码时恰好在 / t m p目录中工作,否则 o p e n ( F I L E H,$ f i l e )语句的运行将会失 败。例如,如果 / t m p中存在文件m y f i l e . t x t,那么r e a d d i r便返回m y f i l e . t x t。当你打开m y f i l e . t x t 时,实际上必须使用全路径名来打开 / t m p / m y f i l e . t x t。正确的代码如下所示: Globbing 读取目录中的文件名时使用的另一种方法称为 g l o b b i n g。如果你熟悉D O S中的命令提示符, 那么一定知道命令d i r * . t x t可用于输出以. t x t结尾的所有文件的目录列表。在 U N I X中,g l o b b i n g 是由s h e l l来完成的,但是l s * . t x t几乎能产生相同的结果,即列出以 . t x t结尾的所有文件。 P e r l有一个操作符,能够进行这项操作,它称为 g l o b。G l o b的句法是: giob p a t t e r n 这里的p a t t e r n是你要匹配的文件名模式。 p a t t e r n可以包含目录名和文件名的各个部分。此 外,p a t t e r n可以包含表1 0 - 1列出的任何一个特殊字符。在列表上下文中, g l o b返回与模式匹配 的所有文件(和目录)。在标量上下文中,每查询一次 g l o b,便返回一个文件。 114使用第二部分 高 级 特 性 下载g l o b的模式与正则表达式的模式不同。 表10-1 globbing的模式 字 符 匹配的模式 举 例 ? 单个字符 f?d用于匹配f u d、f i d和f d d等。 * 任何数目的字符 f * d用于匹配f d、f d d、f o o d和f i l l e d等 [ c h a r s ] 用于匹配任何一个c h a r s ; f [ o u ] d用于匹配f o d和f u d,但不能匹配 f a d M a c P e r l不支持这个特性 { a,b,⋯ } 既可以匹配字符串a, f * { t x t,d o c }用于匹配以f开头并且以. t x t或. d o c结 也可以匹配字符串b, 尾的文件 M a c P e r l不支持这个特性 对于U N I X爱好者来说, P e r l的g l o b操作符使用C语言的s h e l l样式的文件 g l o b b i n g,而不是B o u r n e(或K o r n)s h e l l的文件g l o b b i n g。它适用于安装了 P e r l的任何 U N I X系统,而不管你个人使用的是何种 s h e l l。Bourne shell g l o b b i n g和Korn shell globbing不同于 C语言的 shell globbing。它们在某些 方面很相似,比如 *与?的运行特性相同,但是在其他方面差别很大,请注 意。 下面请看几个g l o b b i n g的例子: 下面是使用g l o b与opendir/readdir/closedir 之间的某些差别: • g l o b只能返回有限数量的文件。对于较大的目录, g l o b可能会报告“太多的文件”,但是 却不能返回任何文件。这是因为 g l o b当前是使用外部程序即 s h e l l来实现的,它只能返回 有限数量的文件。o p e n d i r / r e a d d i r / c l o s e d i r函数则不存在这个问题。 • g l o b返回模式中使用的路径名,而 o p e n d i r / r e a d d i r / c l o s e d i r函数则不能。例如, g l o b (‘/ u s r / i n c l u d e / * . h ’)返回‘ / u s r / i n c l u d e ’作为任何匹配项的组成部分,而 r e a d d i r则不能返 回它。 • g l o b的运行速度通常比o p e n d i r / r e a d d i r / c l o s e d i r慢。同样,它之所以运行速度比较慢,是 因为P e r l必须启动一个外部程序来为它进行 g l o b b i n g,而该程序将在对文件名排序后再 返回这些文件名。 那么你究竟应该使用哪一个函数呢?这完全取决于你。不过,使用 o p e n d i r / r e a d d i r / c l o s e d i r函数往往是个复杂得多的解决方案,在本书中的大多数程序例子中,我们都使用这个 函数。 第10学时 文件与目录使用115下载为了完整起见,P e r l提供了另一种方法,用于编写模式 g l o b。只需将模式放入尖括号运算 符(< >)中,就可以使尖括号运算符像 g l o b函数那样来运行: 用于g l o b b i n g的尖括号运算符的句法比较老,并且可能引起混乱。在本书中,为了清楚起 见,将继续使用g l o b函数。 10.2 练习:UNIX的grep 当你进一步阅读本书的内容时,一些练习将为你展示更多的非常有用的工具。下面这个 练习展示了一个 U N I X的g r e p实用程序的简化版本。 U N I X的g r e p(不要与P e r l的g r e p相混淆) 用于搜索文件中的模式。这个练习展示了一个实用程序,它提示你输入一个目录名和一个模 式。目录中的每个文件均被搜索,以便寻找该模式,与该模式相匹配的文件行将被输出。 使用文本编辑器,键入程序清单 1 0 - 1的程序,并将它保存为 m y g r e p。务必按第1学时中介 绍的方法使该程序成为可执行程序。另外,一定不要将该文件改名为 U N I X系统上的g r e p,它 可能被混淆为实际的g r e p实用程序。 完成上述操作后,键入下面的命令行,设法运行该程序: perl mygrep 程序清单10-1 mygrep的完整清单 第1行:这一行包含到达解释程序的路径(可以修改它,使之适合系统的需要)和开关 - w。 请始终使警告特性处于激活状态。 第3行:use strict命令意味着所有变量都必须用 m y来声明,并且裸单词必须加上引号。 第5 ~ 8行:$ d i r(要搜索的目录)和 $ p a t(要搜索的模式)是从 S T D I N中检索而来的。每 116使用第二部分 高 级 特 性 下载行结尾处的换行符均被删除。 第1 0行:$ f i l e被声明为符合use strict的专用变量。$ f i l e用在本程序的后面。 第1 2行:目录$ d i r被打开,如果这项操作没有成功,则输出一条出错消息。 第1 3行:从目录中检索各个条目,每次检索 1条,然后存入$ f i l e。 第1 4行:确实是目录本身( - d)的任何目录条目均被拒绝。请注意,被核实的路径名是 $ d i r / $ f i l e。核实该路径是必要的,因为当前目录中不一定存在 $ f i l e,它存在于$ d i r中。因此该 文件的全路径名是$ d i r / $ f i l e。 第1 5 ~ 1 8行:文件被打开,再次使用全路径名 $ d i r / $ f i l e,如果文件没有打开,则被拒绝。 第1 9 ~ 2 3行:文件被搜索,逐行进行搜索,找出包含 $ p a t的这一行。匹配的行将被输出。 程序清单1 0 - 2显示了m y g r e p程序的输出举例。 程序清单10-2 mygrep的输出 10.3 目录 到现在为此,本学时中一直在介绍目录结构的问题。为了打开文件,有时需要它的全路 径名,r e a d d i r函数能够读取目录。但是,如果要浏览目录,添加和删除目录,或者清除目录, 则需要更多的P e r l功能。 10.3.1 浏览目录 当你运行软件时,操作系统将对你所在的目录保持跟踪。当你登录到一台 U N I X计算机中 并且运行一个软件包时,通常要进入你的主目录。如果你键入操作系统的命令 p w d,s h e l l便向 你显示你是在什么目录中。如果你使用 D O S或Wi n d o w s操作系统,并且打开一个命令提示符, 该提示符就能反映出你当时是在什么目录中,比如 C : \ W I N D O W S。另外,你也可以在 D O S提 示符后面键入操作系统命令 c d,这样,D O S就会告诉你是在什么目录中。你当前使用的目录 称为当前目录,即你的当前工作目录。 如果你使用程序编辑器或者集成式编辑器 /调试器,并且直接从那里运行 你的P e r l程序,那么“当前目录”可能不是你想像的那个目录。它可能是 P e r l 程序所在的目录,编辑器所在的目录或者是任意其他的目录,这取决于你使 用何种编辑器。如果要确定当前目录究竟是什么,请在你的 P e r l程序中使用 c w d函数。 如果没有全路径名,也可以打开文件,比如: o p e n(F H,“f i l e”)| | d i e可以在你的当前目 录中打开。若要改变当前目录,可以使用下面这个 c h d i r函数: chdir newdir; c h d i r函数将当前工作目录改为 n e w d i r。如果n e w d i r目录不存在,或者你不拥有对 n e w d i r的 访问权,那么 c h d i r返回假。c h d i r对目录的改变是暂时的,一旦 P e r l程序运行结束,就返回运 第10学时 文件与目录使用117下载行P e r l程序之前所在的目录。 如果运行的c h d i r函数不包含一个目录作为其参数,那么 c h d i r就会将你的目录改为你的主 目录。在U N I X系统上,主目录通常是你登录时进入的这个目录。在 Windows 95、Wi n d o w s N T或D O S计算机上,c h d i r会使你进入H O M E环境变量中指明的这个目录。如果 H O M E没有设 置,c h d i r将根本不改变当前目录。 P e r l并不配有任何内置函数来确定当前目录是个什么目录,因为某些操作系统编写时所采 用的方法,使得函数很难做到这一点。若要确定当前目录,必须同时使用两个语句。在程序 的某个位置,最好是在靠近程序开始的地方,必须使用语句 use Cw d,然后,当你想要检查当 前目录时,使用c w d函数: 只能执行use Cwd语句一次,此后,可以根据需要多次使用 c w d函数。 语句use Cwd实际上让P e r l加载一个称为Cw d的模块,使P e r l语言增加一 些新的函数,如 C w d。如果上面介绍的这个代码段返回一条出错消息,说 Ca n ’t locate cwd .pm in @INC(在@ I N C中找不到C w d . p m),或者你不完全理 解各个模块,那么现在不必为此而担心,第 1 4学时将要详细介绍模块方面的 知识。 10.3.2 创建和删除目录 若要创建一个新目录,可以使用 P e r l的m k d i r函数,m k d i r函数的句法如下: 如果目录n e w d i r能够创建,那么m k d i r函数返回真。否则,它返回假,并且将$!设置为 m k d i r运行失败的原因。只有在 P e r l的U N I X实现代码中,p e r m i s s i o n s才真的十分重要,不过在 所有版本中都必须设置 p e r m i s s i o n s。对于下面这个例子,使用的值是 0 7 5 5。这个值将在本学 时后面部分中的“U N I X系统”这一节中介绍。对于 D O S和Wi n d o w s用户来说,只使用0 7 5 5这 个值就足够了,可以省略冗长的说明。 若要删除目录,可以使用r m d i r函数。r m d i r函数的句法如下: 如果目录p a t h n a m e可以删除,r m d i r函数返回真。如果 P a t h n a m e无法删除,r m d i r返回假, 并将$!设置为r m d i r运行失败的原因,如下所示: r m d i r函数只删除完全是空的目录。这意味着在目录被删除之前,首先必须删除目录中的 118使用第二部分 高 级 特 性 下载所有文件和子目录。 10.3.3 删除文件 若要从目录中删除文件,可以使用 u n l i n k函数: u n l i n k函数能删除 l i s t _ o f _ f i l e s中的所有文件,并返回已经删除的文件数量。如果 l i s t _ o f _ f i l e s被省略,$_中指定的文件将被删除。请看下例: 若要检查文件列表是否已被删除,必须对想要删除的文件数量与已删除的文件数量进行 比较,如下面这个例子所示: 在上面这个代码段中,被 u n l i n k删除的文件数量将存放在 $ e r a s e d中。 u n l i n k运行后, $ e r a s e d的值将与@ f i l e s中的元素的数量进行比较,它们应该相同。如果不同,便输出一条出 错消息,显示“剩余的”文件。 用u n l i n k函数删除的文件将被绝对地清除掉,它们将无法恢复,并且不 是被转入“回收站”,因此使用u n l i n k函数时应该格外小心。 10.3.4 给文件改名 在P e r l中给文件或目录改名是很简单的,可以使用 r e n a m e函数,如下所示: r e n a m e函数取出名字为o l d n a m e的文件,将它的名字改为 n e w n a m e。如果改名成功,该函 数返回真。如果 o l d n a m e和n e w n a m e是目录,那么这些目录将被改名。如果改名不成功, r e n a m e返回假,并将$!设置为不成功的原因,如下所示: 如果你设定了路径名,而不只是设定文件名,那么 r e n a m e函还会将文件从一个目录移到 另一个目录,如下例所示: 如果文件n e w n a m e已经存在,该文件将被撤销。 如果文件是在不同的文件系统上,那么 r e n a m e函将不会把文件从一个目 录移到另一个目录中。 第10学时 文件与目录使用119下载10.4 UNIX系统 下面介绍P e r l用户在U N I X操作系统下工作的情况。如果你不是在 U N I X系统上使用P e r l, 那么可以跳过本节,你不会遗漏任何重要的内容。如果你对 U N I X非常有兴趣,可以阅读这一 节的内容。 作为U N I X用户来说,应该知道 P e r l与U N I X系统之间有着很深的渊源关系,有些 P e r l函数 直接来自 U N I X命令和操作系统函数。这些函数中,大部分是你不使用的。有些函数,如 u n l i n k,虽然源于U N I X,但其含义却与U N I X毫无关系。每个操作系统都可以用来删除文件, P e r l能够确保u n l i n k会对操作系统执行正确的操作。 P e r l作出了很大的努力,以确保有关的特 性(如文件I / O)能够在操作系统之间移植,并且它在可能的情况下将所有的兼容性问题隐藏 起来,使你不必为此而担心。 P e r l语言中嵌入了许多U N I X函数和命令,而P e r l语言已经移植到许多非U N I X操作系统中, 这满足了U N I X开发人员和管理员的要求,使他们能够将 U N I X工具包中的一些工具带到任何 地方去使用。 正如下一节的标题所示,这个描述不能被视为 U N I X文件系统访问许可权 以及如何操作文件的完整说明。若要了解它的完整说明,请参见你的操作系统 文档,或者参阅关于U N I X的其他著作,如《UNIX 24学时教程》。 文件访问许可权的简要介绍 在第1学时中,我们讲到,为了使P e r l程序能够像一个标准命令那样来运行,我们提供了一 个命令chmod 755 scriptname,但是没有具体介绍它的含义。7 5 5是赋予文件s c r i p t n a m e的访问许 可权的一种描述。U N I X中的c h m o d命令用于设置文件的访问许可权。 这行数字分别代表赋予文件所有者、文件所属小组以及其他的 非文件所有者和非文件所属小组的访问许可权。在上面这个例子中, 文件所有者拥有的访问许可权是 7,文件所属小组和其他人的访问许 可权是5。表1 0 - 2列出了每种访问许可权的值。 表10-2 文件访问许可权 访问许可权值 权 限 7 所有者/组/其他人可以读、写和执行该文件 6 所有者/组/其他人可以读、写该文件 5 所有者/组/其他人可以读和执行该文件 4 所有者/组/其他人可以读该文件 3 所有者/组/其他人可以写和执行该文件 2 所有者/组/其他人可以写该文件 1 所有者/组/其他人可以执行该文件 若要在P e r l中设置文件的访问许可权,可以使用 U N I X的内置函数c h m o d: c h m o d函能够改变l i s t _ o f _ f i l e s的所有文件的访问许可权,并且返回已经改变访问许可权的 文件数量。m o d e的前面必须有一个数字 0(如果它是一个八进制直接量数字的话),然后是你 120使用第二部分 高 级 特 性 下载 所有者 组 其他人 7 7 5想指明其访问许可权的数字。下面是 c h m o d命令的一些例子: 在本学时前面部分的内容中,我们介绍了 m k d i r函数。m k d i r的第一个参数是文件访问许 可权,它与c h m o d使用的许可权是相同的: U N I X文件的访问许可权常常称为它的“方式”。因此c h m o d是“c h a n g e m o d e(改变方式)”的缩写。 10.5 你应该了解的关于文件的所有信息 如果你想要详细而全面地了解关于一个文件的信息,可以使用 P e r l的s t a t函数。s t a t函数源 于U N I X系统,它的返回值在U N I X系统中与非U N I X系统中略有不同。S t a t的句法如下所示: s t a t函数即可以用来检索已经打开的文件句柄的信息,也可以检索关于某个特定文件的信 息。在任何操作系统下, s t a t均可返回一个包含1 3个元素的列表,来描述文件的属性。列表中 的实际值随着运行的操作系统的不同而有所差异,因为有些操作系统包含的特性是其他操作 系统所没有的。表1 0 - 3显示了s t a t返回值中的每个元素的含义。 表10-3 stat函数的返回值 编 号 名 字 U N I X系统 Wi n d o w s系统 0 d e v 设备号 驱动器号(C:通常是2,D: 通常是3,等等) 1 i n o 索引节号 总是0 2 m o d e 文件的方式 无 3 n l i n k 链接号 通常为0;Windows NT;文 件系统允许链接 4 u i d 文件所有者的用户I D(U I D) 总是0 5 g i d 文件所有者的组 ID (G I D) 总是0 6 r d e v 特殊文件信息 驱动器号(重复) 7 s i z e 文件大小(以字节计) 文件大小(以字节计) 8 a t i m e 上次访问的时间 上次访问的时间 9 m t i m e 上次修改的时间 上次修改的时间 1 0 c t i m e I n o d e修改时间 文件的创建时间 11 b l k s z 磁盘块的大小 总是0 1 2 b l o c k s 文件中的块的数量 总是0 表1 0 - 3中的许多值你可能永远不会使用,但是为了完整起见,我们在表中将它们列出来。 对于含义比较含糊的值,尤其是 U N I X中的返回值,你可以查看操作系统的参考手册,以了解 它的含义。 下面是将s t a t用于文件的一个例子: 第10学时 文件与目录使用121下载通常情况下,为了清楚起见, s t a t的返回值被拷贝到标量的一个赋值列表: 若要按“文件访问许可权的简要介绍”这一节中所说的 3字符形式输出文件的访问许可权, 可以使用下面这个代码,其中的 @ s t u ff包含了访问许可权: 上面这个代码所包含的元素你可能不了解,这没有什么关系。其中有些元素尚未向你介 绍。在$m o d e中由s t a t检索的元素包含了许多“额外的”信息。 & 0 7 7 7只是提取你感兴趣的这 部分信息。最后,% 0是一个p r i n t f格式,用于输出采用 0 ~ 7格式的八进制数字, U N I X希望用 这种格式对文件访问许可权进行格式化。 八进制是以 8为基数的数字表示法。由于历史原因,它用于 U N I X系统, 不过它也用于 P e r l。如果你仍然对它不太理解,请不必担心。如果需要显 示文件的访问许可权,只要使用前面介绍的 p r i n t f函数即可。它并不是经常 出现。 表1 0 - 3中列出的3个时间戳,即访问时间、修改时间和更改(即创建)时间均以特定格式 来存储。时间戳以格林威治时间 1 9 7 0年1月1日零点起的秒数来存储。若要以便于使用的格式 来输出时间,可以使用l o c a l t i m e函数,如下所示: 该函数用于输出文件的修改时间,格式为 Sat Jul 3 23:35:11 EDT 1999。访问时间是指上 次读取文件(或打开文件以便读取)的时间,修改时间是指上次将数据写入文件的时间。在 U N I X系统下,“更改”时间是指更改关于文件的信息(文件的所有者、链接的数量、访问许 可权等)的时间,它并不是文件的创建时间,不过由于巧合,它常常就是文件的创建时间。 在Microsoft Wi n d o w s下,c t i m e域实际上用于存放文件的创建时间。 有时你可能想从s t a t返回的列表中仅仅检索 1个值。若是这样,可以用括号将整个 s t a t函数 括起来,并使用下标将你想要的值标出来: 10.6 练习:对整个文件改名 这个练习为你的工具包提供了另一个小型工具。假定有一个目录名,一个要查找的模式 和要改变成的模式,使用这个实用程序,可以对目录名中的所有文件进行改名。例如,如果 一个目录中包含文件名 C h a p t e r _ 0 1 . r t f、C h a p t e r _ 0 2 . r t f、C h a p t e r _ 0 4 . r t f等,你就可以将所有文 件改名为H o u r _ 0 1 . r t f、H o u r _ 0 2 . r t f、H o u r _ 0 4 . r t f等。当使用基于图形用户界面的文件浏览器时, 想要用命令提示符来执行这种操作通常并不容易,而且显得很笨。 使用文本编辑器,键入程序清单 1 0 - 3中的程序,并将它保存为 R e n a m e r。务必按照第1学 时介绍的方法使该程序成为可执行程序。 完成上述操作后,键入下面的命令行,设法运行该程序: 122使用第二部分 高 级 特 性 下载程序清单10-3 Renamer程序的完整清单 第1 3 ~ 1 5行:$ d i r指明的目录中的条目被读入 @ f i l e s。 第1 7 ~ 1 9行:来自@ f i l e s的每个文件均被赋给$ _,并且该名字被保存在$ o l d n a m e中。然 后,$_中的原始文件名被改为1 9行上的新名字。 第2 0行:在对文件改名之前,这一行要确认目标文件名并不存在。否则,该程序可能将 文件改名为一个现有文件名,撤消其原始数据。 第2 1 ~ 2 5行。该文件被改名,如果改名失败,则输出一条警告消息。请注意,原始目录名 必须附加到文件名上,例如, $ / d i r / $ o l d n a m e,因为@ f i l e s不包含全路径名,所以必须使用全 路径名来进行改名。 程序清单1 0 - 4显示了该程序的示例输出。 程序清单10-4 Rename程序的输出示例 10.7 课时小结 本学时我们介绍了如何使用 P e r l中的m k d i r、r m和r e n a m e函数来创建、删除目录条目并对 其改名。另外,还介绍了如何使用 s t a t来查询文件系统,以便了解关于文件的信息,不光是了 解文件的内容。在本学时中,两个练习提供了一些简单而实用的工具,使你的程序具备更高 的效能。 第10学时 文件与目录使用123下载10.8 课外作业 10.8.1 专家答疑 问题:我在运行下面这个程序时遇到了问题。虽然目录中有文件,但是无法读取,原因 何在? 解答:问题出在第2行上。D I R H A N D L E是个目录句柄,不是文件句柄。你无法用尖括号 (<>)操作符来读取目录句柄。读取目录的正确方法是 @files=readdir DIRHANDLE。 问题:为什么g l o b (“* . *”)不能匹配目录中的所有文件? 解答:因为“* . *”只能匹配文件名中有圆点的文件名。若要匹配目录中的所有文件,请 使用g l o b ( * . * )。g l o b函数的模式可以在许多不同操作系统之间进行移植,因此它的运行特性与 D O S中的* . *不同。 问题:我修改了m y g r e p这个练习,以便使用 o p e n d i r和更多的循环来搜索子目录,但是它 似乎存在某些错误。为什么? 解答:总之,你不要这样做。向下搜索目录树是个老问题,它并不很容易,以前曾经多 次解决过这个问题,而你自己没有必要这样去做。(从事全部这项工作称为“重新发明车轮”。) 如果你只是因为好玩而这样做,这很好,但是不要在这上面耗费太多的时间。请等到第 1 5学 时,你将会了解到如何使用 F i l e::F i n d这个方法。它用起来更加简单,但是更加重要,它能 够进行程序调试。 问题:如果我将* . b a t改为* . t m p,程序清单1 0 - 3中的程序就会出错,为什么? 解答:该程序并不希望你键入 * . b a t作为要搜索的模式。在正则表达式中使用 * . b a t是无效 的,*必须放在另外某个字符的后面。如果你输入了 \ * \ . b a t,该程序完全可以接受这个输入, 不过它不会像你期望的那样运行,因为文件名中从来没有原义字符 *。 为了纠正这个错误,你可以为该程序提供它期望的输入(简单字符串),也可以将程序清 单的第1 9行改为s / \ Q $ o l d p a t / $ n e w p a t /,这样,正则表达式模式中的“特殊字符”将不起作用。 10.8.2 思考题 1) 若要输出文件f o o f i l e的上次修改时间,应该使用: a. print glob(“f o o f i l e”); b. print (stat(“f o o f i l e”) ) [ 9 ] ; c. print scalar localtime (stat (“f o o f i l e”) ) [ 9 ]; 2) unlink函数返回的是: a. 实际删除的文件数量。 b. 真或假,根据函数运行是否成功的情况而定。 c. 试图删除的文件数量。 10.8.3 解答 1) 答案是b或c。如果选择b,输出的时间为 1 9 7 0年以来的秒数,没有什么用处。如果 124使用第二部分 高 级 特 性 下载选择c,则输出格式很好的时间。 2) 答案是a。不过,选择c,在某种情况下也是可以的。如果没有文件可以删除, u n l i n k返 回0,表示假。 10.8.4 实习 • 设法编写一个程序,列出目录中的所有文件、它的子目录中的所有文件等。这只作为一 个编程练习。 第10学时 文件与目录使用125下载下载 第11学时 系统之间的互操作性 到现在为止,我们介绍的所有P e r l特性基本上都属于独立的特性。如果你想完成某项操作, 那么你必须自己亲自执行这项操作,比如给数据排序,创建目录列表,插入配置信息等。问 题是它的工作量很大,你必须重复进行可以在其他地方完成的工作。 关于P e r l,现在有一种非常流行的说法,那就是它是一种非常出色的“胶水”语言。它的 意思是说,P e r l能够使用操作系统作为组件来安装的其他程序,然后将这些程序组合起来,形 成一个更大的程序。它能够启动操作系统的实用程序,用它们来搜集信息,与你进行通信, 然后将它们关闭。 P e r l能够将这些较小的实用程序“胶合”在一起,形成一个大得多而且更加有用的实用程 序。这种能力的好处是使你能够迅速编写在其他情况下需要花费很长时间来编写的代码,并 且能够对代码进行调试。你应该使用对你有用的任何手段,迅速而准确地编写代码。将系统 的实用程序胶合在一起,可使之具备很大的优点。 在本学时中,你将要学习: • system()函数。 • 捕获输出。 • 代码的移植性。 本学时中的大部分代码例子都有两个版本,一个用于 Wi n d o w s和D O S系 统,另一个用于U N I X系统。如果只有一个代码例子,那么你会在课文中找到 关于如何修改该代码,以适合另一种系统的需要,这种修改通常是少量的更 改。 11.1 system()函数 若要运行非P e r l的命令,最简单的方法是使用 s y s t e m ( )函数。s y s t e m ( )函数能够暂停P e r l程 序的运行,然后运行外部命令,接着再运行你的 P e r l程序。s y s t e m函数的句法如下: 该语句中的c o m m a n d是你要运行的命令,如果一切正常,系统的返回值是 0,如果出现问 题,则返回非零值。请注意,这个结果与 Tr u e和F a l s e这两个P e r l标准返回值相反。 下面是在U N I X系统上运行s y s t e m函数的一个例子: 下面这个例子显示在D O S/Wi n d o w s下运行s y s t e m的情况:总的来说,s y s t e m函数在上述两种系统结构下的运行情况是相同的。应该记住的是,两 种操作系统下运行的命令基本上是不同的。若要在 D O S下获得一个文件列表,可以使用 d i r命 令,而在 U N I X下获得文件列表,要使用 l s命令。在非常少见的情况下,比如在 p e r l d o c中, U N I X和D O S系统下的命令是相同的。 当s y s t e m函数运行外部命令时,该命令的输出在屏幕上显示的情况与 P e r l程序输出的情况 是相同的。如果该外部命令需要输入数据,那么来自终端的输入与你的 P e r l程序从终端读入的 输入是相同的。 s y s t e m函数运行的命令继承了 S T D I N和S T D O U T文件描述符,因此,外部命 令执行输入/输出的位置与你的 P e r l程序执行输入/输出的位置是完全相同的。通过 s y s t e m函 数来调用完全交互式的程序是可能的。 请看下面这个在U N I X下运行的代码例子: 现在再看在Wi n d o w s/D O S下运行的代码例子: 上面的每个例子都对 m y f i l e . t x t运行一个编辑器, U N I X的编辑器是 v i,D O S的编辑器是 e d i t。当然编辑器是全屏幕运行的,所有的标准编辑器命令均能运行。当编辑器退出时,控制 权返回给P e r l。 可以使用s y s t e m函数运行任何程序,不只是控制台方式的程序(文本程序)。在U N I X下, 下面这个例子运行一个图形时钟: 在D O S/Wi n d o w s下,下面这个例子用于启动一个图形文件编辑器: 基本的命令解释程序 s y s t e m函数(以及本学时中介绍的大多数函数)允许你使用命令提示符向你提供的命令 解释程序的特性。之所以能够这样做,是因为 P e r l的s y s t e m命令能够调用一个 s h e l l(在U N I X 上是/ b i n / s h,在D O S/Wi n d o w s中是c o m m a n d . e x e),然后将你的s y s t e m命令赋予该s h e l l。这 样,你就能够在 U N I X(&)下,执行重定向(>)、管道传输(|)和后台操作等任务,并且 能够使用命令解释程序提供的其他特性。 例如,若要运行一个外部命令,并且捕获它在文件中的输出,可以使用下面这个命令: 上面这个命令用于运行 p e r l d o c的p e r l f a q 5,并且捕获它在文件中的输出,称为 f a q f i l e . t x t。 这个特定语句在D O S和U N I X中均能运行。 有些特性,比如管道传输和后台操作,在 U N I X操作系统下也能按照你的期望来运行,如 你在下面看到的那样: 第11学时 系统之间的互操作性使用127下载在最后一个代码举例中, x t e r m这个程序启动运行,但是 &将使U N I X的s h e l l启动“后台” 的进程。这意味着虽然该进程继续运行,但是 s y s t e m函数已经完成运行,并将控制权返回给 P e r l。P e r l将不等待x t e r m完成运行。 在U N I X下,P e r l总是将/b i n / s h或类似的命令用于 s y s t e m函数、管道和反 引号(后面将介绍)。不管你的个人s h e l l被配置成什么,它都是这样使用的。 这种用法提供了在各个U N I X系统之间的某种程序的可移植性。 本学时中使用s y s t e m函数、管道和反引号的例子放到 M a c i n t o s h系统上可能无法运行。详 细的说明请参见M a c P e r l文档中的“M a c i n t o s h的特殊特性”这一节。 11.2 捕获输出 s y s t e m函数有一个很小的不足,它没有提供特别好的方法,来捕获命令的输出,并将它 送往P e r l进行分析。如果要以迂回方式进行这项操作,你可以使用下面这个代码: 在上面这个代码段中, s y s t e m运行的命令让它的输出转到一个称为 o u t f i l e的文件中。然后 该文件被打开并读入一个数组。这时数据 @ d a t a包含了d i r命令的输入。 这个方法很麻烦,不是一个十分聪明的办法。 P e r l有另外一个方法处理这个问题,即反引 号。用反引号( ` `)括起来的任何命令均由 P e r l作为外部命令来运行,就像通过 s y s t e m运行的 一样,其输出被捕获,并且作为反引号的返回值返回。请看下面这个使用反引号的代码例子: 在上面这个代码段中,运行的是 d i r命令,其输出在$ d i r e c t o r y中捕获。 在反引号中,可以看到所有标准的 s h e l l处理方式: >负责重定向, | 负责管道传输。在 U N I X下,&负责启动后台任务。不过请记住,在后台运行的命令,或者用 >重定向其输出的 命令,均没有输出可以捕获。 在标量上下文中,反引号返回的命令输出是单个字符串。如果命令的输出包含许多行文 本,那么字符串中出现的所有文本行均用记录分隔符分开。在列表上下文中,命令的输出被 赋予该列表,列表的每一行结尾均有记录分隔符。 现在请看下面这个代码: 在上面这个代码段中,@ d i r中的输出在f o r e a c h循环中处理,每次处理一行。 P e r l还有另一种方法能够起到反引号的作用,你可以使用 q x { }表示法。要执行的命令放入 花括号({ })中,如下例所示: 128使用第二部分 高 级 特 性 下载通过使用花括号,当反引号作为命令的组成部分出现时,可以不必在反引号的前面加上 反斜杠,如下所示: 也可以将上面的代码段改写为下面的形式: 任何字符均可用来取代{ },成对的字符如< >,()和[ ]等均可以使用。 避免shell中的概念混乱 P e r l与命令解释程序之间的界线有时会变得比较模糊,请看下面的两个例子: 在U N I X系统中: 在D O S和Wi n d o w s系统中: 在第一个例子中, $ H O M E究竟是P e r l变量$ H O M E,还是s h e l l的环境变量$ H O M E呢?在 D O S例子中,% w i n d i r %究竟是c o m m a n d . c o m变量w i n d i r,还是P e r l的哈希结构% w i n d i r后随符 号%呢? 问题是$ H O M E被P e r l进行了内插替换,也就是说, $ H O M E是P e r l的标量变量$ H O M E,它 可能不是你想要的。在反引号中,变量展开为它们各自的值,就像使用双引号(“”)一样。 可能的变量名% w i n d i r并不在双引号中展开,只有标量和数组名进行了内插替换。 为了避免这种混乱,可以在不想让 P e r l进行内插替换的变量前面加上一个反斜杠,如下面 这两个例子所示: 或者 现在,$ H O M E是UNIX shell的H O M E变量,% w i n d d i r %是c o m m a n d . c o m的w i n d d i r变量。 另一种方法是用q x { }表示法来代替反引号,并用单引号来限定 q x,如下面这些例子所示: 或者 P e r l将qx” 序列视为特殊标号,并且不展开它里面的 P e r l变量,因此,你可以使用反引号, 并且不必用反斜杠对命令中出现的其他反引号进行转义。 11.3 管道 U N I X与D O S/Wi n d o w s中的管道可用于将不同进程连接在一起,使一个进程的输出成为 下一个进程序的输入。请看下面的一组命令,这些命令差不多可以在 U N I X(如果将d i r改为l s) 或D O S中运行: 第11学时 系统之间的互操作性使用129下载d i r的输出在o u t f i l e中集中,然后使用s o r t对o u t f i l e排序,同时s o r t的输出被存放在n e w f i l e中。 接着m o r e命令显示n e w f i l e的内容,每次显示一屏内容。 管道允许你执行与上面相同的命令序列,但是不带 o u t f i l e和n e w f i l e,如下所示: d i r的输出被赋予s o r t,然后s o r t对数据进行排序。 s o r t的输出被赋予m o r e, 每次显示1页。 它不需要重定向(>)或临时文件。 这种命令行称为管道命令行,两个命令之间的竖线称为管道。 U N I X主要依赖管道来连接 它的较小的实用程序。 D O S和Wi n d o w s均支持管道,但与管道一起运行的命令行实用程序要 少得多。 P e r l程序可以用不同方式纳入管道。首先,你可以编写一个 P e r l程序,以便接收输入,对 输入进行转换,然后插入管道,如下例所示: 在上面这个管道中,To t a l e r可以是你编写的P e r l程序,以便输出目录列表的合计,也可能 是某些统计数字,同时输出目录列表本身。如果你使用 U N I X系统,请将dir /B改为ls -1,管 道就会按照你的要求来运行。程序清单 11 - 1包含To t a l e r程序。 程序清单11-1 To t a l e r的完整清单 第6行:输入的每一行均从 S T D I N读入,再赋予$ _。在管道上,一个程序的 S T D I N被连接 到前一个程序的S T D O U T。因此,在上面的例子中, S T D I N由dir /B馈入信息。 第9 ~ 1 3行:如果遇到一个目录,在 $ d i r s中对它的号码单独相加,目录名被输出,循环再 次启动运行。 第1 4 ~ 1 5行:否则,在$ s i z e s中对文件的大小进行累加,文件名被输出。 第1 7 ~ 1 8行:输出文件的平均大小,同时输出文件和目录的总数。 P e r l参与管道运行的另一种方法是将管道视为既可以读取也可以写入的文件。这是使用 P e r l中的o p e n函数来实现的,如下所示: 在上面这个代码段中, o p e n函数打开一个管道,以便从 dir/B | sort中读取数据。P e r l从该 130使用第二部分 高 级 特 性 下载管道读取数据的这一事实是通过右边的最后一个管道( |)来指明的。当 o p e n函数运行时, P e r l启动执行dir /B | sort命令。当文件句柄R H A N D L E被读取时,s o r t的输出被读入P e r l程序。 现在请看下面这个例子: 这个o p e n函数打开一个管道,以便将数据写入 m o r e命令。左边的管道符号说明 P e r l正在将 数据写入管道。输出到W H A N D L E文件句柄的所有数据均被 m o r e缓存,并每次显示1页。编写 这样的函数是每次显示你的一页程序的输出的好办法。 当你完成对已向程序打开的文件句柄(如 R H A N D L E和W H A N D L E)的操作时,应该正 确地关闭句柄,这一点非常重要,因为由 o p e n函数打开的程序必须正确地关闭,若要关闭文 件句柄,可以使用 c l o s e函数来确保它的正确关闭。当完成句柄操作后,如果不能关闭文件句 柄,那么即使你的P e r l程序已经终止运行,你编写的程序仍会继续运行。 当关闭了管道上打开的文件句柄后, c l o s e函数会指明管道运行是否成功。因此,你应该 像下面这样认真检查c l o s e的返回值: o p e n函数也许无法告诉你管道是否已经成功地启动运行,其原因与 U N I X 的设计有关。当 P e r l创建管道并将它启动时,它不清楚管道是否真的能够工 作。如果管道组装正确,并且启动运行了,那么它认为它将能够正确地终止 运行。当管道中的最后一个程序完成运行时,它应该返回一个成功退出的状 态。c l o s e函数能够读取该状态,以了解是否一切运行正常,否则,就会产生 一个错误。 11.4 可移植性入门 可移植性,这是P e r l擅长的特性之一。无论你的 P e r l代码是在V M S计算机上运行,还是在 U N I X、M a c i n t o s h或M S-D O S系统下运行,都具有很强的可移植性,使你编写的 P e r l代码能够 在P e r l支持的任何结构上天衣无缝地运行。当需要与基本的操作系统打交道时,比如当进行文 件输入/输出时,P e r l将设法隐藏所有不必要的细节,使你的代码能够实实在在地运行。 P e r l之所以具有如此强的可移植性,第 1 6学时将对其中的某些原因进行 详细说明。 不过,P e r l能够向你隐藏的信息是要受到一定限制的。 在本学时中,有些代码例子讲明“这适用于 Wi n d o w s和D O S,而这适用于 U N I X”,有时 又说两种系统都适用,具体情况要根据你使用的系统结构而定。使你自己的程序同时适用于 Wi n d o w s和U N I X,这意味着每个程序必须创建两个版本,一个用于 Wi n d o w s,一个用于 U N I X。当你的程序运行成功并且移植到一个更加特殊的操作系统,如 MacOS 9,那么创建程 序的两个版本将会带来更多的问题。 许多情况下,你为一种操作系统结构(如 Windows NT)编写了一个程序,结果却发现它 是在另一种结构(比如 U N I X)中运行。由于P e r l能够在那么多的不同结构中运行,因此许多 人认为在Windows NT下运行P e r l程序与它在U N I X下运行是相同的。We b服务器和其他应用程 第11学时 系统之间的互操作性使用131下载序经常在不同的操作系统之间转移,因此使你的软件具备可移植性是个非常好的思路。 为每种操作系统创建一个程序的不同版本,使程序能够在任何情况下都能够运行,这是 很费时间、很浪费和低效率的。遵照一些规则,你就能够创建到处都能运行的程序,至少可 以设法使之到处都能运行,并且便于维护。 下面是编写“到处均可运行的”代码时遵循的一般原则: • 始终使警告特性处于打开状态,并使用 use strict命令。这样,就可以确保你的代码能够 用不同版本的P e r l来运行,并且不会出现明显的错误。 • 始终都要检查来自系统请求的返回值,例如,应该使用 open || die,而决不要只使用 o p e n。检查返回值可以在将应用程序从一个服务器移到另一个服务器(而不只是在不同 的操作系统之间移动)时帮助你发现错误。 • 输出表义性强的出错消息。 • 使用P e r l的内置函数,执行你要用s y s t e m函数或反引号(` `)来执行的操作。 • 将依赖系统执行的操作(文件 I/O,终端I/O、进程控制等)封装在函数中,检查以确保 这些操作受当前操作系统的支持。 前面两个原则你已经熟悉。在本书中,所有的代码例子均已检查了关键函数的退出状态, 而从第8学时起,所有较大的代码例子均展示了 use strict和警告消息。 第3个原则不能忽略,因为它是输出表义性很强的出错消息。在下面这些消息中,哪个最 有帮助呢? 显然,最后一条消息最有帮助。当你安装程序后,而且几个月(或几年)后出了问题, 最后一条消息能够说明哪个程序运行失败了( m y s c r i p t . p l),它想要什么(F o o f i l e . t x t),它为 何运行失败(没有这个文件⋯),它在何处运行失败了(第 2 4行)。这些信息可以帮助你迅速 排除故障。花费一点儿时间写一条很好的、表义性强的出错消息,总是值得的。 第4个原则意味着只要可能,你就应该使用 P e r l。若要检索目录列表,最好只使用 $ d i r = ` d i r `;但是,如果该程序移植到非 Wi n d o w s系统中,那么它的运行就会失败。一种好的 解决方案是使用 < * >,而更好的解决方案则是只要可能,就使用 o p e n d i r / r e a d d i r / c l o s e d i r函数。 无论你的程序移植到什么地方,这些解决方案都能运行。 举例说明两个操作系统之间的差别 编写“到处均可运行的”代码时遵循的最后两个原则,即“将依赖系统的操作封装在函 数中”以及“检查程序运行所在计算机上的操作系统”,还需要作一点补充说明和演示。 当你坐在计算机面前键入 P e r l程序时,应该记住,总有一天,你的 P e r l程序有可能在另一 台计算机上运行。你可能建立下一个 Amazon.com web站点,它可能从你的P C移植到一台大型 Windows NT服务器上,再移植到一群Sun 公司 1000 UNIX服务器上;或者你可能只有一些个 人的C G I程序,并且更换了We b提供商,结果发现新提供商配备的是一种不同的服务器。这些 情况经常会出现,必须加以考虑。 那么,你的程序究竟如何知道 Windows NT与U N I X之间的差别呢?这个问题很简单。 P e r l 132使用第二部分 高 级 特 性 下载有一个特殊变量 $ ^ O,即美元符号,插入记号 ^和大写字母O,这个变量包含了程序运行时所 在的操作系统结构。例如,在 Wi n d o w s和D O S下,它包含字符串M S Wi n 3 2。在U N I X下,它包 含你运行的U N I X类型,如l i n u x,a i x和s o l a r i s等。 下面是依赖你运行的操作系统而执行的一些操作任务: • 查找关于系统配置的各种信息。 • 对磁盘和目录结构进行操作。 • 使用系统服务程序(e m a i l)。 在下面这个例子中,可以查看一个代码段,以便找出系统上的可用磁盘空间。如果有人 想要将一个文件上载到一个服务器并且想要确定该文件是否适合在该服务器上运行,那么下 面这个例子是有用的。若要在 Wi n d o w s系统的当前目录中找出可用的磁盘空间,可以使用类 似下面的代码段: 上面这个代码段取出 @ d i r中的目录列表的最后一行,使用正则表达式删除不包括大小 (即bytes free前面的数字和逗号)在内的其他信息。最后,逗号被删除,这样, $ f r e e只包含原 始的空闲磁盘空间。这种方法非常适用于 Wi n d o w s系统。对于U N I X系统,尤其是 L i n u x,则 可使用下面这个代码段: 请注意上面这个代码段与前面那个代码段之间的差别。在 Wi n d o w s下查找磁盘空间的实用 程序是d i r,在U N I X下,它是df -k.。df -k输出的最后一行被分割成若干部分,第 4个域被放入 $ f r e e中。d f的输出随着U N I X系统的不同而各有差异,通常情况下,报告的域的数目是不一样 的,也可能它们使用不同的顺序。你的P e r l代码只需要选择一个不同的域,就很容易得到调整。 因此,现在有两种完全不同的例程可以用来确定空闲的磁盘空间。可以将它们组合起来, 并让适当的例程在每个操作系统上运行,如下所示: 第11学时 系统之间的互操作性使用133下载这个示例程序现在已经扩展为同时包括 D O S/Wi n d o w s版本和L i n u x版本。如果它在任何 其他操作系统的机器上运行,就会输出一条警告消息。该例程几乎已经运行结束。现在你需 要做的事情是在函数中将该例程隔离出来,使得需要的变量可以声明为专用变量,并且最后 的结果可以裁剪,粘贴到任何程序中,并在需要时随时使用。产生的结果代码是: 这时,每当你的程序需要了解空闲磁盘空间量时,只需要调用 f r e e s p a c e ( )函数即可,答案 将被返回。如果想在没有列出的另一个操作系统上运行该函数,就会输出一条出错消息。但 是,将另一个U N I X型O S添加给该函数,将是很难的,你只能添加另一个子句。 11.5 课时小结 本学时你学习了如何使用系统的实用程序来进行各项操作。使用 s y s t e m函数,可以运行 一个系统实用程序(或者一个管道程序)。反引号(` `)能够运行一个系统实用程序,然后捕 获它的输出。接着,捕获的输出存放在一个变量中,供 P e r l使用。o p e n函数不仅可以打开文件, 而且可以打开程序。该程序可以用 p r i n t函数写入尖括号运算符( < >),或者从尖括号运算符中 读取数据。最后,我们介绍了将这些实用程序用于许多不同类型的操作系统的方法,但是不 必为每种系统编写不同的程序。 11.6 课外作业 11.6.1 专家答疑 问题: 如何打开既可以到达一个命令也可以来自一个命令的管道?例如:open (P, “| cmd |”) 134使用第二部分 高 级 特 性 下载这个管道似乎并不能运行。为什么? 解答:这项操作实际上相当复杂,因为从同一个进程读取和写入数据可能导致程序“死 锁”。这时,你的程序期望 c m d输出某些信息,并且等待带有 < P >的数据。同时,因为存在某 些混乱状态,c m d实际上等待你的程序输出带有 print P“. . .”的某些数据。事实上,如果你激 活了警告特性,P e r l将向你报一条消息:“C a n’t do bidirectional pipe(无法执行双向管道操 作)”。 如果你为这种问题做好了准备, I P C::O p e n 2模块将允许你打开一个双向开关。这些模 块将在第1 4学时中介绍。 问题:代码$ a = s y s t e m(“c m d”)无法按我期望的那样捕获 $ a中的c m d的输出。为什么? 解答:你将s y s t e m与反引号(` `)的作用混淆了。 s y s t e m函数不能捕获c m d的输出,你需 要的代码是$ a = ` c m d `。 问题:当我在U N I X下运行带有反引号( ` `)的外部程序时,没有捕获出错消息,原因何 在? 解答:由于所有U N I X程序,包括P e r l程序,都包含两个输出文件描述符,即 S T D O U T和 S T D E R R。文件描述符 S T D O U T用于捕获正常程序输出。文件描述符 S T D E R R用于捕获出错 消息。反引号和带有管道的 o p e n函数只能捕获S T D O U T。简单的答案是使用 s h e l l将S T D O U T 重定向到S T D E R R,然后运行下面这个命令: P e r l的FA Q(常见问题)详细介绍了这个命令和捕获命令的错误时使用的其他方法。键入 perldoc perlfaq8,便可打开FA Q的有关内容。 11.6.2 思考题 1) 若要使你的程序生成的数据每次显示 1页,应使用: 2) $foo的哪个值用于$r=`dir $foo`这个语句? a. $foo的s h e l l的值。 b. Perl的$ f o o值被替换,然后运行d i r。 3) 下列操作中的哪个操作随着操作系统的不同而变更? a. 找出空闲磁盘空间的容量 b. 获取目录列表 c. 删除目录 11.6.3 解答 1) 答案是a或b。如果选择a,m y p r o g . p l的所有输出均送入m o r e。如果选择b,那么写入文 件描述符M的任何数据均送入m o r e,以便进行分页。 2 ) 答案是b。若要防止$ f o o被P e r l展开,你可以使用qx`dix $foo`。 3) 答案只能是 a。b的操作可以用 g l o b,< * >或o p e n d i r和r e a d d i r来完成。 C可以用r m d i r 第11学时 系统之间的互操作性使用135下载完成。 11.6.4 实习 • 使用第8学时中的统计函数,显示程序清单 11 - 1中关于文件大小的更多的统计数据。 • 如果你拥有U N I X系统,将你的U N I X的特定样式添加给f r e e s p a c e ( )函数。使用L i n u x作为 练习的开始。 136使用第二部分 高 级 特 性 下载下载 第1 2学时 使用P e r l的命令行工具 到现在为止,P e r l一直只是一个非常简单的解释程序。你将一个程序键入一个文件,然后 调用P e r l解释程序,以便运行你的程序。不过 P e r l解释程序比这个程序灵活得多。 P e r l解释程序中内置了一个调试程序。使用该调试程序,可以像播放录像带那样运行你的 P e r l程序。可以将程序倒到开头,使它慢速运行,也可以快速运行,还可以将它定格,以仔细 观察程序的内部结构。在查找 P e r l程序中存在的问题时,调试程序常常是个使用得很不充分的 工具。 P e r l也能运行不是键入文件的程序。例如,可以直接从系统的命令提示符处运行一些小程 序。 在本学时中,你将要学习: • 如何使用P e r l的调试程序。 • 如何使用命令行开关来编写 P e r l程序。 12.1 什么是调试程序 P e r l调试程序是个 P e r l解释程序的内置特性。它使你能够取出任何一个 P e r l程序,然后逐 个语句运行该程序。在运行过程中,你可以查看各个变量,修改这些变量,让程序运行较长 的时间,中断程序的运行,或者从头开始运行该程序。 从你的程序角度来看,它与普通程序并无区别。输入仍然来自键盘,输出仍然送往屏幕。 程序并不知道何时停止运行,何时它正在运行。实际上,你可以观察程序的运行情况,根本 不必中断程序的运行。 12.1.1 启动调试程序 若要启动P e r l调试程序,必须打开操作系统的命令提示符。如果你是 D O S和Wi n d o w s用户, 那么要打开M S - D O S的标准提示符C : \。如果是U N I X用户,这个提示符应该是你登录时显示的 提示符(通常是%或$)。 对于运行P e r l的M a c i n t o s h用户来说,只需从S c r i p t菜单中选定D e b u g g e r。这时就会为你打 开带有提示符的D e b u g g e r窗口。 本节中的所有代码例子均使用第 9学时的程序清单 9 - 2中的E m p l o y e e程序。你会发现可以 很容易将一个书签放在这一页上,然后前后翻阅,查找你想要的信息。若要在提示符处启动 调试程序(本例中使用D O S提示符),请键入下面这行命令: P e r l的- d开关可使P e r l以调试方式启动运行。命令行上也指明了被调试的程序。然后显示 关于版本信息的某些消息,如下所示:该调试程序首先显示版本号( 1 . 0 4 0 1,你的版本号可能不一样)和 help (帮助)提示。接着 显示该程序的第一行可执行代码。由于第一个语句实际上包含 7行,从“my @employees=(” 开始,以“);”为结尾,因此所有 7行语句均显示一个描述,以说明它们来自什么文件 (E m p l o y e e),以及它们是在文件的哪一行或哪几行上找到的(第 5至第11行)。 最后,你看到调试文件的提示符 D B < 1 >。1表示调试文件正在等待它的第一个命令。调试 程序提示符后面的光标正等待你输入命令。 这时,你的P e r l程序实际上暂停在第一个指令 -my @employees=(的前面。每当调试程序向 你显示程序中的一个语句时,它就是准备要执行的语句,而不是上一个运行的语句。 现在调试程序已经作好准备,等待你输入命令。 12.1.2 调试程序的基本命令 输入调试程序的第一个和最重要的命令是 h e l p(帮助)命令。如果在调试程序的提示符处 键入h,那么调试程序的所有可用命令均被输出。也可以使用该命令的某种变形,如 h h,它 能输出命令和语句的汇总, h cmd用于输出某个命令的帮助信息。 帮助命令的列表也许比较长,一个屏幕显示不下,开头的几个命令显示后,就需要向下 滚动。若要每次显示一屏调试命令,可以在命令的前面加上一个 |字符。因此,如果想每次查 看一屏帮助命令,请使用命令 | h。 调试程序的最常用特性是每次运行一个 P e r l代码的指令。因此,如果继续使用上面的例子, 若要转至你的P e r l程序的下一个语句,可以使用调试程序的命令 n: 当你键入命令 n后,P e r l就执行E m p l o y e e程序的第5至11行语句。然后调试程序输出要执 行的下一个语句(但尚未运行) m y ( $ L 1,$ F 1 ) = s p l i t(’,’,$ a);并显示另一个提示符。 当程序运行到这个时候, @ e m p l o y e e s被初始化为5个名字和工资等。若要查看这些信息, 可以将它们输出: 实际上,P e r l的任何语句都可以在调试程序提示符后面运行。请注意,来自 @ e m p l o y e e s 的数组元素都是一道运行的。可以输入下面的命令,以便很好地将它们输出: 138使用第二部分 高 级 特 性 下载若要继续运行该程序,只需不断键入 n,如下所示: 显然,调试程序将该程序倒退到这个位置,第 2 3行准备再次运行。 P e r l的s o r t语句实际上 是个循环,调试程序逐步通过 s o r t代码块中的每个语句。如果你不断键入 n,那么调试程序将 不断循环运行,直到s o r t运行结束,这需要花费一定的时间。 若要重复运行上面的命令,也可以在调试程序的提示符处按 E n t e r键。 12.1.3 断点 如果不是每次执行一个指令,逐步执行该程序,你也可以让调试程序连续运行你的 P e r l程 序,直到到达某个语句,然后停止运行。这些停止运行的位置称为断点。 若要设置断点,必须在程序中选定一个要停止运行的位置。命令 l用于列出程序的下面 1 0 行。再次键 l,可以列出下面的 1 0行,如此类推。若要列出从某一行开始的程序,请键入 l l i n e n o,其中l i n e n o是程序的行号。也可以设定要列出的行的范围,方法是键入命令 l start-end。 在程序清单中,标号= = = >用于指明调试程序准备执行的当前行,请看下面的代码: 在这个例子中,第3 3行是设置断点的好位置。它位于 s o r t语句的后面,并且它是程序的主 循环中的第一个语句。你可以在 P e r l程序中的任何位置上设置断点,只要这个断点是个有效的 P e r l语句。但是断点不能设置在花括号(第 3 0行)、标点符号(第 2 9行)、空行(第3 1行)或 只包含注释的代码行上。 若要设置断点,请使用 b breakpoint命令,其中b r e a k p o i n t可以是行号或子例程名。例如, 第12学时 使用Perl的命令行工具使用139下载若要在第3 3行上设置断点,可以输入下面这个命令: 必须知道的另一个关于断点的命令是继续命令 c。命令c向调试程序发出指令,使 P e r l程序 运行到下一个断点或程序的结尾: 在这个代码中,调试程序按照要求使P e r l程序停止在第3 3行上,p r i n t - e m p函数被调用之前。 断点仍然可以设置,因此,键入另一个 c子句,可使程序继续运行 p r i n t _ e m p ( )函数,并且再次 停止在第3 3行上: 若要查看你在程序中已经设置的断点,可以像下面这样使用命令 L: 这个例子显示了调试程序有一个断点,在文件 E m p l o y e e中的第3 3行上。 若要撤消程序中的断点,可以采用设置断点时的相同方法使用命令 d,比如 d line或d s u b n a m e: 12.1.4 其他调试程序命令 如果想查看p r i n t - e m p ( )函数的运行情况,可以用若干不同的方法来执行这项操作。首先使 用命令R,重新启动你的程序: 140使用第二部分 高 级 特 性 下载 “下一个”指令的命令 print_emp ( ) 的输出 待运行的下一行代码命令R用于使P e r l程序回到它的开始处,准备再次执行该程序。你已经设置的断点保持已 设置状态,P e r l程序中的所有变量均复位。在上面这个代码中,断点设置在第 3 3行上。这时可 用下面的命令继续运行该程序: 如果执行命令n,就可以执行下面的指令: 但是,如果用这种方法逐步执行该程序,你就无法查看 p r i n t - e m p ( )中究竟有什么。若要单 步进入p r i n t - e m p ( ),不要使用命令n,而应该使用命令s,即单步执行命令。s命令与n命令的作 用很相似,不过s命令并不仅仅执行函数,然后转入下一个指令,而是执行函数,然后在函数 中的第1个指令处停止运行,如你在下面看到的情况一样: 在这里,显示了 p r i n t _ e m p ( )的第 1个语句。也可以用 b print_emp设置一个断点,在 p r i n t _ e m p ( )中停止运行。现在可以继续运行该程序,使用命令 n逐步通过该函数,如下所示: 还可以在P e r l程序运行时修改程序里的变量。例如,若要给员工每小时临时增加 2 . 5 0美元 的工资,可以输入下面的代码: 在上面的代码段中,变量 $ h o u r l y被输出( 1 0.1 5),并增加 2 . 5 0,然后程序继续运行。 P r i n t f语句输出$ h o u r l y的新值。 最后,若要退出调试程序,只需在调试程序提示符处键入 q。 12.2 练习:查找错误 这个练习向你展示如何使用调试程序来查找程序中的错误。程序清单 1 2 - 1中的程序存在 第12学时 使用Perl的命令行工具使用141下载一个问题(实际上是两个问题)。它应该输出下面这些消息: 但是它并没有输出这些消息。你的任务是键入程序清单 1 2 - 1中的程序,并没法找出里面 的错误。这些错误都不属于语句问题, P e r l的警告特性没有被激活,同时, use strict没有输出 任何消息,不过调试程序应该使得错误可以非常容易地被找到。 当你键入程序后,用调试程序运行 P e r l,以便设法找到错误。请记住,要经常输出有关的 变量和表达式,并单步通过各个函数调用,每次调用一个函数。 程序清单12-1 Buggy程序 这个问题的解决办法请参见本学时结尾处的“思考题”。 12.3 其他命令行特性 调试程序并不是P e r l解释程序中可以用命令行开关激活的惟一特性。实际上,许多非常有 用的P e r l程序都可以在命令行提示符处编写。 M a c i n t o s h用户应该通过 S c r i p t菜单来运行这些命令行代码,方法是选定 l - l i n e r s,然后将命令键入对话框。 12.3.1 单命令行程序(One-Liners) 这种程序的关键是在命令行上赋予 P e r l的- e开关。- e的后面可以是任何 P e r l语句,如下例 所示: 你可以使用多个- e开关来插入多个语句,或者用分号将这些语句隔开,如下所示: 142使用第二部分 高 级 特 性 下载应该注意的是,大多数命令解释程序都规定了引号的使用规则。 Wi n d o w s / D O S命令解释 程序(c o m m a n d . c o m或N T的命令S h e l l)允许你使用双引号将单词括起来,比如上例中的 p r i n t 和Hello Wo r l d,但是不能随意将双引号放入别的双引号中,也不能随意将 >、<、|或^等符号 放入双引号中。关于D O S / Wi n d o w s中引号的详细使用规则,请查看你的操作系统手册。 在U N I X下,一般来说,只要引号配对,即每个开引号有一个闭引号,并且嵌入的引号前 面有一个反斜杠\,那么你的代码就没有问题: 上面这个代码在大多数 UNIX shell(csh、k s h、b a s h等)下均能运行,并能输出带有正确换 行符的消息。若要了解你的 s h e l l的引号使用规则的完整列表,请查看 s h e l l的在线手册页。 - e开关的一个经常的非常有用的用法是与 - d组合起来使用,并将它直接放入 P e r l的调试程 序中,但是没有需要调试的程序: 这时,调试程序就等待你输入命令了。这种特殊的开关并置方法可以用来测试 P e r l语句, 而你不必编写完整的程序。然后再进行测试、调试、编辑、再测试和再调试。你只需要在调 试程序中运行你的语句,直到它能够运行为止。命令行上的 1是指最起码的P e r l程序,它是计 算为1并且返回1的一个表达式。 12.3.2 其他开关 P e r l解释程序中的- c开关可供P e r l用来查看你的代码,以便找出语句上的问题,但是它实 际上并不运行程序: 如果出现语句错误,P e r l就会输出下面这样一条消息: 与- w组合起来后,- c开关就能对你的程序进行编译,然后显示 P e r l认为适当的警告消息。 当你向知识更丰富的P e r l用户或系统管理员了解调试代码的情况时,常常必须提供正在使 用的P e r l解释程序的版本。目前正在使用的 P e r l语言的主要版本是 Perl 5。该解释程序本身有 一个可以查看的版本,你可以在命令行上使用开关 - v,便可了解该版本号,如下如示: 第12学时 使用Perl的命令行工具使用143下载在上面这个代码中,P e r l解释程序的版本是5 . 0 0 4 _ 0 2。若要了解更加详细的信息,比如该 解释程序是如何创建的,何时创建的,等等,可以运行带有 - v开关的解释程序,如下所示: 如果你试图查找P e r l解释程序本身的某个问题,比如安装时出现的一个问题,那么上面这 个输出可能对你有用。在这个输出的结尾处,请注意 @ I N C的各个值。这个特定安装的 P e r l希 望在这些目录中找到它的各个模块。当 P e r l安装后,它不能简单地从一个目录移动到另一个目 录。该解释程序本身对于何处能找到它的各个模块有一个内置的思路,如果改变这个思路, 就会导致P e r l到错误的地方去查找它的模块。关于模块的问题,将在第 1 4学时中详细介绍。 12.3.3 空的尖括号与更多的单命令行程序 迄今为止介绍的尖括号运算符( < >)具有两个功能: 1) 如果尖括号中间是文件句柄,尖括号运算符允许你读取文件句柄,比如 < S T D I N >。 2) 如果尖括号中间是搜索模式,尖括号运算符能返回与该模式匹配的文件列表,这称为 一个g l o b,比如< * . b a t >。 尖括号运算符还有另一个功能。一组尖括号运算符如果中间没有任何东西,那么它可以 读取命令行上所有文件的内容;如果没有给出文件名,则可以读取标准输出。有时空尖括号 运算符称为菱形运算符(因其形状而得名)。例如,请看下面这个小型 P e r l程序: 如果将上面的程序保存为E x a m p l e . p l,那么用下面这个命令行运行该程序: 就可使运算符< >读取f i l e 1的内容,每次读1行,然后读取f i l e 2,接着读取f i l e 3。如果没有 设定文件,则尖括号运算符从文件句柄 S T D I N中读取数据。这个运行特性类似 U N I X实用程序 S e d、a w k等的特性,如果命令行上设定了文件,则从文件中读取输入,否则读取标准输入。 P e r l程序的参数,即去掉 P e r l的参数- w、- c、- d和- e之后,将被存放在称 为@ A R G V的数组中。例如,对于上面这个代码段的参数, $ A R G V [ 0 ]将包含 f i l e 1,$ A R G V [ 1 ]包含f o l e 2,如此等等。 P e r l程序的- n开关可用于将任何- e语句封装在该小程序中: 144使用第二部分 高 级 特 性 下载因此,如果要创建一个简短的单命令行程序,从输入数据中删除前导空格,你可以编写 下面的命令: 上面这个命令实际上运行类似下面这个 P e r l程序: 在上面这个代码段中,名字为 f i l e 1的文件被打开,并被赋予 w h i l e循环中的$ _,每次1行。 该行用S / ^ \ S + / / g进行编辑,然后进行输出。 - p与- n开关的作用相同,差别在于语句执行后各个 文件行便自动输出。因此,重新编写上面这个命令行便产生下面这个命令行: 当你用P e r l的单命令行程序来编辑一个文件时,必须注意不要在打开文件进行读取操作的 同时,又试图对它进行写入操作,像下面这个例子那样: 上面这个代码段试图从称为 d o s f i l e的文件中删除回车符。问题是在 P e r l命令被处理之前, d o s f i l e文件已经被> d o s f i l e改写。编辑文件的正确方法应该是将输入重定向到另一个文件中, 并将文件改为它的原始名字,如下所示: 有些P e r l爱好者认为,编写简短的“单命令行程序( o n e - l i n e r s)”只不过 是一种娱乐。他们认为,程序越复杂,功能越多,就越好。 Perl Journal是介 绍P e r l的一份季刊,它在每一期上刊登了许多单命令行程序。 12.4 课时小结 在本学时中,我们介绍了如何有效地使用调试程序来查找 P e r l程序中存在的问题;介绍了 尖括号运算符( < >)的另一个功能,它使 P e r l能够处理命令行上的所有文件;另外,还介绍 了如何使用P e r l解释程序的- n和- p开关,来编写小型单行P e r l程序。 12.5 课外作业 12.5.1 专家答疑 问题:我真的希望P e r l有一个图形调试程序。是否存在这样的东西? 解答:是的,有几个这样的调试程序。如果你在 Wi n d o w s下使用P e r l,A c t i v e s t a t e拥有很 好的图形调试程序。 问题:调试程序不断输出的m a i n::究竟是个什么东西? 第12学时 使用Perl的命令行工具使用145下载解答:它与P e r l的程序包命名约定有关。它的一些情况将在下一个学时中介绍,因此现在 你不必对它考虑太多。 问题:P e r l是否还有别的命令行开关? 解答:是的,还有一些。你可通过在线手册查看这些开关的完整列表。若要访问这些信 息,请在命令提示符处键入 perldoc perlrun。 12.5.2 思考题 1) 程序清单1 2 - 1中存在哪些错误? 2) 如果命令行上没有给定文件,那么读取 < >时将返回 a. undef。 b. 来自标准输入的数据行。 c. Tr u e。 3) Prel调试程序执行时能够输出 P r e l语句,这称为跟踪方式。你如何使调试程序进入跟踪 方式呢?(提示:必须查看调试程序的帮助消息,才能回答这个问题。) a. 使用T命令,使之进入跟踪方式。 b. 使用t命令,使之进入跟踪方式。 12.5.3 解答 1) 首先,在第1 5行中,范围(2 0 . . 0)无效。范围运算符“. .”不应该是降序,而应该是升 序。这一行应该改为 f o r($ _ = 2 0;$ _ > - 1;$ _ -)循环,将范围倒过来( 0 . . 2 0),或者使用类似 的范围。其次,在第 1 0行上,$ m e s s = s / g l a s s e s / g l a s s /看上去像是对$ m e s s的替换表达式,但实 际上并非如此。替换实际上是对 $ _进行的,因为赋值运算符( =)应该是个连接运算符(= ~)。 2) 答案是b。如果没有给定文件名,那么 < >便开始读取S T D I N。 3) 答案是b。t命令能够在程序执行时输出程序的所有语句。T命令用于输出堆栈跟踪记录, 这是当前正在执行的函数、调用该函数的函数等的列表。 146使用第二部分 高 级 特 性 下载下载 第1 3学时 引用与结构 如果P e r l是你使用的第一个编程语言,那么本学时将会使你感到颇有兴趣。在大多数编程 语言中,你会发现一个概念,即一组数据实际上可以是对另一组数据的引用。有时这些引用 称为指针(在p a s c a l或C语言中),有时这种技术称为间接引用(在汇编语言中),而有些语言 则根本没有指针的概念(在 B A S I C或J a v a中)。如果你以前从未使用过引用、指针或间接引用 等概念,那么可能必须多次阅读本学时讲解的某些部分的内容,否则会感到混淆不清。 P e r l也拥有这些特殊类型的值,不过在 P e r l中,它们都称为引用。在 P e r l中,引用可以用 于许多目的,但在本学时中,你要学习的是如何使用引用来调用带有多个参数的复杂函数和 如何创建复杂的数据类型,如列表的列表。 所谓引用,它非常类似老式图书馆中的卡片目录。目录中的每个索引卡指的是图书馆中 的一本书。卡片可以指明这本书是什么类型的书(比如小说、非小说、参考书等),并指明这 本书放在什么位置。有些卡片目录可能配有对同一本书的若干个引用,它们是不同种类的引 用,并且甚至可以参见该目录中的其他卡片。 P e r l的引用类似卡片目录,可以指向各组数据。引用能够知道它指向的是何种类别的数据 (如标量、数组或哈希),也知道这些数据在什么地方。引用可以被拷贝,但不改变原始数据 的任何东西。对于同一组数据,可以进行多次引用。实际上一个引用可以指向其他的引用。 请牢记下面这些要点,慢慢阅读下面几页内容,并且在我们介绍有关的问题时保持清醒 的头脑: • 引用的基本概念。 • 引用的常见结构。 • 运用所有这些概念而建立的一个简要代码例子。 13.1 引用的基本概念 使用赋值运算符,可以创建和赋值一个普通的标量变量,如下所示: 在这个代码段建立后,可以创建一个称为 $ a的标量变量,它包含字符串“ S t o n e s”。到现 在为止,一切都很正常。这时,在计算机中的某个地方有一个标为 $ a的位置,它包含了该字 符串,如下图所示: 如果将标量$ b赋予$ a,比如$ a = $ b,那么会产生该数据的两个拷贝,它们使用两个不同的 名字,如下图所示: 如果你想要两个独立的数据拷贝,那么拥有两个拷贝是很好的。但是,如果想让 $ a和$ b 都引用同一组数据,而不是引用一个数据拷贝,那么必须创建一个引用。所谓引用,它只是指向一组数据的指针,并不包含实际数据的本身。该引用通常存放在另一个标量变量中。 若要创建对某个既定变量的引用,可以在该变量的前面加上一个反斜杠。例如,若要创 建称为$ r e f的对$ a的引用,只需要像下面这样将引用赋予 $ r e f即可: 这个赋值创建了类似下面这样的条件: $ r e f并不包含用于它自己的任何数据,它只是对 $ a的一个引用。变量$ a根本没有改变,它 仍然可以照常被赋值($ a =“F o o”)或显示(print $a)。 变量$ r e f现在包含对$ a的引用。不能简单地对 $ r e f进行操作,因为它里边没有通常的标量 值。实际上,如果输出 $ r e f,就会显示类似S C A L A R ( 0 x 0 0 0 0 )的信息。若要通过 $ r e f获得$ a中 的值,必须间接引用$ r e f。间接引用可以被视为上面的方块图中按箭头方向的引用。若要通过 引用$ r e f来输出$ a的值,你可以像下面这样使用另一个 $: 在上面的代码段中,$ r e f当然包含了引用。增加的一个 $告诉P e r l,$ r e f中的引用指的是一 个标量值。$ r e f引用的标量值被取出并输出。 也可以通过引用来修改原始值,这是你对数据拷贝所不能进行的操作。下面这个代码用 于修改$ a中的原始值: 这项修改形成了类似下面这样的引用关系: 如果你使用$ r e f而不是$ $ r e f 那么存放在$ r e f中的引用将被撤消并被实际值取代,如下所示: 上面这个代码段运行后, $ r e f不再包含一个引用,它只是一个标量。你可以像任何其他标 量值那样,给引用赋值: 得到的结果如下: 上面的代码段运行后, $ $ o r e f和$ $ n r e f均可用于获取值“ G a n d a l f”。也可以存放对某个引 用的引用,如下所示: 148使用第二部分 高 级 特 性 下载在这个例子中,引用链接类似下面的形式: 如果使用$ b r e f 2来输出书名,那么该引用将是$ $ b r e f 2,如果使用$ b r e f,则该引用是$ $ b r e f。 请注意,$ $ $ b r e f 2多了一个美元符号,它需要增加一层间接引用,才能获得原始值。 13.1.1 对数组的引用 也可以创建对数组和哈希结构的引用。可以像创建对标量的引用那样,使用反斜杠来创 建对数组和哈希结构的引用: 现在标量变量$ a r e f包含了对整个数组@ a r r的引用。直观地说,它类似下面的形式: 若要使用引用$ a r e f来访问@ a r r的各个部分,你可以使用下列代码之一: $ $ a r e f [ 0 ] @ a r r的第一个元素 @ $ a r e f [ 2,3 ] @ a r r的一个片 @ $ a r e f @ a r r的整个数组 为了清楚起见,可以使用花括号将引用与涉及数组的各个部分隔开,如下所示: $ $ a r e f [ 0 ] 与 $ { $ a r e f } [ 0 ]相同 $ $ a r e f [ 2,3 ] 与 $ { $ a r e f } [ 2,3 ]相同 @ $ a r e f 与 @ { $ a r e f }相同 例如,若要使用数组引用$ a r e f,以便输出@ a r r的所有元素,可以使用下面这个代码: 13.1.2 对哈希结构的引用 若要创建对哈希结构的引用,可以使用反斜杠,就像创建标量和数组的引用那样: 上面这个代码段用于创建对哈希结构 % h a s h的引用,并将它存放在 $ h r e f中。这个代码段 创建的引用结构类似下面的形式: 若要使用对哈希的引用% h r e f来访问% h a s h的各个部分,可以使用下面这些代码例子: $ $ h r e f { k e y } 访问% h a s h中的一个关键字,也可以是 $ { $ h r e f } { k e y } % $ h r e f 访问整个哈希结构,也可以是 % { $ h r e f } 若要迭代通过该哈希结构,输出所有的值,可以使用下面这个代码: 第13学时 引用与结构使用149 关键字 数据 数据 数据 数据 关键字 关键字 关键字 数据 数据 数据 数据 数据 下载13.1.3 作为参数的引用 由于整个数组或哈希结构均可被引用,并且该引用可以存放在一个标量中,因此,借助 这些引用,你可以调用带有多个数组或哈希结构的函数。 你可能还记得第8学时中我们讲过,下面这种代码段是不能运行的: 这个代码不能运行,因为 g e t a r r a y s ( @ f r u i t,@ v e g g i e s )将两个数组压缩到单个数组 @ _中。 在g e t a r r a y s ( )函数中,将@ a和@ b赋予@ _,会导致现在存放在@ _中的@ f r u i t s和@ v e g e t a b l e s的 所有元素都被赋予@ a。 当所有数组挤入@ _之后,就没有办法知道一个数组在何时结束和下一个数组在何时开始。 只有一个很大的统一的列表。 这就是引用可以发挥作用的地方。你不必将整个数组传递给 g e t a r r a y s,只要传递对这些数 组的引用,就能够很好地达到你的目的: 函数g e t a r r a y s ( )总是接收两个值,即两个引用,无论这些引用指向的数组有多长。这时, $ f r u i t _ r e f和$ v e g _ r e f可以用来显示或编辑数据,如下所示: 当你将对标量、数组或哈希结构的引用作为参数传递给函数时,有几个问题必须记住。 当你传递引用时,函数能够对引用指向的原始数据进行操作。请看下面这些例子: 150使用第二部分 高 级 特 性 下载在左边的例子中,当按正常情况传递哈希结构时, @ _取得原始哈希结构 % h a s h中每个关 键字值对的各个值。在子例程 c h a n g e h a s h ( )中,现在放入@ _中的哈希结构的各个元素被拷贝 到称为% l o c a l _ h a s h的新哈希结构中。哈希 % l o c a l _ h a s h被修改,该子例程返回。当子例程返回 后,% l o c a l _ h a s h就被撤消,而程序的主要部分中的 % h a s h则保持不变。 在右边这个例子中,对% h a s h的引用通过@ _被传递到子例程c h a n g e h a s h ( )中。该引用被拷 贝到标量$ h r e f中,它仍然指原始哈希 % h a s h。在子例程中, $ h r e f指向的哈希结构被修改,子 例程返回。c h a n g e h a s h ( )返回后,原始哈希结构% h a s h将包含新关键字b e a r。 当数组@ _用于传递子例程参数时,它是个引用的数组。修改 @ _数组的 元素就会改变传递到函数中的原始值。修改传递给子例程的参数,通常被认 为是不慎重的一种做法。如果你想让子例程修改传递给它们的参数,那么应 该传递对子例程的引用。这种操作方法更加清楚。当传递一个引用时,可以 认为原始值是可以修改的。 13.1.4 创建各种结构 创建对数组和哈希结构的引用,可以用来与子例程之间来回传递这些结构,并且可以用 来创建下面我们很快就要介绍的一些复杂结构。不过你应该知道,当你创建了对哈希结构或 数组的引用后,就不再需要原始哈希结构或数组。只要对哈希结构或数组的引用存在,即使 原始数据不再存在,P e r l仍然保留着哈希结构和数组的各个元素。 在下面的代码段中,代码块中创建了一个哈希结构 % h a s h,并且这个哈希结构是该代码块 的专用结构: 在这个代码块中,标量$ h r e f被赋予对% h a s h的引用。当该代码块存在时,即使 % h a s h已经 消失,$ h r e f中的引用仍然有效(因为 % h a s h是代码块的专用结构)。当结构本身已经超出作用 域之后,对该结构的引用仍然可以存在, $ h r e f引用的哈希结构仍然可以修改。 如果你观察上面这个代码块,就会发现,它的唯一目的是创建对哈希结构的引用。 P e r l提 供了一个机制,可以用来创建这样的引用,而不必使用中间的哈希结构 % h a s h。这个机制称为 匿名存储。下面这个例子创建了一个对匿名哈希结构的引用,并把它存储在 $ a h r e f中: 花括号({ })将哈希结构括起来,返回对它的引用,但实际上并没有创建新的变量。你 第13学时 引用与结构使用151下载可以使用前面的“对哈希结构的引用”这一节中介绍的所有方法,对匿名哈希结构进行操作。 也可以使用方括号([ ])创建匿名数组。 同样,也可以使用前面的“对数组的引用”这一节中介绍的方法对数组的引用进行操作。 当引用的变量本身超出作用域时(如果它是个专用变量),那么该引用指向的数据将全部 消失,如下所示: 如果use strict正在运行,那么上面这个代码段甚至不进行编译。 P e r l将$ r e f的最后一个实 例视为全局变量,这是不允许的。即使没有 use strict,P e r l的- w警告特性也会输出一个 undefined value(未定义的值)消息。 这些匿名哈希结构和匿名数组可以组合成某些结构形式,我们将在下一节中介绍这些结 构。每个哈希结构和数组的引用代表一个标量值,并且由于它是单个标量值,因此可以存放 在其他数组和哈希结构中,如下所示: 13.2 结构的配置方法 下面各节将介绍列表和哈希结构的一些常用结构配置方法。 13.2.1 一个例子:列表中的列表 在P e r l中,列表中的列表常常用来代表一种称为二维数组的结构。也就是说,标准数组是 个值的线性列表,如下所示: 二维数组类似一个值的表格,里面的每个元素按照轴上的一个点来进行编址。索引的第 一部分表示行号(从0开始),第二部分是列号,请看下图: 152使用第二部分 高 级 特 性 下载 值1 数据 数据 数据 数据数据数据 数据 数据 数据 值2 值3 值4P e r l实际上并不支持真正的二维数组。 P e r l允许你使用数组引用的数组,模仿建立二维数 组。 若要创建数组的数组,请使用下面这个原义表达式: 请认真观察上面的代码段。它创建了一个正则列表 @ l i s t _ o f _ l i s t s,但是它由对其他列表的 引用所组成。若要访问最里层的列表的各个元素(即二维数组中的单元格),可以使用下面这 个代码: 若要确定最外层的列表中的元素数目,你可以像对其他任何数组那样进行操作,使用 $ # 表示法或者使用标量上下文中的数组名: 若要确定里层列表中的某个列表的元素数目,可能有一点儿麻烦。语句 $ l i s t _ o f _ l i s t s [ 1 ]返 回@ l i s t _ o f _ l i s t s的第二行中的引用。如果将它输出,则显示类似A R R AY(0 x 0 0 0 0 0)这个数据。 若要将@ l i s t _ o f _ l i s t s的一个元素当作数组来处理,请在它的前面加上一个符号 @,如下所示: 若要遍历列表的列表中的每个元素,可以使用下面这个代码: 可以添加下面这样的结构: 13.2.2 其他结构 在上一节中,我们介绍了如何使用引用和数组创建基本的 P e r l结构,即列表的列表。实际 上可以将数量不受限制的数组、标量和哈希结构的变形组合起来,创建更为复杂的数据结构, 比如下面这些结构: • 哈希结构的列表。 • 列表的哈希结构。 • 哈希结构的哈希结构。 • 包含列表的哈希结构,而列表中又包含哈希结构,等等。 由于本书篇幅有限,无法一一介绍所有这些结构。你安装的每个 P e r l所配备的在线文档包 含了一个称为“Perl Data Structures Cookbook(Perl的数据结构大全)”文档。它详细而明白地 描述了这些结构和许多其他数据结构。对于每种数据结构,“Perl Data Structures Cookbook” 文档详细描述了下列信息: 第13学时 引用与结构使用153下载• 说明你的结构(原义表示法)。 • 填充你的结构。 • 添加各个元素。 • 访问各个元素。 • 遍历整个数据结构。 若要查看“Perl Data Structures Cookbook”,请在命令提示符处键入perldoc perldsc。 13.2.3 使用引用来调试程序 当使用引用对程序进行调试时,编程新手常常搞不清楚哪些引用指向什么种类的数据结构。 另外,在你习惯之前,语句也容易混淆。P e r l提供了一些工具,可以帮助你确定有关的情况。 首先,可以输出该引用。P e r l能够显示该引用指向什么结构。例如,下面这个代码行: 可以显示 这个结构意味着变量 $ m y s t e r y _ r e f e r e n c e是对一个数组的引用。此外,变量也可以是对标 量( S C A L A R)、哈希结构( H A S H)或子例程( C O D E)的引用。若要输出 $ m y s t e r y _ r e f e r e n c e指向的数组,可以将它作为数组来处理,如下所示: P e r l的调试程序也配有一些程序工具,帮助你确定某个引用指向什么数据结构。在调试程 序中,你可以像通常那样输出引用。下面这个代码段显示了一个被查看的名叫 $ r e f的引用: 显然,$ r e f是指一个哈希结构。该调试程序包括一个命令,即命令 x,它将输出该引用和 它的内部结构: 在这个代码中,该引用包含一个带有两个元素(关键字‘ f r u i t’和‘v e g e t a b l e’)的哈希 结构。该调试程序甚至能够输出列表的列表之类的复杂数据结构,如下所示: 上面的例子显示了一个引用 $ a,它指向一个数组 A R R AY(0 x 2 0 1 7 0 b d 4)。而这个数组又 包含 3个别的数据引用,即 A R R AY(0 x 2 0 11 5 4 8 4)、A R R AY(0 x 2 0 11 f b b 4)和 A R R AY 154使用第二部分 高 级 特 性 下载(0 x 2 0 11 f a a 0),每个数组包含3个元素。 模块D a t a::D u m p e r包含的一些函数能够显示各个引用的内容。 D a t a::D u m p e r是独一无 二的,它的输出格式是有效的 P e r l格式,它可以存入文件,并在以后被检索,以提供可存储的 结构。D a t a::D u m p e r模块将在第1 4学时中介绍。 13.3 练习:另一个游戏— 迷宫 当你学习了那么多的新奇概念(引用和结构)之后,需要来一点消遣娱乐了。下面这个 练习展示了一种结构和几个引用,并且你可以做一个简单的游戏。 采用探险和狩猎之类的传统游戏方式,你被置于一个迷宫之中,必须找到你的出路。这 个迷宫并无奇特之处,它只是由一些房间所组成,并且每个房间至少有一个门。门可以通向 位于东、南、西、北的相邻房间。这个游戏的目的是找到一间密室。你会发现通往该密室只 有两条路,另外还有许多走不通的路。 首先,键入程序清单 1 3 - 2,并将它保存为 M a z e。运行该程序,得到类似程序清单 1 3 - 1的 输出。 程序清单13-1 Maze的输出示例 程序清单13-2 Maze的完整程序清单 第13学时 引用与结构使用155下载第1 ~ 2行:这两行代码是 P e r l程序正常的开始。 - w使警告特性被激活, use strict用于捕获 错误和不恰当的编程做法。 第4 ~ 9行:用于定义描述迷宫@ m a z e的结构。显示的迷宫是个 4×4的栅格,用一个列表的 列表来表示。列表的每个元素用于描述迷宫中的任何一个房间可以通往哪些房间,因此,如 果你重新设计这个迷宫,请务必留出一条出路。当前的迷宫如下所示: 有一个房间( 2,1)是无法进入的,在这个结构中用一个 - 来表示这个房间。实际上, 不能与n、s、e或w匹配的任何字符串均可使用。 第1 0 ~ 11行:当游戏的玩主向北或向南移动时,迷宫中的当前位置就需变更。哈希结 构% d i r e c t i o n用来根据老的位置和移动方向计算玩主的新位置。如果向“北”移动,则使玩主 的x坐标移动- 1(向上),y坐标保持不变。如果向“东”移动,则玩主的 x坐标不变,而y坐标 增加1。你将在第3 3 ~ 3 4行代码中看到坐标的变更情况。 第1 3 ~ 1 5行:程序中使用的变量用 m y进行声明,以便使 use strict恰当地运行。存放在 $ c u r r _ x和$ c u r r _ y中的玩主当前位置被设置为 0,0。最后目的地$ x和$ y被设置为3,3。 第1 7行:根据栅格中的x,y坐标,该函数显示玩主可以在每个房间中移动的方向。 第2 0行:在$ m a z e [ $ c x ] [ $ c y ]的房间描述中选择字母n、s、e和w,每次选择1个字母。从哈 希结构% f u l l中显示n s e w方向的相应描述。这个哈希结构只用于将短名字 ( n )转换成长名字 156使用第二部分 高 级 特 性 下载 入口 目标( N o r f h ),供显示之用。 第2 5行:该函数取出一个方向(存放在 $ n e w中)和对玩主的当前位置的引用。 第2 8行:方向用l c改为小写字母,s u b s t r只取出第一个字母,并将它赋予 $ n e w。这样, E a s t变为e,We s t变为w,s仍为s。 第2 9行:搜索当前房间的 $ m a z e [ $ $ x r e f ] [ $ $ y r e f ],找出给定的方向( n、s、e和w)。如果 不存在给定的方向,那么它对该房间无效,然后输出一条消息。 第3 3 ~ 3 4行:玩主的x和y坐标被更改。如果方向是 e,则$ d i r e c t i o n { e }是对两个元素的数组 的引用(0,1)。x坐标将递增0,即$ d i r e c t i o n { e } [ 0 ]。Y坐标将递增1,即$ d i r e c t i o n { e } [ 1 ]。 第3 7行:程序的主体从这里启动运行。该循环将不断运行,直到玩主的 x和y坐标 ($ c u r r _ x,$ c u r r _ y)与密室的坐标($ x,$ y)相一致为止。 第3 8行:显示当前房间的“映像”。 第3 9行:需要的移动方向读入 $ m o v e,用c h o m p删除换行符。如果玩主键入以 q开头的任 何信息,则游戏结束。 第4 2行:根据玩主当前需要做的移动和对玩主坐标的引用,调用子例程 m o v e _ t o ( )。 m o v e _ t o ( )子例程通过调整$ c u r r _ x和$ c u r r _ y,使玩主作相应的移动。 若要修改迷宫,使之采用另一种布局,只需改变存放在 @ m a z e中的栅格。迷宫不一定需 要做成正方形,也不需要给每个房间制作映像,甚至不需要存在一条有效的路径。不过请记 住,迷宫不要从它边上的某个房间开始。程序不会检查迷宫的有效性,不过,如果你创建了 一个无效迷宫,P e r l就会发出警告。如果要移动迷宫中的密室,只需改变它的 $ x和$ y的值。 13.4 课时小结 本学时我们介绍了引用的基本概念。首先,讲述了如何创建对 P e r l的基本数据结构— 标 量、数组和哈希结构的引用。然后,介绍了如何使用这些引用,对原始数据结构进行操作。 接着,说明了如何创建对哈希结构或数组的引用,不过这种引用没有与此相关的变量名,这 种引用称为匿名存储。最后介绍了如何使用引用来创建复杂的数据结构,以及何处可以查找 已有数据结构的文档资料。 13.5 课外作业 13.5.1 专家答疑 问题:当我用p r i n t“@ L O L”输出一个列表的列表时,它输出的是 A R R AY(0 x 1 0 1 2 1 0), A R R AY(0 x 1 0 1 4 0 0)等等,为什么? 解答:对于正规数组来说, p r i n t“@ a r r a y”将输出数组的元素,各个元素之间有一个空 格。P r i n t“@ L O L”也产生这样的结果,输出 @ L O L中的各个数组元素。若要输出 @ L O L中每 个数组的组件,你必须使用本学时开头的“一个例子:列表中的列表”这一节中介绍的方法。 问题:我试图使用$ r e f = \ ( $ a,$ b,$ c )创建一个对列表的引用,结果却产生了一个对标量 值而不是列表的引用,为什么? 解答:在P e r l中,\($ a,$ b,$ c)实际上是(\ $ a,\ $ b,\ $ c)的简化形式。你得到的结果 实际上是对括号中最后一个元素 $ c的引用。若要获得一个对匿名数组的引用,你应该使用 第13学时 引用与结构使用157下载$ r e f = [ $ a,$ b,$ c ]。 13.5.2 思考题 1) 语句$ r e f = \“p e a n u t s”;运行后,$ r e f中包含了什么? a. 什么也不包含。该语句无效。 b. peanuts。 c. 对一个匿名标量的引用。 2) 下面这个结构可以创建什么? a. 一个哈希结构的哈希结构,它包含一个列表 b. 一个哈希结构的列表,它包含一个列表 c. 一个列表的列表,它包含另一个列表 13.5.3 解答 1) 答案是c。你可以创建对任何值的引用,而不只是创建对标量、数组和哈希变量的引用。 你也可以用$ r e f = \ 1 0 0;创建对一个数字的引用。如果你的答案是 a,那么最好在一个短程序或 调试程序中试用一些新数据,看看它们能够产生什么结果。 2) 答案是b。在本学时中我们没有具体介绍这种结构,不过你应该能够猜到这是个什么结 构。一个哈希结构(花括号中)的列表(外层方括号)包含一个列表( k i d s的数据)。 13.5.4 实习 • 修改M a z e游戏,使之也能按对角线方向移动。你可以使用 4个新关键字来表示这些方向 (n e、n w、s e和s w是很难编程的)。提示:关键是修改 @ m a z e时使用新符号和% d i r e c t i o n 来表示按什么方向移动,比如 [ 1,1 ] [ - 1,- 1 ]等。 • 设计一种结构(即使是纸上谈兵也行),来描述一种电话帐单。帐单本身包含类似哈希 结构的关键字和数据(名字,电话号码,地址),帐单的某些部分是列表(明细电话项)。 每个明细电话项也可以被视为一个哈希结构(电话接收方,时间)。 158使用第二部分 高 级 特 性 下载下载 第1 4学时 使 用 模 块 你可能已经发现, P e r l是一种非常灵活的编程语言。它能够处理文件、文本、数学运算、 算法和任何计算机语言中通常遇到的其他问题。该编程语言的很大一部分是专门用于编写特 定目的的函数的。正则表达式是该语言的核心部分,对于 P e r l的使用方法来说,它们非常重要, 不过许多编程语言没有正则表达式照样能够很好运行。 P e r l对外部程序(反引号、管道和 s y s t e m函数)的使用是非常广泛的,不过许多语言根本不使用它们。 编程员都希望尽可能将任何有用的特性纳入该语言的核心中。具有这样的包容性,就会 形成一种规模很大并且难以使用的语言。例如,有些语言的设计者认为,支持对 world wide w e b访问的特性应该纳入该语言的核心中。这是个非常好的思路,但是并不是每个人都需要这 个特性。如果1 0年后w e b不再像现在这样重要,那么就必须下决心去掉这个特性,许多已经编 好的软件就会变得支离破碎。 P e r l采取了一种不同的路子。从 Perl 5开始,可以使用“模块”对语言进行扩展。模块是 P e r l例程的集合,它使你能扩展 P e r l的功能范围。你会发现这些模块能将 w e b浏览、图形处理、 Windows OLE、数据库和几乎任何想像到的特性添加给 P e r l。不过请记住,P e r l的运行并不一 定需要这些模块,没有这些模块它照样能够很好地发挥作用。 使用模块,你就能够访问一个很大的工作代码库,以帮助你编写程序。本书的第三部分 将专门介绍如何使用P e r l模块来编写C G I程序。 在撰写本书时,P e r l已经包含3 5 0 0个以上的模块,有2 0多个模块已经可以销售给用户。这 些模块大多数可以免费转用。可以将这些模块用在你自己的程序中,以实现你想要得到的任 何功能。你想解决的许多难题都可以为你解决,你只需安装正确的模块,并且正确地使用这 些模块。 在本学时中,你将要学习下面的内容: • 学习如何在你的P e r l程序中使用模块。 • 简单地了解某些内置模块的情况。 • 了解P e r l提供的核心模块的列表。 14.1 模块的概述 若要在你的P e r l程序中使用模块,可以使用 P e r l的u s e命令。例如,若要将 C w d模块纳入你 的程序,只需将类似下面的命令插入你的代码: use Cwd; 将use Cwd放在代码中的什么位置,这并不重要,不过为了清楚起见和便于维护,它应该 放在靠近程序顶部的位置。 这个特定模块曾经用在第 1 0学时中。不过在第 1 0学时中,你不知道它是如何工作的。当你运行带有use Cwd的程序时,就会出现下列情况: 1) Perl解释程序打开你的程序并读入所有代码,直到 use Cwd语句被找到。 2) 当你的P e r l解释程序安装时,它将得到关于它的安装目录的通知。该目录被搜索,以便 找出称为C w d的模块,该模块是包含P e r l代码的一个文件。 3) Perl读取该模块,该模块运行时需要的所有函数和变量均被初始化。 4) Perl解释程序从上次终止的位置开始,继续读取和编译你的程序。 这就是该程序运行的情况。当 P e r l读取整个程序后,并且在它准备运行时,该模块具备的 所有功能就可以供你使用。 你可能注意到use strict与use Cwd很相似。为了避免概念的混乱, u s e语 句是个通用指令,它可以使 P e r l解释程序执行某项操作。如果使用 u s e s t r i c t,它会改变解释程序的运行特性,使之对引用和裸单词变得比较严谨, 不过并不存在称为 s t r i c t的模块。如果使用 use Cwd,它将一个模块纳入你 的程序。你不必过分担心它们之间的差别,差别很小,不会对你产生很大 的影响。 当你将use Cwd插入你的程序中时,一个新函数就可以供你使用,这就是函数 c w d。c w d 函数能够返回你的当前工作目录的名字。 14.1.1 读取关于模块的文档 所有P e r l模块都配有它们自己的文档资料。事实上,如果你能够使用某个模块,那么就可 以访问它的文档,因为文档往往嵌入模块之中。 若要查看模块的文档,请使用带有模块名的 p e r l d o c程序。例如,若要查看 C w d的文档, 只需在操作系统的命令提示符处键入下面的命令: 然后就可以每次显示1页文档。下面是一页示例文档,它作了一定的压缩: 160使用第二部分 高 级 特 性 下载在这个例子中,C w d模块实际上允许你使用 3个新函数,即c w d、g e t c w d和f a s t g e t c w d。如 果你想使用这些函数,请阅读关于 C w d模块的文档。 如果你很想知道模块是如何工作的,就应该去了解它。模块主要是用 P e r l编写的,存放在系统的文件树中。 C w d模块存放在C w d . p m文件中。该文 件的位置可以是不一样的,不过它通常存放在 P e r l的安装目录下的某个位置 中。变量@ I N C包含C w d . p m的可能存放位置的名字,若要输出该变量,请在 命令提示符处键入perl -v。 由于许多模块是其他P e r l程序员免费提供的,所以模块文档的质量差异很大。比较主流的 模块,即销售的标准模块,本书中提到的模块,以及流行的模块,如 T K和L M P等,都配有很 好的文档。如果你不知道模块是如何工作的,请查阅第 1 6学时的内容,以了解有关的资料, 或者干脆询问模块的作者。 14.1.2 什么地方可能出错 如果你的P e r l安装正确,并且根本没有受到破坏,那么不应该出现任何错误。但是,世界 并不是完美无缺的,有时某些地方仍会出错。 如果你看到下面这个出错消息: 那么你应该检查安装的P e r l的版本。请在系统的命令行提示符处键入下面的命令: perl -v 如果P e r l报告的版本号小于 5,比如4 . 0 3 6,那么你拥有的 P e r l版本就太旧了,必须对它进 行升级。Perl 5的许多特性它都没有,而且所有老的软件中都存在着安全隐患。实际上第 1 3学 时中的代码举例都无法在 Perl 4中运行,现在你应该注意到这个问题了,请立即进行版本的升 级。 另一个潜在的错误消息如下所示: 这种错误通常意味着存在下列 3个问题中的一个: • 模块的名字拼写有误。 模块的名字是区分大小写字母的。 Use Cwd不同于use cwd。有些模块名包含冒号(::), 比如F i l e::F i n d,你必须正确键入冒号。 • 要使用的模块不是标准产品的组成部分,它没有安装在系统的正确位置上。 安装的每个P e r l均配有大约1 5 0个模块,这属于“标准产品”。本学时的后面部分内容中列 出了其中的一些模块。所有这些模块都应该能够正确运行。你或你的系统管理员必须另外安 装不是“标准产品”中的模块。 本书的附录包含如何安装这些额外模块的说明。 • 安装的P e r l不完整,或者受到了破坏,也可能安装不正确。这种情况是经常发生的。 第14学时 使 用 模 块使用161下载P e r l解释程序会按照出错消息输出的 @ I N C中的路径查看已安装的模块。如果这些模块移 动了位置,被删除,或者无法使用,最简单的解决办法是重新安装 P e r l。在处理这个问题之前, 首先要搞清出错的模块是否属于标准模块。已经安装的任何附加模块都可能放到其他位置中, 这是正常的。关于如何在非标准位置上安装和使用模块的详细说明,请参见本书的附录。 14.2 已安装模块简介 下面我们要简单介绍一下作为P e r l核心产品的组成部分且已经安装在你的系统上的一些模块。 14.2.1 文件和目录简介 在第1 0学时中,我们介绍了如何打开目录并且读取这些目录中包含的文件名列表。接着, 提出了如何读取子目录的问题,但是当时我们没有讲述这个问题。现在,我们就要说明如何 遍历目录和子目录的方法。 你可以编写一个常用程序,以便在不知道文件所在的确切目录的情况下查找这个特定的文 件。例如,你可能想在目录文档下的某个位置上查找名字为 i m p o r t a n t . d o c的文件,如下所示: 这个插图显示了一个目录结构,它位于名字为 d o c u m e n t s的父目录下。如果使用 o p e n d i r / r e a d d i r / c l o s e d i r来查找d o c u m e n t s下的某个位置上的一个文件,那是非常不容易的。首先,必 须搜索d o c u m e n t s来查找该文件。然后必须搜索 d o c u m e n t s下的每个目录,即a c c o u n t i n g、m i s c 和p e r s o n a l,接着再搜索这些目录下的每个目录,如此等等。 这是过去3 0年来程序员一次又一次解决的一个老问题。如果你自己编写一个程序来解决 这个问题,纯粹是浪费时间。为此,P e r l的设计人员采取了一个简单的解决方案,加上了 F i l e:: F i n d模块。若要在你的程序中使用 F i l e::F i n d模块,只需将下面这个命令输入到你的程序中 的某个位置,最好是靠近程序的顶部: 这时一个称为f i n d的新函数就可以供你使用了。 f i n d函数的句法如下所示: f i n d函数的第二个参数是要搜索的一个目录列表。第一个参数对你来说是新的,它是个子 例程引用。你创建的子例程引用很像是标量或数组的引用,它只不过是前面加上一个反斜杠 的子例程名。必须使用子例程名前面的 &,才能取得子例程的引用。然后为 d i r l i s t中找到的每 个文件和目录调用指明的子例程。 程序清单1 4-1显示了为查找丢失的i m p o r t a n t . d o c而使用的程序。 程序清单1 4-1 查找一个文件时所用的程序 162使用第二部分 高 级 特 性 下载第1~2行:这两行代码是 P e r l程序通常开始运行时的代码。 - w使警告特性激活, use strict 用于捕获错误。 第3行:F i l e:Fi n d模块被插入你的程序。它使你可以运用 f i n d函数。 第5行:为‘/ d o c u m e n t s’下的每个文件和目录调用该函数。如果你有 1 0 0个文件和1 2个目 录,该例程将被调用11 2次。 第6行:当w a n t e d ( )函数被调用时,$ F i l e::F i n d::n a m e将包含到达被查看的当前文件的 路径,$ _只包含文件名。这行代码用于确定文件名是 i m p o r t a m t . d o c。如果是,则输出全路径 名。 第1 0行:用子例程引用 \ & w a n t e d和一个目录调用f i n d函数。并为‘/ d o c u m e n t s’下的每个 文件和目录调用w a n t e d ( )函数。 由f i n d调用的函数将可以使用下列变量: • $File::F i n d::name 当前路径名,目录和文件名。 • $File::F i n d::dir 当前目录名。 • $_ 当前文件名(不带目录)。有一点总是很重要,那就是你不能改变函数中的 $ _的值。 如果你改变了这个值,应该将它改回来。 程序清单1 4 - 2包含了另一个F i l e::F i n d例子。这个例子用于删除 C:和D:驱动器上带有 扩展名. t m p的所有文件。这些文件在你的硬盘上不断增加并变得拥挤不堪。你可以很容易使 用这个程序,从U N I X系统中删除文件,或者执行各种文件维护操作。 程序清单14-2 删除临时文件所用的程序 程序清单1 4 - 2中的大多数程序与程序清单 1 4 - 1中的程序相似。 第7行:对传递过来的文件名进行测试,以确保它是个正规文件。请记住,这个子例程将 同时为文件和目录而调用。 第9~11行:对文件名进行核实,以了解文件名的结尾是否包含 . t m p。如果包含. t m p,则用 第14学时 使 用 模 块使用163下载u n l i n k将文件删除。 14.2.2 拷贝文件 另一个常见操作是拷贝文件,可以在 P e r l中使用下列步骤执行这项操作: 1) 打开源文件,以便读取该文件。 2) 打开目标文件,以便写入。 3) 读取源文件并写入目标文件。 4) 关闭源文件和目标文件。 当然,在执行每个操作步骤后,必须确保没有发生任何错误,并且每个写入操作均取得 了成功。告诉你一个比较容易的方法, P e r l提供了F i l e::c o p y模块,它能进行文件的拷贝操 作。下面是该模块的一个例子: 上面这个代码段用于将s o u r c e f i l e的内容拷贝到d e s t i n a t i o n。如果拷贝成功,c o p y函数返回 1,如果拷贝出现问题,则返回 0,并且它会将变量$!设置为相应的错误代码。 F i l e::c o p y模块也提供了一个m o v e函数。m o v e函数能够将文件从一个目录移到另一个目 录。如果可以通过对文件改名来移动文件,那么该文件将被改名。当源文件和目标文件在同 一个文件系统或磁盘上时,通常采取改名的方法。如果无法通过对文件改名来移动文件,那 么文件首先拷贝到目标文件名,然后将原始文件删除。请看下面这个例子: 在上面的代码段中,文件 i m p o r t o n t . d o c从当前目录移到目标目录 d : / a r c h i v e s / d o c u m e n t s中。 如果m o v e函数运行失败,那么就可能存在不完整的目标文件。如果 m o v e运行失败,u n l i n k函 数能够删除部分拷贝的目标文件。 14.2.3 用于通信的Perl模块 P e r l模块的功能并不限于对文件和目录进行操作。还可以使用 N e t::P i n g模块来确定你的 系统是否能够在网络上正确地进行通信。 N e t::P i n g模块是根据U N I X实用程序p i n g而得名的,而实用程序 p i n g又是根据潜水艇利 用声波来测定位置的“乒”声音而得名。 p i n g实用程序将一个数据包发送到网络上的另一个 系统。如果该系统正在运行,那么它就会发出应答,同时 p i n g命令报告数据包发送成功。下 面显示的N e t::P i n g的工作方式与上面完全相同: 在上面这个代码段中,N e t::P i n g模块提供了一函数叫做p i n g e c h o。该函数拥有两个参数, 164使用第二部分 高 级 特 性 下载第一个参数是要查找的主机,在上例中是 w w w. y a h o o . c o m。第二个参数用于指明p i n g e c h o应该 等待多长时间才能收到对方的应答,这个时间以秒为单位计算。 由于P e r l在Windows 95/98/N T上运行时所具备的性质,在撰写本书时 (1 9 9 9年夏季)N e t::P i n g模块还不能运行。 N e t::P i n g需要依赖a l a r m函数, 而在Wi n d o w s下该函数不能运行。 A c t i v e s t a t e是开发在Wi n d o w s下运行的P e r l 的主要公司,它已宣布准备实现用于 Wi n d o w s的许多遗漏的功能,并将这些 修改纳入P e r l。 14.2.4 使用English模块 使用E n g l i s h模块,P e r l的某些无名的特殊变量将采用比较长的名字,如下例所示: 在上面的代码段中,while(< >)通常从S T D I N中读取一个行输入,并将它赋予 $ _。现在它 仍然如此。但是,使用 use English,变量$ _也叫做$ A R G。下面显示了特殊变量及其对应的英 文变量的部分列表。 特 殊 变 量 英 文 名 若要了解特殊变量及其对应的英文变量的完整列表,请查看 E n g l i s h模块的在线文档。 14.2.5 diagnostics模块 P e r l模块d i a g n o s t i c s能够帮助你查找程序中的错误。当你键入本书中的代码例子时, P e r l 解释程序肯定会发出你不太理解的出错消息。例如,请看下面这个短程序: 它使P e r l发出下面的警告消息: d i a g n o s t i c s模块会使P e r l具体说明它的错误和警告。可以修改这个示例程序,使它像下面 这样包含诊断模块: 第14学时 使 用 模 块使用165下载修改后的程序能够输出一个文字更详细的诊断消息: 如果认真观察一下这两个消息,就会发现它们之间有着明显的关系。第一条消息的意思 很清楚,P e r l要求你的电子邮件地址应该写成 h e l p \ @ s u p p o r t . c o m。第二条消息经过解释,变 得更加清楚了一些。由于 use strict是有效的, @ s u p p o r t变量应该已经用 m y作了声明。但是 @ s u p p o r t不是个变量,它是电子邮件地址的一部分,不过它被 P e r l转换错了。 该消息前面的字母用于指明你遇到的是何种类型的错误。(W)表示是个警告,(D)表示 你使用了一个不该使用的语句,(S)是个严重警告, ( F )表示这是个致命的错误。除了( F) 之外的所有消息类型,你的 P e r l程序都会继续运行。 P e r l共有6 0页用于描述它的出错消息。如果你在理解 P e r l的简要出错消息时遇到了问题, use diagnostics有时能够帮助你理解出错消息的含义。 通过浏览p e r l d i a g在线手册页,就可以看到出错消息和诊断消息的完整列 表。 14.3 标准模块的完整列表 关于P e r l中包含的模块的完整列表,本书将不作详细的说明。下面是标准 P e r l产品中的模 块列表及其简单的说明。如果想知道模块的作用以及它如何运行,请使用 p e r l d o c,以查看该 模块的文档资料。 模 块 名 说 明 A u t o L o a d e r 允许P e r l只在需要时对函数进行编译 A u t o S p l i t 对模块进行分割,以便自动加载 B e n c h m a r k 允许对P e r l函重复定时,以便加速基准测试 C G I 允许非常容易地访问用于We b编程的Common Gateway Interface (公用 网关接口,第 1 7 ~ 2 4学时介绍) C PA N 用于访问P e r l模块的存档文件,以便安装新模块 C a r p 生成出错消息 D i r H a n d l e 提供与目录句柄之间的对象接口 E n v 将操作系统的环境映射到变量中 E x p o r t e r 允许你编写自己的模块 E x t U t i l s::* 允许你编写自己的模块或者安装模块 F i l e::* 提供更多的文件操作模块,如 F i l e::C o p y F i l e::S p e c::* 允许对文件名进行跨操作系统的操作 F i l e C a c h e 打开的文件数量可以超过操作系统通常允许的数量 F i n d B i n 找出当前正在运行的程序的名字 G e t o p t::* 允许你处理程序中的命令行选项 166使用第二部分 高 级 特 性 下载(续) 模 块 名 说 明 I 1 8 N::C o l l a t e 允许按特定语言排序 I P C::* 用于进程间的通信,比如使用双通或三通管道进行通信 M a t h::* 允许你使用带有任意精度浮点数、整数和复数的扩展数学运算库 N e t::* 允许你获得关于网络主机的信息。例如, N e t::h o s t e n t可将I P地址 (如2 0 4 . 7 1 . 2 0 0 . 6 8)转换成主机名(如w w w. Ya h o o . c o m) P o d::* 用于访问P e r l的Plain Old Documentation格式化例程 S y m b o l 允许你对P e r l自己的符号表进行查看和操作 S y s::H o s t n a m e 用于获取你的系统的I P主机名 S y s::S y s l o g 允许将信息写入U N I X系统的出错记录 Te r m::* 为光标位置和清屏等提供终端控制的函数接口 Te x t::A b b r e v 创建缩写表 Te x t::P a r s e Wo r d s 允许对文本进行分析,以便搜索单词 Te x t::S o u n d e x 使用S o u n d e x方法,根据标点对单词进行分类 Ti e::* 将P e r l的变量与函数连接起来,使你可以实现自己的数组和哈希结构 Ti m e::* 允许对时间进行分析和处理。例如,你可以将“ Sat Jul 24 16:21:38 EDT 1999”这种格式的时间转换成 1 9 7 0年1月1日以来的秒数 c o n s t a n t 允许定义常量值 i n t e g e r 使P e r l有时能够用整数而不是浮点数进行数学运算 L o c a l e 允许进行基于语言的字符串比较(各国语言字符的字符串比较) 下一步进行的操作 如果你想免费了解能使用哪些种类的模块,请使用 We b浏览器,以便访问网址 h t t p: / / w w w. c p a n . o rg。模块按类别进行大致的排列。 有些模块需要C编译器和起码的开发环境来进行安装。在 Wi n d o w s计算机上可能没有这些 模块。A c t i v e s t a t e的P e r l包含一个名叫 P P M的实用程序,它可以用来浏览和安装预安装的模 块。 本书的附录包含一个按步骤操作的说明,用于在 U N I X和Wi n d o w s计算机上安装模块。这 些操作说明将告诉你如何使用 C PA N模块(用于 U N I X)和A c t i v e s t a t e的用于安装新模块的 P P M实用程序。 14.4 课时小结 在本学时中,我们介绍了如何使用模块来扩展 P e r l语言的功能,以便执行许多其他的任务。 这种将新功能添加给P e r l的通用方法将在本书的其他学时内容中广泛使用。另外,本学时介绍 了一些常用的模块,并且给出了标准 P e r l产品包含的模块的完整列表。 14.5 课外作业 14.5.1 专家答疑 问题:在F i l e::F i n d模块中,变量名中的双冒号(::)表示什么?它是否与$ F i l e::F i n d:: d i r中的相同? 解答:P e r l模块能够为变量名建立一些备用区域,称为名空间,这样,模块的全局变量名 第14学时 使 用 模 块使用167下载与你自己的全局变量名就不会混淆在一起了。因此在 C w d模块中的全局变量将称为$ C w d::x。 你的大多数全局变量实际上拥有 $ m a i n::x这个全名,而不是简名 $ x。不过就目前来说,这并 不重要。 问题:我有一台安装了Windows 95/98/NT的计算机,我想使用的模块无法通过 A c t i v e s t a t e 的P P M实用程序进行安装。我应该如何安装该模块? 解答:很遗憾,C PA N的大多数模块要求你拥有完整的 U N I X型开发环境,可以对模块进 行编译和安装,这种环境在Wi n d o w s计算机上很难安装。如果你能够非常方便地使用 C编译器, 则可以下载一个开发环境,创建你自己的模块,但是这样做并不容易。 问题:我有一个包含r e q u i r e而不是u s e的老式P e r l程序。r e q u i r e的功能是什么? 解答:r e q u i r e语句与u s e相类似。由于Perl 4没有u s e关键字,它使用的是 r e q u i r e。r e q u i r e 语句可使解释程序查找一个库文件,并将它纳入你的程序,这个功能类似 u s e的功能。但是它 们之间的主要差别是:每当 r e q u i r e语句在运行时(运行期),便执行r e q u i r e功能;而u s e命令则 是在你的程序第一次加载(编译时)时执行其功能。 14.5.2 思考题 1) 如果你想在程序中两次使用 c w d函数,应该使用use Cwd; 几次? a. 一次。 b. cwd的每个实例使用一次,因此是两次。 c. 一次也不用,因为c w d是个内置函数。 2) 什么模块能够为$ _变量提供一个别名? a. LongVa r s b. English c. $_没有别名 14.5.3 解答 1) 答案是a。当你用u s e将模块插入你的程序后,它的所有函数均可供程序的其余部分使用。 2) 答案是b。use English使得$ _可以使用别名$ A R G。 14.5.4 实习 • 请翻到本书的附录,设法按照那里的说明,通过 C PA N安装模块B u n d l e::LW P。对于第 2 4学时中的代码例子来说,需要使用该组模块中的一个模块。 168使用第二部分 高 级 特 性 下载下载 第1 5学时 了解程序的运行性能 编写P e r l程序,用于查找文件中的数据,或者与用户进行交互操作,这是非常有用的。但 是,当程序运行结束时,将会发生什么情况呢?它的运行结果消失了,你没有得到任何东西 来展示你的程序的性能,你会感到怅然若失,一无所获,就像什么也没有发生一样。 数据库能够解决这个问题。数据库可以用于存储数据,供以后使用。设计良好的数据库 可以被任何种类的程序使用,以便进行数据的查询、报告和输入。若要设计数据库,必须认 真考虑你想存储何种数据,以及如何对它进行存储。另外还要考虑如何访问数据,是每次由 一个人访问,还是许多用户同时访问。 在本学时中,我们将要介绍两种方法,以便存储数据供以后检索。 在本学时中,你将要学习下面的内容: • 创建D B M文件并将数据存储在该文件中。 • 将普通文本文件作为数据库来使用。 • 从文件中的随机位置读取数据和将数据存入文件中的随机位置。 • 为同时访问而锁定文件。 15.1 DBM文件 若要使你的程序能够以非常有条理的方式来存储数据,最简单的方法之一是使用 D B M文 件。D B M文件是已经与一个 P e r l的哈希结构连接起来的文件。若要读取和写入 D B M文件,只 需对一个哈希结构进行操作即可,就像从第 7学时以来进行的操作那样。 若要将哈希结构与D B M文件连接起来,可以使用P e r l函数d b m o p e n,如下所示: d b m o p e n函数将h a s h与一个D B M文件连接起来。你提供的 f i l e n a m e实际上在硬盘上创建两 个不同的文件,即 f i l e n a m e . p a g和f i l e n a m e . d i r。P e r l使用这两个文件来存储哈希结构。这些文 件不是文本文件,不应该对它们使用编辑器。另外,如果这两个文件中的一个是空的,或者 与文件中的数据量相比似乎非常大,请不必对此担心,这是正常的。 m o d e是指对P e r l创建的两个D B M文件的访问许可权。如果是 U N I X系统,可以使用一组明 确的访问许可权,它们用于控制谁能够访问你的 D B M文件。例如, 0 6 6 6允许每个人拥有对 D B M的读和写访问权。 mode 0644允许你读和写这些文件,但其他人只能读这些文件。如果 是Wi n d o w s,只使用0 6 6 6,因为你不必担心是否拥有对任何文件系统的访问许可权。 如果成功地将哈希结构键入 D B M文件,那么d b m o p e n函数返回真,否则返回假。请看下 面这个例子: 上面这个语句执行后,哈希结构 % h a s h就与称为d b m f i l e的D B M文件相连接。 P e r l在你的 磁盘上创建一对文件,称为 d b m f i l e . p a g和d b m f i l e . d i r,以便存放该哈希结构。如果你将一个值 赋予该哈希结构,如下所示, P e r l就用该信息更新D B M文件:如果要取出信息,P e r l就从D B M文件中检索关键字和数据,如下所示: 若要使哈希结构与 D B M文件断开连接,请像下面这样使用带有哈希结构名字的 d b m c l o s e 函数: 当切断哈希结构与 D B M文件的连接后,存放在它里面的项目 f e l i n e和c a n i n e仍将位于该 D B M文件中,这是D B M文件的重要特点。在两次调用 P e r l程序之间,存放在与D B M文件相连 接的哈希结构中的项目均保留不变。 通常对哈希结构执行的函数,也可以对与 D B M文件连接的哈希结构执行。哈希结构的函 数k e y s,v a l u e s和d e l e t e按通常情况运行。可以清空哈希结构和 D B M文件,方法是将哈希结构 赋予一个空列表,比如 % h a s h = ( )。也可以对哈希结构进行初始化,方法是在用 d b m o p e n将哈 希结构与D B M文件连接起来之后,将哈希结构赋予一个列表。 15.1.1 需要了解的重点 当你将哈希结构与D B M文件连接起来时,必须了解下列要点: • 关键字和数据的长度现在是受限制的。通常情况下,哈希结构的关键字和数据长度是不 受限制的,与 D B M文件相连接的哈希结构拥有一个有限的单个关键字和一个数据,合 起来的长度通常为 1 0 2 4字符左右,这是对 D B M文件的一个限制。可以存储的关键字和 值的总数没有变更,它只受文件系统的限制。 • 运行d b m o p e n以前的哈希结构中的值均被丢失,最好只使用新的哈希结构。请看下例: 在上面这个代码段中,当执行 d b m o p e n时,哈希结构% h中的值,即d r o m e d a r e的关键字将 会丢失。 • dbmclose函数执行后,与D B M文件相连接时哈希结构中的值将会消失: 哈希结构与D B M文件连接时哈希结构中的值将保留在 D B M文件中,此后,哈希结构本身 将是空的。 15.1.2 遍历与DBM文件相连接的哈希结构 让我们观察一下尚未与 D B M文件相连接的哈希结构。如果你编写一个 P e r l程序,用于将 约会、电话号码和其他信息存放在哈希结构中。过一段时间后,该哈希结构就会变大。由于 在两次运行你的P e r l程序之间,哈希结构的值均被保留,因此它们决不会跑到别的地方去,除 非故意将它们删除。 170使用第二部分 高 级 特 性 下载如果你的D B M文件(称为r e c o r d s)搜集了许多信息,那么下面这个代码段就会出现一些 问题: 这个代码不存在什么问题。哈希结构首先与一个 D B M文件相连接,然后使用 keys %recs 从哈希结构中取出关键字。用 foreach my $key对关键字列表进行迭代操作,然后输出每个关 键字和值。 如果% r e c s中的关键字列表很大,那么语句 keys %recs的执行需要花费一定的时间。 P e r l 还有另一个函数,可以允许你对哈希结构进行迭代操作,每次取出一个关键字。这个函数称 为e a c h。e a c h的句法如下: e a c h函数返回由两个元素组成的列表,这两个元素中一个是哈希结构的关键字,另一个 是它的值。对 e a c h进行的每个连续的调用,可以返回哈希结构的下一个关键字值对。当关键 字用完后,e a c h就返回一个空表。对较大的哈希结构进行迭代操作时,更好的方法是使用下 面的代码: 你不一定必须将 e a c h函数用于与 D B M文件相连接的哈希结构,可以将 e a c h用于任何哈希结构。 15.2 练习:一种自由格式备忘记事板 既然你拥有一种将数据存放在磁盘上的简易方法,那么现在可以很好地运用这种方法。 下面这个练习展示了一种自由格式的备忘记事板。程序清单 1 5 - 2显示了该程序( m e m o p a d) 的清单。它用于按关键字存储信息,并使你可以使用简单的查询方法来搜索和检索信息。程 序清单1 5 - 1显示了使用m e m o p a d的会话示例。 若要查询m e m o p a d程序,只需键入一个问题的名字,后随一个问号。若要用新的方式进 行编程,键入“ X is Y”形式的短语,其中 X是问题, Y是与该问题相关的信息。如果键入 “like pattern?”(其中的p a t t e r n是在该问题中要搜索的一个正则表达式),你就可以搜索数据 库,寻找相似性。符合该正则表达式的所有问题均被输出。若要退出该程序,请在提示符处 键入q u i t。 程序清单15-1 使用m e m o p a d的会话示例 第15学时 了解程序的运行性能使用171下载每当该程序运行时,赋予 m e m o p a d程序的所有信息均被存储,因为数据均存放在与一个 D B M文件相连接的哈希结构中。 程序清单15-2 memopad的完整清单 第1 ~ 2行:这两行是P e r l程序通常的起始行,#!行带有 - w,这意味着警告特性是激活的。 另外,use strict命令可以防止你犯不应该犯的错误。 第6行:使用d b m o p e n,哈希结构% a n s w e r s与D B M文件a n s w e r s相连接。在磁盘上创建两 个文件,即a n s w e r s . p a g和a n s w e r s . d i r。 第7行:w h i l e ( 1 )执行该永久循环。该循环中的某个位置是个 l a s t或e x i t命令,用于退出该 循环。 第9行:这一行代码看上去容易使人混淆,因为它会立即产生若干情况。 l c将它的标量参 数改为小写字母。由于 < S T D I N >用在标量上下文中,因此从 S T D I N中读入一行输入,然后转 172使用第二部分 高 级 特 性 下载换成小写字母,其结果被赋予 $ _。c h o m p用于删除结尾处的换行符。 第1 0行:如果输入行只包含单词 q u i t,则退出w h i l e循环。 第11行:如果输入行(现在位于 $ _中)与单词 l i k e相匹配,然后与某个文本相匹配,再 与?相匹配,那么文本被保存在 $ 1中,在匹配的模式中使用括号。 第1 2行:第11行的匹配模式中的字符串保存在 $ p a t t e r n中。 第1 3~1 7行:逐个关键字对哈希结构 % a n s w e r s进行搜索,寻找与 $ p a t t e r n中的字符串相匹 配的关键字。当找到每个关键字时,便将它输出。 第1 8行:(这一行是第11行上开始的i f语句的继续。)否则,如果输入行以问号结尾,那么 问号前面的所有数据(不包含问号)将被存放在带括号的 $ 1中。 第1 9行:$ 1中的模式保存到$ s u b j e c t中。 第2 0~2 4行:如果关键字$ s u b j e c t是在哈希结构% a n s w e r s中,则输出该关键字和相关的数 据。否则,该程序发出应答消息 I don’t know(我不知道)。 第2 5~2 7行:(这一行是第11行上开始的i f语句的继续。)否则,如果输入行采用 X is Y的 形式,则第一部分(X)存入$ s u b j e c t中,最后一部分存入$ i n f o中。 第2 8行:$ i n f o中的信息作为$ s u b j e c t存放在哈希结构% a n s w e r s中。 第3 4行:D B M文件与% a n s w e r s断开连接。 15.3 将文本文件用作数据库 许多情况下,数据库是一种较小和较简单的结构,比如小型系统上的一个用户列表,小 型网络上的本地主机,常用的 We b站点的列表,或者个人地址文件等,它们都属于简单的数 据库形式。而对于简单数据库来说,使用普通文本文件就可以了。但是首先必须考虑到这种 数据库存在的某些不足。 将文本文件用作数据库,比使用复杂文件(如 D B M文件)或大型数据库(如 O r a c l e或 S y b a s e)有着一些明显的优点。下面列举其中的一些优点: • 文本文件数据库可以移植。它们可以在各种不同的系统之间进行移动,不会遇到太大的 麻烦。 • 文本文件数据库可以使用文本编辑器进行编辑,不必使用特殊工具就能在纸张上打印。 • 文本文件数据库开始时的创建工作很简单。 • 文本文件数据库可以输入到其他程序中,如电子表格、文字处理程序和其他数据库中, 没有什么太大的麻烦。凡是可以输入数据的程序,几乎都允许你输入文本。 但是将文本文件用作数据库也有它的不足。若要全面了解它的不足,你应该知道文本文 件通常的结构。文本文件数据库的传统结构方式是:文本文件中的每一行都是一个记录,每 一行中的各个列称为域。但是,对于你的系统来说,文本文件是个字符流。因此,下面的文 本文件 实际上可以作为下面这个连续字符串来存储: 第15学时 了解程序的运行性能使用173下载其中[ s p a c e ]代表一个空格字符, [ n e w l i n e ]代表你的操作系统中的一个换行符,有时它是 个回车符,有时是个回车符和换行符。每个记录和每个域的字符均封装在一个很长的字符流 中,出色的列行显示格式是人能阅读的编辑器、打印机和 P e r l展示的数据格式。 知道这种结构的特点后,再让我们来看一看文本文件数据库的缺点: • 不能将数据插入文本文件,只能部分或全部改写文本文件。除了将数据附加在文件的结 尾处,如果将数据插入文件中的任何位置,就会拷贝文件中新插入的数据后面的所有数 据。 新数据:Susan 555-6613被插入“B o b”的后面 所有这些数据必须拷贝到: 在文件中以这种方式拷贝数据很容易出错,并且速度很慢。 • 相反的情况也一样。从文本文件的中间位置删除数据是很难的。被删除的这部分数据后 面的全部数据都必须拷贝到间隔位置。如果要从原始文本数据库中删除 M a u r y,你必须 这样操作: 通过删除下面的数据 所有数据必须拷贝到: • 若要在文本文件数据库中查找某个记录,你必须顺序搜索该文件,通常从上向下进行搜 索。在D B M文件中,搜索一个记录就像在哈希结构中查找这个记录一样容易,而文本 文件则必须查看其每一行,以确定它是个正确的记录。这个过程很慢,随着数据库变得 越来越大,搜索过程也越来越慢。 将数据插入文本文件或从文本文件中删除数据 尽管文本文件数据库存在上述缺点,但是它并非一无是处。如果你的文本文件数据库比 较小,你可以将文本文件当作一个数组来处理,这样将数据插入数据库或者从数据库中删除 数据将是非常容易的。例如,如果下面这个数据库 被保存到一个称为p h o n e . t x t的文件中,那么用一个较短的 P e r l程序就可以将该数据库读入 一个数组,如下所示: 174使用第二部分 高 级 特 性 下载这里的r e a d d a t a ( )函数读取文件p h o n e . t x t,并将数据放入@ D ATA中,它不带换行符,并返 回该数组。如果增加另一个函数 Wr i t e d a t a ( ),就可以像下面这样读写该数据库: 这时,若要将记录插入该数据库,只要使用r e a d d a t a ( )函数将数据读入一个数组。使用p u s h、 u n s h i f t或s p l i c e函数,将记录插入该数组,然后用w r i t e d a t a ( )函数像下面这样再次写出该数组: 若要从文本文件数据库中删除文本,你可以在重新写入数组之前,对数组 @ P H O N E L I S T 使用S p l i c e,p o p或s h i f t函数。也可以使用一个循环 ,人工编辑该数组,比如使用下面这个 g r e p 循环: 在上面的代码段中,各个记录从 r e a d d a t a ( )拷贝到@ P H O N E L I S T。g r e p对@ P H O N E L I S T 数组进行迭代操作,测试每个元素,以确定它是否与 A n n不相匹配。凡是不匹配的元素均被再 次赋予@ P H O N E L I S T。然后数组@ P H O N E L I S T被送回给w r i t e d a t a ( ),以便进行写入操作。 15.4 随机访问文件 如果你具有冒险精神,可以像前面提到的那样在文件中进行随机读写操作。下面各节将 简单介绍几种工具,可以用来进行随机读写操作,不过我们不准备详细介绍这些工具,因为 你并不经常需要使用它们。 15.4.1 打开文件进行读写操作 到现在为止,你已经了解到打开文件的 3种方法。文件可以被打开以便进行阅读、写入、 以及将数据附加到它的结尾处。文件还可以打开以便同时进行阅读和写入操作。表 1 5 - 1列出 了打开文件的不同操作方式。 两项说明: • 设定“附加”的方式是很麻烦的。在某些系统上,比如在 U N I X上,写入文件的数据总是 写到文件的结尾处,无论读取文件的指针位于何处。(后面我们很快还要说明这一点。) • 决不应该使用+ >。一旦文件打开,它的内容就会被删除。 第15学时 了解程序的运行性能使用175下载表15-1 打开文件的不同方式 如果语句不存 是否截断 o p e n命令 读 写 附加 在是否创建 现有数据 o p e n(F,“< f i l e”) 是 否 否 否 否 或者o p e n (F: file”) o p e n ( F,“> f i l e”) 否 是 否 是 是 o p e n ( F,“> > f i l e”) 否 是 是 是 否 o p e n ( F,“+ < f i l e”) 是 是 否 否 否 o p e n ( F,“+ > f i l e”) 是 是 否 是 是 o p e n ( F,“+ > > f i l e”) 是 是 是 是 是 15.4.2 在读写文件中移动 当文件打开时,操作系统始终跟踪你在文件中所处的位置。这个指针称为读指针。例如, 当文件初次打开以便进行阅读时,读指针位于文件的开始处,如下所示: 当你读完整个文件后,读指针位于文件的结尾处,如下所示: 若要将指针移到文件中的某个位置,你可以使用 s e e k函数。s e e k函数带有两个参数。第一 个参数是个打开的文件句柄,第二个参数是文件中你想寻找到的位移。第二个参数是该位移 处于什么位置。0是文件的开始处;1是文件中的当前位置; 2是文件的结尾。下面是在文件中 进行搜索的一些示例代码: t e l l函数返回文件中的当前读指针的位置。例如,当运行上面的代码段后, t e l l ( F )便返回 2 4,即“This is at the beginning”的长度,因为文件指针就位于该文本的后面。 这一节只是简单提到s e e k、t e l l和o p e n等命令。关于这些命令的详细说明,请参 见在线文档。s e e k、t e l l和o p e n函数在p e r l f u n c手册页中作了说明,可以在命令提示符 处键入perldoc perlfunc,访问p e r l f u n c手册页。另外,在p e r l o p e n t u t一节中,对o p e n命令 进行了更加详细的介绍,可以在命令提示符处键入perldoc perlopentut,查看该文档。 15.5 锁定文件 假设你编写了一个非常出色的 P e r l程序,并且全世界的人都想使用它。如果你使用 U N I X 176使用第二部分 高 级 特 性 读指针 读指针 下载或Windows NT计算机,或者使用 Windows 95或Windows 98计算机,那么可能有许多人同时 运行你的程序。你也可以将程序放到 We b服务器上,它运行得如此频繁,以致于你的程序的 许多实例互相重叠了。 现在假定你的程序将一个数据库用于它的工作,例如使用前面刚刚介绍的文本文件数据 库,不过下面介绍的情况适用于任何一种数据库。请看下面这个代码,它使用上一节介绍的 一些函数: 这个代码看上去没有任何问题。如果两个人几乎同时运行你的程序,并且试图添加不同 的记录,这很可能带来一些问题,它存在相当多的错误。在下面这个插图中,这一组特定的 P e r l语句几乎是同时在同一个系统上由两个人来运行的(第二个人运行程序的时间比第一个人 稍晚一些)。请认真观察。 时间 序号 第1个人 第2个人 从第1个人的角度来看,数据是在第 2步上读取的,新记录(“D a v i d”)在第3步上被添加 给@ P H O N E L,并在第4步上进行写入操作。 从第2个人的角度来看,数据是在第 3步上读取的,新记录(“J o y”)在第4步上被添加给 @ P H O N E L,并在第5步上进行写入操作。 下面是它存在的错误:第 2个人在第3步上读取的数据并不包含记录“ D a v i d”。它尚未由 第1个人写入。因此第2个人将“J o y”添加给数组@ P H O N E L,它并不包含“D a v i d”。与此同 时,第1个人将@ P H O N E L的拷贝写入数据库,该记录包含“ D a v i d”。 当第2个人的程序实例最终运行到第五步时,它将改写第 1个人写入的数据。该数据库最 终包含记录“J o y”而不是“D a v i d”。这显然是个错误。 这个问题实际上比你上面看到的还要严重。上面介绍的情况过分简单化 了。更加使人头痛的是, w r i t e d a t a ( )函数看上去是一次性打开和写入数据的, 但是实际上并非如此。多重处理的操作系统实际上能够在写入数据的中间停 止程序的运行,并暂时转入另一个程序的运行,然后过几个毫秒又恢复第一 个程序的运行。这两个程序都能够同时将不同的数据写入同一个文件。这会 使你的数据文件遭到破坏,或者被删除。 这种类型的问题有一个正式的名字,称为竞态条件。程序中的竞态条件很难发现,因为 竞态条件的出现和消失取决于一个程序有多少个实例正在同时运行。与竞态条件相关联的错 误往往并不明显。 第15学时 了解程序的运行性能使用177下载让多个程序同时更新相同的数据是很困难的,不过使用称为锁的机制就能解决这个问题。 文件锁可以用于防止一个程序的多个实例同时更改一个文件。 锁定文件会带来两个问题,不过最大的问题是不同的操作系统和不同的文件系统需要使 用不同类型的锁定机制。下面两节将介绍如何锁定文件以防止出现上面所说的问题。 15.5.1 锁定UNIX和NT下的文件 若要锁定U N I X和Windows NT下的文件,可以使用 P e r l的f l o c k函数。f l o c k函数提供了一 个“诱导式”锁定机制。这意味着你编写的程序如果需要访问文件,那么它就必须使用 f l o c k, 以确保没有其他人在同一时间将数据写入该文件。但是,其他程序如果想要修改文件,则仍 然可以修改文件,这就是为什么它称为“诱导式”锁定而不是强制锁定。 你可能已经熟悉一种类型的诱导式锁定,即交通信号灯。这种信号灯是为了防止许多车 辆同时进入十字路口的相同区域。但是,只有人人都按照信号灯的指示来行车,信号灯才能 正常发挥作用。文件锁的情况也一样。可能同时访问一个文件的每个程序必须使用 f l o c k函数 防止撞车。诱导式锁定并不能防止其他进程访问数据,它只能防止其他进程被锁定。 f l o c k函数带有两个参数,一个是文件句柄,另一个是锁的类型,请看下面的语句: 如果锁定成功,那么 f l o c k函数返回真,否则返回假。有时,调用f l o c k会导致你的程序暂 停运行,以等待其他锁被打开。下面将很快要说明这个问题。如果使用 use Fcnt1 qw(:f l o c k ), 那么你将使用符号名作为l o c k _ t y p e,而不是使用比较难以记住的数字。 文件锁有两种类型,一种是公用锁,一种是专用锁。通常情况下,当想要读取文件时, 可以使用公用锁,而当将数据写入文件时,可以使用专用锁。如果一个进程拥有对文件的专 用锁,那么这是那里存在的唯一的锁,其他进程则根本不拥有锁。但是,只要不存在专用锁, 那么许多进程可以同时拥有公用锁。这是因为只要没有人写入文件,那么许多进程就可以安 全地同时读取文件。 下面是l o c k _ t y p e可以使用的一些值: • lock_SH 这个值要求在文件上设置公用锁。如果另一个进程拥有该文件的专用锁,那 么f l o c k函数就会暂停运行,直到专用锁被清除,然后再取出该文件的公用锁。 • Lock_EX 这个值要求对已经打开而用于写入的文件设置一个专用锁。如果其他进程拥 有一个锁(公用锁或专用锁均可),那么f l o c k就暂停运行,直到这些锁被清除。 • Lock_UN 这个值用于释放一个锁。但是,很少需要这样做。你只要关闭文件,就可以 写出所有未写的数据并释放文件锁。如果释放仍然打开的文件上的锁,就会导致数据被 破坏。 当你关闭文件或者当你的程序退出时,即使退出时存在一个错误,用 f l o c k设置的锁也会被释放。 在试图读写的文件上加锁是很复杂的。由于打开文件句柄和锁定文件至少需要两个步骤 的进程,因此设置文件锁就会带来一些问题,首先必须打开文件,然后才能给文件加锁。如 178使用第二部分 高 级 特 性 下载果用o p e n(F H ,“> f i l e n a m e”),然后用f l o c k函数给文件加了锁,那么在你获得该锁之前,你 已经修改了该文件(用 >对文件截尾了)。通过截尾你可能修改了该文件,而其他进程则对该 文件设置了锁。 若要解决这个问题,就需要某种称为信标文件的东西。信标文件是个牺牲性文件,它没 有什么重要的内容,凡是对该文件拥有锁的人,均能处理该文件。 若要使用信标文件,你只需要有一个可以用作信标的文件名和两个函数,用于将信标文 件锁定和解锁。如程序清单 1 5 - 3所示。这不是完整的程序,不过它可以作为其他程序的组成 部分。 程序清单15-3 通用锁函数 这些锁函数可以将你当前不想运行的任何代码括起来,即使这些代码与读写文件毫无关系。 例如,下面这个代码段(即使同时被若干个进程运行)只允许每次有一个进程输出一条消息: 上面代码中的g e t _ l o c k ( )和r e l e a s e _ l o c k ( )函数将在需要给文件加锁时用于锁定文件。 拿着文件锁又等待用户输入(或者其他速度较慢的事件),这不是个 好主意。需要该锁的所有其他程序都会停止运行,以等待该锁被释放。 你应该获取你的锁,运行你锁定的敏感代码,然后释放文件锁。 15.5.2 在加锁情况下进行读写操作 下面我们介绍的是文本文件数据库的r e a d d a t a ( )和w r i t e d a t a ( )函数与文件锁一道运行的情况。 若要进行这项操作,需要一个信标文件和上一节介绍的g e t _ l o c k ( )和r e l e a s e _ l o c k ( )子例程。 程序清单1 5 - 4的第一部分是上一节中的锁代码。 程序清单15-4 在加锁情况下文本文件的输入/输出 第15学时 了解程序的运行性能使用179下载程序清单 1 5 - 4中的大部分代码你已经见过。本学时的前面部分中介绍过 g e t _ l o c k ( )、 r e l e a s e _ l o c k ( )、r e a d d a t a ( )和writedata ()等函数。 这个程序的关键部分从第34行开始。在这部分代码中,用 g e t _ l o c k ( )函数设置了一个文 件锁。这时用r e a d d a t a ( )函数将文件读入@ P H O N E L,并对数据进行操作,然后用 w r i t e d a t a ( )函 数将数据重新写入同一个文件。当所有这些操作完成时,如果其他程序等待释放该锁的话, r e l e a s e _ l o c k ( )函数将锁释放。 15.5.3 Windows 95和Windows 98下的加锁问题 情况表明,Windows 95和Windows 98不支持文件锁定。为何不支持呢?因为在这些操作 系统下,每次只有一个程序能够打开文件进行写入操作,因此文件加锁就没有必要。如果在 Windows 95或Windows 98系统上使用f l o c k函数,就会看到下面这个出错消息: 不过,幸好这些操作系统通常每次只支持一个用户进行文件操作。 180使用第二部分 高 级 特 性 下载本书中的程序清单都涉及到使用前面介绍的 g e t _ l o c k ( )和r e l e a s e _ l o c k ( )函 数进行文件锁定的问题。在 Windows 95或Windows 98下使用这些函数就会出 错,因为在这两个操作系统下无法实现 f l o c k函数。这些操作系统的程序清单 中可以省略这些函数。程序清单中将配有相应的说明来提醒你。 15.5.4 在其他地方使用文件锁的问题 在某些情况下,你可能同时有多个程序在读写文件。由于某个原因,你无法使用 f l o c k函 数。即使在能够使用 f l o c k函数的平台上,该函数也不是在所有情况下都适用的。例如,在 U N I X系统下,对网络文件系统( N F S)上的文件使用f l o c k函数是不可靠的。也可能你使用的 是U N I X服务器与Windows NT客户机相混合的操作环境,在这种环境中, U N I X通常支持f l o c k 函数,但是基本的文件系统都不支持 f l o c k。 在P e r l的常见问题(FA Q)列表的第五部分“文件与格式”中,你会看到如何不使用 f l o c k 而对文件进行锁定的说明。若要阅读该文档,请查看该文档中的 perlfaq 5这一节。 15.6 课时小结 在本学时中,我们介绍了在两次调用 P e r l程序之间存储数据的几种方法。首先讲述了 D B M文件以及如何使用D B M文件将哈希结构与你的硬盘连接起来,接着介绍了文本文件如何 将它们用作简单的数据库,最后,为了防止同时访问文件带来的问题,介绍了如何锁定文件 和保证数据安全的方法。 15.7 课外作业 15.7.1 专家答疑 问题:我能将第1 3学时中的数据结构存储在D B M文件或文本文件中吗? 解答:简单的回答是不行,或者说并不容易。而详细的回答是行,不过首先你必须将这 种“结构”转换成代表数据的字符串和包含该数据的结构,然后必须将它用作与 D B M相连接 的哈希结构中的一个值。进行这项操作的一个模块是 D a t a::D u m p e r。 问题:如何锁定D B M文件? 解答:D B M文件可以使用前面介绍的信标锁定系统来锁定。你只需使用程序清单 1 5 - 3中 的g e t _ l o c k ( )和r e l e a s e _ l o c k ( )函数,将它们放在D B M的o p e n和 c l o s e函数的前后: 问题:我是否能够以某种方式查看 f l o c k函数准备暂停但实际上并不让它暂停运行? 解答:是的,可以。一个值可以传递给 f l o c k函数,使它不暂停运行(称为非锁定 f l o c k)。 若要查看f l o c k是否暂停运行,请像下面这样在锁类型的后面加上 | L o c k _ N B: 第15学时 了解程序的运行性能使用181下载你甚至可以等待一会儿,看看是否给文件加了锁,如果你最终没有给文件加上锁,就会 输出一条消息: 15.7.2 思考题 1) 与D B M文件相连接的哈希结构中的关键字能够存储长度不限的关键字,是吗? a. 是 b. 否 2) 为什么将数据插入普通文件非常难? a. 周围的数据必须移动以便放置插入的数据。 b. 普通文件不能打开后同时进行读和写操作。 c. 文件被编辑时,必须锁定。 3) 关于锁定文件的说明是在FA Q的哪一节中介绍的? 15.7.3 解答 1) 答案是b。按照默认设置,D B M文件中关键字与值的合计长度是 1 0 2 4字符。 2) 答案是a。数据在文件中不能随意“向上”和“向下”移动,因此移动周围的数据非常 困难。如果选择c也是可以的,不过只有在多个程序同时使用文件时才能成立。 3) FA Q的第5节“文件与格式”。 15.7.4 实习 • 编写一段小程序,用来更新文件中的计数器,使每次运行程序时计数器递增 1。当程序 的多个实例同时运行时,记住要使用文件锁定特性。 182使用第二部分 高 级 特 性 下载下载 第1 6学时 P e r l语言开发界 本学时你可以得到一个稍事休息的机会,让我们聊一聊 P e r l语言的演变历史和它的文化背 景。 也许你认为这一学时应该作为本书的附录或者前言,但如果作为附录或者前言,往往很 容易被读者忽略。为了充分利用 P e r l的潜在功能,你必须了解P e r l语言开发界的一些情况。 了解是什么因素使得P e r l语言开发界能够顺利地进行它们的开发活动,这将有于了解你可 以使用什么资源,为什么存在这些资源,这些资源如何运行,为什么 P e r l能够成为这样一种编 程语言。许多资源都能够帮助你回答这些问题,本学时将帮助你查找这些资源。 在本学时中,你将要学习: • 关于P e r l的一些历史知识。 • 什么是C PA N,你如何使用它。 • 何处获得帮助。 16.1 Perl究竟是一种什么语言 为了获得P e r l的文化背景知识,它如何运行,以及你可以使用哪些资源,你有必要知道究 竟是什么因素使得P e r l能够一脉传承发展至今。 16.1.1 Perl的简单发展历史 1 9 8 8年,I n t e r n e t还是个非常不同的系统。首先,它的规模比较小,并且与它今天的样子 大不相同。当时的 I n t e r n e t大约只有6万台计算机,而今天它的数量超过 1千万台,并且仍在迅 速增加。 当时Wold Wide We b尚未问世,直到1 9 9 1年在的C E R N计算机网络上才提出了World Wi d e We b的思路,到了1 9 9 3年,出现了第一个图形浏览器 M o s a i c。 I n t e r n e t上的大部分信息都是文字信息。 U s e n e t新闻提供了一个传信系统,使得有兴趣的 用户组可以互相保持接触。当时的电子邮件与今天的情况非常相似,主要是文本邮件。文件 传送和远程登录形成了I n t e r n e t上的拥挤信息。 1 9 8 8年1月,Larry Wa l l宣布,他编写了另一个软件工具,以替代 U N I X下的a w k和s e d等工 具,他将它称为“P e r l”。P e r l的原始手册对它作了如下的描述: P e r l是一种解释性语言,它非常适合浏览各种文本文件,从这些文本文件中提取有关的信 息,并且根据这些信息打印报表。另外,它也是非常适合执行许多系统管理任务的语言。该 语言注重实用性(使用方便、有效、完整),而不注重形式上的美观(小巧、精致)。从语言 创建者的观点来看,它综合了 C、s e d、a w k和s h等语言的某些最佳特性,因此熟悉这些语言的 用户使用P e r l语言是不会遇到多大困难的。(语言发展的历史也留下了 c s h、P a s c a l甚至B A S I C -P L U S的某些遗迹。)P e r l的表达式句法与C语言的表达式句法非常接近。如果你有一个问题,原先使用s e d、a w k或s h来解决这个问题,但是 s e d、a w k和s h感到力不从心,或者这个问题需 要运行得稍快一些,而你又不想用 C语言来编写解决这个问题的程序,那么可以使用 P e r l。另 外,也有一些翻译程序,可以将你的 s e d和a w k脚本转换成P e r l脚本。 P e r l的第二个版本于1 9 8 8年6月推出,它与最新的 P e r l版本非常相似。Perl 2的大多数特性 都很容易理解和使用。它曾经是并且现在仍然是一种功能丰富而完善的编程语言。正如 P e r l手 册所说,当时P e r l的特性主要是用来进行文本处理和执行系统编程任务。 对于P e r l来说,1 9 9 1年是不寻常的一年。 1月份,Larry Wa l l与Randal Schwartz撰写的 《Programming Perl》一书的第一版出版。这本书曾经是(并且它后来的版本仍然是) P e r l语言 的权威参考书。这本书的粉红色封面上印有一只骆驼,这是 P e r l语言的正式标记。(骆驼并不 是一种好看的动物,但是它稳健可靠,值得信赖,并且用处极大。) 这本书的出版时间恰好与 Perl 4的推出时间相一致。 Perl 4是第一个广泛销售的 P e r l版本, 尽管它最后修改是在 1 9 9 2年,但是直到今天,我们仍然能够在 I n t e r n e t上的遥远角落看到它的 踪影。如果你在网上遇到它,你不应使用它。 1 9 9 4年1 0月,Perl 5问世。它推出了专用变量、引用、模块和对象等特性,其中“对象” 我们尚未介绍。1 9 9 6年1 0月,《Programming Perl》一书的第二版(“蓝色骆驼”)上市,它记 录了这些新特性。 16.1.2 开放源 P e r l取得成功的原因之一与 P e r l语言的开发和销售方式有关。 P e r l解释程序是一个开放源 软件。开放源是软件开发人员给一个老概念赋予的新术语,它称为“免费分配的软件”。这种 软件可免费提供给用户,凡是希望修改软件的源代码的人,都可以查看、调整和修改该源代 码。采用这种模式的其他软件包是 L i n u x和F r e e B S D操作系统, Apache We b服务器,以及 N e t s c a p e的开放源浏览器M o z i l l a。 使用开放源模式实际上是开发软件的一种非常有效的方式。由于开放源代码是由志愿者 缩写的,因此软件中通常不会包含不必要的代码。他们认为必要的特性,就会建议纳入源代 码中。这种软件的质量非常好,因为对软件有兴趣的每个人都有权并且有责任认真关注它的 开发过程,以找出它存在的错误。查看该代码的人越多,错误就越少。 Eric S Raymond撰写了一系列生动的文章,来介绍开发源代码的开发模 式,比如为什么它运行得这么好,为什么它在经济上非常合算,以及它是如 何开发而成的。第一篇文章“权威与廉价市场” (The Cathedral and the B a z a a r )对开放源开发模式如何工作进行了很好的介绍。这些文章的 U R L在本 学时的“课时小结”这一节中列出。 Larry Wa l l给P e r l解释程序申请了版权,因此他拥有 P e r l的版权,可以根据自己的意愿来处 理该软件的版权。但是,与大多数软件一样,用户可以购买 P e r l的使用许可证。软件许可证说 明了软件可以如何来使用和分销,当你打开从商店购买的软件时,会发现它是个印刷得很精 美的软件。Larry Wa l l为你提供了两个不同的软件许可证,供你选择,即 G N U普通公用许可证 184使用第二部分 高 级 特 性 下载和P e r l艺术家许可证。当你阅读这两种软件版本的许可证后,就可以根据协议条款来选择你需 要的许可证,以便将P e r l转售给其他用户。 两个许可证的文本都很长,现在将它们的内容概括如下: • 你可以将P e r l解释程序的源代码转售给其他用户,并将版权声明复制给他。 • 你可以修改原来的源代码,只要将你的修改明确标为你自己做的修改,并且既可以放弃 这些修改,也可以清楚地指明这不是 P e r l的标准版本。你也必须提供 P e r l的标准版本。 • 将P e r l转售给别的用户时,你可以收取合理的费用。也可以收取一定的支持费用,但是 不得将P e r l本身销售给其他用户。你可以将 P e r l纳入你销售的其他产品中。 • 使用P e r l编写的程序不受本许可证的约束。 • 对P e r l不作任何担保。 你不得将类似上面这样的对 P e r l许可证条款的概述用于法律目的。这些概述只是为了使你 对这两种许可证的条款有一个大致的了解。 在你想要将 P e r l纳入另一个软件包之前,应该亲自阅读许可证的内容, 并且弄清你的行为是否符合这两种许可证的规定。 P e r l艺术家许可证包含在 销售的每个 P e r l 中,其 文件名为 A r t i s t i c ( 艺 术家 )。可 以通过网址 h t t p : / / w w w. g n u . o rg查看G N U普通公用许可证的内容。 有了许可证,P e r l就可以在开放论坛中进行开发和改进。运用这种方法,凡是想要阅读和 提出修改建议的用户,都可以看到 P e r l的全部源代码。这种方法有助于实现出色的编程,避免 陷入专用的、隐蔽的和模糊不清的软件解决方案之中。 16.1.3 Perl的开发 P e r l解释程序、语言以及该语言包含的各个模块的开发是在一个邮件列表上进行的,在这个 邮件列表上,Perl的开发人员可以提出修改建议,查看错误报表,对Perl源代码的修改进行争论。 凡是希望参与这个开发过程的人,均会受到欢迎,这正是开放源的特色。不过,为了防 止混乱,人们提出的修改意见将由一个核心开发人员小组负责筛选,这些开发人员负责批准 和拒绝人们的修改意见,并维护 P e r l开发的核心内容。修改意见要进行评估,看它们对 P e r l是 否有利,这些修改究竟有多大用处,是否有的人的修改取得了成功。 Larry Wa l l负责监督这个 开发过程,担当着仁慈的总监的角色,允许进行确实有利的修改,并否决他认为对 P e r l不利的 修改。 已经推出的P e r l版本按两种方法编号。1 9 9 9年8月以前,它们按照主次修补次数( m a j o r. m i n o r _ p a t c h l e v e l)的格式进行编号。因此,4.0 3 6_1 8是指P e r l第4版,第3 6次发布,第1 8次修补。有 时P e r l版本号不包含修补次数。截止到1 9 9 9年秋撰写本书时,P e r l的当前版本是5.0 0 5。 P e r l的下一个版本是 5 . 6。这个版本的编写方案比较传统,它采用主、次版本号格式。因 此,Perl 5.6的下一个版本将是5 . 7等等。 16.2 Perl综合存档文件网(CPAN) P e r l提供了另一些模块,以便进一步扩大你的开发环境。这些模块均包含在 C PA N中。 第16学时 Perl 语言开发界使用185下载16.2.1 什么是CPAN P e r l综合存档文件网即C PA N,它是P e r l文档和软件的一个大型集合体。该软件是由为 P e r l 语言家族编写模块、程序和文档的志愿软件人员共同开发的。 C PA N中可以使用的模块非常广泛。在撰写本书时, C PA N的建立大约已有4年历史,可供 安装的模块超过3 5 0 0个。这些模块涵盖的编程问题的范围极其广泛。表 1 6 - 1是个简短的列表, 可以使你对C PA N中的模块有个大致的了解。 表16-1 CPA N中的部分模块一览表 T K 用于P e r l程序的图形接口。可以使用特定的工具箱模块来访问特定的图 形库,比如Win32 API、G t k、G n o m e、Q t或X I I工具箱 N e t::* 网络模块。它们是用于M a i l、Te l n e t、I R C、L D A P和4 0多个其他程序的接口 M a t h::* 包含3 0多个模块,用于复数、快速傅立叶转换和矩阵操作的各种结构 D a t e::* Ti m e:: * 用于将日期/时间转换成各种不同格式和将各种格式转换成日期/时 间并对它们进行操作的模块 D a t e::* Tr e e::*` 用于对链接列表和B -树状结构之类的数据结构进行操作的模块 D B I::* 数据库的通用结构 D B D::* 商用和免费数据库的接口,这些数据库包括 O r a c l e、i n f o r m i x、I n g r e s、 O D B C、M s q I、M y s q l、S y b a s e和许多其他数据库 Te r m::* 对文本模式的屏幕(如 D O S的C o m m a n d窗口或U N I X终端仿真程序)进 行精确控制的模块 S t r i n g::*,Te x t::* 包含几十个模块,用于对文本进行分析和格式化 C G I::* 用于We b页的创建、服务、提取和分析的各个模块 U R I::* H T M L::* LW P::* G D,G r a p h i c s::*,I m a g e::* 用于对图形和图像进行操作的各个模块 Wi n 3 2::*,Wi n 3 2 A P I::* 用于对Microsoft Wi n d o w s进行操作的模块 需要记住的最重要一点是,对于大多数问题来说,已有的模块至少可以部分地解决某个 问题。C PA N中的这些解决方案已经具有相应的代码,经过了测试,并且有许多程序员对代码 进行了审查,以保证它们的正确性和完整性。 C PA N中的所有模块的版权均由各自的开发人员所拥有,因此你应该阅读每个模块所附的 R E A D M E文件,以了解使用模块时应该遵守的条款。大多数情况下,这些模块的销售条款与 P e r l本身的条款相同,也就是要按照 P e r l艺术家许可证或G N U普通公用许可证的条款进销售。 C PA N也是一个标准模块的名字,它用于帮助用户将辅助模块安装到你的 P e r l中。C PA N 模块在本书附录“安装模块”中作了说明。 16.2.2 为什么人们愿意提供自己的开发成果 在过去半个世纪的计算机编程发展历程中,程序员一次又一次解决着一些相同的问题。 从5 0年代以来,搜索、排序、通信、读取、写入,这些编程问题实际上没有多大的变化。关 于计算机编程理论和管理的一些著作在二三十年以后仍然可用。 一次又一次地解决相同的编程问题并不总是一件有意思的事情,并且常常会产生一些质 量不高的解决方案,这叫做“仿制车轮”。最终,程序员对解决一些有意思的编程问题产生了 极大的兴趣。 1 8 6使用第二部分 高 级 特 性 下载令程序开发人员感到尴尬的一种情况是:花费了很长的时间,投入了大量精力,以便解 决一个复杂的问题,结果却发现可以使用一个简单而巧妙的方案就可以解决这个问题。这种 尴尬促使程序开发人员去寻求一些方法以便与其他人共享代码。共享代码带来的一个非常有 趣的副产品是可以产生更好的代码,因为其他程序员将会发现你的代码中存在的问题,而你 自己却没有注意到。 C PA N是P e r l语言开发界为了避免进行不必要的开发工作所作努力的结果。它所包含的模 块能使你不会经历“仿制车轮”的尴尬。 大多数模块的质量是很好的,模块与 P e r l一样,都是在开放源生产方式下开发的。当你在 系统中安装一个模块时,你将自动拥有该模块的源代码。你可以自己查看源代码,并且根据 许可证条款的规定,将源代码的各个部分用于你自己的程序,并可修改源代码,甚至可以与 源代码的开发者联系,提出修改建议。 从表面上看,C PA N的开发是大量程序员共同努力的结果,但开发人员为 C PA N提供自己 的开发成果的实际原因是千差万别的。有时是为了帮助其他人解决类似的编程问题,有时是 为了达到一个很好的目标,有时是为了得到同行们的尊重和崇敬,这是一种很强的动力。不 管原因是什么,最终结果是大量的成果可以用于你自己的程序。 16.3 下一步你要做的工作 当你阅读了本书前面的三分之二的内容之后,应该对 P e r l的基本概念有所了解了。不过你 并没有学到该语言的全部知识。在我的书架上,至少放着 5、6本关于P e r l语言的著作,除去重 复的内容,它们总共有2 3 0 0页左右,尽管如此,仍然有许多问题没有包括。 你无法从一种资源中学到 P e r l的全部知识,不过下列各节能够告诉你下面应该做的一些工 作。 这里介绍的一些资源都是按照你查找资源时应该遵循的次序列出的。虽然有些例外情况, 但是总的来说,按照这个顺序来学习,可以用最快的方式来解决你的问题。 16.3.1 要做的第一步工作 当你遇到一个P e r l的问题时,要确定必须采取的第一步操作是很困难的。你会感到不知所 措,如果你为解决这个问题花了一点儿时间之后,你可能变得心烦意乱。不过,不要慌,要 相信自己能够解决所有问题的。不管怎样,这是重要的第一步。大多数人在解决一个问题时 如果进展不大,就会灰心丧气,这会影响你清晰的思路,结果会把事情搞得更糟。 这时,应该暂时放下你的工作,让自己平静下来,精神放松。最终你一定能够解决你的 难题。 16.3.2 最有用的工具 你的P e r l工具箱中的最有用的工具就是 P e r l本身。首先,你必须搞清要解决的问题究竟是 个什么性质的问题。通常问题可以分为两类,一类是语句错误,另一类是逻辑错误。 如果你的问题与语句相关,通常可以将它分为两个较小的问题。一个问题是某些 P e r l元素 的使用不正确,另一个问题是键入错误。请运行你的程序,查看出错消息。出错消息通常能 够指明P e r l对出错代码行的最佳判断。你可以查看这个代码行,看看是否存在下列情况: 第16学时 Perl 语言开发界使用187下载• P e r l的出错消息是否专门指明你应该查看代码行上的哪个位置。然后请查看这个位置。 P e r l解释程序可以成为你查找错误的最佳助手。 • 左括号、左方括号和左花括号是否有匹配的右括号? • 是否仔细检查了输入文字的拼写?请再检查一次。你会惊奇地发现有多少程序错误是拼 写不正确造成的。 • 是否漏掉了什么东西?比如逗号或句号? • 指定的某一行的前面一行是否正确? • 如果你回到本书中介绍某种类型的语句这一节,能找到类似你编写的代码的示例代码 吗? • 如果你从另一个源代码中拷贝了代码,是否检查了其他位置,以便找出类似的一段代 码?它可能存在错误。 如果你的P e r l程序能够运行,但是它不能产生正确的结果,那么你的逻辑可能有问题。在 你分析原因之前,请执行下列操作步骤: 1) 确保程序中以#!开头的代码行包含一个 - w。 2) 确保程序顶部附近的某个位置上有 use strict。 许多明显的逻辑错误都是- w和use strict能够捕获的简单错误,请使用这些工具。如果仍有 问题,请继续阅读下面的内容。 16.3.3 查找程序中的错误 如果你能够肯定你的程序的语句是正确的,但是它无法执行正确的操作,那么你就必须 进行基本的程序调试。 调试程序时首先和最常用的一个方法是使用普通的 p r i n t语句。如果你在程序中小心地使 用这个语句,它能够对正在运行的程序进行某种运行期诊断。请看下面这个例子中 p r i n t是如 何运行的: 请记住,当你的程序完成时,必须取出所有的 p r i n t调试语句。建议你将某个字符串插入 这些语句(“D E B U G”),这样就可以在以后将它们全部找出来。通过输入到 S T D E R R文件句 柄,可以将你的正常输出与诊断信息分开。如果你将原义符号 _ L I N E _和_ F I L E _纳入你的诊断 信息,P e r l就能输出当前代码行和文件的名字。 可以试用的另一种方法是使用 P e r l调试程序,几乎可以将调试程序用于任何 P e r l程序。观 察程序按步骤运行的情况,会给你以很大的启发。第 1 2学时对P e r l调试程序的使用方法作了详 细的说明。 16.3.4 首先要靠自己来解决问题 如果你的程序语句完全正确,代码逻辑也没有问题,但是仍然无法得到你想要的结果, 那么你应该寻求外界的帮助。首先可以通过 P e r l文档来寻找解决问题的办法。 1 8 8使用第二部分 高 级 特 性 下载正如我们在第1学时中介绍的那样,用户安装的每个 P e r l产品都配有一套完整的文档资料。 如果是Perl 5.005版本,它的文档资料超过 1 7 0 0页。每个模块,每个函数,以及 P e r l语言的大 多数特性,都在文档资料中作了介绍,并且在常见问题列表中也有相应的说明。 若要得到可用文档资料的清单,请在命令提示符处键入 perldoc perl。它列出了手册的每 一节内容和对P e r l的总的描述。 常见问题列表包含了初学者和专家们对 P e r l编程语言提出的最常见的问题。你至少应该对 它进行一次浏览,以便对里面列出的各种问题有个基本的了解,即使你并不完全理解它的答 案,也值得浏览一下。 如果因为某个原因,你的系统上没有安装 P e r l文档资料,或者 p e r l d o c并没有显示该文档, 那么首先应该告诉你的系统管理员,来查找该文档。正确安装该文档是非常重要的,因为在 线文档与你运行的P e r l版本是完全匹配的。其他任何文档很可能与此不同。 如果你无法访问在线文档,也可以通过网址 h t t p:/ / w w w. p e r l . c o m找到该文档。 16.3.5 从别人的程序错误中吸取教训 U s e n e t是个分布式传信系统, 8 0年代初开发成功,并立即应用到方兴未艾的 I n t e r n e t上。 U s e n e t分为成千上万个讨论组,涉及的问题从医疗、园艺、信息处理、科幻小说到曲棍球比 赛和电动剃须刀,无所不包。并且还有许多地区性讨论组,世界上每个地区都有。下面是 P e r l 的一些特定新闻组: c o m p . l a n g . p e r l . a n n o u n c e 关于P e r l的新版本、新模块和信息的新闻 c o m p . l a n g . p e r l . m o d e r a t e d 小信息量讨论组,对P e r l进行适度讨论 comp.lang perl.misc 大信息量讨论组,讨论与P e r l相关的任何问题 若要读取U s e n e t的新闻,需要一个新闻阅读器。找到新闻阅读器并不难,可以访问任何 一个软件下载站点,抓取一个新闻阅读器。可以访问若干个 We b站点,比如 d e j a . c o m或 S u p e r n e w s . c o m,它们镜像了We b格式的U s e n e t新闻组,只要求你拥有一个 We b浏览器就可以 阅读新闻。 在这些新闻组中,人们可以提出他们在使用 P e r l时遇到的问题,其他人则可以回答这些问 题,并且全部是在自愿的基础上进行。另外,这里也可以讨论与 P e r l有关的人们普遍感兴趣的 问题。 在我的整个编程生涯中,我认为在信息处理领域中不存在什么原始的问题。你所遇到的 问题,其他人以前就遇到过。关键是要找到提出问题的人和他能提供什么答案。很可能至少 有一个人提出的问题与你在某个新闻组中提出的问题非常相似。 d e j a . c o m维护了关于U s e n e t的许多情况的在线历史记录。使用它的搜索引擎,利用几个选 择准确的关键字,就能够找到你的问题的答案。 例如,你想了解如何编写用于抓取 We b页的P e r l程序,可以访问 d e j a . c o m站点的P o w e r S e a r c h屏幕,然后将下列信息填入屏幕: 在这个例子中,你可以将所有其他域置空。当搜索结果返回时(几乎会有 1 0 0个匹配的搜 索结果),大多数匹配的搜索结果均与你提的问题有关。记住,你在 U s e n e t中阅读文章时应该 第16学时 Perl 语言开发界使用189下载注意下列要点: • 并非所有答案都正确。任何人都可以提出问题,任何人也可以回答问题。你可以阅读几 个答案,自己确定哪些答案有道理。你从这些答案中得到的收获是各不相同的。 • 如果你无法确定某个答案是否正确,可以根据这个答案,自己来核实有关的信息。可以 访问关于该问题的在线手册页,现在你知道何处可以查找在线手册了。 • d e j a . c o m对5年中的新闻进行了存档。它提供的答案在 5年前是正确的,但现在的正确性 也许要打折扣了。 16.3.6 请求他人的帮助 如果你已经查看了在线文档,参考书和 U s e n e t的历史信息,但是仍然没有找到问题的答 案,那么就应该求助于其他人了。 请求他人的帮助应该是你最后采取的措施,当然不是你首先采取的措施。专家是回答问 题的最佳人选。他们能够接受你提出的措词糟糕的问题,并且在某个时候为你的问题提供出 色的解决方案。不过与我提到的所有其他资源不同的是,人不具备解决问题的无限能力。他 们会感到疲倦,他们会有心情不好的日子,他们尤其会厌倦一次又一次回答同一个问题。 虽然你所问的这个人很可能知道问题的答案,但是应该记住,你要他回答你的问题必然 要占用他的时间,并且是利用他的经验。在麻烦他人帮你解决问题之前,你有责任先从其他 地方寻求解决问题的方法。 若要在U s e n e t上提出问题,必须使用新闻阅读器或者前面讲到的一种 We b新闻接口。在你 提出问题时,请遵循下列原则: 1) 在你做其他事情之前,先要看一看新闻组是否拥有常见问题列表。 P e r l新闻组有一个这 样的列表,它是随着 P e r l解释程序一道提供给你的。对于其他新闻组,请先搜索 d e j a . c o m,找 出该组的常见问题的列表,然后再发布你的消息。 2) 应该将问题提供给正确的新闻组。一般的 P e r l语言问题应该提供给comp.lamg. perl.misc 新闻组。与C G I相关的编程问题应该在 c o m p . i n f o s y s t e m s . w w w. a u t h o r i n g . c g i上发布。通过该新 闻组的常见问题列表,你就会知道是否在正确的地方发布了你的问题。 3) 为你发布的问题选择一个比较好的主题行。它应该很好地描述你要提的问题,避免毫 无用处的文字(“帮帮忙”、“新问题”之类的文字都是多余的),既要有表义性,也要简明扼 要。 4) 确保问题的主体包含下列元素: a. 说明你究竟想干什么(甚至应该说明为什么要这样做)。 b. 说明到现在为止你已经做了哪些试验。 c. 说明你遇到过哪些错误。 如果你发布了你的代码的出错消息或代码的引用,那么也应该发布足够的代码,使回答问 题的人能够知道你的代码的运行情况。如果你打算处理数据,则应包含一些代码行作为例子。 问题的主体不应包括下列元素: a. 大的代码段。 b. EXE等二进制文件或u u e n c o d e d编码的文件。 c. MIME附件。相反,你可以将例子和代码纳入文本的主体中。 1 9 0使用第二部分 高 级 特 性 下载5) 务必发布一个有效的电子邮件地址,以备有人想要回答你的问题但又不想公开回答时 使用。 6) 最重要的一点是:你应该非常有礼貌。你是寻求素不相识的人为你提供帮助。任何人 都没有帮助你的义务。你应该多说“请”和“谢谢你“,并且不要使用不礼貌的评语。不要 使用欺骗性手段来谋求他人的帮助,比如说“帮助一个可怜的小女孩编写她的 C G I程序”,或 者说”我将为你提供一个免费的 We b页,如果你⋯⋯”这些话语显得非常失礼,而且有些低 声下气。 当你将文章发布在新闻组上之后,应该等待其他人提供解决方案。 U s e n e t新闻要花费数 天时间才能传遍全世界,人们不可能跟踪和阅读每一篇文章。你应该有耐心,在等待解决方 案的同时,可以再次提出这个问题。无论你做什么,不要过早在 U s e n e t上再次提出这个问题。 至少要等待两个星期之后,再提出这个问题。你应该改变提出问题的措词,使主题行更加清 楚,然后再试一次。 对你的文章的反应可能立即出现(在几分钟内),也可能在一个月或更长时间之后出现。 正如在前面讲过的那样,对你提出的问题的解决方案的质量差别很大。有些很有参考价值, 有些可能是错的。有些回答很有礼貌,有些则非常粗鲁。按照网络礼仪,你应该感谢为你提 供解决方案的人。如果有人过分热情,你不必在意。 16.4 其他资源 如果想了解P e r l、P e r l编程和P e r l开发界的情况,可以查看下列辅助资源: • Larry Wa l l、Tom Christiansen和Randal Schwartz撰写的《Programming Perl》一书。这 本书被人们视为 P e r l程序员的圣经。当你学习了 P e r l的基本知识后,可以将这本书作为 最佳参考书来使用。 • Tom Christiansen和Nothn To r k i n g t o n撰写的《The Perl Cookbook》一书。这是涉及P e r l 的各种问题、代码举例、解决方案以及对数百个问题的评论的总汇,它采用食谱的书写 格式。它提出了每个问题,每个问题的解决办法,然后是每种解决办法的代码举例和说 明。 • P e r l季刊。这份季刊自称是“ P e r l语言开发界之声”。这是一份真正的技术性刊物,里面 的文章都是P e r l语言开发界(每天使用 P e r l的程序开发人员)的成员撰写的,而不是由 权威学者或专业撰稿人撰写的。它的创刊号就声称:“我们的目标是⋯⋯使之成为一份 知识性出版物,以探讨P e r l的技巧,编程的技巧和其他的一些技巧⋯⋯” 若要进一步深入了解这些问题,请查看下列信息: I n t e r n e t的发展历史:H o b b e的Internet Ti m e l i n e,网址为: P e r l的发展历史:C PA S T,网址为: P e r l季刊,网址为: C PA N,网址为: 第16学时 Perl 语言开发界使用191下载在线文档: 在你的系统上,也可以查看 h t t p:/ / w w w. Perl.com。 Eric S. Raymond的开放源文章: 16.5 课时小结 本学时你学习了关于 P e r l的发展历史以及开放源开发模式如何用于 P e r l,还学习了关于 C PA N的一些知识,它为什么会存在,以及谁负责对它进行维护。最后,学习了在开发 P e r l程 序时遇到问题时可以使用何种类型的资源来加以解决。 16.6 课外作业 16.6.1 专家答疑 问题:如果说We b是在P e r l之后问世的,那么为什么P e r l是个C G I语言? 解答:P e r l之所以属于C G I语言,其原因与计算机可以用来玩游戏是相同的。原因并不在 于它们为了什么目的而发明,而在于 P e r l非常适合作为一种 C G I语言。下一个学时我们将要详 细介绍为什么P e r l是个非常好的C G I语言。 问题:我在U s e n e t上提出了一个编程问题,但是却得到了一个粗鲁和令人恼火的答复。 我应该怎么办? 解答:首先,这个使你恼火的答复是否包含某些好的建议。如果有,那么你应该采纳这 些建议,不要在乎粗鲁无礼的一面。否则你可以对这个答复不予理睬。人生苦短,不要把生 命浪费在无谓的纠纷上。 问题:有没有一种简便的方法可以用来搜索 C PA N? 解答:有的。位于h t t p : / / s e a r c h . c p a n . c o m上的We b页包含一个通用搜索函数,你可以用来 浏览C PA N最近的修改情况,并可按分类查看各个模块。 16.6.2 思考题 1) 关于用P e r l进行C G I编程的问题首先应该传送给哪个 U s e n e t组? a . c o m p . i n f o s y s t e m s . w w w. a u t h o r i n g . c g i b . c o m p . l a n g . p e r l . m i s c 2) 如果你的系统没有这个文档,应该怎么办? a .要求系统管理员安装该文档。 b .将消息发送到c o m p . l a n g . p e r l . m i s c。 c、设法从辅助来源那里获取该文档 ,比如h t t p : / / w w w. Perl.com。 16.6.3 解答 1) 答案是a。c o m p . l a n g . P e r l . m i s c可以作为你提出C G I问题的第二个地方。 2) 答案是a和c。可能是先a后c这个顺序。文档可能已经安装,系统管理员可以帮你找到 它。如果不行,w w w. P e r l . c o m有一组文档的最新拷贝。 1 9 2使用第二部分 高 级 特 性 下载下载 第三部分 将Perl 用于C G I 第1 7学时 CGI 概述 第1 8学时 基本窗体 第1 9学时 复杂窗体 第2 0学时 对HTTP 和C G I进行操作 第2 1学时 c o o k i e 第2 2学时 使用C G I程序发送电子邮件 第2 3学时 服务器推送和访问次数计数器 第2 4学时 建立交互式We b站点下载 第1 7学时 C G I概述 毫无疑问,人们普遍认为 I n t e r n e t的爆炸性流行主要是因为有了 Wold Wide We b。自从 1 9 9 3年第一个图形 We b浏览器的问世以来, I n t e r n e t便以惊人的速度迅速发展, 1 9 9 3年前后 I n t e r n e t上的主机数量每2 0个月翻一番,而目前则每 1 2个月翻一番。专用网络即 I n t r a n e t的增长 速度甚至更快。 1 9 9 3年以来,We b的内容已经变得越来越杂, We b用户希望每个We b页不只是能够显示静 态(不变的) We b内容。成功的 We b站点必须显示动态 We b页,也就是能够提供最新信息的 We b页。要使复杂的We b页能够跟上内容的迅速变化,这几乎是不可能的,因此出现了公用网 关接口(C G I)。 为了学习后面7个学时的内容,你必须具备关于超文本标记语言( H T M L) 的某些知识。如果你对 H T M L不熟悉,不必担心,它学习起来并不困难,也 不需要通过本书来学习更多的这方面的内容。 H T M L是一种标记语言,常用于创建 We b页。H T M L由纯文本组成,其格 式化代码嵌入文本之中,以指明 We b浏览器应该如何显示文本。例如, HTML is not hard to learn这句话是个普通文本,而< I > < / I >这些标记则 不属于普通文本。它们称为标记,用于描述应该使用何种格式来显示文本。 在上面这个例子中, We b浏览器应该用斜体字来显示单词 n o t。(请记住,并 非所有浏览器都具有图形显示功能。) 关于H T M L的详细说明,不属于本书要讲解的范围。介绍并不困难,但是 有大量的资料需要加以说明。H T M L的技术规范由World Wide We b集团(W 3 C) 负责维护,该机构的网址是 h t t p : / / w w w. w3c.org,可以通过该网址找到许多很 好的教材。《HTML 24学时教程》是介绍H T M L的一本好书。 在本学时中,你将要学习: • We b是如何运行的。 • 在编写C G I之前你应该具备什么知识。 • 如何编写你的第一个C G I程序。 17.1 浏览Web 也许你已经知道, We b是指试图进行数据交换的两个不同系统之间进行的交互操作。试 图抓取We b页的系统称为客户机系统。客户机系统通常运行一个称为 We b浏览器的程序,比如 N e t s c a p e、Internet Explorer和O p e r a等,这是你习惯于日常使用 We b的应用范围。We b浏览器 配有浏览按钮和书签,用于在屏幕上绘制 We b页。 在We b的另一端是称为We b服务器的系统。该系统负责接收客户机查看 We b页的请求,从 本地磁盘上检索We b页,并将We b页发送给客户机系统,即你的 We b浏览器。图1 7 - 1显示了这种交互操作的情况。 17.1.1 检索一个静态Web页 当客户机需要检索一个 We b页时,它要查看统一资源定位器( U R L),以确定使用什么协 议、服务器,以及在该服务器上提出的是什么请求。典型的 U R L类似下面的形式: U R L可以分割成下列部分: • http 这个部分是指使用的协议。 H T T P即超文本传输协议,它是传送 We b页时使用的协 议。你也会看到文件传输协议( f t p)或保密H T T P(h t t p s)等协议。 • w w w.google.com 这部分是服务器名,也称为主机名,它包含你想要的文档。有时,这 部分不是主机名,而是个 IP 地址,通常写作 4个数字,数字之间用圆点隔开,比如 2 0 9 . 1 8 5 . 1 0 8 . 1 4 7。不过这些地址不如主机名那样可靠。 • : 80 这部分是个端口号,用于确定你的客户机与服务器是在哪个端口上互相进行连接。 这部分通常是可有可无的。使用的协议决定了使用什么端口。 h t t p协议通常使用端口8 0。 • more.html 这部分是指对服务器提出的请求。通常这是你想检索的一个文档。有时它写 作一个路径名,例如 / a r c h i v e s / f o o . h t m l,也可以用其他字符作为结尾(? &),不过它基 本上指客户机要求向服务器检索的文档。 这时客户机为h t t p执行下列操作部骤(见图1 7 - 2): 1) 主机名(w w w. g o o g l e . c o m)转换成I P地址。 2) 使用I P地址和端口号与w w w. g o o g l e . c o m上的服务器建立连接。 3) 向服务器提出检索We b页m o r e . h t m l的请求。客户机等待服务器应答。 4) 服务器发出应答,在上例中,服务器发出 m o r e . h t m l的内容,然后断开与服务器的连接。 5) 客户机在屏幕上显示服务器应答的 We b内容。 196使用第三部分 将Perl 用于C G I 下载 图17-1 We b浏览器检索一 个We b页 Web 浏览器 Web 服务器 Web 页 图17-2 客户机向服务器提 出检索 We b页的请 求 Web 浏览器 Web 服务器 Web 页客户机与服务器之间进行“通信”的详细情况将在第 2 0学时中介绍。 17.1.2 动态Web页— 使用CGI 在检索通常的 We b页时,服务器只是根据你想要的文档并从它的磁盘存储器上检索这个 文档,然后将它发送给客户机,如图 1 7 - 3所示。 图1 7 - 3中的服务器根本不对数据进行任何处理,它只是查看客户机提出的请求,并将请 求的数据传送给客户机。 在We b上创建动态内容的方法之一是使用 C G I程序。C G I是We b服务器用来在服务器上运 行程序以便生成 We b内容的公认的方法。当 U R L指明C G I应该在它上面运行 C G I程序来生成 We b内容的服务器时,该服务器就启动该程序运行,该程序则生成 We b内容,然后服务器将内 容传递给客户机,如图1 7 - 4所示。 每当客户机请求检索一个实际上是 C G I程序的We b页时,便出现下列操作: 1) 服务器启动C G I程序的一个新实例。 2) CGI程序使用它需要的信息生成一个 We b页,或者生成另一个应答。 3) 该We b页被送回给客户机。 4) CGI程序退出。 C G I程序可以是任何类型的程序。它可以是个 P e r l脚本,就是你将要在这里学习的一项内 容。它也可以是用C、UNIX shell,p a s c a l,L I S P、T C L或任何其他编程语言编写的程序。而 许多C G I程序是用P e r l编写的,这完全是一种巧合。 P e r l恰好非常适合编写用于文本处理的程 序,而C G I程序的输出常常是文本。 C G I程序的输出几乎可以是任何形式的信息。它可以是图形、 H T M格式的文本、压缩文 件、流式视频信息,或者你在 We b上找到的任何其他类型的内容。总的来说,你编写的 C G I程 序将生成H T M L格式的文本。 C G I不是一种语言,它与 P e r l之间并不存在特殊的关系,与 H T M L语言也 没有任何关系,与H T T P之间也没有多少关系,它只是 We b服务器与代表服务 器运行的程序之间的一个公认接口。 C G I的技术规范由美国国家超级计算机 应用中心维护,该中心的网址是h t t p : / / w w w. n c s a .uiuc.edu/cgi/ interface.html。 你可以在后面 7个学时中了解 C G I的详细特性。 第17学时 CGI 概述使用197下载 图17-3 检索静态 We b的示 意图 Web 浏览器 Web 服务器 Web 页 图17-4 C G I 脚本 生成的 We b页 Web 浏览器 Web 服务器 CGI 程序17.2 不要跳过这一节内容 当你准备编写C G I程序时,首先必须搞清几个问题,否则,第一次编写 C G I程序的经历一 定不会使你感到愉快。你预先查找这些信息是比较容易的,而在调试程序时要搞清这些问题 则要困难得多。 若要使用C G I,必须拥有We b服务器。C G I编程新手遇到的常见问题是他们试图在 We b服 务器没有正确安装的情况下就编写 C G I程序。若要获取We b服务器,可以从两种方法中选择一 个。可以在商用 We b服务器上租用一定的空间,也可以运行自己的服务器。决定权在你的手 里,可以根据你愿意支付的费用,需要的带宽,以及你的技术熟练程度来作出决定。 若要获得商用We b服务器,你可以搜索 We b并查找一个。这些商用服务器常常称为“ We b 托管”公司,它们收取的费用和提供的特性可以根据情况而千差万别。如果你打算编写 P e r l C G I程序,应该确保Perl 5将作为C G I的编程语言。很少有 We b托管公司不支持Perl 5作为C G I 编程语言或者根本不允许 C G I编程的。应该避免使用不支持 Perl 5的公司提供的服务,有许多 其他公司可以供你选择。 还应该确保We b托管公司允许你使用自己的脚本。有些公司声称它们允许使用 Perl CGI程 序,但是接着又要求你使用它们公司的程序,有时要收取一定的费用。你也应该避免使用这 些公司提供的服务。 另外还有一些公司,它们收取一定的费用来“审查”你的C G I程序,它之所以要收取费用, 原因是你可以使用这些程序。如果你选择这些公司中的一个,应该安装你自己的服务器,以 便进行相应的测试,因为“审查”是非常昂贵的。 如果你具备某些专门技能并愿意阅读全部说明的话,运行你自己的个人 We b服务器并不 十分困难。首先,必须选择一个 We b服务器。如果你运行Wi n d o w s,可以从几十种免费的或者 接近免费的We b服务器中选择你要安装的服务器。一定要确保它们支持 P e r l作为C G I脚本语言。 少数商用We b服务器也可以用于Wi n d o w s,比如M i c r o s o f t的Internet Information Server(IIS)。 如果你拥有一台 U N I X计算机,也可以使用少量商用 We b服务器。请与 U N I X供应商联系 索取We b服务器清单。 I n t e r n e t上最流行的 We b服务器是A p a c h e,它是完全免费的。如果你拥有一个 C编译器, 那么Apache We b服务器是很容易安装的。如果你习惯于编辑配置文件,那么它的运行也是非 常容易的。 A p a c h e甚至可以用于 Miorosoft Wi n d o w s平台。关于 A p a c h e的信息,请访问 h t t p : / / w w w. a p a c h e . o rg。 如果你运行自己的We b服务器,在试图编写C G I程序之前,应该确保We b服务器运行正确, 并且能够为静态We b页提供服务。如果你的 We b服务器不能为静态We b页提供服务,这说明你 的C G I程序很可能无法运行。 还应该检查We b服务器的配置,以确保你已将C G I脚本正确地激活。如果不激活这个特性, 那么C G I编程的初学者就会遇到非常头痛的麻烦。 检验表 无论你是运行自己的We b服务器,还是租用商用We b服务器上的空间,都必须花费一点时 间来完成下面这个检验表中的操作,确实如此。请将这些信息写下来,以后你就可以省去很 多麻烦。 198使用第三部分 将Perl 用于C G I 下载• 如果你是从商用We b服务器主机上租用空间,那么该主机将为你提供所有的信息。这些 信息可能位于主机的 We b站点上的FA Q中,也可能包含在你建立帐户时发送给你的文档 中。如果你没有接收到这些信息,可以与 We b托管公司联系索取这些信息。如果你想使 C G I程序正确运行,获取这些信息是非常重要的。 • 如果你已经配置和安装了自己的 We b服务器,那么这些信息应该是配置进程的组成部分。 如果你遇到了问题,请查看一下是否能够找到一个回答这些问题的 FA Q,或者是否能够 找到要检验的配置文件。 如果你要进行C G I编程,必须知道下列信息: • We b服务器上P e r l的位置 你必须知道P e r l解释程序安装在We b服务器上的什么位置。由 于你必须修改程序中的#!行代码以便反映该路径的情况,所以必须知道这个信息。如 果你的We b托管公司运行M i c r o s o f t公司的操作系统,那么你可以不需要这个信息。 • We b服务器日志文件的位置 如果不知道We b服务器的错误日志保存在什么地方,就很 难调试你的C G I脚本程序。你应该设法找到这个位置,这很重要。 • 用于C G I程序的扩展名 We b服务器有时要将服务器上保存的普通静态 We b页与根据文 件名来运行的C G I程序区分开来。C G I程序的扩展名通常是. c g i或. p l。有时则根本不用扩 展名。 • C G I程序目录的位置 We b服务器有时需要C G I程序文件名的扩展名,有时则需要将文件 放在一个专门的目录中。(很少同时需要这两者。)该目录通常称为/c g i - b i n,并且位于 We b站点的顶层目录中(或者靠近顶层的目录中)。 • C G I目录的URL 许多情况下,你使用的 We b服务器的U R L中C G I目录附加在它的结尾 处,例如: http://www .myserver.com/cgi-bin/或者h t t p : / / w w w . m y s e r v e r . c o m / c g i /. 17.3 编写你的第一个CGI程序w 了解了上述关于C G I编程的有关说明、注意事项、检验表等信息后,你就可以准备键入你 的第一个C G I程序了。程序清单1 7 - 1显示了这个程序。 键入这个程序并将它保存为 h e l l o。如果在检验表中必须将某个扩展名用于 C G I程序(正如 “用于C G I程序的扩展名”项中所说的那样),那么请使用该扩展名。这样,如果必须使你的 C G I程序的文件名带有 . c g i扩展名,那么将该脚本程序保存为 h e l l o . c g i。如果必须使用扩展 名. p l,请将该脚本程序保存为h e l l o . p l。 程序清单17-1 你的第一个C G I程序 第1行:这一行是个标准#!行。你必须替换检验表中的“ We b服务器上的P e r l位置”信 息项中的路径,使该脚本程序能够运行。当然 - w用于激活警告特性。 第2行:C G I模块纳入了该程序。q w(:s t a n d a r d)使得一组标准函数从C G I输入到你的程 第17学时 CGI 概述使用199下载序中。 第3行:use strict是个很好的编程命令,对于 C G I程序来说也是一样。 第5行:从C G I模块中输入h e a d e r函数。它输出一个标准标题,服务器(和客户机)必须 看到它后才能处理C G I程序的输出。 第6行:当标题输出后,所有输出就会正常显示在浏览器中。在本例中,当 C G I运行时, 浏览器将显示Hello world。 这就是各个代码行的具体内容。 不过,事情并没有结束,你还必须安装这个 C G I程序并对它进行测试,你的工作只完成了 一半。 17.3.1 在服务器上安装CGI程序 究竟如何安装C G I程序,主要取决于你拥有何种服务器,你是否能够在本地访问它,或者 是否只能用F T P将文件发送给该服务器。下列各节将介绍如何为不同的环境安装 C G I程序。 1. 本地访问UNIX We b服务器上的文件系统 如果你能够使用t e l n e t、r l o g i n或其他方法登录到UNIX We b服务器上去,请使用下列说明 来安装C G I程序: 1) 使用F T P,将C G I程序h e l l o . c g i(或h e l l o . p l)放在U N I X服务器上。也可以使用v i将该程 序写入服务器,这也是个好方法。 2) 使用m v或c p命令,将C G I程序转到正确的目录中。你应该在“ C G I程序目录的位置” 下的检验表中找到正确的目录。 3) 在U N I X下,必须使该程序成为可执行程序。可以使用下面这个命令来执行这项操作: 如果该程序的名字是 h e l l o . p l,则在该命令中使用该名字。该命令使得文件所有者能够写 入该文件,而其他人则可读取和执行该文件(对于 C G I程序来说,这是正确的)。 2. 只能用F T P来访问UNIX We b服务器 如果你只能使用F T P来访问服务器,请按下列说明来安装 C G I程序: 1) 使用你的F T P客户程序将h e l l o . c g i(或h e l l o . p l)程序转入C G I程序目录。你应该已经在 “C G I程序目录的位置”下的检验表中找到正确的目录。务必以文本方式或 A S C I I方式来传送 文件,不要使用二进制方式将 C G I程序传送到服务器中。如果使用文本方式的 F T P实用程序, 那么它的默认方式通常是文本方式。 2) 必须使C G I程序成为可执行程序。对于仅为文本的 C G I程序,下面这个命令应能运行: 如果h e l l o . p l是该程序的名字,则上述命令应该使用 h e l l o . p l。该命令使得该文件可供文件 所有者写入,而其他所有人则可以读取和执行该文件(对于 C G I程序来说,这是正确的)。 3) 如果你拥有一个图形 F T P程序(如C u t e - F T P),必须找到Set Permissions(设置访问许 可权),Change Mode(改变方式),Set File Attributes(设置文件属性),或Set File Access M o d e (设置文件访问方式)等选项卡,以便设置访问许可权。 不管用何种方法设置访问许可权,文件所有者需要读/写/执行权限,用户组需要读/ 执行权限,其他用户需要读/执行权限。如果该程序需要数字式访问权限,请使用 7 5 5。 200使用第三部分 将Perl 用于C G I 下载3. 本地访问NT We b服务器上的文件系统 如果你能够本地访问NT We b服务器的文件系统,请使用 N T的E x p l o r e r或文件拷贝实用程 序将C G I程序放入正确的目录,即“ C G I程序目录的位置”中指定的这个目录。 4. 只能使用F T P来访问NT We b服务器 如果你只能使用F T P来访问NT We b服务器,请使用F T P客户程序将h e l l o . c g i(或hello.pl ) 程序放入C G I程序目录。你应该已经找到“ C G I程序目录的位置”下的检验表中的正确目录。 务必用文本方式或 A S C I I方式来传送文件,不要使用二进制方式将 C G I程序传送到服务器。如 果使用文本方式的F T P实用程序,其默认方式通常是文本方式。 17.3.2 运行你的CGI程序 若要了解你的C G I程序是否能够运行,请打开浏览器,并将它指向你在检验表中设定的地 址,即C G I目录的U R L,并将C G I程序名附加在该U R L的后面。例如,可以输入下面的 U R L: 当保存C G I程序时,应该使用h e l l o . c g i或你用于C G I程序的任何名字。 这时会发生下列两种情况中的一种: 1) 你的浏览器加载一个带有H e l l o,w o r l d消息的We b页。 2) 它没有加载这个We b页。 如果你的C G I程序不能运行,不管原因是什么,请查看下一节的说明。下一节专门介绍如 何查找类似这样的程序问题。 C G I程序的安装和调试过程非常困难,而且的确很难。不过你不 要灰心,因为C G I程序的运行不会是一帆风顺的,你应该坚定信心。一旦排除了 C G I程序的故 障,你将不必重新对它进行调试。 如果你的C G I程序能够按照要求来运行,那就太好了。这说明你已经成功地安装了你的 We b服务器和C G I程序,并且使它们能够正确运行。不过你仍然应该浏览下一节的内容。总有 一天,你的某个C G I程序可能发生故障,你至少应该熟悉诊断程序故障的操作步骤。 17.4 CGI程序无法运行时怎么办 下面几节为你提供一个C G I程序的通用调试指南。在你阅读所有这些内容以便找出你的第 一个C G I程序中的问题之前,请回头看一看前面的内容,以确保没有跳过任何步骤。当你学到 本学时结尾的时候,应能发现你的 C G I程序中存在的任何问题。 这几节中介绍的诊断操作均假设你要调试的 C G I程序名是h e l l o . c g i。如果你的程序使用别 的名字,请改过来。 17.4.1 这是你的CGI程序吗 • 第一个需要解决的产生问题的原因是 C G I程序本身。如果 C G I程序不能运行,那么调试 We b服务器的配置是毫无意义的。 C G I程序可以像所有 P e r l程序那样以交互方式来运行,用交互方式来运行 C G I程序对于程 序的调试来说是非常有用的。若要运行你的 C G I程序,请在命令提示符处输入下面这个命令, 将它启动: 第17学时 CGI 概述使用201下载这时P e r l解释程序应该输出下面这行信息作为应答: 这个提示行表示C G I模块试图获取你的C G I窗体的值。这些值将在第1 8学时中介绍。 看到这个提示后,你应该输入文件结束字符作为应答。在 U N I X下,它是C t r l+D,你只 需按下C t r l键并键入D。在Wi n d o w s中,可以按下C t r l + Z。然后P e r l应该输出下面这两行消息: C o n t e n t - Ty p e:t e x t / h t m l这条消息表示后面的信息应该转换为文本或 H T M L。这条消息的 含义将在第2 0学时中全面介绍。现在,你只需要知道重要的是这条消息是你的程序用 h e a d e r 函数输出的“第一个”信息,并且这条消息是必须输出的。如果在 C o n t e n t - Ty p e消息之前输出 了别的什么消息,那么C G I程序的运行将会失败。 问题:P e r l应答的语句有误。 解决办法:找出语句错误。 问题:P e r l应答的信息是Ca n ’t locate CGI.pm in @INC...(在@ I N C中无法找到C G I . p m . . . )。 解决办法:你安装的P e r l不完整。P e r l配有默认的C G I模块。如果你想要安装它,请参阅 本书的附录。 17.4.2 服务器存在的问题 当排除了你的脚本程序是问题的根源之后,就应该检查脚本程序的安装和服务器的配置 是否正确。 问题:服务器应答的消息是Not Found(未找到)或404 Nat Found(404未找到)。 解决办法:这些消息通常表示存在下列问题之一: • 你使用的U R L不正确。当你应该键入 h t t p : / / w w w. s e r v e r. c o m/c g i - b i n / h e l l o . c g i时,你却 键入h t t p : / / w w w. s e r v e r. c o m / c g i / h e l l o . c g i。请返回到检验表,核实你的 C G I目录的U R L是 否正确。 • 你将脚本程序放入 We b服务器上的目录不正确。请核实检验表,确定 C G I程序的目录是 否正确。如果不正确,请将脚本程序转到正确目录中。 问题:你的脚本程序的文本显示出来了。 解决办法:之所以显示该程序,原因是 We b服务器认为该程序实际上是个文档。 • 你使用的C G I程序扩展名错了。你没有使用 . p l,而是使用了. c g i或者其他错误的扩展名。 请查看检验表,确保你使用了正确的 C G I程序扩展名。 • 你将脚本程序放入了不正确的目录中了,同时使用了错误的 U R L来访问它。请将脚本程 序放入正确的C G I程序目录中,并且确保你使用的 U R L是正确的。 • 服务器配置有误。如果你是使用自己的 We b服务器,请重新阅读它的文档,并核实你的 We b服务器安装是否正确。有时安装服务器时包括了一个测试用的 C G I脚本程序。如果 是这样,请测试这个 C G I脚本程序。如果你使用一个商用 We b托管服务器,请核实你将 脚本程序放入了正确的目录之中,否则与 We b主机联系,请求其帮助。 问题:服务器应答的信息是F o r b i d d e n(禁止)或403 Error(403错误)。 202使用第三部分 将Perl 用于C G I 下载解决办法:对C G I程序的访问权限设置不正确。这个问题最有可能出现在 UNIX We b服务 器上。 可以查看对h e l l o . c g i程序的访问权限,方法是在命令提示符处键入 ls -1 hello.cgi。如果你 拥有对服务器的 F T P访问权,可以查看文件访问权,方法是键入 d i r。该访问权限应该类似下 面的形式: 访问权限是左边的字符 r w x r- x r- x。如果不是这样,请回到安装说明,详细了解如何正确 地设置对C G I程序的访问权限。 17.4.3 排除服务器内部错误或500错误 如果服务器应答的消息是Internal Server Error(服务器内部错误)或500 Error(5 0 0错误), 这意味着你的C G I程序运行失败了。这个通用故障消息是由许多不同问题产生的。 检查“Internal Server Error”时使用的最重要工具是服务器的日志文件。当 We b服务器收 到客户机要检索 We b页的请求时,它就会将每个请求写入一个文件,供以后分析时使用。服 务器遇到的任何错误也会记录在这个文件中,包括 C G I程序生成的出错消息。 请查找服务器的出错日志文件的位置,你在检验表中应该看到了这个文件位置。日志文 件的编写通常是将新的项目附加在日志文件的底部。若要查看 U N I X下的最后几个日志文件项 目,请在提示符后面键入下面这个命令以便查看日志文件的底部的项目: tail server_log 有些We b服务器配有一个实用程序,它常常是 C G I程序本身,用于查看日志文件。如果你 只拥有对服务器的 F T P访问权,那么必须下载该日志文件,并在你的本地 P C上查看该日志文 件,以便找出错误项。 如果你无权访问服务器的错误日志文件,那么就存在一个很大的隐患。查找“ I n t e r n a l Server Error”将是一件漫无边际的工作。按照下面显示的检验表,最终你应该能够找到存在 的问题。(你在服务器的日志文件中找到的消息是不精确的信息,不同的服务器的消息文本各 不相同。 日志项:No such file or directory:exec of /cgi-bin/hello.cgi failed(没有这个文件或目录: / c g i - b i n / h e l l o . c g i运行失败) 出错的原因: • 脚本程序的#!行可能不正确。应该确保#!行中 P e r l的位置与检验表中 We b服务器上 的P e r l位置相一致。通过使用 F T P中或本地的l s或d i r命令,核实P e r l实际上已经安装在该 位置上了。 • 如果你使用F T P将C G I程序传送到服务器,可能没有使用 A S C I I方式进行传输。用二进制 方式将 Wi n d o w s中编写的脚本程序转移到 U N I X服务器(并反方向传送),这是行不通 的。 • 对C G I程序的访问权设置不正确( U N I X下)。请在“服务器存在的问题”这一节中查看 关于F o r b i d d e n的说明。 日志项:C a n’t locate CGI.pm in @INC...(@inc中找不到C G I . p m )。 出错的原因: 第17学时 CGI 概述使用203下载• 安装的P e r l不完整,受到了破坏,或者太旧了。显然 P e r l无法找到C G I模块,C G I模块是 P e r l的标准组成部分。你必须重新安装该模块,或者与系统管理员联系,请他重新安装 P e r l。安装方法请参见本书附录。 日志项:Syntax error,w a r n i n g,Global symbol requires,e t c (语句错误、警告、需要全局 符号等)。 出错的原因: • 你的P e r l程序显然存在键入错误或语句不正确的问题。请按“这是你的 C G I程序吗”这 一节中的说明,确定问题的性质。 日志项:Premature end of script headers(脚本程序标题过早结束) 出错的原因:这个出错消息说明了这样一种情况,即你的脚本程序在运行,而 C G I模块的 h e a d e r函数输出的C o n t e n t - Ty p e标题并不是脚本程序发出的第一个消息。有时在日志文件中的 这个消息前面或后面还会出现一条辅助消息。这个辅助消息更有助于确定出错的原因。你可 以试用下面的方法来确定出错的原因: • 在调用h e a d e r函数前,务必不要输出任何信息,包括出错消息。在 h e a d e r函数之前输出 的任何东西都会导致这个错误。 在程序的开始处而不是在调用 h e a d e r函数时,你会看到Perl CGI程序输出 “C o n t e n t - Ty p e:t e x t / h t m l \ n \ n”这条消息。显然输出这个消息和调用 h e a d e r函 数被认为是做同样的事情,但实际并非如此。 h e a d e r函数要考虑这样一个问 题,即\ n \ n在每个服务器上并不总是表示相同的意思,它会为该服务器输出 相应的序列。 • 一个称为输出缓冲的问题会导致 s y s t e m函数在h e a d e r函数输出之前产生输出,并在输出 中出现反引号( ` ` )。若要确保h e a d e r函数的输出总是显示在前面,可以将 Perl CGI程序的 开始部分重新编写为下面的形式: 17.5 课时小结 在本学时中,我们介绍了 C G I程序如何运行的基本知识,讲述了静态 We b页与动态We b内 容之间差别,并且在后面几个学时中还要进一步阐明这些问题。你还编写了第一个 C G I程序并 且使它运行了起来。 此外我们还提供了如何调试C G I程序的指南,这对今后几个学时的学习来说是非常有用的。 17.6 课外作业 17.6.1 专家答疑 问题:我没有加载C G I模块,是否必须使用该模块? 204使用第三部分 将Perl 用于C G I 下载解答:坦率地说,你确实必须使用该模块。 C G I并不是个很容易使之正常工作的模块。目 前已经发布的许多程序试图仿制 C G I模块的功能,可惜都不太成功。它们存在着大量的安全漏 洞,并且无法实现兼容。此外,它们不符合 I n t e r n e t标准。在第 1 6学时中,我们讲述了为何 “仿制车轮”是件并不高明的事情。 C G I是个很难仿制的车轮,我们都无法第一次或者第 1 0 0 次使之恢复正常。 标准产品中包含的Perl CGI模块已经被成千上万个程序开发人员测试过,非常耐用,你可 以放心地使用它。 本书附录中讲到,如果必要的话,你可以安装只供你自己使用的 C G I模块。你没有理由不 使用这个模块。本书中的所有代码例子都需要使用 C G I模块,有关说明的前提是你已经安装了 该模块。 问题:我拥有c g i - l i b . p l的拷贝,可以使用它吗? 解答:你不应使用它。c g i - l i b . p l的所有函数都在C G I模块中。c g i - l i b . p l库非常老,并且得 不到维护。 问题:为什么人人都必须将P e r l用于C G I?为什么不使用C或T C L? 解答:P e r l的特性对C G I特别有用。这些特性主要包括: • Perl具有非常出色的文本处理功能。 • 你很快就会了解的P e r l的出色功能将使它成为编写 C G I程序的安全语言。 • P e r l是一种优秀的“胶水”语言,它非常适合将操作系统的实用程序、数据库和 C G I等 不同技术组合在一起。 • Perl很容易使用。 问题: 如果我遇到了关于 P e r l 和 C G I 方面的问题,是否应该将一条消息发送给 c o m p . l a n g . p e r l . m i s c新闻组? 解答:也许不合适。更合适的新闻组是 c o m p . i n f o s y s t e m s . w w w. a u t h o r i n g . c g i。不过首先你 应该查看h t t p : / / w w w. w 3 . o rg / C G I /网址上的FA Q。 17.6.2 思考题 1) CGI程序可以用下面的语言编写: a. 只能用P e r l、UNIX Shell或C语言。 b. 只能用C语言。 c. 能够在服务器上运行的所有编程语言。 2) We b是在P e r l之前问世的。 a. 是。 b. 否。 17.6.3 解答 1) 答案是c。P e r l并不是编写C G I程序时使用的惟一语言,它的某些特性在编写 C G I程序时 更加容易并且更加可靠。 2 )答案是b。P e r l是在1 9 8 7年开发而成的,而We b直到1 9 9 1年才在C E R N问世。 第17学时 CGI 概述使用205下载17.6.4 实习 对“H e l l o,w o r l d!”程序稍作修改,增加一些功能。使用 l o c a l t i m e输出当前时间,并且 用H T M L标记给它增添某些颜色和一两个表格,要有创意。请记住,在你的 P e r l程序中输出 H T M L,可以在加载We b页时使该程序出现在最终的 We b页中。 206使用第三部分 将Perl 用于C G I 下载下载 第18学时 基 本 窗 体 当你浏览We b时,肯定会填写几个 H T M L窗体。H T M L窗体,比如电子邮件 We b窗体、购 物窗体、宾客留言簿、在线拍卖窗体、邮件列表和订单窗体等,可以用于搜集信息,如 We b 浏览器用户的登录信息和We b站点的首选设置等。 当用户点击这些窗体上的 S u b m i t(提交)按钮时,将会出现什么情况呢?几乎在所有情 况下,该窗体的数据都会传递给一个 C G I程序。在本学时中,我们将要介绍如何从窗体中取出 数据,如何在你的C G I程序中对数据进行操作。 在本学时中,你将要学习: • 如何处理Perl CGI程序中的基本窗体。 • 如何调试C G I窗体。 • 如何编写更加安全的C G I程序。 18.1 窗体是如何运行的 你肯定使用过 We b上的窗体,甚至知道窗体是如何布局的,以及它们是如何运行的。但 是,如果要确保你处理的是同一个 We b页,那么就必须了解H T M L窗体的基本知识。 18.1.1 HTML窗体元素概述 在你开始学习窗体如何运行之前,首先应该了解 H T M L是如何展示窗体的,以及窗体中 的所有元素起着什么样的作用。 本书中介绍的H T M L不应该被视为最典型的H T M L。本书中讲述的H T M L 足以展示必要的 C G I特性,但是没有涉及到太多其他的东西。在本书的代码 例子中展示的 H T M L中没有使用 < H E A D >或< B O D Y >标记,也没有使用 < D O C T Y P E >标记。此外,屏幕都非常简单朴素,你可以添加自己的 H T M L, 使屏幕更加生动和完整。 H T M L窗体是H T M L文档的一个部分,用于接收用户的输入。当浏览器加载包含窗体的 H T M L文档时,各个不同的H T M L标号便在We b页上建立各个用户输入区域。用户的输入被放 入各个窗体元素中,比如复选框、单选按钮、选项菜单和文本输入项元素。当用户使用 We b 浏览器对输入元素的操作完成后,窗体通常被提交给 C G I程序,以便进行处理。 程序清单1 8 - 1显示了一个创建的典型H T M L窗体。 程序清单18-1 一个小型H T M L窗体图1 8 - 1给出了:N e t s c a p e浏览器中显示的程序清单1 8 - 1的窗体。 < F O R M >标记用于设定完整的H T M L文档中的窗体的开始。m e t h o d属性用于设定该窗体是使用 G E T还是P O S T方法来提交窗体。如果这个属性没有设定,浏览器将使用G E T方法将窗体提交给C G I 程序。G E T与P O S T方法的差异将在后面说明。a c t i o n属性用于设定接收窗体数据的C G I程序的U R L。 < I N P U T >标记用于为用户提供一个输入域,在这里,它是一个空白文本框。该文本框被 赋予一个名字,它恰好是“ n a m e”。 < T E X TA R E A >标记用于使浏览器显示一个多行文本框,以便接收输入的数据。值得注意 的重要属性是n a m e属性,在这里,该域的名字是 d e s c r i p t i o n。H T M L窗体中的每个元素都必须 拥有不同的n a m e属性。当C G I程序被赋予该窗体以便进行处理时, n a m e属性用来区分各个域。 “每个属性都有它自己的名字”这一原则有一个例外,那就是单选按钮。单选按钮按小组 放在一起。单选按钮组中每次只能选定一个按钮。每个单选按钮组都有它自己的 n a m e属性。 最后显示s u b m i t(提交)按钮。当用户单击该按钮时,窗体的值就被传递给 C G I程序,以 便进行处理,这将在下一节中介绍。 HTML 4.0的技术规范包含了很少的不同窗体元素类型,因此,我们不想在本书中将它们 全部完整地加以介绍。例如,许多窗体元素包含了一些属性,以便安装窗体元素的某些特性, 比如前面介绍的窗体中的 T E X TA R E A中的 r o w s和c o l s。在本书的其他学时中,每当使用 H T M L窗体元素时,都只使用最基本的属性。 在网址h t t p : / / w w w. w 3 c . o rg上,你可以找到 HTML 4.0完整的技术规范, 包括有效的窗体及其属性。 18.1.2 单击submit时出现的情况 当用户在他的We b浏览器上填写窗体信息时,将会发生一连串的事件: • We b浏览器接收窗体上的数据,放入名字与值对中(见图 1 8 - 2)。例如,在这个示例窗 体中,名字为 b o d y的域接收了文本输入域的值。名字为 s e x的域将接收单选按钮的值。 图18-1 N e t s c a p e中显示的 程序清单 1 8 - 1的窗 体 208使用第三部分 将Perl 用于C G I 下载We b浏览器在发生任何情况之前执行所有这些操作。 • 对窗体域的a c t i o n部分的U R L进行访问。这是C G I程序的U R L(见图1 8 - 3)。 • 使用C G I方法G E T或P O S T之一,窗体上的域的名字和值被传送到 C G I程序(见图1 8 - 4)。 你不必过分担心这个传输机制。 • C G I程序接收这些值,并生成一个应答,并将应答送回给浏览器(见图 1 8 - 5)。该应答可 以是个H T M L页,或者包含另一个窗体的 H T M L页,也可以是转到另一个 U R L的H T M L 或者是C G I程序能够生成的任何其他东西。 18.2 将信息传递给你的CGI程序 当由于窗体的提交而使C G I程序运行时,从窗体传递过来的域的名字和值(称为参数)必 须由C G I程序来处理。这是使用p a r a m函数来处理的。 如果没有任何参数,那么 p a r a m函数返回传递到 C G I程序中的域的名字。如果 C G I程序接 收到程序清单1 8 - 1中的窗体,p a r a m函数将返回b o d y、s e x、n a m e和s u b m i t。 如果带有参数, p a r a m函数返回该参数的值。例如, p a r a m (‘s e x’)将根据你的选择,返 回单选按钮的值m a l e或f e m a l e。 程序清单1 8 - 2包含了用于输出这些参数的简短的 C G I程序。 程序清单18-2 用于输出参数的C G I程序 第18学时 基 本 窗 体使用209下载 图18-2 浏览器对数据与域 的名字进行匹配 图18-3 浏览器与服务器进 行联系 图18-4 数据被送往服务器 浏览器 浏览器 浏览器 Web 服务器 Web 服务器 CGI 程序 CGI 程序 图18-5 We b服务器的 C G I 程序作出的应答 浏览器 浏览器 Web 服务器 CGI 程序如果p a r a m函数指定的参数没有用于该窗体,则 p a r a m返回u n d e f。 GET与POST方法 在程序清单1 8 - 1中的窗体内,< F O R M >标号拥有一个属性,称为 m e t h o d。M e t h o d属性用 于设定We b浏览器应该如何将数据传送到 We b服务器。目前可以使用的方法有两个。 第一个方法称为G E T,如果你在< F O R M >标号中没有设定方法,那么这就是默认的方法。 使用G E T方法,通过在 U R L中对窗体值进行编码,就可以将这些值传递给 C G I程序。当你在 We b上冲浪时,可能看到下面这样的 U R L: C G I程序运行时,能够将 U R L的剩余部分解码,使之分解成域和值。当你调用 p a r a m函数 时,它实际上也是进行这样的操作。你不应该试图自己对这些值进行解码。 p a r a m函数能够全 面地进行这项操作,你没有理由使用别的方法来提取这些值。 另一个方法是P O S T,它产生的结果完全相同,但使用的手段不同。不是将所有的窗体值 编码后放入U R L,而是通过访问We b服务器,然后将H T M L窗体值传送给C G I程序,作为其输 入数据。另外,现在你不必清楚地了解这个过程究竟是如何运行的, C G I模块会为你处理好这 个问题。只要调用p a r a m函数,就可以读取这些值,对它们解码,然后将它们传递给你的程序。 你可能已经从 I n t e r n e t下载了一些C G I程序,或者在别的书中见过一些例 子,通过对环境变量 Q U E RY _ S T R I N G进行解码,或者使用变量 R E Q U E S T_ M E T H O D来确定窗体是使用 G E T还是P O S T方法。这些程序试图重复进行在 标准C G I模块中已经做的工作,但是也可能没有这样做。你应该避免自己执 行这项操作。 那么究竟你应该选择哪一种方法呢?每种方法都有它的优点和缺点。 G E T方法使得We b浏 览器能给生成We b页的特定U R L做上书签。例如下面的U R L 可以做上书签,并且总是由浏览器返回。从 C G I程序s a m p l e . p l的角度来看,它不知道你刚 刚是否查看了该窗体。它像往常一样接收通常的 C G I参数。如果能够使用G E T方法的U R L编码 值来反复调用一个C G I程序,这称为幂等性。 但是你可能不是特别想使浏览器能在你的站点中做上书签,以便直接运行你的 C G I程序, 坦率地说,用于以G E T方法启动C G I程序的U R L是很讨厌的。 P O S T方法根本不对U R L中的窗体数据进行编码,当它为 We b页进行处理时,它依靠浏览 器发送数据。但是,由于数据并没有被编码后放入 U R L,因此你无法使用 P O S T方法给C G I程 序生成的We b页做上书签。 210使用第三部分 将Perl 用于C G I 下载18.3 Web安全性 在你将C G I程序放到World Wide We b上去之前,必须了解下面几个问题。通过将 C G I程序 放在We b页上,你就为远程用户(使用 We b浏览器)赋予对你的系统的有限访问权。使用普通 H T M L文档,他们只能从你的 We b站点检索静态文档。但是,使用 C G I程序,他们就能在你的 We b服务器上运行程序。 懂得如何编写安全而保密的 C G I程序后,你与你的We b服务器管理员一定会感到更加高兴。 编写这样的程序并不难,只需要掌握几条简单的注意事项。 18.3.1 建立传输明码文本的连接 当We b浏览器从 We b服务器中检索 We b页时,H T M L是通过一个明码文本信道来发送的 (见图1 8 - 6)。这意味着当数据一路通过 I n t e r n e t时,它并不进行加密、编码,否则就无法被对 方理解。 用户填入窗体然后提交给你的 C G I程序的数据,传输时所用的协议与初始 We b页使用的协 议相同。任何人只要访问窗体,就可以查看它的所有域(见图 1 8 - 7)。 用明码传送数据时存在的问题确实是你应该担心的问题。 I n t e r n e t不是一个安全的地方, 在We b浏览器与We b服务器之间的线路上的任何人都能够窃听线路上来回传送的信息。 应该记住,决不应该以普通 C G I窗体来发送下列几种类型的数据: • 任何形式的口令。 • 个人信息(社会保险号,电话号码)。 • 财务信息(帐号,个人身份识别号,信用卡号码)。 请记住这些基本原则。决不要在 I n t e r n e t上发送你不会写在明信片上任何信息。 你会说:“等一等,我曾经在 I n t e r n e t上看到一些窗体,要求查询所有上 述信息,并且说这是安全的。”使用某些辅助工具,可以在 We b上执行相当 安全的事务处理。若要执行安全的 We b事务处理,实际上必须对浏览器/服 务器之间的全部会话进行加密。这是通过运用 h t t p协议的安全版本 h t t p s来实 现的。 第18学时 基 本 窗 体使用211下载 浏览器 Internet Web 服务器 “将药品运往我在⋯的家” 图18-6 明码文本被传送到 服务器 浏览器 Web 服务器 “很抱歉,比尔,你的信用卡无效” Internet图18-7 服务器用明码文本 作出应答18.3.2 注意不安全数据 在编写安全的C G I程序时需要考虑的另一个问题是:你编写的程序将根据 We b页提供给你 的输入来执行P e r l命令。I n t e r n e t或者你的I n t r a n e t(专用网)上有许多人属于不良之徒,他们 以损害你的We b服务器为乐,并因此而感到自己了不起。还有一些并无恶意的用户可能不小 心将无效数据发送给你的C G I程序。 请看程序清单1 8 - 3中的H T M L窗体和程序清单1 8 - 4中的C G I程序。 程序清单18-3 目录清单We b窗体 程序清单18-4 名字为d i r e c t o r y. c g i的不安全C G I程序 程序清单1 8 - 3为用户提供了一个简短的窗体,用于接收一个目录名,再将它传送给称为 d i r e c t o r y. c g i的C G I程序。在程序清单 1 8 - 3中, d i r e c t o r y. c g i程序接收该目录,并为 D O S / w i n d o w s用户对它执行ls -1命令,它与d i r等价,为用户提供一个目录列表。 这种类型的程序使得远程 We b冲浪者能查看你的整个目录结构。 C G I程序并不检查该目录 名是什么,如果浏览器想要查看你的敏感数据,它就可以查看。 更重要的一个问题是: $ d i r e c t o r y可能根本不包含任何目录。如果 We b浏览器为d i r n a m e发 回了值/ h o m e;cat /e t c / p a s s w d,这时,C G I程序运行的命令将类似下面的形式: 这个命令将能有效地将系统的口令文件拷贝发回给 We b浏览器。实际上,所有UNIX shell 命令或M S - D O S命令都可以这样运行。如果你的 We b服务器尚未正确地安装,那么任何用户都 可以上I n t e r n e t。 P e r l拥有一个机制,可以帮助你避免做这样的傻事。#!行上的 - T开关可以激活数据受感 染特性。当数据从外部信息源(如文件句柄、网络套接字、命令行等)接收过来时,它就被 做上“t a i n t e d (受感染)”标记。受感染的数据不能用在反引号、系统函数调用(如 o p e n函数)、 系统命令或可能破坏安全性的其他地方。 当受感染检查正在进行时,不能将 o p e n函数、s y s t e m函或反引号用于你的 P e r l程序,除非 首先明确设置PAT H环境变量。 程序清单1 8 - 5显示了这个程序的更加安全的版本。 程序清单18-5 directory. c g i程序的更安全版本 212使用第三部分 将Perl 用于C G I 下载若要了解关于受感染的数据、如何消除数据的感染以及如何编写安全的 P e r l程序的详细信息,请参见P e r l产品包含的P e r l s e c手册页。 18.3.3 从事无法执行的操作 H T M L/C G I窗体也可能遭到另一种情况的损害。请看程序清单 1 8 - 6中的H T M L窗体。 程序清单18-6 一个简单的窗体 在这个窗体中, c o l o r域允许的最大宽度是 1 5。这对吗?大概差不多。 H T M L技术规范规 定,文本域的l e n g t h最多允许这么多的字符。但是,浏览器可能发生故障,有人可能故意在这 个域中放入1 5个以上的字符,方法是不使用你的窗体,或者创建一个新窗体。 如果你希望某个域拥有一个特定值,请不要依赖 H T M L、J a v a或J a v a S c r i p t来保证这个值 的正确性。例如,如果 c o l o r域的绝对限值应该是 1 5,那么P e r l程序可以像下面这样对它进行 处理: 18.3.4 拒绝服务 通过拒绝服务,任何We b服务器的性能都会受到削弱。由于 We b服务器是代表远程用户处 理访问请求的,因此,如果远程用户发出的访问请求太多, We b服务器就会不堪重负。这种 做法有时是恶意的,而且常常是恶意的。许多时候,一些公司为 We b提供了许多服务,结果 为了响应用户的访问请求,负担太重,因此不得不关闭这些服务,重新考虑自己的做法。 静态H T M L页或C G I程序也会出现拒绝服务的情况。 为了防止拒绝服务的问题,你有时会感到无能为力,除非手头拥有足够的服务系统,能 够处理浏览器的负荷。如果你的 C G I程序花费很长时间来执行或使用相当一部分系统资源(如 文件访问的频率,C P U使用的密度),以便使程序能够运行,服务器将很容易受到拒绝服务的 攻击。你应该设法尽量缩小你运行的 C G I程序的规模,并使之更快地运行。 第18学时 基 本 窗 体使用213下载18.4 宾客留言簿 这个例子使你能够为We b站点编写定制的宾客留言簿。宾客留言簿是个 H T M L窗体,在这 个窗体中,用户可以指明来宾的名字并配有一些说明。宾客留言簿可以用来收集关于某个问 题的反馈信息,作为一个简单的消息板,或者将问题提交给帮助桌。数据保存在一个文件中, 并且可以在窗体信息填满后显示出来。它也可以在它自己的 We b页上显示。 程序清单1 8 - 7提供了一个简短的H T M L代码段,它展示了一个用于虚构帮助桌的宾客留言 簿窗体。你当然可以修改这个窗体,使之适合你自己的需要。 程序清单18-7 帮助桌窗体 这个帮助桌窗体需要运行一个名叫 / c g i - b i n / h e l p d e s k . c g i的C G I程序。程序清单1 8 - 8显示了 这个C G I程序。如果你想要将该 C G I程序放在另外某个位置,或者将它称为另一个名字,请务 必将正确的U R L输入程序清单1 8 - 7的帮助桌窗体中。 程序清单18-8 帮助桌C G I程序 214使用第三部分 将Perl 用于C G I 下载程序清单1 8 - 8中的大部分代码是你已经熟悉的 P e r l程序,不过请特别注意下列几个问题: • g e t _ l o c k ( )和r e l e a s e _ l o c k ( )这两个函数对于这个窗体是绝对必要的。对于任何一个C G I程序来 说,你始终必须假设任何时候都可能有C G I程序的多个实例正在运行。写入帮助桌日志文 件的h e l p d e s k . c g i的多个实例将会出错,因此在将信息写入文件之前,该文件应被锁定。在 读取文件之前,它不锁定,因为一边读取日志文件,又一边写入文件,那将是很糟糕的。 • 这个C G I程序有两个目的。当作为程序清单 1 8 - 7中的窗体的目标操作来调用时,它将新 项目写入日志文件。当不使用该窗体来调用该程序时,它只显示日志文件的内容。 18.5 课时小结 在本学时中,我们介绍了 H T M L窗体与C G I程序如何进行交互操作,如何使用 C G I模块的 p a r a m函数使你的C G I程序能够转换窗体的内容。另外,还介绍了怎样才能使你的 C G I程序更 加安全,如何处理受感染的数据。我们还介绍了一个简单的 C G I宾客留言簿应用程序,你可以 对它定制和修改,以便适应你自己的需要。 18.6 课外作业 18.6.1 专家答疑 问题:我无法使用窗体的提交功能,老是出错,怎么办? 解答:请使用第1 7学时中介绍的C G I调试指南,找出存在的问题。仅仅因为它是个窗体, 第18学时 基 本 窗 体使用215下载并不意味着调试该窗体与调试普通 C G I程序有什么不同。 问题:我在 I n t e r n e t上看到了这个出色的程序,但是我不懂得为什么它试图使用 $ E N V { Q U E RY _ S T R I N G }来获得窗体参数。为什么? 解答:因为该程序的开发人员决定放弃该 C G I模块的窗体处理功能。这个情况说明它可能 是该C G I模块以前的一个非常老的 P e r l程序,也可能程序开发人员决定使用他自己的窗体处理 代码。不管属于哪种情况,这表明你应该用警惕的目光观察这个程序,并且小心地使用它。 问题:我通过命令行提示符运行程序,其#!行上有一个选项 - T,我得到一条出错消息 Too late for -T option(运行- T选项太晚了),然后程序停止运行了。为什么? 解答:你应该尽快将- T选项赋予P e r l程序,这样它就知道要去寻找受感染的数据。当你的 程序中的#!行被处理时,这就太晚了, P e r l已经处理了你的没有感染的命令行选项。若要从 命令行提示符来运行P e r l程序,例如在调试程序中运行,你也必须在命令行提示符上设定 - T: Perl -T -d foo.cgi 问题:P e r l的数据受感染功能是否能使我避免在 C G I程序中犯一些愚蠢的错误?它们现在 是否能够确保安全? 解答:没有一个C G I程序是绝对安全的。 P e r l的数据受感染功能在很大程度上可使你不犯 愚蠢的错误,不过它们无法保证你编写出安全的程序。 18.6.2 思考题 1) 在数组上下文中,不带参数的 p a r a m函数将返回 a. undef。 b. 窗体元素的数目。 c. 窗体元素名的列表。 2) 如果你使用C G I模块,P O S T与G E T方法之间的差别是清楚的。 a. 是。 b. 否。 3) HTML窗体上的p a s s w o r d域的输入类型是安全的,因为它在发送前会对口令进行加密。 a. 是。 b. 否。 18.6.3 解答 1) 答案是c。如果不带参数,p a r a m将返回来自提交的窗体的元素名列表。 2) 答案是a。 3) 答案是a。在普通H T T P和C G I程序中,所有窗体域都是以明码文本传送的,因此是不 保密的。口令域输入类型只在你键入口令时将该域隐藏起来而已。 18.6.4 实习 • 对帮助桌窗体稍作修改。将时间戳添加给每个项目,并且给输出添加某些颜色。 • 问题:d i s p l a y ( )函数从最老的项目开始输出帮助桌窗体中的各个项目。请修改 d i s p l a y ( ) 函数,使之首先输出最新的项目。 216使用第三部分 将Perl 用于C G I 下载下载 第1 9学时 复 杂 窗 体 We b上的窗体不只是简单的单页面窗体。有时窗体要跨越若干页。这些复杂的窗体以调 查、查询和购物车等应用程序的形式出现。 这些比较复杂的窗体需要使用某些不同的编程技巧,本学时你将要学习这些技巧。 在本学时中,你将要学习: • 如何创建多页窗体。 19.1 复杂的多页窗体 使用C G I程序来编写复杂的多页窗体时,你会遇到一个特殊的编程难题。We b浏览器与We b 服务器之间的连接根本不是一个持久的连接。 We b浏览器与服务器建立连接,检查 We b页,然 后便断开与We b服务器之间的连接。在服务器与你的We b浏览器之间并不保持不间断的连接。 更为复杂的是:浏览器每次与 We b服务器连接时,We b服务器并不认为该浏览器预先访问 过该站点。服务器并不每次都能很容易地识别该浏览器。 类似的一种情况是:图书馆的读者与没有记忆力的图书馆管理员之间进行谈话,读者每 次只能向管理员提出一个问题。 读者向图书管理员借阅一本书,比如关于亚利桑那州的一本书,图书管理员可以检索这 本书。图书管理员之所以能够检索这本书,是因为这个请求很容易满足。但是读者不能要求 借阅同一个专题的另一本书。图书管理员不能记住上一个借书请求,因此他无法借给你同一 个专题的另一本书。如果借书的请求改为“给我另一本关于亚利桑那州的书”,图书管理员仍 然无法满足读者的要求,因为他检索的书可能与第一次检索的这本书一样。 若要检索同一专题的第二本书,惟一的办法是说:“我需要另一本关于亚利桑那州的书, 我已经有了一本名叫《在亚利桑那州定居》的书”。这个借阅请求带有足够的能够说明问题的 信息,使图书管理员能够知道什么应答是不适当的。 为We b页编写多页窗体,也可以使用同样的解决办法。每个问题 /答复会话必须包含足够 的信息,使We b服务器能够知道它需要做什么。你可以用几种不同的方法来创建这样的会话, 其中的一种方法,即使用隐藏的 H T M L域,将在本学时中介绍。 19.2 隐藏域 要使We b窗体能够“记住”信息,最容易的方法是使用隐藏域,将以前的信息嵌入 We b窗 体。隐藏域是H T M L窗体的组成部分,它使域和值成为H T M L的组成部分,但是在显示窗体时, 窗体中并不出现这些域和值。在 H T M L中,这些域和值编写为下面的形式: 如果将上面的 H T M L代码放入一个窗体,新的名字(“f u l l n a m e”)和值(“Pink Floyd”) 将成为窗体的组成部分。如果该窗体被提交给一个 Perl CGI程序,p a r a m函数将返回一个关键 字和隐藏域的值。在线商店 如果要举一个如何使用隐藏域的例子,可以看一看在线商店,它使用一系列的 We b页, 使人们能够根据在线目录来选购商品。目前,我们只是向你介绍复杂窗体的运行情况,在本 学时后面部分的内容中,要介绍另一个复杂的窗体,它包含用于创建一个在线调查的代码。 如果不能实现某种形式的安全 We b事务处理,那么请不要使用这个在线 商店的例子,请注意,这个例子并不包含任何真实的个人信息,如电话号码 或信用卡号码等,因为隐藏域就像正规的 H T M L窗体,它根本不具备任何安 全性。 图1 9 - 1所示的在线商店第一页显示了该商店的商品清单。 当用户单击Go to Store(去商店)按钮时,C G I程序接收来自窗体的值,然后显示完整的 目录,如图1 9 - 2所示。 第二页显示完整的目录。当第一页(带有商店拥有商品的目录)提交时, C G I程序接收各 个值,然后当它为完整的目录输出 H T M L时,它将商品的指定数量作为隐藏域放入新窗体。 每当C G I程序接收来自H T M L窗体的值时,新页将包含隐藏域中的旧值,以及普通窗体元 素中的新值。 采用这个方法,你可以避免“健忘的图书管理员”存在的问题,当提交完整目录的窗体 时,窗体中的隐藏域便提醒 C G I程序从第一个窗体中选定哪些项目以及从当前窗体中选择哪些 项目。 如果需要第三页,前两页中的值可以作为隐藏域存放在第三页上,如图 1 9 - 3所示。 关于H T M L页上的隐藏域,有几个问题应该加以说明。首先,隐藏域中的值是任何人都 能够查看的。若要查看这些值,用户只需要查看该页的 H T M L源代码。大多数We b浏览器都配 有一个选项,可以用于查看 H T M L源代码。 其次,隐藏域中的值可以由远程用户进行修改,如果他们确实想要这样做的话。若要修 图19-1 在线商店的第一页 图19-2 显示在线商店的商 品目录 218使用第三部分 将Perl 用于C G I 下载改隐藏域的值,可以使用修改后的 We b浏览器,或者使用 H T T P人工提交该窗体。例如,在线 商店不应该将价格存放在隐藏域中,它只能存放数量。 C G I程序应该在需要显示价格时才查看 价格。 当你设计窗体时,看一看别人是如何设计窗体的,这将会对你有所帮助。 这样你也会对他们是否使用隐藏域来保存信息这个问题有所了解。大多数 We b浏览器都有一个View Page Source(查看页源)选项。你应该将这个选项 用在任意窗体上,以了解它是如何形成一个整体的。但是不要拷贝这个窗体, 大多数时候,拷贝会侵犯窗体的原开发人员的版权。 19.3 多页调查窗体 调查窗体是查找跨越若干不同的 We b页窗体的常见地方。有时这些窗体太长,一个 We b页 放不下,它们通常可以分成不同的类别。 接着,简单的多页We b调查窗体可以用于查找关于你的个人信息的各个方面。这个调查窗 体可以展示4个不同的We b页,并且可以改为支持你需要的任何数目的We b页。这4个We b页是: • 第一页用于提出一系列的一般问题,有时它们可以用来查找你拥有哪些种类的个人信 息。 • 第二页用于提出一些关于你的习惯爱好的特殊问题,还有一个根据第一个调查页提出的 问题。 • 第三页是供你输入你的名字和对调查的说明的 We b页。 • 在调查完成后输出的一条感谢你的消息。 相同的C G I程序可以用来执行所有这 4个功能。它决定了哪一页用来打开下一页。这是根 据刚才显示的这一页来决定的。程序清单 1 9 - 1显示了调查程序的核心。 通过包含代码 use CGI::Carp qw (fatalsToBrowser), 你的C G I程序的d i e ( )消息(它 通常被写入We b服务器的日志文件)将作为 We b页的组成部分来输出。当你 编写更长的C G I程序时,它将有助于程序的调试。 第19学时 复 杂 窗 体使用219下载 图19-3 在线商店的发货信 息调查的结果保存在一个文本文件中,但是该程序根本不显示该结果。该程序只不过进行 调查结果的收集和存储。你必须编写另一个 C G I程序,以便显示调查的结果。 程序清单19-1 调查程序的第一部分 第6 ~ 8行:在调查过程中,每个 H T M L窗体都包含输入域。每个输入域的名字都出现在这 个数组中。s a v e()函数和r e p e a t _ h i d d e n()函数将在以后使用这个数组。 第1 2 ~ 1 3行:如果不将任何参数传递给该 C G I程序,也就是说,它没有作为窗体发送的结 果来加载,那么就调用p a g e _ o n e()函数来输出调查窗体的第一页。 第1 4 ~ 1 7行:如果名叫p a g e o n e的H T M L窗体参数被传递给这个C G I程序,便调用p a g e _ t w o() 函数。如果传递的参数是p a g e t w o,便调用函数p a g e _ t h r e e。 第1 9行:如果 H T M L窗体参数传递给这个 C G I程序,但是传递的参数不是 p a g e o n e或者 p a g e t w o,则调查完成,其结果被保存,并在 s u r v e y _ d o n e()函数中输出感谢你的消息。 每个We b页上的 s u b m i t按钮都提供了一个关于下面应该加载哪一页的线索,你可以在图 1 9 - 4中看到这个情况。由于 s u b m i t按钮的名字作为一个参数被传递给 C G I程序,因此它可以用 来显示刚刚提交给程序的是 We b页的哪个版本。 程序清单1 9 - 2是调查程序的第二部分。 程序清单19-2 调查程序的第二部分 220使用第三部分 将Perl 用于C G I 下载第2 2 ~ 2 4行是个 P e r l新结构,你以前没有看到过,它称为“ here document”。h e r e d o c u m e n t使你可以设定一个跨越若干行的字符串,它包含其他的引号,可以像一个普通的双 引号那样来运行。若要开始编写一个 here document,你可以使用< <,后随一个单词。这个引 号的内容就继续下去,直到在一行的开头再次出现该单词为止,如下例所示: 用于标识here document开始的单词,即上面这个代码段中的 E N D _ O F _ Q U O T E,或者程 序清单1 9 - 2中的E N D _ PA G E _ O N E,后面必须跟一个分号。在 here document的结尾,该单词必 须出现在第一列的开头,并且后面不能有任何字符,如空格或分号。在 here document内,变 量像它们在普通双引号字符串(“”)中那样展开,因此在here document中,必须慎重使用$和 @字符。 使用here document时,可以将大量的H T M L代码嵌入你的P e r l程序,而不会夹杂许多引号 和多个p r i n t语句,从而造成混乱。 程序清单1 9 - 2中的函数只是用来输出一个 H T M L窗体。< F O R M >标记并不包含动作和方法。 第19学时 复 杂 窗 体使用221下载 图19-4 哪个按钮用于执行 哪个操作的示意图 浏览器 下一页 谢谢你 提交结果 1. 将参数 “pageone” 发送给CGI 程序 0. 没有发送任何 参数。第1页 输出 3. 将参数“pagetwo” 发送给CGI程序 4. CGI程序输出给 浏览器 5. 将参数“pagethree” 发送到浏览器 6. 最后一页输出 2. CGI程 序输出 CGI 程序 上一页 浏览器当没有设定< F O R M >的a c t i o n属性时,当前的 C G I程序(即产生窗体的 C G I程序)将在提交窗 体时重新加载。当不提交m e t h o d属性时,便使用默认方法G E T。 请注意,窗体上的提交按钮的名字是 p a g e o n e。当该窗体被提交时,一个称为 p a g e o n e的 参数将被发送到该C G I程序,它的值并不重要。被提交的这个参数将提示 C G I程序加载第二个 We b页。 程序清单1 9 - 3是C G I程序的第三部分。 程序清单19-3 调查程序的第三部分 第4 7行:正如第4 6行中的注释所表示的那样,这一行中的函数用于输出作为隐藏域的该 窗体的所有域的值。数据 @ s u r v e y _ a n s w e r s包含H T M L窗体上所有可能的“ n a m e =”值。当第 一次运行时,大多数域将不存在,因为调查的这些部分尚未填入相应的的值。 第4 8 ~ 4 9行:@ s u r v e y _ a n s w e r s中可能的每个参数均被检查,每个参数均被定义。输出 H T M L标号,用于存放当前窗体上的值。 第5 6行:这个函数用于输出调查的第二页。 第5 7 ~ 6 0行:这个函数在调查的第二页中调用。如果调查的第一页填入了正确的值,那么 p a r a m(‘p e t t y p e’)将保存d o g或c a t,这个值将存放在$ p e t中。如果被调查人跳过了这个问题, 同时p a r a m(‘p e t t y p e’)没有定义,那么就改用g o l d f i s h。 222使用第三部分 将Perl 用于C G I 下载第6 1 ~ 7 6行:来自第一页的H T M L窗体参数均转入该窗体,作为其隐藏域。 如果你在这时查看调查窗体,即它的第二页,那么第一页的所有答案均作为隐藏域存放 在第二页的结尾处。程序清单 1 9 - 4显示了第三页的代码。 程序清单19-4 调查程序的第四部分 函数p a g e _ t h r e e()是非常明了的。它只是输出窗体中的一个文本框和一个文本区域。在 结尾处,它再次调用 r e p e a t _ h i d d e n()函数,以便将所有隐藏域放入调查窗体的第三页。程 序清单1 9 - 5显示了C G I调查程序的结尾部分。 程序清单19-5 调查程序的最后部分 第19学时 复 杂 窗 体使用223下载第9 6行:调用这个函数只是为了输出感谢你的消息。当某人遍历调查窗体的 3个页面后, 这样做总是一件很好的事情。然后调用 s a v e()函数。 第1 0 3行:这里的s a v e()函数几乎是第 1 8学时中的s a v e函数的复制品。它用 g e t _ l o c k() 将调用文件锁定,再使用类似 r e p e a t _ h i d d e n()中的方法写入对问题的答案,然后用 r e l e a s e _ l o c k()函数对文件解锁。 你可以随意修改这个调查程序,以适应你自己的需要。它的设计非常灵活,并且可以用 于许多不同的目的。 19.4 课时小结 在本学时中,你学习了如何创建多页 We b窗体的方法。当你进行这项操作时,了解到程 序需要解决的几个问题,最重要的是要记住从一页转到另一页时会出现的一些情况。你还学 会了如何使用隐藏域将信息存放在服务器无法记住的 We b页上,然后就可以使用隐藏域来创 建框架调查窗体了。 19.5 课外作业 19.5.1 专家答疑 问题:H T M L窗体难道一定是如此不顺眼吗? 解答:本书中介绍的这些窗体是简单的、缺乏特色的框架式的窗体,有的人称它们是不 顺眼的窗体。本书的目的是教你进行 P e r l和C G I编程,而不是教你如何使用 H T M L。实际上, 本书中讲到的大多数 H T M L程序与标准无关,并且它是不完整的,它没有使用 < H E A D >标记, 和< H T M L >标记,也没有 D T D标题。通过提供基本的 H T M L,我想你能够对它进行修改,使 之符合你的需要。 前面讲过,给窗体增色的好办法是查找 We b,寻找你喜欢的窗体。通过查看源代码,你 就会对如何将这些We b页组合在一起有个大致的了解。 问题:我看到这样一个出错消息: Ca n ’t find string terminator “x x x x” anywhere before EOF at ...。这是什么意思? 解答:这个错误是因为在程序的某个位置上有一个左引号,但是没有匹配的右引号而造 成的。当你使用“ here document”时,这意味着无法找到你给“ here document”做上结尾标 记的单词。它的格式如下: 在上面这个例子中,“here document”开头和结尾的单词 M A R K必须完全相同。结尾的单 224使用第三部分 将Perl 用于C G I 下载词这一行上,它的前面不能有任何东西,后面也不能有任何东西。 M S - D O S和Wi n d o w s的文本 编辑器有时并不在程序的最后一行的后面放上行尾符。如果你的“ here document”以文件的 结尾为结束,请在它的后面放上一个空行。 19.5.2 思考题 1) 为了使你的程序能够记住很长的多页 We b事务处理,你需要使用 a. 数据库和c o o k i e。 b. 隐藏的H T M L窗体域。 c. 隐藏的H T M L窗体域、c o o k i e和数据库的某种组合。 2) 使用H T M L的< F O R M >标记时,如果不带a c t i o n属性,那么它将 a. 无法运行。 b. 导致s u b m i t按钮使用原先生成We b页的C G I程序。 c. 导致s u b m i t按钮重新加载当前页。 3) 上面介绍的调查程序有一个小错误,是什么错误? a. print<标记和类似的标记。 c. 调查程序没有输出结果。 19.5.3 解答 1) 答案可以是b或c。你可以只使用隐藏的H T M L域,也可以只使用c o o k i e。如果只使用数 据库,那是不行的。 2) 答案是 b。重新加载当前页将会删除当前窗体的所有答案。如果没有 a c t i o n属性, < F O R M >标记将把当前页的U R L用作替换U R L。 3) 答案是b。p r i n t < < E O P;肯定是个有效的语句,它称为“ here document”。选择c是不正 确的,因为程序不是使用该方法设计的(参见“实习”这一节)。 19.5.4 实习 • 编写一段C G I程序,用于显示调查的结果。也可以创建一个表格,以下面的形式显示这 些结果: 猫/狗 拥有一只 夜间活动 服装 被谁丢弃 旅行者 有危险吗 猫 否 是 普通 教授 是 是 两者之一 金鱼 否 专用 船长 否 否 狗 是 否 普通 玛丽-安尼 是 是 • 补充问题,编写一个C G I程序,将调查结果汇总成下面的形式: 猫/狗的比例 猫 4 0 % 狗 4 5 % 其他 1 5 % 拥有该宠物的人 猫 2 0 % 狗 1 5 % 金鱼 3 0 % 无 3 5 % 的比例: 夜行者: 是 3 5 % 否 4 0 % 第19学时 复 杂 窗 体使用225下载下载 第2 0学时 对H T T P和C G I进行操作 在本学时中,你将要学习如何对 We b进行一系列有趣的操作。可以使用 C G I程序,使We b 站点变得更加灵活,并且更加便于管理。 在本学时中,你将要学习: • 如何将H T M L程序从服务器传送到你的浏览器。 • 如何使C G I程序能够发送H T M L文档。 • 如何将值直接传递给C G I程序。 • 服务器端的包含程序如何运行。 • 如何查询浏览器和服务器,以便找到你要的信息。 20.1 HTTP通信概述 在第1 7学时中,我们介绍了 We b浏览器(N e t s c a p e和Internet Explorer等)与We b服务器 (A p a c h e和I I S等)之间如何进行基本的通信。该学时介绍的通信方式显得过分简单了一些。现 在我们对C G I程序的使用变得更加得心应手了,因此可以更加深入地探讨这个问题。在本学时 的后面部分中,我们将要介绍进行这种通信时使用的一些方法,以便执行某些有意思的任务。 这种通信方式可以用一个协议来加以描述,这个协议称为超文本传输协议( H T T P)。该协 议目前的两个版本是HTTP 1.0和HTTP 1.1。在本学时介绍的一些例子中,两个版本均可适用。 描述 I n t e r n e t上使用的这些协议的 I n t e r n e t标准文档称为 “Request For C o m m e n t (说明请求) ”,即通常所说的 R F C。R F C由I n t e r n e t工程组负责维护, 你可以通过网址 h t t p : / / w w w. i e t f . o rg在We b上查看。专门介绍 H T T P的文档是 R F C 1 9 4 5和R F C 2 6 1 6。请注意,这些文档的技术性很强。 当你的We b浏览器初次与We b服务器连接时,浏览器向服务器发送一条初始消息,它类似 下面的形式: G E T用于指明你试图接收的是什么 U R L,以及你想要接受的是哪个版本的协议。在这个 例子中,你接受的是HTTP 1.0版的协议。 c o n n e c t i o n行用于指明你希望这个连接为检索多个 We b页保持打开状态。按照默认设置, 浏览器为检索每一帧、每一页和 We b页上的每个图形分别建立一个连接。命令 k e e p - A l i v e要求 服务器使连接保持打开状态,以便使用相同的连接检索多个项目。第20学时 对HTTP和CGI进行操作使用227下载 A c c e p t行用于指明通过这个连接你愿意接受何种类型的数据。第一个 A c c e p t行的结尾处的 * / *表示你愿意接受任何种类的数据。下一行( i s o - 8 9 5 9 - 1等)表示字符编码可用于该文档。 在这个例子中,A c c e p t - E n c o d i n g表示g z i p(GNU Zip)可用于对来自服务器的数据进行压缩, 以便加快传输速度。最后, Ac c e p t - L a n g u a g e用于指明该浏览器能够接受何种语言(英语、大 不列颠英语、德语和法语等)。 H o s t是你希望租用的We b站点的系统名。由于可以使用虚拟租用,因此该系统名可以不同 于U R L中的主机名。 最后一行,该浏览器将自己的身份通知 We b服务器,这个身份是 M o z i l l a / 4 . 5 1 [ e n ] C - c 3 2 f 4 0 4 p(Wi n N T; U)。在We b技术中,该浏览器称为用户代理。 然后,服务器发送一个应答消息,它类似下面的形式: 这时,该应答消息后随你要检索的 We b页内容。 在这个消息中的G E T行用于指明服务器是否将这个 We b页发送给你。状态 2 0 0表示一切运 行正常。服务器还在 S e r v e r行上标明自己的身份。在这个例子中,该服务器是 N e t s c a p e - Enterprise/3.51 G We b服务器。 C o n t e n t - L e n g t h行表示2 2 2 2字节的内容将被发送给浏览器。使用这个消息,你的浏览器就 能够知道一个We b的内容完整性是 5 0 %还是6 0 %等。C o n t e n t - Ty p e是发送给浏览器的 We b页的 种类。如果是H T M L页,这一行就设置为t e x t / h t m l。如果是图形页,它就设置为 i m a g e / j p e g。 L a s t - M o d i f i e d日期表示自从该We b页上次被检索以来是否被修改了。大多数 We b浏览器都 将We b页缓存起来,这样你就可以两次阅读一个 We b页,这时,该日期就可以与浏览器已经拥 有的保存拷贝日期做比较。如果服务器上的We b页尚未修改,就没有必要再次下载整个We b页。 20.1.1 举例:人工检索Web页 如果你愿意的话,可以人工检索 We b页。当想要测定We b服务器发送的是否是正确的 We b 页时,常常可以使用这个特性。 若要运用这个特性,需要一个专门的程序,称为 Te l n e t客户程序。Te l n e t客户程序是个远 端访问程序,用于远程登录到 U N I X工作站。不过它常常用于执行调试 H T T P之类的任务。 如果你有一台U N I X计算机,可能已经安装了 Te l n e t。如果你拥有一台Microsoft Wi n d o w s 计算机,Te l n e t可能已经作为你的网络实用程序的一部分安装好了。你只需要打开 S t a r t菜单, 使用R u n选项,就可以运行Te l n e t客户程序。如果尚未安装该程序,或者使用的是 M a c i n t o s h计 算机,你可以在任何较好的下载站点找到免费的 Te l n e t客户程序。 若要启动与We b服务器的通信,请在提示符处输入下面这个 Te l n e t命令: 这里的w w w. w e b s e r v e r. c o m是We b服务器的名字,8 0是你想要连接到的端口号(端口 8 0通 常是We b服务器接收信息的端口)。如果你的Te l n e t客户程序是个图形处理程序,你必须在对话 框中设置这些值。当Te l n e t进行连接时,你可能看不到提示符或连接消息。请不必担心,这是正常的。 H T T P期望客户机首先发出请求,而服务器却没有发出提示。在 U N I X下,你会得到一条消息, 其内容如下: 其他类型的系统,如Wi n d o w s和M a c i n t o s h,则看不到这条消息。 你必须认真和迅速地键入下面这行命令: 键入这行命令后,请按 E n t e r键两次。这时We b服务器应该作出响应,发出正常的 H T T P标 题和We b站点的顶层页,然后切断连接。 20.1.2 举例:返回非文本信息 你的C G I程序不一定将H T M L信息返回给浏览器。实际上,你的浏览器能够检索的任何信 息,C G I程序都能够发送。 C G I模块中的h e a d e r函数告诉浏览器,它准备使用 M I M E内容类型(C o n t e n t - Ty p e)标题 来接收何种类型的数据。 C o n t e n t - Ty p e标题用于描述后随的数据内容,这样,浏览器就知道如 何处理该数据。 按照默认设置,h e a d e r函数将一个t e x t / h t m l的内容类型描述发送给浏览器。浏览器识别后 随的内容是带有H T M L的文本。 通过告诉浏览器将会收到不同类型的数据,你就可以控制浏览器如何来处理该数据。数 据可以作为图形来显示,也可以传递给浏览器的插件,甚至可以由浏览器启动的外部程序来 运行。 若要使h e a d e r函数能够发送非普通 t e x t / h t m l标题的某些信息,请使用 - t y p e选项,如下所 示: 可以发送给浏览器的某些常用 M I M E内容类型是 t e x t / p l a i n(指不需要转换的文本), i m a g e / g i f和i m a g e . j p e g(指G I F和J P E G图形),以及a p p l i c a t i o n / a p p n a m e(指应用程序a p p n a m e 特定的数据)。一种特殊的M I M E内容类型称为a p p l i c a t i o n / o c t e t - s t e a m,它是指浏览器应该保 持到一个文件的原始二进制数据。 如果你需要创建一个“当日图形”的 We b站点,或者创建一个We b标题广告,就可以使用 这种内容类型。每天修改 We b页,以便反映新图形的变化情况,这是很麻烦的。如果你出差 在外,谁为你更新“当日图形”呢?为此,你可以使用一个静态 H T M L页,并且使用Perl CGI 程序,每天自动产生一个不同的图形。 在你的We b页中,使用下面这样的H T M L代码: 在上面这个H T M L代码中,请注意< I M G >标记的目标是个C G I程序,不是. g i f或. j p g。接着, 你需要一个放满图形的目录,图形的数量至少要与一个月的天数相同。你可以调用你喜欢的 228使用第三部分 将Perl 用于C G I 下载任何图形,只要文件名以. j p g结尾即可。请注意,该程序能够非常容易地使用 G I F图形。 C G I程序d a i l y _ i m a g e . c g i类似程序清单2 0 - 1所示的形式。 程序清单20-1 当日图形的代码 第7行:这一行用于设定图形所在的目录。可以修改这个设置,以指明你将图形放在什么 位置。 第8行:这一行非常奇怪,因为这个 C G I程序并不输出文本,并且因为它嵌入到的 H T M L 页时并不将输出显示为文本,你不能只是输出出错消息。如果无法打开 $ i m a g e d i r目录,则变 量$ e r r o r包含将要显示的. j p g文件的名字。 第1 0 ~ 1 6行:这个子例程将图形显示在标准输出中,它将发送到浏览器。在 Wi n d o w s平台 上,S T D O U T被视为一个文本文件,将. j p g输出到S T D O U T将会损坏图形。因此,b i n m o d e用于 使S T D O U T和I M A G E成为二进制文件句柄。在 U N I X下,你不需要使用b i n m o d e,但是它不会 造成损害。请注意第1 2行,如果图形打不开,就没有必要输出出错消息,程序只要退出即可。 第1 9行:这一行用于输出标准 H T T P标题,不过C o n t e n t -Ty p e将是i m a g e / j p e g,而不是通 常的t e x t / h t m l。 第2 5行:图形目录被打开以便读取。如果图形目录没有打开,那么便用错误图像 $ e r r o r来 调用函数d i s p l a y - i m a g e()。 第2 6行:这一行比较复杂,因此要循序渐进来操作。首先用 r e a d d i r读取目录。然后从该 列表中取出以. j p g结尾的文件名。最后,对产生的列表进行排序,并赋予 @ j p e g s。 第20学时 对HTTP和CGI进行操作使用229下载20.2 如何调用CGI程序的详细说明 到现在为止,我们介绍了启动 C G I程序时使用的两种方法。第一种方法也是最简单的方法 是通过一个链接来调用 C G I程序的U R L,或者让用户将 U R L键入浏览器。因此类似下面的这 行代码可以用于启动和运行称为 t i m e . c g i的程序: 当通过该链接进行操作时, C G I程序t i m e . c g i将由服务器运行,它的输出则作为一个新 We b页来显示。这个示例代码简单明了,容易操作,与第 1 7学时中的“H e l l o, Wo r l d!”很相 似。 启动C G I程序的另一个方法是使它成为一个 H T M L填充式窗体的目标程序。例如,当单击 S u b a m i t按钮时,下面这个窗体便调用 C G I程序p r o c e s s . c g i: 这个调用C G I程序的方法具有另一个优点,即你可以将参数传递给 C G I程序,以便进行处 理。好了,这就是H T M L窗体的总体情况。 20.2.1 将参数传递给CGI程序 通过链接将信息传递给P e r l程序,这是不是很好呢?例如,是否可以在文档中设置一个点 击的链接,它将“运行 C G I程序f o o . c g i,其X值等于t h i s,Y值等于 t h a t”呢?你稍加努力,就 可以达到这个目的。 首先必须在标记中使用一种特殊的U R L。该U R L的格式在图2 0 - 1中做了说明。 每个参数都是你想要传递到 C G I程序中的一个值的名字(类似一个指明的 H T M L窗体元 素),该值是该名字的值。例如,若要创建一个链接,单击这个链接时,它将运行一个 C G I程 序,其参数s i g n设置为A r i e s,y e a r设置为1 9 6 9,那么你可以输入下面这行代码: 在这个C G I程序中,它的参数将像通常那样由 C G I模块的p a r a m函数进行处理: 230使用第三部分 将Perl 用于C G I 下载 图20-1 包 含 各 个 参 数 的 U R L 指明后随的参数 参数分隔符 参数值服务器的名字 协议名 CGI 程序的路径名 参数名 参数名 参数值你可以根据需要传递任意数量的参数。如果你想传递一个空参数,即没有值的参数,只 需要像下例中的a u t h o r那样将它置空即可: 20.2.2 特殊参数 当你调用带有此类参数的C G I程序时,应该了解使用某些特殊参数时要考虑的问题。某些 字符属于特殊字符,不能成为 U R L的组成部分。例如,?(问号)是个特殊字符,它可以作 为U R L的主要部分与参数之间的分隔标号。其他特殊字符还有 &、空格和引号等。 特殊字符的完整列表在I n t e r n e t标准文档RFC 2396中列出。 若要将这些特殊字符中的某一个插入 U R L,你必须对字符进行转义。在这种情况下,对 字符进行转义意味着应该将它的 A S C I I值转换成一个两位数十六进制数字,并在它的前面加上 一个百分比符号。对“H e l l o , Wo r l d !”的编码如下所示: 显然,创建一个U R L转义字符串是非常麻烦的。 C G I模块提供了一个函数,它能够自动为 你创建这样的字符串。下面这个代码段展示了如何输出一个带有正确编码的 U R L: 上面这个代码可以产生一个正确进行 U R L字符转义的H T M L链接。请注意 CGI 模块是如 何用于代码的use CGI qw(:all escape);。如果你使用 C G I模块,那么e s c a p e函数通常不能 供你的程序使用,你必须显式要求使用该函数。 下面这个程序创建了一个带有转义值的长得多的 U R L: 第20学时 对HTTP和CGI进行操作使用231下载当C G I程序用p a r a m函数取出这些参数时,在 U R L的结尾处的最后一个 &将被该C G I程序 忽略。 20.3 服务器端的包含程序 当你设计We b页时,该页上的最常见的内容是静态的。有时该页的某些部分要进行修改, 但是总的来说,该页的内容将保持不变。请看这样一个 We b页,它显示了一家公司的当前股 票价格。该页的主要部分是静态的,比如导航栏、图形、徽标、使用信息、页眉、页脚和标 题等。该页的重要部分,即股票价格,是通过读取某处的数据库和填写空格来生成的。 为了帮助你创建这种We b页,大多数We b服务器都支持一个特性,称为服务器端的包含程 序(S S I),也称为服务器分析的 H T M L。该特性使得 We b站点的开发者能够创建基本静态的 HTML We b页,并使该页的某些部分由 We b服务器在运行中重新编写(见图 2 0 - 2)。可以将该 We b页视为填空式H T M L文件,而C G I程序则为你将数据填入空格。 你的服务器管理员必须激活 S S I,使这些示例代码能够运行。为了使服务 器能够正确地读取带有嵌入式 S S I的H T M L,有时你必须为 H T M L赋予带 有. s h t m l或. s t m扩展名的名字。请与你的服务器管理员联系,以便了解 S S I是如 何在你的特定Web服务器上使用的,因为它支持的命令及其语句是各不相同的。 当We b服务器从磁盘上读取静态 H T M L页时,它要寻找它能够替换的各个值的“标记”。 在服务器分析的 H T M L页中的 Apache We b服务器下,标号 标记,应该确保你在不使用 S S I来运行程序时你的 C G I程序运 行正确。 在We b页已经加载的情况下使用浏览器中的“ view source(查看源代码)”选项,就能够 知道服务器是否正在执行你的 S S I程序。如果你看到 We b页源代码中的 S S I标记,服务器就不 能识别和分析它们。 问题:Te l n e t示例代码无法运行,为什么? 解答:如果Te l n e t未能建立连接,那么应该确保你是针对 We b服务器的名字来使用 Te l n e t 的,并且使用的端口是正确的,也许它是端口 8 0。你必须查看Te l n e t客户程序的文档,以便正 确地设置端口号。 另一个常见问题是你无法看到自己键入的字符。有些 Te l n e t客户程序能够将你键入的字符 反馈给你,有些则不能。请不必对此担心,你只需要认真进行操作就行了。这些字符必须认 真发送。当你键入G E T行后,务必按两下E n t e r键。 20.7.2 思考题 1) 下面这个U R L能够按照你的期望运行吗? a. 是。 b. 否。你不能像这样将两个参数传递给一个 C G I程序。 c. 否。名字Ben Franklin中的空格是不允许的。 第20学时 对HTTP和CGI进行操作使用237下载2) 服务器端的包含程序由什么来进行处理和展开? a. 浏览器。 b. We b服务器。 c. 操作系统。 20.7.3 解答 1) 答案是c。你应该使用转义符正确地隐藏空格和其他特殊字符。 2) 答案是b。We b服务器负责将SSI HTML标记转换成它们的值,然后将它们发送给浏览 器。 20.7.4 实习 • 使用Te l n e t客户程序,连接到你喜欢的 We b站点之一,并设法人工检索 We b页。 238使用第三部分 将Perl 用于C G I 下载下载 第2 1学时 c o o k i e 在第1 9学时中,我们讲述了如何使用 H T M L中的隐藏域使你的We b浏览器记住各个We b之 间的信息。你必须理解这个进程,因为从 C G I程序的一个实例到另一个实例,有时需要在它们 之间传递信息。进行这项操作的唯一方法是将一些信息存储在浏览器中。 将信息存储在浏览器中的另一个方法是使用 H T T P的c o o k i e。正如它的名字所表示的那样, HTTP Cookie是指在H T T P连接期间浏览器与 C G I程序之间传递的信息。使用 Co o k i e,可以比 使用H T M L隐藏域更加灵活地用浏览器来存储信息。 在本学时中,你将要学习 • 什么是c o o k i e。 • 如何编写和检索c o o k i e。 • 如何处理和避免c o o k i e的常见问题。 21.1 什么是cookie 可以将c o o k i e视为电影院的入场券。你可以到电影院购买一张入场券,以便在以后的某个 时间拿着入场券到电影院去看电影。看完电影你就可以离开电影院,往回家路上走,买一点 爆玉米,并做你喜欢做的任何事情。当你准备看电影时,你向电影院的收票员出示电影票。 收票员并不知道你如何、何时和为何购买电影票,但是,只要你持有电影票,收票员就允许 你进入电影院。电影票使持票人有权在以后进入电影院去看电影。 HTTP cookie只不过是C G I程序要求浏览器持有的一个信息包。这个信息包可以由另一个 C G I程序或原来的程序在任何时候回收。当有人要检索正常的 HTML We b页时,c o o k i e甚至可 以重新传回给服务器。 c o o k i e可以包含任何种类的信息,比如关于多页 We b窗体的信息、访问 信息、用户喜欢的信息等。 每当C G I程序要求创建c o o k i e时,c o o k i e可以从服务器传送到浏览器(见图 2 1 - 1),这个进 程称为安装c o o k i e。 C G I程序可以在晚些时候回收,以便检索存储在 c o o k i e中的信息,如图2 1 - 2所示。 图21-1 c o o k i e从C G I程序 传送到浏览器 图2 1-2 浏览器将 c o o k i e送 回到服务器 浏览器 浏览器 Web 服务器 CGI 程序 CGI 程序 Web 服务器cookie因何而得名 在计算机界,c o o k i e是个非常老的术语。它是指例程或程序之间传递的任何一组信息,它 使c o o k i e的持有者能够执行某项操作。某些类型的 c o o k i e称为神秘的c o o k i e,因为它们包含的 数据非常神秘,只有c o o k i e的发送者和接收者才能理解其含义。 CGI cookie并不神秘。 21.1.1 如何创建cookie 若要创建c o o k i e,你可以使用C G I函数c o o k i e。c o o k i e函数的句法如下: c o o k i e函数以一种特殊的方式使用参数。调用 c o o k i e时使用的每个参数都带有名字。实际 上,在P e r l中以这种方法将参数传递给函数是非常方便的,因为你不必记住参数的顺序,它们 将按你使用它们时的顺序进行命名。 当你用这个句法来调用 c o o k i e函数时,该函数便返回一个 c o o k i e(该c o o k i e应该存放在一 个标量变量中),然后该c o o k i e可以被赋予C G I模块的h e a d e r函数,以便发送给浏览器。创建 c o o k i e时必须要的唯一参数是 - v a l u e。- n a m e参数允许同时将若干个 c o o k i e发送给浏览器,而检 索时则可以单个检索,也可以成组检索。其他参数如 - e x p i r e s、- p a t h、- d o m a i n和- s e c u r e等, 将在下一节介绍。 C G I模块中的h e a d e r函数负责管理将 c o o k i e发送给浏览器的实际操作。这意味着必须使用 c o o k i e函数来创建c o o k i e,然后紧接着就调用 h e a d e r函数。在c o o k i e和标题发送之前,不应该 将任何其他种类的数据发送给浏览器。 若要使用C G I程序创建一个c o o k i e并将它发送给浏览器,你可以使用类似下面这样的C G I程序: 当上面这个代码段运行之后,浏览器上就安装了一个称为 s a m p l e的c o o k i e。该c o o k i e包含 了“This cookie contains no MSG(该c o o k i e不包含任何消息)”这样一个信息。 实际上该c o o k i e并没有安装。浏览器可以因为许多原因而拒绝接受某个 c o o k i e。请参见本学时后面部分中的“ c o o k i e存在的问题”这一节。 若要在你的C G I程序中从浏览器中检索 c o o k i e,可以使用相同的 c o o k i e函数。如下面的例 子所示,如果不带任何参数, c o o k i e函数返回浏览器拥有的服务器的一个 c o o k i e列表: 240使用第三部分 将Perl 用于C G I 下载按照默认设置,当c o o k i e安装在浏览器上之后,它将返回给驻留在同一个服务器上的任何 C G I程序。也就是说,只有安装 c o o k i e的服务器才能检索这些 c o o k i e。若要查看以前创建的 Sample cookie,可以使用另一个C G I程序: 上面的代码段使用带有一个参数的 c o o k i e函数,这个参数就是你想查看其值的 c o o k i e的名 字。该值被检索并输出。 c o o k i e应该被浏览器保留到浏览器运行终止。当浏览器重新启动时, cookie sample将不复 存在。如果你想创建一个比较永久的 c o o k i e,请参见本学时后面部分中的“设置 c o o k i e终止运 行的时间”这一节。 大多数浏览器都配有一个选项,用于在 c o o k i e被安装时查看这些 c o o k i e。 在N e t s c a p e中,你可以在 A d v a n c e d选项卡上的 P r e f e r e n c e s选项下找到查看 c o o k i e的各个选项。在 Internet Explorer中,这个选项出现在 Internet Options 对话框的A d v a n c e d选项卡上,还有一个单选按钮可用于控制你是否可以在安 装c o o k i e时查看它们。 21.1.2 举例:使用cookie 使用这个例子,你可以创建一个小程序,让用户可以使用 We b浏览器来设置他查看的 We b 页的颜色。该程序实际上能够同时执行若干项操作: 1) 通过查看程序的各个参数,以便观察默认背景色的变化。 2) 用正确的背景色在浏览器上设置 c o o k i e。 3) 将We b页的背景色设置为正确的颜色。 4) 显示一个C G I窗体,使你能够改变其颜色。 程序清单2 1 - 1包含改变颜色的程序。 程序清单21-1 ColorChanger程序的完整清单 第21学时 cookie使用241下载第7 ~ 1 0行:如果该程序作为 C G I窗体的目标程序来调用,那么 p a r a m(‘c o l o r’)函数值返 回一个定义的值,即一个新颜色。否则,它不返回任何值, $ r e q u e s t e d _ c o l o r则保持未设定状 态。 第1 2 ~ 1 4行:这些行用于检索名叫 b g c o l o r的c o o k i e。它可能存在,也可能不存在。如果它 不存在,那么它存放在$ o l d _ c o l o r中,这是上次保存到c o o k i e中的屏幕颜色值。 第1 5 ~ 1 9行:如果颜色已经改变(即 c o o k i e的值与新值不一致),那么新c o o k i e必须用新值 进行设置。 第2 0 ~ 2 4行:否则,输出一个纯标题,不带 c o o k i e。请记住,浏览器将无限期保留以前的 c o o k i e。 第2 5 ~ 4 2行:这些代码行用于创建一个标准 H T M L窗体。不过请注意第3 0行,在这一行上, 被取代的颜色被送入H T M L输出。 21.1.3 另一个例子:cookie查看器 程序清单 2 1 - 2中列出的一个非常短的程序是个 c o o k i e查看器,它用于帮助你调试使用 c o o k i e的C G I程序。它列出了存储在 We b浏览器上的所有 c o o k i e,这些c o o k i e恰好来自同一个 We b服务器。 242使用第三部分 将Perl 用于C G I 下载程序清单21-2 Cookie查看器 第1 0行:用c o o k i e函数检查所有c o o k i e的名字,并赋予$ c o o k i e,每次检索1个c o o k i e。 第11 ~ 1 2行:输出每个c o o k i e的名字和值。 该c o o k i e查看器运行时,可以获取使用 c o o k i e()函数能够得到的所有 c o o k i e的列表,然 后对这些名字迭代运行c o o k i e,输出每个c o o k i e的名字和值。 21.2 高级cookie特性 c o o k i e的基本概念简单明了,你将 c o o k i e赋予浏览器,过一会儿浏览器又将它送回给服务 器。不过c o o k i e的基本特性并不止此。可以将 c o o k i e设置为可以存在较长的时间,这种 c o o k i e 称为永久性c o o k i e。你可以让这些c o o k i e只返回到另一个特定的 U R L,它们能够指明关于你的 连接的安全程度之类的信息。 21.2.1 设置cookie终止运行的时间 到现在为止,你在浏览器上安装的 c o o k i e都是临时的。一旦浏览器关闭, c o o k i e就消失。 当你使用c o o k i e将值保存在窗体上的多个页中(而不是保存隐藏的 H T M L值)时,使用临时 c o o k i e是完全合适的。当一个新浏览器启动时,你不想让 c o o k i e返回给服务器,因为用户不会 从中间开始填写窗体,他将再次从头开始时填写。 在有些情况下,你可能希望 c o o k i e能够保留更长的时间。也许你想在浏览器关闭和重新启 动之后使c o o k i e持续数天、数周、数月时间。用P e r l的C G I模块来创建这种c o o k i e是非常容易的。 若要为c o o k i e设置一个终止日期,可以在创建 c o o k i e时使用- e x p i r e s选项。- e x p i r e s选项必 须后随一个想使c o o k i e终止运行的日期。可以如表 2 1 - 1所示用若干种格式设置这个日期。 表21-1 cookie的终止日期格式 格 式 示 例 含 义 秒数 + 3 0 s 从现在起3 0秒后终止 分钟数 + 1 5 m 从现在起1 5分钟后终止 小时数 + 1 2 h 从现在起1 2小时后终止 月数 + 6 M 从现在起6个月后终止 年数 + 1 Y 从现在起1年后终止 n o w c o o k i e立即终止运行 任何负时间值 - 1 0 m c o o k i e立即终止运行 一个特定时间 S a t u r d a y,28-Aug-1999 22:51:05 GMT 第21学时 cookie使用243下载当设定一个特定时间时,必须完全使用表 2 1 - 1中列出的时间格式。所有其他的各种设置 值都是指从当前时间起的时间偏移量。系统将为你计算出完全合格的时间值,然后发送给浏 览器。 下面这个小程序用于在浏览器上安装一个将在 8天后终止运行的c o o k i e: 21.2.2 cookie的局限性 要使c o o k i e能够永久运行是做不到的。这就是说,如果将一个 c o o k i e发送给浏览器,希望 从现在起该c o o k i e能够在数周、数月或者数年内保持运行,那么你一定会大失所望的。 当你读到后面的“ c o o k i e存在的问题”这一节内容时,就会知道浏览器并不是必须将 c o o k i e存储起来的。实际上,它们根本不必接受你的 c o o k i e,它们并不通知你这些 c o o k i e并没 有保留起来。 浏览器可以随时清除它们的 c o o k i e,以便为来自其他站点的新 c o o k i e腾出地方,或者根本 毫无理由就这样做了。有些浏览器允许用户编辑 c o o k i e,或者添加新的c o o k i e。 用户可能不小心删除 c o o k i e,也可能故意将c o o k i e删除掉。如果用户安装了浏览器或操作 系统的新版本,c o o k i e就会被清除,或者放到别的什么地方。只要改用另一种浏览器, c o o k i e 就会“不知去向”。当浏览器尚未激活时, c o o k i e通常存放在一个文件中,该文件可以供用户 编辑,删除,或者遭到损坏。 如果你有兴趣的话,我们可以告诉你,大多数浏览器是在没有激活时将 c o o k i e存放在文件中的,这些文件通常是文本文件,你可以使用编辑器查看 这些文件。N e t s c a p e将c o o k i e存放在用户主目录下的 c o o k i e s . t x t文件中(不同 的系统下该目录将各不相同)。Internet Explorer将c o o k i e存放在\ Wi n d o w s \ C o o k i e s下。 因此,将重要信息存放在一个 HTTP cookie中真的不是个好主意。你想永久存放在 c o o k i e 中的任何信息不应该被轻易改变位置,这些信息包括用户喜欢的信息,输入指定 We b页的可 替换项目关键字,上次刚刚访问的信息等。 21.2.3 将cookie发送到其他地方 按照默认设置,c o o k i e只能送回到曾经发出 c o o k i e的服务器。有时,你希望将 c o o k i e送回 到服务器,但有时你并不希望如此。以神秘的 We b站点C o n g o . c o m为例,这个销售书籍的 We b 站点拥有两个We b服务器,即w w w. c o n g o . c o m和s h o p p i n g . c o n g o . c o m,如图2 1 - 3所示。主要的 We b站点(w w w. c o n g o . c o m)包含公司的所有信息,可以连接到其他站点,并且最重要的是可 244使用第三部分 将Perl 用于C G I 下载以连接到在线书店。 w w w. c o n g o . c o m包含一个注册用的H T M L窗体/C G I程序,使用户可以将他们的名字添加 到电子邮件的地址列表,设置他们喜欢什么类型的书籍。以后,当用户浏览 w w w. c o n g o . c o m 时,他就可以阅读关于他感兴趣的新书的信息。用户浏览器上的 c o o k i e负责告诉 w w w. c o n g o . c o m,应该向他介绍哪些书籍的情况(见图 2 1 - 4)。 问题是当用户从 w w w. c o n g o . c o m转到位于 s h o p p i n g . c o n g o . c o m站点上的在线书店时, c o o k i e没有被发送到s h o p p i n g . c o n g o . c o m服务器。HTTP cookie只返回给原先发送c o o k i e的这个 服务器。如果w w w. c o n g o . c o m发送了该c o o k i e,它并不发回给shopping.congo. com。 那么你应该怎么办呢?如果让用户填写另一个首选项窗体,并且从 s h o p p i n g . c o n g o . c o m给 他发送一个新c o o k i e,那将是不切实际的。更好的办法是限制这个 c o o k i e只能使用一个特定的 域名。例如,当原始 c o o k i e从w w w. c o n g o . c o m发送出来时,可以将该 c o o k i e送回给任何 congo.com We b站点,如图2 1 - 5所示。 若要进行上述操作,可以在创建 c o o k i e时使用带有- d o m a i n参数的c o o k i e函数: 第21学时 cookie使用245下载 图21-3 两个互相连接的We b 站点 Web服务器 HTML 链接 www: congo.com shopping congo.con Web 服务器 图21-4 只返回给单个 We b 站点的c o o k i e 浏览器 Cookie 域 服务器 服务器 图21-5 返回给两个 We b站 点的c o o k i e 浏览器 Cookie 域 www. congo.com www. congo.com shopping congo.com www. foo. com Web 服务器 Web 服务器 Web 服务器 www: congo.com www: congo.com shopping congo.con在上面这个代码段中, cookie $c o o k i e得以创建,并且限制为 c o n g o . c o m域。任何We b服 务器,如果其主机名以c o n g o . c o m为结尾,将使它的c o o k i e由浏览器返回给该服务器。 域的参数至少必须由两个部分组成,并且不能是绝对的顶层域,即 . c o m 或. n e t。这样,就可以避免浏览器将 c o o k i e从一个. c o m域移植到另一个 . c o m 域。 21.2.4 限制cookie返回到的位置 将c o o k i e限制为只能返回到某个服务器,这也是可能的。当你创建一个 c o o k i e时,按照默 认设置,该c o o k i e可以返回到We b站点上的任何U R L,包括非CGI URL。例如,如图2 1 - 6所示 的汽车销售We b站点的组织结构。 让s a l e s(销售)C G I程序和e n g i n e e r i n g(工程设计)C G I程序驻留在不同的目录中,是有 意义的。如果sales CGI程序准备建立一个c o o k i e,那么engineering CGI程序便将它接收过来, 反过来也一样。这样的结果是人们所不希望的,为两个站点同时编写 C G I程序的开发人员必须 采取协调措施,以确保不会重复使用对方的 c o o k i e名字。 为了解决这个问题,可以使用 c o o k i e函数的- p a t h选项。该选项用于指明 c o o k i e应该返回到 的路径名(相对于U R L顶层的路径名)。例如,若要发送只返回到sales CGI程序的一个c o o k i e, 可以使用下面的代码段: 按照默认设置, c o o k i e返回到服务器上的每个站点,就像已经使用了选项 - p a t h = >‘/’ 一样。若要限制只能返回到一个 C G I程序,可以在- p a t h选项中使用C G I程序的U R L: 上个学时中我们讲过, C G I模块中的s c r i p t _ n a m e函数能够返回当前 C G I程序的部分U R L。 这可以有效地创建这样一个 c o o k i e,它只返回到在浏览器上安装该 c o o k i e的程序。 246使用第三部分 将Perl 用于C G I 下载 图21-6 一个多用户 We b站 点的目录树结构21.2.5 带有安全性的cookie 有些c o o k i e你可能只想在一条安全的连接上传输它们。使用 c o o k i e函数的- s e c u r e参数,就 可以只在连接是安全的时候从浏览器发送 c o o k i e。下面的代码用于将一个包含账号的 c o o k i e发 送给浏览器。包含此类敏感信息的 c o o k i e只能在安全的连接上发送。 以后,如果你要检索该c o o k i e,只要像平常那样使用c o o k i e函数即可。如果连接是安全的, 而且该c o o k i e是在该浏览器上,那么该浏览器就可以在需要时将 c o o k i e发回给服务器。 你不应该依赖这个方法来检查连接是否安全,也不应该依赖账号的准确性。请记住,用 户负责控制We b浏览器及其c o o k i e文件。c o o k i e可以在不安全的连接上发回给服务器,甚至可 以有一个无效号码。 21.3 cookie存在的问题 在你将c o o k i e投入应用之前,应该知道与c o o k i e相关的一些问题。由于这些原因和将来可能 产生的其他原因,你应该认真设计你的Web页和CGI程序,使得cookie完全成为可以选择的选项。 例如,如果你使用c o o k i e来存放用户的首选项,那么倘若 c o o k i e无法使用,你应该使用一 组默认首选项。编码时请采取相应的防范措施。 21.3.1 cookie的生存期很短 本学时中多次讲到, c o o k i e的寿命很短。C o o k i e可以从用户的系统中删除,可以由用户编 辑,也可以毫无理由地被浏览器甩掉。 浏览器可以接受c o o k i e,将它使用一会儿,然后毫无理由就将它忘掉。如果你使用 - e x p i r e 选项安装了一个永久性c o o k i e,浏览器仍然可以甩掉这个 c o o k i e,并且根本不通知用户。 21.3.2 并非所有浏览器都支持cookie 并非所有浏览器都支持 HTTP cookie,这是千真万确的事实。适用于 H T T P和We b信息传 输的I n t e r n e t标准并不能保证浏览器必须支持 c o o k i e。 并不是说大多数浏览器都不支持 c o o k i e,大多数浏览器是支持 c o o k i e的。N e t s c a p e(自从 1 . 1版以来),Internet Explorer(所有版本),Ly n x , O p e r a,以及大多数流行的 We b浏览器都支 持c o o k i e。在大多数浏览器中,有一个选项可供用户关闭对 c o o k i e的支持。 即使你使用C G I模块的u s e r _ a g e n t函数,确定你想使用的浏览器应能支持 c o o k i e,也不要 完全指望它。 21.3.3 有些人不喜欢cookie 这一节的标题也许很难理解,为什么世界上竟然有人不喜欢 c o o k i e呢? 第21学时 cookie使用247下载在We b上冲浪实际上是一种匿名活动。正如你在上一学时中看到的那样,当浏览器要求 检索一个We b页时,这个检索请求是在真空中发生的。服务器不一定知道浏览器所在的位置, 也不知道该浏览器上次曾经要求检索过该站点上的一个 We b页。 请记住,一个浏览器不一定代表一个用户,一个浏览器可以被一个家庭、 网吧、I n t e r n e t网吧或公共访问点(如图书馆)中的许多人共享。为一个人安 装(或修改)一个c o o k i e,实际上也为若干人安装了 c o o k i e。 c o o k i e可以用来跟踪人们曾经访问过某个站点的哪个位置以及他们曾经点击过什么。如果 你非常在乎隐私问题,那么这个情况你应该注意。 例如,前面的“将 c o o k i e发送到其他地方”这一节中我们提到的一个虚构在线书店 c o n g o . c o m能够跟踪We b冲浪者点击了哪些书籍以便了解其详细信息,并使用该信息编写符合 读者需要的书目,提供给We b冲浪者。 从表面上看,这些特性很好。但是对于那些想要维护隐私权的人来说,这会带来两个问 题。首先,现在有一个机构负责跟踪 We b冲浪者感兴趣的是什么种类的书籍。如果这些信息 与We b冲浪者的名字和地址有关(也许这些信息是从 c o n g o . c o m共享信息的另一个站点的填写 式窗体中获得的),那么We b冲浪者将会收到与他选购书籍相关的垃圾邮件。与 c o o k i e搜集站 点共享的信息越多,就能获得关于 We b冲浪者更详细的信息。 除了隐私问题外,如果We b冲浪者查看的头两本书属于“计算机”书籍, We b站点就会停 止向We b冲浪者提供“传奇”类和“烹饪”类书籍。 We b站点将把We b冲浪者“转移”到他们 想要的书籍类别。 你会惊奇地发现 c o o k i e是多么频繁地用来在你的浏览器上搜集和存储信 息。请打开你的浏览器上的c o o k i e确认特性,以便访问流行的We b站点。 为了避开对c o o k i e的使用,人们想了多办法。支持 c o o k i e的We b浏览器均配有关闭 c o o k i e 的特性,有些浏览器在安装 c o o k i e时允许你查看这些c o o k i e。可以使用某些辅助软件包对发送 到浏览器和浏览器返回的 c o o k i e进行筛选,还可以对它们进行编辑。 We b站点的设计使你可以 对其他We b站点进行匿名冲浪,而c o o k i e不会搜集关于你的信息。 总之,有些人将HTTP cookie视为侵犯隐私权的一个特性,因此你在使用cookie时应该慎重。 21.4 课时小结 在本学时中,我们全面介绍了如何使用HTTP cookie在浏览器上存储信息,供别的C G I程序 在以后使用。还介绍了按照预定时间使c o o k i e终止运行,仅为特定We b服务器激活,或者为特定 目录激活c o o k i e等特性。最后,讲述了不使用c o o k i e的许多理由以及使用c o o k i e会带来的问题。 21.5 课外作业 21.5.1 专家答疑 问题:我应该如何将多个项目放入一个 HTTP cookie? 248使用第三部分 将Perl 用于C G I 下载解答:最容易的方法是将多个项目组合在单个 c o o k i e中,用域分隔符将各个项分开,如下 例所示: 然后,当你检索c o o k i e时,可以使用S p l i t将各个项目分开: 问题:如何使用c o o k i e来跟踪用户在We b页上点击了哪些链接? 解答:在解答这个问题之前,必须指出,有些人将这种跟踪视为是侵犯他人的隐私权。 说明这一情况后,再来说明跟踪的一般方法: 1) 编写你的<A HREF>链接,将它们纳入一个 C G I程序,将真实的目标U R L作为参数来 传递: 2) 上例中的r e d i r e c t . p l程序应该使用C G I模块的p a r a m函数,以便从参数 t a rg e t中获得真实 的U R L(h t t p : / / w w w. congo.com): 3) 然后使用该值中的目标U R L创建一个c o o k i e,其名字你可以在以后查看,如下所示: 4) 然后将重定向的项目与c o o k i e一同发送给浏览器: 以后,当浏览器返回到你的 We b站点时,你就可以查找名字为 t r a c k e r的c o o k i e,它包含了 用户退出你的站点时访问过的 U R L。 问题:我在传送c o o k i e时能够将浏览器重定向到另一个 We b页吗? 解答:当然可以。C G I模块的r e d i r e c t函数也能像h e a d e r函数那样带有一个- c o o k i e参数。 21.5.2 思考题 1) 用c o o k i e来长期存储信息,为什么有时会失败? a .浏览器会“甩掉”c o o k i e的信息。 b .软件更新时c o o k i e可能丢失。 第21学时 cookie使用249下载c .用户可能关闭浏览器对c o o k i e的支持。 2) 若要使c o o k i e在一周后终止运行,c o o k i e函数的- e x p i r e选项应该使用什么参数? a. +7d b. +1w c. +10080m 3) 为什么有些人认为c o o k i e会侵犯隐私权? a. cookie可以用来跟踪用户点击的链接。 b. 被跟踪的c o o k i e信息可以共享,以便建立关于用户的档案资料。 c. cookie信息可用于将某些类别的信息“传递”给用户。 21.5.3 解答 1) 3个答案均成立。 2) a和c均成立。参数+ 1 w无效。 3) 3个答案均成立。 21.5.4 实习 • 扩展背景色修改程序,以便设置前景色和字体,并随机选定一个图形,以便显示在 We b 页上,方法是编辑<I M G>标记的目标对象。 250使用第三部分 将Perl 用于C G I 下载第21学时 cookie使用251下载下载 第2 2学时 使用C G I程序发送电子邮件 毫无疑问,在你进行 We b冲浪时,要填写一个窗体,以便在以后用来发送电子邮件。这 些窗体常常用作信址列表、故障报告、客户支持、爱好者邮件和其他各种可以想像到的用途。 在本学时中,我们将要介绍如何用 P e r l程序发送邮件,并且讲述一个简短的 We b页示例, 你可以用它来生成电子邮件。我们将使你能够创造性地使用这个 We b页。 在本学时中,你将要学习: • 关于如何运行I n t e r n e t电子邮件特性的简单介绍。 • 如何在U N I X和非U N I X系统下发送邮件。 • 如何建立发送邮件的We b窗体。 22.1 Internet邮件入门 在你将编程技巧用于以P e r l来发送电子邮件之前,首先必须学习一些关于电子邮件特性如 何在I n t e r n e t上运行的一些知识。 在P e r l问世之前,在美国的国家计算机安全委员会( N C S A)尚未注意到We b的远大前景 并且调制解调器的速度还比较慢的时候,全球的许多人就已经在使用电子邮件在所谓的 U N I X 至U N I X拷贝(UNIX-to-UNIX copy, UUCP)的系统上进行通信了。当你在这个老式系统上发 送电子邮件时,本地系统把你的电子邮件封装好,然后转发给系统链中的下一个系统,下一 个系统又将电子邮件封装好,转发给下一个系统,如此传递下去。线路上的每个系统都要给 邮件添加一点信息,表示它对邮件进行了处理,然后传递下去,如图 2 2 - 1所示。 很明显,这种邮件传递的方法可以称为存储与转发法。后来U U C P系统被别的方法所取代, 不过存储与转发的基本方法仍然没有变。当你从你的 P C发送电子邮件时,另一个系统负责接 收该邮件,再将它转发给另一个系统,然后该系统又将邮件转发给下一个系统,直到最后由 目标系统接收到邮件为止。 不过,如今这些协议完全发生了变化。目前最常用的方法是使用简单邮件传输协议 (Simple Mail Transport Protocol, SMTP)将邮件发送到系统链上(见图 2 2 - 2)。若要检索邮件, 连接的目标端通常使用邮局协议( Post Office Portocol, POP)或 I n t e r n e t邮件访问协议 (Internet Message Access Protocol, IMAP)。下面用于发送电子邮件的协议是 S M T P。 图22-1 将邮件从一个系统 传递到下一个系统 UUCP 源主机 UUCP Mail UUCP 目标主机22.1.1 发送电子邮件 若要发送电子邮件,需要两样东西,即邮件传输代理或 S M T P中继主机。 遗憾的是,它们都是很难理解的术语,不过下面将对它们加以解释。 邮件传输代理(Mail Transport Agent, MTA)是驻留在你的计算机上的一个程序,它通常 是你的操作系统所配备的一个程序,负责接收电子邮件并正确地将它们转发。当你的操作系 统安装时,M TA通常已经作好正确的配置。 U N I X系统上的常用M TA称为s e n d m a i l。s e m d m a i l 程序负责取出一个电子邮件并确定如何将它传递到目的地。 若要在U N I X下发送电子邮件,请在命令行上使用下面这个语句: 上面这个代码段将一个短邮件发送到 f o o @ b a r. c o m。s e n d m a i l程序负责为你解决所有难以 处理的工作,比如决定使用哪个邮件中继主机,处理被拒绝的返回邮件等。 如果你使用Microsoft Wi n d o w s或M a c i n t o s h操作系统,那么你将不具备内置的 M TA。不过 P e r l模块使你能够直接发送邮件。 N e t : : S M T P模块可以在没有介入的 M TA的情况下发送邮件, 但是你必须知道你的S M T P中继主机的名字。这个名字是用于发送邮件的“邮件主机”的主机 名,当你用你的帐户进行登录时,你将被赋予该主机名。请索取中继主机的名字,并将它写 在某个地方,以后你会用到它。 你可以使用不同的“邮件主机”,以便发送和接收邮件。本学时中你需要 发送邮件的主机名。 请记住,依靠S M T P中继的程序必须将正确的中继主机内置于软件之中,否则该进程将不 能运行。 正确的“S M T P中继主机名”取决于你从何处发送你的邮件。如果你从 家中发送邮件,那么你的家庭 I n t e r n e t服务提供商( I S P)帐户为你赋予一个 S M T P中继主机名。如果你用租用的 We b服务器上的帐户发送邮件,那么就需 要该服务器的中继主机的名字。当邮件从中继主机并不知道的一个系统发送 过来,邮件中继主机便拒绝转发该邮件。 22.1.2 发送邮件时首先应该注意的问题 在下一节中,我们将要介绍一个新函数,即 s e n d _ m a i l,使用这个函数,你就能够用 P e r l 程序发送电子邮件。这个函数虽然非常有用,但同时它也有很大的危险性。将邮件发送给某 个人,将会在一定程序上侵犯他的隐私权。你会要求邮件的收件人在你的邮件上耗费一定的 252使用第三部分 将Perl 用于C G I 下载 图22-2 发送电子邮件时使 用的不同协议 SMTP中继 目的地你 SMTP SMTP SMTP POP或 IMAP或 SMTP时间和磁盘空间,还会要求你与收件人之间的每个系统为你中继该邮件。对于一个完全陌生 的人来说,这样做是很不合适的。 下面是你在使用P e r l或任何其他工具发送电子邮件时应该注意的问题: • 首先使用众所周知的地址(比如你自己的地址)测试你的代码并发送一些短邮件。这时, 随时都可能产生一些问题,你应该设法避免发生问题。 • 不要发送有人主动提供的商业性电子邮件。这类商业性电子邮件通常称为垃圾邮件,这 类邮件已经成为 I n t e r n e t上的一个令人头痛的大问题。少数人喜欢接收这类邮件,而其 他人的反应则不同,他们有的对垃圾邮件非常反感,有的则痛恨之极。发送此类邮件的 企业将会成为许多人唾骂的对象。当你得到一个邮件地址后,应该问一问是否可以在以 后向它发送电子邮件。如果有人要求从你的邮件地址列表中删除他的地址,那么你应该 尊重他的要求。 • 无论对方要求还是没有要求,都不要一次就发送很长的邮件,要按适当的速度来发送。 首先,你的本地邮件中继主机会因为急匆匆发送邮件而不堪重负,你的本地 I S P将会终 止你的帐户,以控制受损害的程度。其次,如果目标 I S P因为你的邮件太大而无法承受, 该I S P就会阻塞从你的域发送过来的全部邮件。如果根本无法向较大的域(如 a o l . c o m、 h o t m a i l . c o m等)发送邮件,那么你的日子一定不会好过,并且很可能使你的帐户与你的 I S P之间的联系被中断,结果造成人们对你的指控。 • 应该提供很好的返回邮件的地址,尤其是在邮件报头中要写明这个地址。应该确保你的 电子邮件的F r o m:(或Reply To:)地址正确无误,尤其是当邮件是从一台计算机发送时 更应保证地址的正确性。你可以使用 P e r l伪造电子邮件,但是伪造的邮件包含一个返回 给你的指针。伪造的邮件会使你陷入巨大的麻烦之中。 • 请始终都使用你自己的邮件中继主机。滥用其他系统的邮件中继主机会使你的帐户迅速 停用,并使你遭人指控,甚至出现更糟糕的问题。 • 不要将很长的电子邮件或者许多很短的邮件发送给靠不住的人,这称为邮件炸弹,可能 导致你的帐户被停用,并引起法律上的麻烦。 上面这些建议并非全部仅仅是一些好的网上礼仪。如果违背这些原则, I S P可能将你从它 的服务对象列表中删除掉,而且 I S P和邮件的收件人会指控你。当注册你的 I S P帐户时,I S P会 告诉你,上述原则会成为中断对你提供服务的理由,并且可能让你对系统受到的损害负责。 对于你自己的行为,应该有所约束,对于你接受他们的恩惠,不要苛求。 I n t e r n e t具有长期的记忆能力。真的发送过垃圾邮件的人将会被人们长 久记住并遭到唾骂。一旦因为发送垃圾邮件而变得臭名昭著,要想挽回名誉 是很难的。 22.2 邮件发送函数 下面各节将介绍如何编写一个 P e r l短函数,供你在C G I程序中用来发送电子邮件。不过这 里存在一个问题。该函数运行的方式主要取决于你是否拥有本地 M TA(如s e n d m a i l程序),或 者是否亲自将邮件发送到 S M T P中继主机。因此请预先考虑好,确定需要将下面的哪一节中的 第22学时 使用CGI程序发送电子邮件使用253下载函数用于你的特定程序。 22.2.1 用于UNIX系统的邮件函数 如果你拥有U N I X系统,并且s e n d m a i l可能已经配置好了(也许尚未配置好),那么你阅读 本节内容是对的。如果你没有 U N I X或s e n d m a i l,只是因为好奇而阅读本节内容,这也对你有 好处,不过,程序清单2 2 - 1中展示的函数也许对你没有多大帮助。 即使你拥有U N I X系统,下一节“用于非 U N I X系统的邮件函数”也是值 得一读的。下一节将介绍使用模块(即面向对象的模块)的新方法。 程序清单22-1 send_mail函数 第6行:sendmail 的位置和它需要的参数在这里被放到一个变量中。 s e n d m a i l程序可能位 于你的系统上的不同位置,也可以带有不同的参数。 第8行:$ s e n d m a i l中设定的s e n d m a i l程序启动并打开,以便对文件句柄 M A I L进行写入操 作。 第9 ~ 1 4行:电子邮件的报头被写入 M A I L。 第1 5 ~ 1 7行:邮件的正文被写入M A I L文件句柄。每行都附加了一个 \ n。 若要使用该函数,只要像下面这样用 4个参数调用它: 该函数的运行要求你在系统上正确安装和配置 s e n d m a i l。如果没有安装和配置,请阅读下 一节“用于非U N I X系统的邮件函数”,那里介绍的解决方案也可以在 U N I X下使用。 必须将变量 $ s e n d m a i l改为你的系统上的 s e n d m a i l程序的正确位置。它的位置通常是 / u s r / l i b,不过它也可以是 / u s r / s b i n , / l i b,或者你的系统上的任何其他目录。你必须花一点时间 才能找到它。 254使用第三部分 将Perl 用于C G I 下载如果程序的运行没有按你的期望进行,请确保你的系统上的邮件程序配 置正确。可以使用 m a i l或p i n e之类的邮件实用程序来发送测试邮件。如果这 些实用程序不能正确运行,那么说明 s e n d m a i l的安装很可能不正确。你必须 首先解决这个问题,或者使用下一节介绍的方法来运行这些实用程序。 在程序清单2 1-1中,s e n d m a i l程序是用下列选项启动的,你可以根据情况修改这些选项。 • -t 从输入数据而不是命令行中获得邮件的报头 ( F r o m、To、S u b j e c t等)。 • -oi 忽略单行程序上的“.”(圆点)。如果不使用本选项,就会中断你的邮件。 • -odq 对邮件进行排队,而不是立即将它们发送出去。如果你愿意,可以不使用本选项。 但是,如果有太多的邮件要立即发送,那么你的邮件系统将会应接不暇。使用 - o d q是一 种很礼貌的做法。 s e n d _ m a i l ( )函数的其余部分的功能是不言自明的。 22.2.2 用于非UNIX系统的邮件函数 在没有安装s e n d m a i l之类的内置M TA的Wi n d o w s和其他操作系统下,你会遇到一些复杂的 问题。M TA不是个简单的邮件传输工具,试图用几行 P e r l代码就复制它的功能,是很不容易 的事情。不过这是可能做到的。 首先,使用P e r l模块N e t : : S M T P,你可以通过P e r l运行的任何操作系统来发送邮件。使用 该模块,你就能够非常容易地发送邮件而不会遇到太大的困难。 问题是在标准的P e r l产品上并没有安装该模块。为了获得该模块,必须将它加载到 We b服 务器所在的系统上,或者加载到你想发送邮件的任何位置上。 N e t::S M T P模块是l i b n e t组件 的组成部分,它包含各种非常有用的网络模块。 L i b n e t组件位于本书所附光盘上。 本书的附录“安装模块”提供了相当详细的如何安装 P e r l模块的指南。 它讲述了如何在U N I X、Wi n d o w s和M a c i n t o s h操作系统下,安装各个P e r l模块。 此外,如果你的系统管理员没有安装模块的公用拷贝,你还会在附录中找到 如何安装模块的专用拷贝的说明。 程序清单2 2 - 2显示了用于不带M TA的操作系统的s e n d _ m a i l函数。它包含某些非常奇特的 新语句,你可能对它们不太熟悉。请务必要阅读后面的说明。 程序清单22-2 用于非M TA系统的s e n d _ m a i l函数 第22学时 使用CGI程序发送电子邮件使用255下载第5行:引入N e t : : S M T P模块,使邮件的发送稍为容易一些。 第1 0行:N e t : : S M T P对象得以创建,并与正确的中继主机相连接,该主机是你在第 9行上 设置的。 第1 3 ~ 2 3行:电子邮件的报头和正文被发送到中继主机。详细说明请参见后面的各个 N e t : : S M T P函数。 若要使用该函数,只需使用代表电子邮件各个部分的 4个参数来调用它: 这个函数令你感到奇怪的第一件事情是 $ s m t p = N e t : : S M T P - > n e w($ r e l a y);这行代码。 这行代码用于创建一个称为“对象”的东西。“对象”实际上并不是一个标量,也不是哈希结 构或者数组,它是个稍有不同的东西。 $ s m t p中的值现在代表一个到达邮件程序的连接,你可 以对这个连接进行各种操作,请将它视为一个特殊种类的值,可以用它来调用与该值相关的 函数。 你感到奇怪的下一件事情是 $ s m t p - > m a i l ( $ f r o m );这行代码。- >用于将一个对象连接到一 个对它进行调用的函数,因此, m a i l是个使用上一行创建的$ s m t p对象来调用的函数。 为了使用N e t : : S M T P模块,你并不需要理解对象语句的全部特征,只需顺便了解一下就够 了。对于N e t : : S M T P对象,可以使用的函数包括下列几个: • $smtp->mail(addr) mail函数用于指明你发送邮件时使用的是什么身份。当然,有时你 可以就你的身份问题撒点儿谎。 • $smtp->to(addr) to函数用于指明你要将邮件发送给谁。如果你调用的 t o函数带有一个名 字列表,那么每人都会收到一个邮件拷贝。这些人的名字列表不一定出现在邮件正文中, 除非你亲自将这些名字明确放入邮件正文中,比如发送 B C C。 • $smtp->data(); d a t a函数用于指明你准备发送邮件正文。 • $smtp->datasend(data) 这个函数用于发送邮件的实际文本。你必须输出你自己的报头 域(To :、F r o m:等)。报头域,比如D a t e :和R e c e i v e d :,是自动生成的。在报头与正文 之间,还必须输出一个空行— $ s m t p - > d a t a s e n d (“\ n”)。你的邮件正文跟随在这个空 行的后面,并且也用$ s m t p - > d a t a s e n d ( )来发送。 • $smtp->dataend() dataend函数用于指明你已完成邮件正文的发送,在运行这个函数之 256使用第三部分 将Perl 用于C G I 下载前,邮件并未发送。 • $smtp->quit() 本函数用于断开与S M T P服务器的连接。 22.3 从Web页发送邮件 既然你有了一个邮件发送函数 s e n d _ m a i l ( ),那么从We b页来发送邮件的其余工作就非常简 单了。只要设计一个 We b页,编写一个C G I程序与它配合运行。程序清单 2 2 - 3显示了一个电子 邮件示例的H T M L窗体。该窗体并非完美无缺,你可以随意使用自己的设计风格来改进这个 窗体。 程序清单22-3 用于发送电子邮件的H T M L窗体 用于发送邮件的C G I程序并不比它大多少。下面显示了这个 C G I程序: 第22学时 使用CGI程序发送电子邮件使用257下载在上面这个代码中的小程序中,有几个问题你应该注意。首先,必须将程序清单 2 2 - 1或 2 2-2中的s e n d _ m a i l函数插入该程序,使该程序能够运行。哪个程序清单中的函数最好,并 且适合于你,就使用该程序清单中的那个函数。 其次,注意To :地址是通过硬连线与程序相连接的,正如 We b m a s t e r @ m y h o s t . c o m的情况 那样。必须将这个地址改为你想要将邮件发送到的那个地址。该地址不是从用户那里获得的 原因很简单,因为你不希望用户使用 We b窗体将邮件发往任意的地址。如果有人滥用你的窗 体,将恶意邮件发送给某个人,那么你和你的系统将成为人们指责的目标。因此这不是个好 主意。 如果你希望用一个窗体将邮件发送到多个目的地,请使用下拉列表(或者单选按钮),为 你提供一个地址选择表: 然后,在你的程序中,使用下面这样的代码段: 无论你如何进行操作,不要让实际的 To:地址从窗体传递过来并用在你的程序中。请传 递一个没有问题的值(在上面的例子中是 1至3),并在你的C G I程序中对该值进行相应的转换, 即使看起来不可能,也要允许传递不正确的值(上面的例子中的 e l s e语句)。 核实电子邮件地址 也许你已经发现C G I程序并不试图确定用户输入的电子邮件地址是否有效。它这样做是很 有理由的,因为它无法确定该地址是否有效。 这个原因一定会使你大吃一惊。 设计I n t e r n e t上的电子邮件系统的要求之一是要能够了解目的地址是否有效。然而这是不 可能的。 困难源于本学时开头介绍的程序清单 2 2 - 1和2 2 - 2。从发送邮件系统的角度来看,它无法看 到邮件传输链的结尾环节。它必须将邮件全部传递给传输链上的第二个系统,第二个系统又 将邮件传递给第三个系统,以此类推。这些“传递”过程的延迟时间是很重要的,更重要的 是,发送邮件的系统在将邮件送出去后就无法控制邮件了。 标准的解决办法是设法清除掉显然无效的地址,无法确定是否有效的地址则属例外。电 子邮件地址的 I n t e r n e t标准(R F C - 8 2 2)有一个标准电子邮件地址的模板。但是,有些符合 R F C - 8 2 2标准的有效地址实际上是无效的,而有些不符合 R F C - 8 2 2标准的地址却是有效的、可 以传递邮件的地址。 258使用第三部分 将Perl 用于C G I 下载编写对电子邮件地址进行匹配的正则表达式是不行的。例如,表达式 / ^ [ \ w. - ] + \ @ ( [ \ w, - ] \ . ) + \ w + $ /看上去是可行的,它甚至与 m e @ s o m e w h e r e . c o m这个地址相匹配。但是,它拒绝下 面这个完全有效的电子邮件地址: 与符合R F C - 8 2 2标准的电子邮件地址相匹配的一个正则表达式长达 4 7 0 0个字符,因为太 长,所以本书没有将它列出,你也很难键入。同时它也无法与 I n t e r n e t上的每个传输邮件的地 址相匹配。 那么究竟怎么办呢? 若要确定电子邮件地址是否有效,唯一的办法是将一个邮件发送到该地址,然后等待对 方的答复。如果由于某个原因,你希望确保对方地址上有人(比如将来将邮件发送给他,因 为他要求发送),请发送一个电子邮件,要求他回答。当对方的答复返回时,就知道你发送了 一份有效的电子邮件。 22.4 课时小结 在本学时中,我们介绍了如何从 We b页发送电子邮件。同时,介绍了 s e n d _ m a i l ( )函数的两 个版本,它们可以用在任何 P e r l程序中来发送电子邮件。我们还讲述了 I n t e r n e t电子邮件的基 础知识以及基本的电子邮件礼仪。 22.5 课外作业 22.5.1 专家答疑 问题:能不能使用从浏览器中搜集到的信息来获取 We b冲浪者的电子邮件地址? 解答:虽然能够这样做看起来是很好的(它可以消除获取电子邮件地址时的错误),但这 是不可能的。浏览器并不包含用户的电子邮件地址。 C G I模块中的r e m o t e _ h o s t函数返回的值 实际上并不是用户接收电子邮时使用的地址。如果你使用安全的 We b事务处理,那么 r e m o t e _ u s e r函数也许不是用户的电子邮件地址中的“名字”部分。同时请记住,浏览器可能 提供某些不准确的此类信息, N e t s c a p e和Internet Explorer的某些插件也会这样说谎。 另外,用户可能使用图书馆、朋友家、办公室或网吧中的 We b浏览器,因此浏览器的地 址甚至与用户的电子邮件地址并无关系。 问题:我能核实电子邮件地址吗? 解答:你可以试试。例如,大多数最新的电子邮件地址包含 @ ( a t符号),你可以用它进行 测试。但是,本地计算机(例如 p o s t m a s t e r、r o o t)上的计算机不需要@。 问题:我试着运行C G I电子邮件程序,但在消息中出现“From nobody⋯⋯(来自无人⋯⋯)” 这行文字,为什么? 解答:是这样的:s e n d m a i l程序记录了电子邮件发送者的用户 I D。实际上,电子邮件的发 送“人”是We b服务器本身。 We b服务器常常以一个特殊用户 I D— n o b o b y、We b、h t t p d或 r o o t来运行,该地址记录在电子邮件报头中。不必担心,只要你输出一个正确的 F r o m :行,作 第22学时 使用CGI程序发送电子邮件使用259下载为邮件报头的一部分,当用户答复该邮件时,那么这就是你看到的一行信息。 问题:我应该如何将文件附加给电子邮件消息? 解答:你应该查看C PA N中的M I M E模块。 22.5.2 思考题 1) $foo=Net::SMTP->new(‘m a i l h o s t’)这个模块有何功能? (如果你没有阅读“用于非 U N I X系统的邮件函数”这一节,请现在阅读。) a .它会产生一个句法错误。 b .它创建一个对象,称为$ f o o,代表与S M T P邮件服务器的连接。 c .它将N e t : : S M T P模块纳入当前程序之中。 2) 下面几个电子邮件地址中哪一个可能是无效地址? a . f o o ! b a r ! b a z ! q u u x b .“”@ b a r. c o m c . s t u ff % j u n k !“Wo w z e r s”! f o o . c o m ! b l a t 22.5.3 解答 1) 答案是b。如果你回答是a,那么可能出现了键入错误,也可能运行了 Perl 4。选择c是 不正确的,因为它实际上描述的语句是 use Net::SMTP。 2) 这是个巧妙的问题。这几个地址都可能是有效的电子邮件地址。 22.5.4 实习 • 对简单的C G I电子邮件程序进行下列简单的修改: • 搜集用户的浏览器信息,将它附加给邮件正文。 • 给用户发送一个礼仪邮件拷贝(如果你在真实的 We b站点上发送这样的邮件,请务必告 诉他这一情况)。这样做时你也要小心,因为有人不喜欢这样的邮件。 • 让用户在发送邮件之前能够“预览”邮件。必须使用第 1 9学时中介绍的方法之一,使第 一页(电子邮件输入屏)中的数据可供第二页(电子邮件核实屏幕)使用,最后供邮件 发送程序使用。 260使用第三部分 将Perl 用于C G I 下载下载 第2 3学时 服务器推送和访问次数计数器 在本学时中,我们将要讲述两个常用的 C G I编程方法。你可以使用这些方法制作更加有趣 的We b页,使之具备初步的动画功能,或者让人们竞相使用。 在本学时中你要学习: • 使用服务器推送方法来刷新 We b页。 • 访问次数计数器。 • 代理和缓存。 23.1 什么是服务器推送 在传统的We b页中,加载速度慢或者不断增大的文档是很难使用的,因为 We b页只能一次 看一页。比如,运行C G I程序的We b页需要花费很长的时间来运行。 首先,浏览器为了等待C G I程序结束运行,可能会超过原定的时间。浏览器为了等待程序 运行的结果,通常等9 0秒钟左右,然后显示了一条消息,声称无法访问该站点。 其次,C G I程序有时会输出一条消息说:“I’m still working, 20%complete (我仍在运行, 已完成1 0%)”,过一会儿又说:“I’m still working ,20% complete(我仍在运行,已完成2 0%)”, 等等。输出这些消息是好的,问题是这些消息并不按固定间隔出现(因为缓存的缘故),当你 完成程序的运行时,会有一个很长很长的 We b页。 你希望的是浏览器显示如图 2 3 - 1所示的信息。 服务器推送技术利用了这样一个特性,即浏览器能够按各个部分来接收 We b页,然后依 次重新显示这些We b页,就像你是依次取出不同的 We b页一样。 在撰写本书时, M i c r o s o f t的I n t e r n et Explorer并不支持执行服务器推送 技术所需的协议。这是很遗憾的,因为使用服务器推送技术是使 We b页的 内容动起来的一种简便方法。对于需要支持 Internet Explorer或者不支持这 个特性的其他浏览器的 We b页来说,你应该使用客户机拖拉之类的其他技 术。 图23-1 浏览器显示进度递 增的信息 全部 完成!23.1.1 激活服务器推送特性 你的We b服务器必须正确地安装,以便激活服务器的推送特性。为此,必须将 C G I程序作 为未分析标题(nonparsed header)CGI程序来运行。当你使用未分析标题 C G I程序时,服务器并 不要求输出C G I标题,数据应该按原始状态直接发送给浏览器。通常来说, We b服务器要检查 来自C G I程序的输出,以确保它们的正确性,因此,当 C G I程序运行失败时,便出现错误 5 0 0。 未分析标题C G I程序将它们的输出直接发送到浏览器,如图 2 3 - 2所示。 你如何运行C G I程序以及服务器如何对标题不进行检查,这取决于 We b服务器本身。例如, 如果是Apache We b服务器,那么在 C G I程序的文件名前面加上前缀 n p h -,就能使程序作为未 分析标题程序来运行。例如, p u s h . c g i是个已分析标题C G I程序,n p h - p u s h . c g i便是未分析标题 C G I程序。但是We b服务器管理员可以修改这个命名规则的运行方式。 在M i c r o s o f t的I n t e r n e t信息服务器(Internet Information Se r v e r, IIS)下,所有C G I程序都 是作为未分析标题程序来运行的。 C G I模块的h e a d e r函数通常向你隐藏了这个情况,因此,在 I I S下不必为服务器推送特性作任何修改。 如果你不清楚如何运行带有未分析标题的 C G I程序,可以查看We b服务器的文档资料,或 者求助于你的系统管理员。 23.1.2 一个小例子:更新Web页上的时钟 服务器推送技术的第一个例子是:编写一个简单的程序,以便更新 We b页上的时钟。该 时钟的运行方式是:让 We b服务器每隔5秒钟左右推送出一个 We b页,而We b页上将显示新的 时间。We b服务器将不断推送出新的时间,直到浏览器删除该页,或者用户点击浏览器的 S t o p 按钮,停止加载该页为止。 该C G I模块包含一组函数,目的是使服务器的推送操作比较容易一些。服务器推送的 We b 页也称为多部分文档。 程序清单2 3 - 1包含了H T M L时钟的源代码。你必须键入该代码,然后用一个名字保存该代 码,使We b服务器能够像上一节介绍的那样,将该程序作为未分析标题 C G I程序来运行。 程序清单23-1 HTML时钟的源代码 图23-2 未分析数据不经过 检查就通过了服务 器 Web服务器 至浏览器 未分析标题 已分析标题 Web服务器 262使用第三部分 将Perl 用于C G I 下载第4行:当C G I模块加载时,你必须指明正在执行服务器推送操作,因此这一行代码将命 令: p u s h赋予该C G I模块。另外,当你编写未分析标题脚本程序时,必须使用 - n p h将这个情况 通知C G I模块。 第8行:m u l t i p a r t _ i n i t负责告诉浏览器,它后随的是个多部分We b页。它输出的是m u l t i p a r t _ i n i t,而不是通常用在普通We b页上的h e a d e r函数。 第9行:w h i l e ( 1 )能够有效地创建一个 w h i l e循环,该循环将永远重复运行下去,这种循环 称为无限循环。 第1 0行:m u l t i p a r t _ s t a r t给要刷新的We b页的开始做上标号。如果一个 We b页已经显示,本 行代码将使浏览器清除该We b页,并等待接收新的内容。 第11行:这一行代码用于指明该页的内容。第 4学时我们曾经讲过,标量上下文中的 l o c a l t i m e用于输出格式为“Sun Sep 5 15:15:30 1999”的时间。 第1 2行:m u l t i p a r t _ e n d用于给要刷新的We b页的结尾做上标号。本行代码只应该后随另一 个m u l t i p a r t _ s t a r t或者程序的结尾。 请注意w h i l e循环如何给m u l t i p a r t _ i n i t和m u l t i p a r t _ e n d函数加上方括号。该循环能够有效地 一次又一次重复显示同一个 We b页,只有We b页上的时间是变化的。 23.1.3 另一个例子:动画 在程序清单 2 3 - 2中显示的下一个例子(与上一个例子非常相似)中,显示了来自目录 / i m a g e s的一连串图形。这些图形使用服务器推送方法每次显示一个图形。目录中的每个文件 被读取,并且作为一连串推送的 We b页显示在浏览器中。 程序清单23-2 用服务器推送方法实现的动画 第23学时 服务器推送和访问次数计数器使用263下载程序清单2 3 - 2中的程序,大部分都与第 2 0学时中介绍的“当日图形”和程序清单 2 3 - 1的例 子非常相似。 需要介绍的重要代码是第 1 6行,即m u l t i p a r t _ s t a r t ( - t y p e = >‘i m a g e / j p e g’),它用于指明 C G I程序并不在连续的 We b页上输出普通文本或 H T M L输出信息,而是输出 J P G图形。为了执 行动画操作,该程序既可以直接输出 J P E G,也可以输出包含<IMG SRC>标号的H T M L。 23.1.4 客户机拖拉技术 使We b页依次加载的另一种技术称为客户机拖拉。使用客户机拖拉技术时, H T M L中嵌入 了一些标记,以便告诉浏览器在一个间隔时间之后重新加载 We b页(或另一个 U R L)。例如, 下面这个在We b页的<H E A D>节中的H T M L 将使浏览器在6秒钟后加载We b页h t t p : / / f o o . b a r. c o m。C G I模块直接支持客户机拖拉命令。 当We b页的标题输出时,你可以设定应该重新加载的 We b页,或者加载另一个 We b页取代 它的位置,方法是使用C G I模块的h e a d e r函数的- R e f r e s h选项,如下所示: 客户机拖拉方法实际上用于加载两次“刷新”之间的一个完整新页。这意味着如果你的 We b页需要依次显示,比如像幻灯片那样来显示,就必须使用 U R L中嵌入的c o o k i e或参数来跟 踪下面显示哪个 We b页。在两次刷新之间,必须在服务器与客户机之间建立一个新的连接, 并且We b服务器必须为每次刷新启动一个新的 P e r l程序。这意味着不能太频繁地进行 We b页的 刷新。 使用客户机拖拉方法和服务器推送方法时遇到的主要问题是: • 有些浏览器(比如Internet Explorer)不支持服务器推送技术。 • 有些浏览器不支持客户机拖拉方法。 无论你用何种方法来编写你的程序,一种浏览器与另一种浏览器之间有些特性是互不兼 容的。你必须决定你将容许存在哪些错误,并且根据情况编写代码。 23.2 访问次数计数器 在We b页上,你常常会看到一种称为访问次数计数器即访问者数量指示器的东西。可以 想像,它表示We b页已被人们访问了多少次。图 2 3-3显示了一个计数器的例子。 访问次数计数器有许多问题值得注意。首要的问题是计数器中的数字表示什么意思。“访 问次数”的数目很大,表示访问这个 We b页的人很多。如果访问的人很多,是否表示这是个 很好的We b页呢?不一定。如果你访问一个 We b页,这个We b页中可能有你需要的信息,也可 能没有你要的信息。 We b页的质量好坏在于它是否含有对你有价值的信息,而不在于对其他 人是否有价值。 264使用第三部分 将Perl 用于C G I 下载实际上访问次数计数器是一种瞎子当裁判的选美比赛。计数器中的数字不一定是访问你 的We b页的人数,它最多只不过是一个很不准确的估计数字。为什么这些计数器如此不准确 呢?我想有下列几个原因。 首先,没有一条规定说,访问次数计数器必须从 0开始计数。当给你发放最后一本支票簿 时,第一张支票是1号支票吗?当你给支票排序时,完全可以选择你的起始序号。如果你很聪 明,你会选择一个大号码,这样,你看上去在银行中开立了一个长期帐户。如果支票号码很 小,那么商店服务员一定会对你的 I D号码多看两眼,并且也许根本不会考虑接收你的支票。 We b站点操作员常常在开始时将访问次数计数器的数字设置得比较大,使他们的 We b点看上去 比实际上更“受欢迎”。 访问次数计数器存在的第二个问题是 We b机器人程序,也叫做 We b蜘蛛、We b爬虫等。这 些自动化进程能够搜索We b上的数据,有时只是为了查看一组特定的数据,有时为了建立感兴 趣的We b站点的索引。你是否想过为什么 A l t a Vi s t a、G o o g l e或H o t B o t要建立它们的索引呢?它 们搜索We b,检索We b页,最后访问次数计数器的数字升高到比它们的实际访问次数高。 第三个问题是 We b浏览器上的R e f r e s h(刷新)按钮。每次在你的 We b页被刷新时,访问 次数计数器就会升高一格。如果有人点击重新加载按钮,实际上你并没有计算你的 We b站点 的“访问者”数目,是不是? 最后也是最重要的一个问题是缓存问题。在第 1 7学时中,我们介绍了浏览器如何与 We b 服务器进行通信的方块图。它展示了一个重要细节,如图 2 3 - 4所示。 第23学时 服务器推送和访问次数计数器使用265下载 图23-3 访问次数计数器的 举例 图23-4 代理服务器为浏览 器检索We b页 Web服务器 文档A 代理 文档A 文档A如果We b浏览器位于大型I S P如a o l . c o m或h o m e . c o m等的域中,这些I S P拥有数百万个用户, 那么这些I S P通常要使用缓存代理。缓存代理位于你的 We b浏览器与We b服务器之间。当你检 索一个We b页时,检索请求先送到缓存代理那里,然后由缓存代理为你在 I n t e r n e t上取出该 We b页,再将该We b页发送给你的浏览器,此前它要为自己将该 We b页的拷贝存储起来(见图 2 3 - 5)。如果同一个域中的另一个人想要检索该 We b页,缓存代理就不必到 I n t e r n e t上去检索这 一页,它可以使用保存在缓存中的拷贝。 存储We b页拷贝的代理服务器人为地降低了访问次数计数器指示的访问次数。奇怪的是, 它还使r e m o t e _ h o s t值多次重复,因为该We b页被许多人所检索。 大公司和大学中的 We b冲浪者常常位于作为缓存代理的防火墙的后面。 从这些站点之一检索的每一页都有可能没有计入访问次数计数器,因为它被 缓存代理挡住了。 23.2.1 编写一个访问次数计数器程序 阅读了上一节内容后,如果你接着读下去,一定有兴趣为你的 We b页编写一个访问次数 计数器程序。访问次数计数器有两个基本类型,一种是简单的文本计数器,另一种是图形计 数器。下面介绍的第一个计数器例子是文本计数器,第二个计数器是图形计数器,并且我们 还讲述制作非常出色的访问次数计数器的一些思路。 若要使用该访问次数计数器,请将它用作服务器端的包含程序的一部分,我们在第 2 0学 时中已经讲过这种包含程序。如果你调用访问次数计数器 C G I程序h i t s . c g i,可以使用下面这 个S S I,将它纳入任何We b页: 程序清单2 3 - 3显示了该访问次数计数器的源代码。 程序清单23-3 访问次数计数器程序 266使用第三部分 将Perl 用于C G I 下载 图23-5 代理服务器从它的 缓存中检索We b页 Web服务器 代理 文档A 文档A第1 8行:这里需要一个锁,因为访问次数计数器文件可能被许多进程同时读取和写入。 第2 0 ~ 2 3行:$ c o u n t e r f i l e中的文件内容被读取。它是迄今为止的访问次数。 第2 8 ~ 3 0行:访问次数计数器的内容被重新写回到 $ c o u n t e r f i l e中的文件。 第3 2行:最后,锁被释放。 程序清单2 3 - 3中的大部分代码你并不会感到有什么特殊。但是请注意,它使用了文件锁, 并且这个示例程序遵循第1 5学时中介绍的文件锁定原则。 当两个人几乎同时加载We b页时,就有必要对文件加锁。如果对 We b访问次数计数器文件 的读取和写入操作稍稍失去同步,那么计数器的数字就会增加得太快或太慢,也可能会产生 一个受到破坏的文件。这些结果将会进一步降低计数器的准确性。 23.2.2 图形访问次数计数器 若要改进访问次数计数器,可以采取 3种不同的方法。首先,可以制作一个图形,代表计 数器的每个可能的值,并且根据需要来显示该图形。如果你接到多个访问者对 We b站点的访 问请求,那么这种办法比较费时。 第二种方法是让一个 Perl CGI程序生成必要的图形,以便显示访问次数计数器本身。 C PA N中的G D模块可以用于以P e r l程序来创建图形,因此你可以将它用于这个目的。不过关于 G D模块的具体特性不在本书讲解的范围之内。 最容易的方法是创建 1 0个图形,分别代表 0至9的1 0个数字。然后,当计数器中的数字递 增时,你的程序只需输出带有< I M G>标记的H T M L,将数字放入正确的位置(见图 2 3 - 6)。 当然,必须创建代表数字的图形。程序清单 2 3 - 4中的Perl CGI程序将图形命名为 d i g i t _ 0 . j p g, d i g i t _ 1 . j p g,直至d i g i t _ 9 . j p g。 若要使用图形访问次数计数器,可以将它用作服务端的包含程序的一部分,如第 2 0学时 中描述的那样。如果你调用访问次数计数器的 C G I程序g r a p h i c a l _ h i t s . c g i,你就可以将它纳入 任何We b页,如下所示: 第23学时 服务器推送和访问次数计数器使用267下载程序清单2 3 - 4显示了该图形访问次数计数器程序的源代码。 程序清单23-4 图形访问次数计数器的程序 程序清单2 3 - 4实际上与程序清单2 3 - 3相同,只有某些很小的修改。 第9行:这行代码在 $ i m a g e _ u r l中包含了构成数字的各个图形的基本 U R L。请记住,它必 须是浏览器加载图形时查看的 U R L,而不是到达本地磁盘上的图形的路径。 第3 4 ~ 3 5行:访问次数计数器中的数字 $ h i t s对每个字符进行分割,再赋予 $ d i g i t,每次赋 予一个数字。然后为每个数字输出< I M G>标号。 23.3 课时小结 在本学时中,我们讲述了在 We b页上实现动画的两种方法。可以使用服务器推送技术, 268使用第三部分 将Perl 用于C G I 下载 图23-6 图形访问次数计数 器的输出迫使浏览器连续更新 We b页。如果这种方法不行,或者在某种浏览器上无法实现,可以使用 客户机拖拉技术,获得类似的效果。然后介绍了访问次数计数器,并且说明了计数器为什么 不那么准确。 23.4 课外作业 23.4.1 专家答疑 问题:服务器推送技术不起作用,为什么? 解答:许多因素会导致服务器推送技术不起作用。首先,浏览器必须支持服务器推送技 术;第二,We b服务器必须使用未分析标题;最后,你的 C G I程序必须正确地运行。如果从命 令行提示符处以交互方式运行 C G I程序,应该确保它的输出按固定时间间隔产生,并且输出必 须正确。 问题:如果访问次数计数器很不准确,有没有别的办法可以用来测定对 We b站点的访问 次数? 解答:几乎没有。查看服务器日志与使用访问次数计数器同样不可靠。测定访问站点次 数的方法之一是使用实现重定向(参见第 2 0学时)的点击链接,另一种方法是让访问者填写 一个窗体。使用H T M L窗体中的P O S T方法,是避免你的 We b页被缓存代理进行缓存的惟一完 全可靠的方法,它可以确保 P O S T方法提交的窗体不能被任何代理进行缓存。 23.4.2 思考题 1) 为了执行服务器推送操作,需要使用 C G I模块中的哪些函数? a . m u l t i p a r t _ s t a r t和m u l t i p a r t _ e n d b . m u l t i p a r t _ i n i t , m u l t i p a r t _ s t a r t和m u l t i p a r t _ e n d c . p u s h _ s t a r t和p u s h _ e n d 2) 所有浏览器都支持客户机拖拉技术,因为它是 H T M L标准的组成部分。 a .是。 b .否。 3) 缓存代理能够保证不对何种类型的 We b页进行缓存? a .来自使用P O S T方法的H T M L窗体的答复页。 b .服务器推送的内容。 c . C G I程序的任何输出。 23.4.3 解答 1) 答案是b。m u l t i p a r t _ i n i t用于使浏览器准备接收多部分构成的 We b页。m u l t i p a r t _ s t a r t和 m u l t i p a r t _ e n d用于给每个We b页做上开始和结束标记。 2) 答案是b。完全不是。<M E TA>标记可以被浏览器忽略,这是 H T M L标准规定的。另 外,使用h e a d e r函数中的- R e f r e s h选项并不能保证浏览器按需要重新加载 We b页,浏览器也有 一个该命令的选项。 第23学时 服务器推送和访问次数计数器使用269下载3) 答案是a。原因已经在“专家答疑”这一节中作了解释。 23.4.4 实习 • 修改程序清单2 3 - 3(或程序清单2 3 - 4)中的访问次数计数器程序,为不同类型的浏览器 保存不同的计数。为此,你必须为你要跟踪的每种浏览器建立不同的文件。不要忘记为 你不能识别其身份的浏览器保留一个额外的文件。 270使用第三部分 将Perl 用于C G I 下载下载 第2 4学时 建立交互式We b站点 如果你在We b站点上使用C G I程序的目的是使访问者能够进入你的站点,并且为他们提供 一个可以访问的有价值的站点,那么除了建立访问计数器外,还必须设计一个更好的站点。 We b上最有价值的站点是能够提供频繁更新内容的站点。如果你的 We b页上的信息是静态 的,人们就没有理由再次访问它。经过几次访问后,他们就会知道你的站点没有太大的变化, 因此不会再来访问。 要使人们重访你的We b站点,方法之一是让他们在某种程度上参与站点的活动。人们作为一 个群体,喜欢互相交谈。人们希望成为一个群体的成员,并且具有参与感,这是人的一个共性。 本学时中介绍的程序提供了一个进行每项操作的工具。第一个程序提供了一种方法,用 于扫描I n t e r n e t上的另一个信息源的内容,改变该内容的格式,并且在你的站点上显示这些内 容,同时加上一些警告。第二个程序使得你的站点的访问者可以参与一个调查活动。 在本学时中,你将要学习: • 如何借用另一个We b站点的内容。 • 如何创建一个交互式民意测验站点。 24.1 借用另一个站点的内容 也许你看到过这样的We b站点,在We b页的某个位置显示了最新的股票行情信息、新闻标 题或体育比赛的比分,尽管运行该站点的人与发布这些内容的机构并无关系。 出现这种情况的通常原因是:在你查看的系统上运行的程序常常可以访问信息的原始站 点,并将信息拖拉过来,然后改变信息的格式,显示在目标 We b页上。图2 4 - 1显示了这个进程 的运行情况。 这里你会看到你的 We b服务器通过它的C G I程序,已经变成一个 We b客户机。当服务器为 自己检索We b页时,它能将这些页连接在一起,然后再次显示该信息。 24.1.1 注意内容的版权问题 在你继续读下去,了解如何借用其他站点的内容之前,有几个问题必须了解。首先,如 图24-1 取出一个 We b页, 改变其格式,然后 将它重新显示 粘贴在一起的新Web 页 你的Web 服务器 远程Web 服务器 Original Material果你要显示的信息不是你自己的信息,而是来自另一个 We b站点或数据库的信息,那么这些 信息也许会受到版权法的保护。从另一个 We b站点那里借用信息,然后在你自己站点上显示, 这会使你陷入严重的法律争端。如果触犯版权法,可能导致你的 We b站点和I S P被关闭,你可 能被罚款、监禁或者受到法律指控。 触犯版权法也是一种粗鲁的行为。 如果你想在自己的 We b站点上使用其他来源的信息,首先要获得其他信息源的许可。大 多数We b站点运营商允许你显示来自他们站点的内容。他们通常要求你考虑下列几个问题: • 你应该清楚地注明内容的来源,可以用标题、链接和文字来注明。 • 你应该清楚地注明内容的版权,说明该内容是经过允许而使用的。 • 也许你无权通过“深层次链接”访问他们的 We b站点,也就是说通过几个层次的链接访 问他们站点的We b页。他们希望你只链接到顶层的 We b页。 • 你只能偶尔更新你的 We b页版本。将其他站点的服务器隐藏在信息之下,以便使你的站 点看上去更加出色,这种做法是不可取的。 S l a s h d o t . o rg是个为技术用户提供内容的 We b站点,它的经营者允许我们使用他们的站点 来演示本书中的示例代码。当你在自己的 We b页上运行这些示例代码之前,应该征得该站点 的同意。如果要与 S l a s h d o t . o rg取得联系,你可以在 h t t p : / / w w w. s l a s h d o t . o rg上通过他们的We b 站点和FA Q找到有关的详细说明。 24.1.2 举例:检索标题 如果要在你的We b站点上显示S l a s h d o t站点的新闻标题,请按下列步骤操作: 1) 通过服务器分析的HTML We b页,启动headlines.cgi CGI程序。 2) 然后该C G I程序查看它是否拥有存储在磁盘上的最新新闻标题拷贝。如果有,就使用 之。如果没有,便从S l a s h d o t . o rg的We b站点检索这些标题。 3) 接着C G I程序分析标题文件并显示标题。 若要从另一个 We b站点中检索 We b页或其他内容,需要一个模块,它不是标准 P e r l模块 LW P : : S i m p l e的组成部分。LW P模块使你能够从I n t e r n e t上检索所有类型的信息,比如 We b页、 F T P数据、新闻组文章等。 LW P::S i m p l e模块被封装为 l i b w w w - p e r l模块包的一部分。这个模块包 包含了许多个模块,分别用于检索 We b页、分析H T M L、分析U R L,遍历We b 站点,还可以做许多其他事情。使用这些模块的好处是它们的安装是非常值 得的。L i b w w w - p e r l模块包位于本书所附的光盘上。 LW P::S i m p l e模块一旦安装,你就可以像下面这样检索 We b页: 现在$ c o n t e n t包含了该U R L上的We b页的文本。这不是非常容易吗? 程序清单2 4 - 1到2 4 - 3展示了检索Sl a s h d o t的标题并显示这些标题时使用的程序。 程序清单24-1 Slashdot的标题程序的第一部分 272使用第三部分 将Perl 用于C G I 下载程序清单24-2 Slashdot的标题程序的第二部分 程序清单24-3 Slashdot的标题程序的第三部分 第3 ~ 6行:为了编写本程序,需要许多不同的模块。应该使用模块 F c n t l,因为你必须锁定 该程序的一部分,这样每次就只能由一个用户来运行该程序。必须使用 LW P : : S i m p l e模块(尤 第24学时 建立交互式Web 站点使用273下载其是g e t函数),以便从S l a s h d o t的We b站点检索新闻标题。当然,你也需要 C G I模块,因为这 是个C G I程序。 第8行:它包含文件的U R L,该文件只包含新闻标题。文件的格式类似下面的形式: 这里的每条新闻都用< s t o r y>标记括了起来。该文件采用称为 X M L标记语言的某种简化 形式。这使得P e r l程序能够很容易地处理该文件,你在下面可以看到这一点。 第9行的变量$ c a c h e包含了你临时存放 S l a s h d o t标题所用文件的名字。使用该文件后,每 当程序被调用时,你就不必访问 S l a s h d o t的服务器来检索信息,因为你拥有一个本地拷贝。 当然,现在你应该熟悉g e t _ l o c k ( )和r e l e a s e _ l o c k ( )这两个子例程,因为你已经在另外 3个学 时中看到过这些函数。之所以需要使用这些函数,原因是 $ c a c h e中的文件不应该每次都被多 个程序更新,因此它必须锁定。 第2 3行:如果缓存文件不存在,请找出该文件,如果缓存文件已经存在 6 0分钟以上,就 应该重建该文件。P e r l中的- M函数返回P e r l程序启动以来该文件的修改时间,但返回的时间采 用小数表示的格式。因此,如果该文件已经存在了一天, - M函数返回1,如果文件存在了 6个 小时,- M返回0.25 (四分之一天),如果文件存在了1小时,- M返回0 . 0 4 1 6 6 6 6 (约1/2 4天)。 第2 4行:用于检索包含标题的U R L,如前面介绍的LW P::S i m p l e模块的g e t函数。下面几 行用于将被检索的$ d o c中的文档写入缓存文件。如果 g e t方法运行失败,它返回u n d e f,并且在 第2 5行中对此进行检查。 请注意,g e t _ l o c k ( )和r e l e a s e _ l o c k ( )函数序列位于 i f语句的外面。这一点非常重要。如果 C G I程序的一个实例正忙于更新缓存文件,你就不需要另一个实例来查看缓存文件是否存在, 或者上次它是何时被修改的。 该程序的最后一部分最简单明了。第 3 3 ~ 3 5行用于输出简介和上次更新缓存文件的时间。 第3 4行有点儿复杂,我们对此作一说明。首先, s t a t用于获取关于$ c a c h e中文件的信息,并将 它作为一个列表返回。第二,列表的第 9个元素(上次修改时间)被取出。第三,用该时间用 274使用第三部分 将Perl 用于C G I 下载 图24-2 S l a s h d o t . c g i程序 的输出l o c a l t i m e,在标量上下文中,它返回格式很好的时间字符串。 第4 0和4 3行用于从S l a s h d o t中取出标题文件的<t i t l e>和< u r l >节。与标题和U R L相匹配的 部分由正则表达式保存在 $ 1中,然后分别赋予$ t i t l e和$ l i n k。由于< u r l >元素总是出现在< t i t l e > 元素之后,因此,当< u r l >元素被看到时,$ t i t l e和$ l i n k均可在第4 5行上输出。 在一般情况下,这些正则表达式不应该用来与 H T M L相匹配。它们在这里起作用,原因 是S l a s h d o t的X M L标题文件格式化很好,每一行只有一个 X M L元素。如果文件格式要变更, 该程序无法进行处理,你应该查看 S l a s h d o t的FA Q,以了解什么发生了变化。 当程序最后运行起来时,其输出将类似图 2 4 - 2显示的形式。 当然,你应该运用你的H T M L技巧使这个输出更加漂亮一些。 24.2 调查窗体 人人都希望成为某个重要人物。人人都希望他的观点能够引起人们的重视,甚至成为一 个非常重要的观点,而且每个人都希望知道他的观点与别人的观点相比较的结果。这就是调 查要达到的目的。 在下面这个练习中,我们将要介绍一个小程序,用来创建一个调查窗体,然后再创建一 个用于输出调查结果的程序。调查程序是个文本文件,它包含一个问题,后面是一些选项, 该文件被放入We b服务器上的一个目录下,其名字带有扩展名 . t x t。该文本文件类似下面的形 式,但是没有其他标点符号或空行: 第一个程序用于查看该目录,找出带有扩展名 . t x t的文件(如果存在多个文件,则取其最 后一个文件),然后将问题作为窗体的一部分来显示,如图 2 4 - 3所示。 使用纯文本文件的优点是: C G I程序可以运用文本文件来显示问题,并在以后显示问题的 答复。如果你想增加新的调查文件,可以将另一个 . t x t文件添加到该目录中, C G I程序能够自 动开始使用该文件。它不需要进行很多的维护。 第24学时 建立交互式Web 站点使用275下载 图24-3 调查窗体当用户选定一个选项且提交该窗体时,第二个 C G I程序便取出问题的答复,并将它写入与 问题在同一个目录中的一个文件中。如果问题文件称为 f o o . t x t,那么答复将存放在 f o o . a n s w e r 文件中。当程序写完答复后,它将重新读取所有的答复,并且显示调查结果。 24.2.1 调查窗体程序的第一部分:提出问题 在这个调查中提出问题的程序是非常简单明了的。比较复杂的部分是遍历存放调查信息 的目录,找出目录中最后一个扩展名为 . t x t的文件。实际上,由于提出问题和写出调查结果的 这两个程序都需要查找该文件,因此可以将这部分程序编写成可以重复使用的函数,这样, 你就可以在两个地方使用它了。程序清单2 4 - 4显示了调查窗体程序的第一部分。 程序清单24-4 显示调查窗体的程序的第一部分 在第6行上,$ s u r v e y _ d i r包含了调查文件所在的目录。若要创建一个新调查文件,只要将 一个扩展名为. t x t的文本文件放入该目录即可,如本节开头介绍的那样进行操作。该目录必须 是可供We b服务器的进程写入的目录。能够写入的目录意味着至少拥有 7 5 5(在U N I X系统中) 访问权,或者拥有声明的客户写入权限(在 Wi n d o w s下)。 函数f i n d _ l a s t _ f i l e ( )根据扩展名. t x t或. a n s w e r,在$ s u r v e y _ d i r中按字母顺序寻找带有该扩展 名的最后一个文件。这个通用型函数在以后供 g e t _ f i l e _ c o n t e n t s ( )函数使用,并且用于下一节 276使用第三部分 将Perl 用于C G I 下载中的调查写入程序。如果目录中不存在该类型的任何文件,那么 f i n d _ l a s t _ f i l e ( )返回u n d e f。 函数g e t _ f i l e _ c o n t e n t s ( )再次将扩展名. t x t或. a n s w e r作为参数,返回该目录中最后一个文件 的内容。为了找到该文件名,它使用 f i n d _ l a s t _ f i l e ( )函数。 程序清单2 4 - 5中显示的该程序的剩余部分是很短的。 程序清单2 4-5 显示调查窗体程序的第二部分 当这个代码从第3 6行上开始运行时,函数 g e t _ f i l e _ c o n t e n t s ( )将来自最后一个. t x t文件的内 容的第一行加载到$ q u e s t i o n中,将文件的其余部分加载到 @ a n s w e r s中。 必须将第4 0行中的/ c g i / w r i t e s u r v e y. c g i改为用于C G I调查程序第二部分的任何一个名字。 从那里开始,输出标题,该窗体的开始部分被发送到浏览器。 @ a n s w e r s中的每一行输出 时旁边都有一个单选按钮。第一个答复/单选按钮的值为 0,第二个的值为 1,以此类推,直 到@ a n s w e r s中不再遗留任何答复为止。窗体的正文如下所示: 当该窗体被提交时,a n s w e r的参数被传递给负责写出答复的 C G I程序,该程序称为第4 0行 中的/ c g i / w r i t e s u r v e y. c g i,但是你可以修改这个名字。该程序在下一节中介绍。 24.2.2 调查窗体程序的第二部分:计算调查结果 当用户点击调查窗体上的 S u b m i t按钮后,实际程序就开始运行了。用户的选择被记录到 一个文件中,调查结果必须制成表格,然后显示出来。 下面这个程序清单看上去很长,但是它的主体部分是你在以前已经见过的子例程。你在 本书中的许多地方见过的文件锁定子例程 g e t _ l o c k ( )和r e l e a s e _ l o c k ( ),以及显示调查窗体程序 中的g e t _ f i l e _ c o n t e n t s ( )和f i n d _ l a s t _ f i l e ( )子例程,构成了这个C G I程序的主体。 程序清单2 4 - 6中的代码是该程序的开始部分。请记住,由于你在以前已经看到过该程序 的大部分代码,因此不应该对它的长度感到害怕。 程序清单24-6 接收调查结果的程序的第一部分 第24学时 建立交互式Web 站点使用277下载到现在为止,程序清单 2 4 - 6中的所有代码对你来说应该是熟悉的。这里定义的子例程既 可以来自上一个程序(比如 g e t _ f i l e _ c o n t e n t s ( )和f i n d _ l a s t _ f i l e ( )),也可能是 g e t _ l o c k ( )和 r e l a s e _ l o c k ( )例程。同样,必须确保存放在 $ s u r v e y _ d i r中的目录能够被We b服务器写入。 由于这部分代码都很简单明了,不必多加说明,因此我们可以转入程序清单 2 4 - 7的介绍。 程序清单24-7 接收调查结果的程序的第二部分 278使用第三部分 将Perl 用于C G I 下载这部分程序紧接着上一部分程序的结尾。当前提出的问题和对问题的答复分别存放在第 4 6行中的$ q u e s t i o n和@ p o s s _ a n s w e r s中。 从第5 0行起,本程序要查看用户是否提供了对调查问题的答复。请记住,如果用户愿意 的话,他可以只点击 S u b m i t按钮,而不提供答复。如果提供了答复,便在第 5 3行上用 g e t _ l o c k ( )函数取出一个锁,以防止多人同时更新调查结果。 在第5 6行上,找到了最后一个 . t x t调查文件,比如说 f i r s t . t x t,同时,可以用替换方式 将. t x t改为. a n s w e r,以便给出f i r s t . a n s w e r。答复文件被打开,当前的答复被附加给该文件,同 时,r e l a s e _ l o c k ( )函数对文件进行解锁,因为现在其他人可以安全地打开该文件了。 第6 6行: g e t _ f i l e _ c o n t e n t s ( )函数用于获取调查结果,而不是调查的问题。这时,名 叫% r e s u l t s的哈希结构得以创建,其关键字是问题的答复,即数字 0、1、2等,它的值是看到 每个关键字的次数。 第7 4行起输出每个可能的答复。如果 % r e s u l t s中没有任何项目与某个答复相对应,那么调 查结果必须是0。答复与给出的次数将在第 7 6行上输出。 图2 4 - 4显示了产生的调查窗体。如果你想使这个调查窗体看上去更好些,可以让 C G I程序 显示对调查问题的答复(用带颜色的表格显示调查结果)和其他所有特性,使 H T M L页看起 来更加美观。 用于保存调查程序的目录(上例中的 / w e b / h t d o c s / p o l l)必须是可供全世界 的人都能写入的目录,以便使该程序能够运行。在Windows NT下,可以设置该 目录的属性,使它可以被G u e s t(客方)写入。在U N I X下,可以使用c h m o d命令 将访问权限设置为7 7 7。另外,如果用人工运行调查结果的接收程序,而不使用 浏览器,那么它会创建一个We b服务器无法写入的. a n s w e r文件。如果是这样的 话,你必须删除该文件,然后调查程序才能正确运行。 第24学时 建立交互式Web 站点使用279下载24.3 课时小结 在本学时中,创建了两个程序,使你的 We b页变得更加丰富多彩。首先,创建了一个从 其他站点检索内容并将内容显示在你自己的 We b页上的程序。我们还讲述了向其他人借用 We b 内容时应该注意的一些问题。接着又创建了一个调查程序,使 We b站点的访问者能够参与站 点正在进行的活动。 24.4 课外作业 24.4.1 专家答疑 问题:在We b服务器上拥有一个可供所有人写入的目录(用于民意测试),是否会带来安 全上的漏洞? 解答:是的,不过这个漏洞并不大。如果你的服务器是以一种合理的方式安装的,那么 就无人能够将内容上载到你的站点。但是,如果你的服务器允许任何人将任何东西上载到任 何地方,那么那里就可能存在滥用危险。如果你愿意的话,可以在创建 . t x t文件的同时,创 建. a n s w e r文件,以便解决这个问题。请记住使用 c h m o d使. a n s w e r文件成为所有人都能写入的 文件。 问题:如果我仅仅从某个站点借用了一些新闻标题,我会不会受到指控? 解答:是的,你会受到指控,以前曾经发生过这样的的事情。 1 9 9 9年2月,M i c r o s o f t与 Ti c k e t m a s t e r两公司之间就因为这样的问题而对簿公堂。据说 M i c r o s o f t公司使用“深层次链接” 进入了Ti c k e t m a s t e r公司的We b站点,为此Ti c k e t m a s t e r提出了诉讼。在这个案件中,没有出现 侵犯版权的问题,但是有足够的证据说明 M i c r o s o f t公司进入了“深层次链接”。如果涉及到侵 犯版权的问题,问题就严重了。 问题:有一个站点,我想从中借用其新闻标题。它类似 S l a s h d o t的站点。但是该站点不具 备很好的X M L或文件供分析,因此我必须改用普通的 H T M L文件。我应该如何分析该文件? 解答:如果你要分析 H T M L文件,请不要使用正则表达式,也不要自己对它进行分析。 对H T M L文件进行分析并不像它看起来那样容易,并且几乎无法得到正确的结果。另外,即 280使用第三部分 将Perl 用于C G I 下载 图24-4 经过格式美化的调 查窗体使你试图用正则表达式来分析某些 H T M L文件,它也不是到处都能取得成功的。 C PA N包含用 于分析H T M L文件的一些模块,这些模块都在 C PA N的H T M L节下,即H T M L::*下。 24.4.2 思考题 1) 若要从We b服务器检索一个H T M L文件,我应该怎么做? a .使用LW P。 b .打开到达该系统的套接字,然后检索数据。 c .使用‘lynx -dump’或‘netscape -print’。 2) 如果LW P : : S i m p l e模块的g e t函数运行失败,它返回什么? a .出错消息,即“No Documcnt(无文档)” b .空字符串,即“” c . u n d e f 24.4.3 解答 1) 答案是a。虽然b和c也可以,但是这两种操作方法很不可靠,使用起来也很难。 2) 答案是c。这个答案在程序清单2 4 - 2后面的程序分析中作了解释。 24.4.4 实习 • 即使不使用图形模块,你也能很容易创建图 2 4 - 4中表示民意测验结果的条形图。为了创 建这个条形图,你需要一个颜色正确的 1×1(或非常小的). g i f文件。若要制作条形图, 你只需显示带有相应高度和宽度标号的 . g i f文件,如下所示: 大多数图形浏览器都能将这个小型 . g i f文件放大为规定的大小。 你的任务是:使该民意测验程序输出带有条形图的结果。你必须计算各种类别投票的总 数,再将这个总数除以每种类别投票的数量,确定条形图的正确宽度。例如,投票总数是 1 0 0, 一个类别的投票数目为4 0,那么可以将条形图的最大宽度乘以 . 4。 • 程序清单2 4 - 4中的调查程序可能因为有人多次投票而使调查结果产生偏差。你能设法避 免这种情况吗?你可以保存一个文件,里面是所有被调查人地址的列表,不允出现重复。 这可以防止有人在缓存代理的后面进行投票(第 2 3学时介绍了缓存代理)。请设计一种 方法,只允许访问该站点的每个人投票一次。(正如你所知道的那样,这种方法不可能 做到非常简单明了,它只是一种思路验证方法。) 第24学时 建立交互式Web 站点使用281下载下载 第四部分 附录 附录 安装模块下载 附录 安 装 模 块 在P e r l中安装模块并不困难,如果你想真正掌握 P e r l,那么学会如何安装这些模块是非常 重要的。本附录包含了关于如何安装你需要的模块的信息。 在P e r l的文档资料中,你可以得到在各种操作系统下安装模块的详细说 明。名叫“P e r l m o d i n s t a l l”的文档甚至包含了在 O S/2和V M S之类的操作系 统下安装模块的说明。 A.1 选择正确的模块 首先,必须选择正确的模块。可以通过站点 h t t p : / / w w w. p e r l . c o m/C PA N上的C PA N寻找你 要的模块。你必须确定对哪个模块感兴趣。 C PA N模块大体上是按它们的功能来命名的。例如, I m a g e::s i z e带有一个图形,并且能 够报告该图形的大小,该模块可用来与 We b页一道运行。不过,有些模块使用一些特殊的名 字。LW P是根据P e r l库l i b w w w - p e r l而得名的。 还可以在C PA N上找到模块包。这些模块包含有若干相关的模块,这些模块通常是一些必 须要有的模块,它们全部放在一个大模块包中。例如,l i b n e t模块包可以像一个模块那样来安装, 不过在安装过程中,你会得到若干个与网络相关的模块。LW P就是l i b n e t模块包中的一部分。 当你安装一个模块时,还会自动获得该模块需要的所有文档。 A.2 在何种操作系统下安装 在下面各节中的每个安装模块的例子中,你将安装来自 C PA N的D a t e::M a n i p模块。若要 安装你自己的模块或模块包,只要用你的模块包取代 D a t e::M a n i p即可。 A.2.1 在Windows95/98/NT下安装 在Wi n d o w s下,假定已经安装了来自ActiveState Tool 公司的P e r l,安装模块的最容易的方 法是使用ActiveState To o l公司已打包的模块。 若要在Wi n d o w s下安装预装模块,首先必须启动 Perl Package Manager (PPM)。该实用程 序通过提供一个用于模块安装的交互式界面,从而简化了模块安装进程。为了启动 P P M,你 必须显示一个D O S命令提示符,如图A - 1所示,应该连接到I n t e r n e t。 在命令提示符处,键入如下所示的 P P M。这时P P M实用程序应该启动运行。如果它没有 启动运行,你必须查找与ActiveState Perl一道安装的p p m . b a t文件,并用全路径名运行它:若要搜索某个模块,请使用下面所示的 s e a r c h命令。之所以你必须使用该命令,原因是 A c t i v e S t a t e并没有C PA N中的所有模块的预装模块包,它只有比较常用的模块。另外,为了进 行安装,必须正确拼写模块名。 当你找到你想要的模块(比如这个例子中的 D a t e - M a n i p)后,就可以使用i n s t a l l命令,对 该模块进行安装,如下所示: 这时D a t e::M a n i p模块就安装好了。 如果你想下载模块包并用人工进行安装(也许 P C没有与I n t e r n e t连网或者它位于防火墙的 后面),可以在A c t i v e S t a t e的We b站点(h t t p : : / / w w w. A c t i v e S t a t e . c o m)上找到下载和人工安装 模块的说明。 A c t i v e S t a t e维护了一个关于它销售的 P e r L产品的特定FA Q,你可以在那里找到 必要的说明。 不使用P P M来安装模块,比如使用Wi n d o w s下你自己的C编译器来进行安 装,这不是本书要讲解的内容。 P e r l的原始产品中包含了在Wi n d o w s下你自己 安装P e r l的说明,但这不是初学者能够做的工作。如果你能够进行这项操作, 那么自己来安装模块就不会太难,因为安装过程是大致相同的。 286使用第四部分 附 录 下载 图A-1 可以从 D O S命令提 示符处开始安装模 块的操作A.2.2 在UNIX下使用CPAN来安装模块 在U I N X下安装模块是很有趣并且会遇到许多问题的,但是它也可能是非常容易的。你需 要一个ANSI C编译器(用于安装P e r l的编译器就很好),如果供应商要求的话,你还必须拥有 编译器许可证。你不需要 G N U压缩程序g z i p/g u n z i p的拷贝,有些U N I X供应商将它作为一个 标准实用程序提供给用户使用。如果你没有这个拷贝,可以从网址 h t t p : / / w w w. f s f . o rg下载一个 拷贝。 有些 U N I X供应商(比如 H P公司)在它们的操作系统中配备了一个 C 编译器,但是它不是 ANSI C编译器,这是 C编译器的一个非常简化了的版 本,因此你必须花钱购买实际的 C编译器,或者交费下载和安装 GNU C编 译器。 最后一个问题是:你在安装模块的计算机上必须拥有根(管理员)访问权限。通常情况 下,P e r l是作为整个系统范围的实用程序来安装的。将模块安装到系统目录中,你必须拥有足 够的访问权限(即根权限)才能进行这种操作。 P e r l产品配有一个称为 C PA N的模块,用来帮助你安装其他的模块。若要开始安装操作, 你必须使用C PA N模块的s h e l l命令来启动P e r l,如下所示: $ perl -MCPAN -e shell 如果你是初次运行该命令, C PA N模块就会要求你确定从何处取得 P e r l的模块以及你想要 如何安装这些模块。大多数情况下,默认答案就足以满足你的要求。然后它会问你临时目录 的位置在什么地方(这是 C PA N对你想使用的目录进行镜像的目录),并且问你是否通过代理 程序来访问I n t e r n e t。 当C PA N结束对你的提问后,你会看到下面这个提示: 在这个提示后面,你可以使用命令 i /p a t/,搜索关于模块包的信息,其中 p a t用于说明 你要搜索的模式。例如,若要查找 D a t e::M a n i p模块,请输入下面这个命令: C PA N模块必须与一个C PA N服务器取得联系,以使获取该索引的新拷贝。这种情况只有 在需要时才会出现,并且这个进程只需很短时间就能完成。当查询结束时, C PA N就会答复下 面这样的信息: 若要安装该模块,请键入下面的命令: 这时,C PA N模块开始按步骤执行索取、编译、测试和安装模块的各个进程。它显示的信 息相当零乱,不过它类似下面这个大大简化了的例子(#后面的注释通常并不出现,这里增 加了注释,目的是使它更加清楚): 附录 安 装 模 块使用287下载你得到的输出可能与上面的情况有很大的不同。现在该模块已经测试和安装好了。 A.2.3 在UNIX下用另一种方法安装模块 虽然你可以不使用 C PA N模块在U N I X下安装各个模块,但是大多数情况下不需要用下面 这种方法来安装模块。我们只是为了完整起见才介绍这种安装方法,但是只要可能,都应该 使用C PA N模块来安装各个模块。 首先,必须从C PA N下载你要安装的模块。它是个压缩了的综合模块包。例如,如果要安 装的模块是D a t e : : C a l c,你必须得到它的新版本,它的名字类似 D a t e - C a l c - X . Y. t a r. g z。当你下 载了该模块包后,进入该目录,对该模块包进行拆包操作,如下所示: 拆包后便产生一个子目录,称为 D a t e - C a l c - 4 . 2。若要转入该子目录,请使用 c d ,并键入下 面的命令: 现在你就拥有一个 m a k e程序的描述文件,这对于安装进程来说是个必不可少的文件。接 着,使用下面这样的m a k e命令,安装该模块: 这个进程的运行需要花费一定的时间。 288使用第四部分 附 录 下载在下一个提示符后面,你必须测试该模块,以了解它的安装是否正确。请键入下面这个 make test命令: 你始终都应该运行 make test命令,以确保模块安装正确。它能省去你以后好几个小时的 调试时间。当测试完成后,必须像下面这样安装该模块。这个操作步骤通常是以根用户身份 来进行的,因为安装时必须写入系统目录: 这样,你的安装操作就完成了。 A.2.4 在Macintosh系统上安装模块 在M a c i n t o s h系统上安装模块是比较困难的。你应该查看 M a c P e r l的FA Q,了解关于可以用 来安装模块的方法的信息。 MacPerl FA Q可以在网址h t t p : / / W W W. m a c p e r l . c o m上找到。 A.3 当不允许你安装模块时该怎么办 如果你能够在系统上安装程序,你就能够安装模块。你能够这样做,取决于模块的复杂 程度和你会遇到何种困难。有时系统管理员不允许你安装某个模块,因为他不想让其他人使 用该模块。在某些情况下,只有你或者一组人才想使用某些特定模块 ,在整个系统范围内安装 这些模块太复杂了。 无论哪种情况,在你自己的目录中安装P e r l模块的专用拷贝并不难。 首先,必须使用前面给出的说明(只有一些小的例外)安装模块。你可以指定安装程序, 将模块安装到特写的目录中。如果在 Wi n d o w s下使用P P M,在你安装模块前,必须告诉 P P M, 你想将模块安装到另一个目录。为此可以使用下面这样的 s e t命令: 然后该模块被组装在目录C : \ m y p e r l中。 附录 安 装 模 块使用289下载在U N I X下,当你使用C PA N模块时,可以使用下面的m a k e p l _ a rg设置项来设定安装目录: 或者,如果你使用m a k e实用程序人工安装模块,你可以在第一个代码行上使用 P R E F I X参 数,设定安装目录: 无论使用哪种方法,你要安装的模块将被安装到 / h o m e / c l i n t p / p e r l / l i b目录中。如果需要的 话,你可以再将该模块移到另一个目录中。 你应该注意,不要将模块在不同操作系统的计算机之间移动。经过编译 的模块只能在一种类型的操作系统上运行,这与 P e r l本身的情况是一样的。 另外,不要试图在不同版本的 P e r l之间移动模块,有时它不能运行。在这种 情况下,你必须重新安装该模块。 使用安装在特殊位置中的模块 若要使用安装在非标准目录中的模块,必须使用命令 use lib。例如,如果你使用上一节中 的说明将模块D a t e : : M a n i p安装在目录/h o m e / c l i n t p / p e r l / l i b中,就会得到一个图 A - 2所示的文 件树。 在你的程序开始处,只需要使用下面的代码: 这时P e r l在搜索它自己的目录之前,首先搜索该目录,找出它要的模块。还可以使用这种 方法将模块的新版本安装在系统上(以便达到测试目的),但不会改写老的版本,也不会带来 不兼容的问题。 290使用第四部分 附 录 下载 图A-2 安装 D a t e : : M a n i p模 块后形成的文件树
还剩285页未读

继续阅读

pdf贡献者

cff2

贡献于2015-05-16

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf