Perl 5.10 入门教程,第二部分

Perl 5.10 入门教程 讨论了 Perl 的核心元素:变量(标量、数组和哈希)、数学运算符和一些基本流程控制(for 语句)。现在我们要与世界交互。(Perl 5.10 正则表达式入门 探讨了正则表达式、匹配和替换。 Perl 5.10 网络编程入门 展示了如何编写网络程序。)

本节讨论了如何切片和切块字符串,如何处理文件以及如何定义自己的函数。首先,你需要理解 Perl 语言的一个核心概念:条件和比较。

比较运算符

像所有优秀的编程语言一样,Perl 允许你提出诸如“这个数字是否大于那个数字?”或“这两个字符串是否相同?”等问题,并根据答案执行不同的操作。

当你处理数字时,Perl 有四个重要的运算符:<>==!=。这些是“小于”、“大于”、“等于”和“不等于”运算符。(你也可以使用 <=,“小于或等于”,和 >=,“大于或等于”。)

你可以使用这些运算符与 Perl 的一个 条件 关键字之一,如 ifunless 一起使用。这两个关键字都接受一个 Perl 将要测试的条件,以及一个用大括号括起来的代码块,如果测试通过,Perl 将运行该代码块。这两个词与它们的英文对应词一样工作——if 测试成功,如果条件结果为真,而 unless 测试成功,如果条件结果为假。

use 5.010;

if ($year_according_to_computer == 1900) {
    say "Y2K has doomed us all!  Everyone to the compound.";
}

unless ($bank_account > 0) {
    say "I'm broke!";
}

注意区分 === 的区别!一个等号表示“赋值”,两个等号表示“比较等于”。这是一个常见的、恶性的错误。

use 5.010;

if ($a = 5) {
    say "This works - but doesn't do what you want!";
}

你可能想知道开头的那行额外代码做什么。就像前一篇文中的 use feature :5.10; 代码一样,这启用了 Perl 5.10 的新功能。(为什么是 5.010 而不是 5.10?版本号不是一个单一的十进制数;最终可能会有 Perl 5.100,但不太可能有 Perl 5.1000。现在就相信我吧。)

你并不是在测试 $a 是否等于五,而是将 $a 赋值为五,并覆盖了它的旧值。(未来的文章将展示如何避免运行代码中的这个错误。)

ifunless 都可以跟一个 else 语句和代码块,如果测试失败,将执行这个代码块。你还可以使用 elsif 来串联多个 if 语句。

use 5.010;

if ($a == 5) {
    say "It's five!";
} elsif ($a == 6) {
    say "It's six!";
} else {
    say "It's something else.";
}

unless ($pie eq 'apple') {
    say "Ew, I don't like $pie flavored pie.";
} else {
    say "Apple!  My favorite!";
}

你并不总是需要一个 else 条件,有时要执行的代码可以放在一行上。在这种情况下,你可以使用 后置条件 语句。这个名字可能听起来有些吓人,但如果你能读懂这句话,你就已经理解它们了。

use 5.010;

say "I'm leaving work early!" if $day eq 'Friday';

say "I'm burning the 7 pm oil" unless $day eq 'Friday';

有时这可以使你的代码更清晰。

whileuntil

两个稍微复杂一些的关键字是 whileuntil。它们都与 ifunless 一样接受一个条件和一段代码,但它们的行为类似于 for 循环。Perl 测试条件,运行代码块,并根据条件为真(对于 while 循环)或为假(对于 until 循环)重复运行该代码块。

试着猜一下这段代码会做什么

use 5.010;

my $count = 0;

while ($count != 3) {
   $count++;
   say "Counting up to $count...";
}

until ($count == 0) {
   $count--;
   say "Counting down to $count...";
}

当你运行这个程序时,你会看到以下内容

Counting up to 1...
Counting up to 2...
Counting up to 3...
Counting down to 2...
Counting down to 1...
Counting down to 0...

字符串比较

这就是比较数字的方法。字符串呢?最常用的字符串比较运算符是eq,它用于测试字符串相等性——也就是说,两个字符串是否具有相同的值。

还记得混淆===的痛苦吗?你也会混淆==eq。这是少数几个Perl将一个值视为字符串还是数字确实很重要的案例之一。尝试以下代码

use 5.010;

my $yes_no = 'no';
say "How positive!" if $yes_no == 'yes';

为什么这段代码认为你说的是“是”?记住,Perl在必要时会自动将字符串转换为数字;==运算符暗示你正在使用数字,所以Perl将$yes_no的值(“no”)转换为数字0,同样“yes”也转换为数字0。因为这个相等性测试是成立的(0等于0),条件为真。将条件改为$yes_no eq 'yes',它就会按照预期执行。

情况也可能相反。数字5在数值上等于字符串" 5 ",所以用==比较它们是有效的。当你用eq比较5和" 5 "时,Perl会首先将数字转换为字符串"5",然后询问两个字符串是否具有相同的值。因为它们不具有相同的值,所以eq比较失败。这个代码片段将打印出数值相等!,但不会打印出字符串相等!

use 5.010;

my $five = 5;

say "Numeric equality!" if $five == " 5 ";
say "String equality!"  if $five eq " 5 ";

字符串的更多乐趣

你经常会想要操作字符串:将它们分割成更小的部分,将它们组合在一起,并更改它们的内 容。Perl提供了三个函数,使字符串操作变得简单有趣:substr()split()join()

如果你想要检索字符串的一部分(比如,前四个字符或中间10个字符),请使用substr()函数。它接受两个或三个参数:你想要查看的字符串,从哪个字符位置开始(第一个字符是位置0)以及要检索的字符数。如果你省略字符数,你将检索字符串的剩余部分。

my $greeting = "Welcome to Perl!\n";

print substr($greeting, 0, 7);     # "Welcome"
print substr($greeting, 7);        # " to Perl!\n"

substr()的一个整洁且常被忽视的特性是,你可以使用负数字符位置。这将检索一个从字符串末尾开始的子字符串。

my $greeting = "Welcome to Perl!\n";

print substr($greeting, -6, 4);      # "Perl"

(记住,在双引号内,\n代表单个换行符。)

你还可以通过使用substr()来给字符串的一部分赋新值来操作字符串。一个有用的技巧是使用长度为0来在字符串中插入字符

my $greeting = "Welcome to Java!\n";

substr($greeting, 11, 4) = 'Perl';    # $greeting is now "Welcome to Perl!\n";
substr($greeting, 7, 3)  = '';        #       ... "Welcome Perl!\n";
substr($greeting, 0, 0)  = 'Hello. '; #       ... "Hello. Welcome Perl!\n";

split()将字符串分割成多个片段,并返回一个片段列表。split()通常接受两个参数:用于分割字符串的正则表达式以及你想要分割的字符串。(下一篇文章将更详细地讨论正则表达式;目前,你需要知道的是,这个正则表达式代表单个空格字符:/ /。)你分割的字符不会出现在列表的任何元素中。

my $greeting = "Hello. Welcome Perl!\n";
my @words    = split(/ /, $greeting);   # Three items: "Hello.", "Welcome", "Perl!\n"

你还可以指定第三个参数:要放入列表中的最大项目数。分割会在列表包含这么多项目时停止

my $greeting = "Hello. Welcome Perl!\n";
my @words    = split(/ /, $greeting, 2);   # Two items: "Hello.", "Welcome Perl!\n";

当然,你可以分割的,你也可以join()join()函数接受一个字符串列表,并使用指定的字符串将每个元素连接在一起,这个字符串可以是空字符串

my @words         = ("Hello.", "Welcome", "Perl!\n");
my $greeting      = join(' ', @words);       # "Hello. Welcome Perl!\n";
my $andy_greeting = join(' and ', @words);   # "Hello. and Welcome and Perl!\n";
my $jam_greeting  = join('', @words);        # "Hello.WelcomePerl!\n";

文件句柄

关于字符串就讲到这里。现在是时候考虑文件了——毕竟,如果无法在关键位置进行字符串操作,字符串操作还有什么意义呢?

要从文件中读取或写入,您必须先打开它。当您打开文件时,Perl会询问操作系统该文件是否可访问——如果您正在尝试读取,文件是否存在(或您正在尝试创建新文件,则可以创建它),以及您是否有必要的文件权限来完成您想做的事情?如果您可以使用该文件,操作系统将为您准备它,Perl会为您提供文件句柄

要使用open()函数创建文件句柄,请请求Perl为您创建,该函数接受两个或三个参数:要创建的文件句柄、文件模式以及您要处理的文件。首先,我们将集中讨论读取文件。以下语句使用文件句柄$logfile打开文件log.txt

open my $logfile, 'log.txt';

打开文件涉及Perl和操作系统共同承担的几个后台任务,例如检查您要打开的文件实际上是否存在(或如果您正在尝试创建新文件,则创建它),并确保您有权操作文件(例如,您是否有必要的文件权限)。Perl会为您完成所有这些,因此通常您不必担心这些。

一旦您打开了文件进行读取,您可以通过使用<>构造(也称为readline)来检索其行。在尖括号内放置您的文件句柄。您得到的取决于您得到什么:在标量上下文中(这是一种更技术性的说法,“如果您将其分配给标量”),您将从文件中检索下一行,但如果您正在寻找列表,您将得到文件中所有剩余行的列表。

当然,您也可以close您已打开的文件句柄。您不一定必须这样做,因为Perl足够聪明,可以在程序结束时关闭文件句柄,当您尝试重用现有的文件句柄,或者当包含文件句柄的词法变量超出范围时。

以下是一个简单的程序,它将显示文件log.txt的内容,并假设文件的第一行是其标题

open my $logfile, 'log.txt' or die "I couldn't get at log.txt: $!";

my $title = <$logfile>;
print "Report Title: $title";

print while <$logfile>;
close $logfile;

此代码可能看起来相当密集,但它结合了您之前见过的想法。while运算符逐行遍历文件的每一行,将每一行放入Perl代词$_中。(代词?是的——把它想成。)对于读取的每一行,Perl都会打印该行。现在,这个代词应该有道理。当您从文件中读取它时,打印它。

为什么不使用say?文件中的每一行都以换行符结尾——这是Perl知道它是行的方式。无需添加额外的换行符,因此say会使输出双倍空格。

写入文件

当您要向文件写入时,也会使用open()。有两种方式可以打开文件进行写入:覆盖追加。当您以覆盖模式打开文件时,您将擦除其以前包含的内容。在追加模式中,您将新数据附加到现有文件的末尾,而不会擦除任何已存在的内容。

要表示您想要一个用于写入的文件句柄,请使用单个>字符作为传递给open的模式。这将以覆盖模式打开文件。要将其以追加模式打开,请使用两个>字符。

open my $overwrite, '>', 'overwrite.txt' or die "error trying to overwrite: $!";
# Wave goodbye to the original contents.

open my $append, '>>', 'append.txt' or die "error trying to append: $!";
# Original contents still there; add to the end of the file

一旦您的文件句柄已打开,请使用谦卑的printsay运算符来写入它。指定您想要写入的文件句柄以及您想要写入的值列表

use 5.010;

say $overwrite 'This is the new content';
print $append "We're adding to the end here.\n", "And here too.\n";

自由地生活,或者死去!

大多数这些open()语句都包括or die "some sort of message"。这是因为我们生活在一个不完美的世界,程序并不总是按照我们的意愿行事。打开调用失败总是可能的;也许您正在尝试写入您无权写入的文件,或者您正在尝试读取一个不存在的文件。在Perl中,您可以使用orand来防范这些问题。

一系列通过 or 分隔的语句会一直执行,直到你找到一个可行的语句或者返回一个真值。此行代码要么以覆盖模式成功打开 $output,要么导致 Perl 停止。

open my $output, '>', $outfile or die "Can't write to '$outfile': $!";

die 语句会在程序中以错误信息结束。特殊变量 $! 包含 Perl 对错误的解释。如果你没有权限写入文件,你可能会看到类似这样的信息。请注意,你将同时获得实际的错误信息(“权限被拒绝”)和发生错误的行。

Can't write to 'a2-die.txt': Permission denied at ./a2-die.pl line 1.

这种防御性编程对于使你的程序更具容错性很有用——你不想写入一个没有成功打开的文件!(在文件名周围加上单引号可以帮助你看到文件名中任何意外的空白字符。当你遇到这种情况时,你可能会拍手称快。)

以下是一个例子:作为你的工作的一部分,你编写了一个程序,该程序将结果记录在一个名为 vitalreport.txt 的文件中。你使用了以下代码

open my $vital, '>', 'vitalreport.txt';

如果这个 open() 调用失败(例如,vitalreport.txt 由另一个用户拥有,而这个用户没有给你写入权限),你将永远不会知道,直到有人检查文件后,并疑惑为什么重要报告没有被写入。(想象一下,如果那个人是你的老板,在你年度绩效评估的前一天,会发生什么。)当你使用 or die 时,你可以避免所有这些问题。

open my $vital, '>', 'vitalreport.txt' or die "Can't write vital report: $!";

你不必再怀疑你的程序是否写入了你的重要报告,你将立即得到一个错误信息,该信息既告诉你出了什么问题,也告诉你错误发生在程序的哪一行。

你可以使用 or 来测试文件操作之外的更多情况。

use 5.010;
($pie eq 'apple') or ($pie eq 'cherry') or ($pie eq 'blueberry')
        or say 'But I wanted apple, cherry, or blueberry!';

在这个序列中,如果你有一个合适的饼图,Perl 将跳过链中的其余部分。一旦有一个语句生效,其余的语句将被忽略。相反,and 操作符会评估你的语句链,但会在其中一个语句不工作时停止。

open my $log, 'log.file' and say 'Logfile is open!';
say 'Logfile is open!' if open my $log, 'log.file';

只有当 open() 成功时,这条语句才会显示 Logfile is open!,你能明白为什么吗?

再次强调,虽然有多种方式可以条件性地执行代码,但这并不意味着你必须在一个程序中使用每一种方式,也不意味着你必须使用最聪明或最有创造性的方式。你有大量的选择。考虑在特定情况下使用最易于阅读的方式。

子程序

到目前为止,示例 Perl 程序只是一系列语句。如果你正在编写非常小的程序,这很正常,但随着你的需求增长,你会发现这很受限。这就是为什么大多数现代编程语言都允许你定义自己的函数;在 Perl 中,我们称之为 subs

子程序,通过使用 sub 关键字声明,为你的程序添加了新的功能。当你想使用这个新功能时,通过名称调用它。例如,以下是一个名为 boo 的子程序的简短定义

use 5.010;

sub boo {
    say 'Boo!';
}

boo();   # Eek!

子程序很有用,因为它们允许你将程序分解成小而可重用的块。如果你需要在程序中的四个不同地方分析一个字符串,编写一个 analyze_string 子程序并调用它四次要容易得多。这样,当你需要对字符串分析例程进行改进时,你只需要在一个地方进行改进,而不是在四个地方。

与 Perl 的内置函数可以接受参数并返回值一样,你的子程序也可以。每次调用子程序时,传递给它的任何参数都会出现在特殊数组 @_ 中。你还可以使用 return 关键字返回单个值或列表。

use 5.010;

sub multiply {
    my (@ops) = @_;
    return $ops[0] * $ops[1];
}

for my $i (1 .. 10) {
     say "$i squared is ", multiply($i, $i);
}

使用my关键字在multiply函数中有一个有趣的优点。它表示这些变量是该子程序私有的,这意味着在程序其他地方使用的@ops数组中存在的任何值都不会被覆盖。这意味着你可以避免程序中的一类难以追踪的bug。你不必使用my,但你也不必在钉木板时砸伤手指。它们都是好主意。

你还可以在一条语句中将多个词法变量(使用my声明的)赋值。你无需修改任何其他代码,只需将multiply函数中的代码更改为类似以下内容:

sub multiply {
    my ($left, $right) = @_;
    return $left * $right;
}

如果你没有明确使用return语句,子程序将返回最后一条语句的结果。这种隐式返回值有时很有用,但会降低程序的可读性。记住,你阅读代码的次数比你编写的次数多得多!

综合起来

前一篇文章演示了一个简单的利息计算器。你可以通过将利息表写入文件而不是屏幕来使其更有趣。另一个变化是将代码分解为子程序,使其更容易阅读和维护。

[下载此程序]

#! perl

# compound_interest_file.pl - the miracle of compound interest, part 2

use 5.010;

use strict;
use warnings;

# First, we'll set up the variables we want to use.
my $outfile   = 'interest.txt';    # This is the filename of our report.
my $nest_egg  = 10000;             # $nest_egg is our starting amount
my $year      = 2008;              # This is the starting year for our table.
my $duration  = 10;                # How many years are we saving up?
my $apr       = 9.5;               # This is our annual percentage rate.

my $report_fh = open_report( $outfile );
print_headers(   $report_fh );
interest_report( $report_fh, $nest_egg, $year, $duration, $apr );
report_footer(   $report_fh, $nest_egg, $duration, $apr );

sub open_report {
    my ($outfile) = @_;
    open my $report, '>', $outfile or die "Can't open '$outfile': $!";
    return $report;
}

sub print_headers {
    my ($report_fh) = @_;

    # Print the headers for our report.
    say $report_fh "Year\tBalance\tInterest\tNew balance";
}

sub calculate_interest {
    # Given a nest egg and an APR, how much interest do we collect?
    my ( $nest_egg, $apr ) = @_;

    return int( ( $apr / 100 ) * $nest_egg * 100 ) / 100;
}

sub interest_report {
    # Get our parameters.  Note that these variables won't clobber the
    # global variables with the same name.
    my ( $report_fh, $nest_egg, $year, $duration, $apr ) = @_;

    # Calculate interest for each year.
    for my $i ( 1 .. $duration ) {
        my $interest = calculate_interest( $nest_egg, $apr );
        my $line     =
            join "\t", $year + $i, $nest_egg, $interest, $nest_egg + $interest;

        say $report_fh $line;

        $nest_egg += $interest;
    }
}

sub report_footer {
    my ($report_fh, $nest_egg, $duration, $apr) = @_;

    say $report_fh "\n Our original assumptions:";
    say $report_fh "   Nest egg: $nest_egg";
    say $report_fh "   Number of years: $duration";
    say $report_fh "   Interest rate: $apr";
}

注意,当你将程序分解为子程序时,程序逻辑变得多么清晰。将程序编写为小而命名的子程序的一个优点是,它几乎可以成为自文档化的。考虑以下四行

my $report_fh = open_report( $outfile );
print_headers(   $report_fh );
interest_report( $report_fh, $nest_egg, $year, $duration, $apr );
report_footer(   $report_fh, $nest_egg, $duration, $apr );

这种代码在你六个月后再次回到它时非常有价值,需要弄清楚它做了什么——你是愿意花时间阅读整个程序来找出它做什么,还是愿意阅读四行代码,告诉你程序1)打开一个报告文件,2)打印一些标题,3)生成一个利息报告,4)打印报告页脚?

试试看!

本文探讨了文件(文件句柄、open()close()<>)、字符串操作(substr()split()join())和子程序。这里有两组练习——同样,一个是简单的,一个是复杂的

  • 你有一个名为dictionary.txt的文件,其中包含字典定义,每行一个,格式为“单词 空格 定义”。(这里是一个示例。)编写一个程序,可以从命令行查找一个单词。提示:@ARGV是一个特殊的数组,包含你的命令行参数,你需要使用split()的三参数形式。)尝试增强它,以便你的字典也可以包含具有多个定义的单词,格式为“单词 空格 定义:其他定义:其他定义,等等...
  • 编写一个用于分析Apache日志的程序。你可以在http://www.w3.org/Daemon/User/Config/Logging.html找到关于常见日志格式的简短描述。你的分析程序应计算每个URL的请求总数、每个状态码的结果总数以及输出的字节数。

编程愉快!

标签

反馈

这篇文章有什么问题吗?请通过在GitHub上创建问题或拉取请求来帮助我们。