Perl初学者入门 - 第2部分
编者按:这个久负盛名的系列正在进行更新。您可能对更新的版本感兴趣,可在以下位置找到:
目录 |
•本系列第1部分 |
在上一篇文章中,我们讨论了Perl的核心元素:变量(标量、数组和哈希)、数学运算符和一些基本流程控制(for
语句)。现在是时候与世界互动了。
在本期文章中,我们将讨论如何切片和切块字符串,如何处理文件以及如何定义自己的函数。但是,首先,我们将讨论Perl语言的一个核心概念:条件和比较。
比较运算符
在上篇文章中,我们跳过了Perl的一个重要元素:比较运算符。像所有优秀的编程语言一样,Perl允许你提出诸如“这个数字是否大于那个数字?”或“这两个字符串是否相同?”等问题,并根据答案执行不同的操作。
当你处理数字时,Perl有四个重要的运算符:<
、>
、==
和 !=
。这些是“小于”、“大于”、“等于”和“不等于”运算符。(你也可以使用 <=
、“小于或等于”和 >=
、“大于或等于”)。
你可以使用这些运算符与Perl的条件关键字之一(如if
和unless
)一起使用。这两个关键字都接受一个Perl将测试的条件,以及一个花括号中的代码块,如果测试成功,Perl将运行该代码块。这两个词与它们的英文对应词完全一样——如果条件最终为真,则if
测试成功,如果条件最终为假,则unless
测试成功。
if ($year_according_to_computer == 1900) {
print "Y2K has doomed us all! Everyone to the compound.\n";
}
unless ($bank_account > 0) {
print "I'm broke!\n";
}
请注意=
和==
之间的区别!一个等号表示“赋值”,两个表示“比较相等”。这是一个常见的、邪恶的错误。
if ($a = 5) {
print "This works - but doesn't do what you want!\n";
}
不是测试$a
是否等于五,而是使$a
等于五并覆盖了它的旧值。(在后面的文章中,我们将讨论一种方法来确保这个错误不会在运行代码中出现。)
if
和unless
都可以跟一个else
语句和代码块,如果测试失败,则执行该代码块。你也可以使用elsif
来串联多个if
语句。
if ($a == 5) {
print "It's five!\n";
} elsif ($a == 6) {
print "It's six!\n";
} else {
print "It's something else.\n";
}
unless ($pie eq 'apple') {
print "Ew, I don't like $pie flavored pie.\n";
} else {
print "Apple! My favorite!\n";
}
while
和 until
两个稍微复杂的关键字是 while
和 until
。它们都接受一个条件和一段代码块,就像 if
和 unless
一样,但它们的行为类似于 for
循环。Perl 会测试条件,运行代码块,并在条件为真(对于 while
循环)或为假(对于 until
循环)的情况下不断重复运行。
看看下面的代码,在继续阅读之前试着猜测它会做什么
$a = 0;
while ($a != 3) {
$a++;
print "Counting up to $a...\n";
}
until ($a == 0) {
$a--;
print "Counting down to $a...\n";
}
运行这个程序后,你会看到以下内容
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 会根据情况将值视为字符串或数字的情况之一。试试这段代码
$yes_no = "no";
if ($yes_no == "yes") {
print "You said yes!\n";
}
为什么这段代码认为你说了“是”?记住,Perl 在必要时会自动将字符串转换为数字;==
运算符意味着你在使用数字,所以 Perl 将 $yes_no
的值(“no”)转换为数字 0,“yes”也转换为数字 0。由于这个相等性测试是成立的(0 等于 0),因此 if
块被运行。将条件更改为 $yes_no eq "yes"
,它就会像它应该做的那样运行。
情况也可以相反。数字 5 在 数值上 等于字符串 " 5 "
,所以使用 ==
比较它们是有效的。但是当你使用 eq
比较数字 5 和字符串 " 5 "
时,Perl 会先将数字转换为字符串 "5"
,然后询问两个字符串是否有相同的值。由于它们不相同,所以 eq
比较失败。这段代码片段将打印 数值相等!
,但不会打印 字符串相等!
$a = 5;
if ($a == " 5 ") { print "Numeric equality!\n"; }
if ($a eq " 5 ") { print "String equality!\n"; }
更多关于字符串的有趣之处
你通常会想要操作字符串:将它们拆分成更小的部分、将它们组合起来,并更改它们的内容。Perl 提供了三个使字符串操作变得容易和有趣的功能:substr()
、split()
和 join()
。
如果你想获取字符串的一部分(比如前四个字符或中间的 10 个字符块),请使用 substr()
函数。它接受两个或三个参数:你想要查看的字符串、开始的位置(第一个字符是位置 0)和要检索的字符数。如果你省略了字符数,你将检索字符串的其余部分。
$a = "Welcome to Perl!\n";
print substr($a, 0, 7); # "Welcome"
print substr($a, 7); # " to Perl!\n"
substr()
的一个既巧妙又常被忽视的特性是,你可以使用一个 负 字符位置。这将检索一个从字符串 末尾 开始的子字符串。
$a = "Welcome to Perl!\n";
print substr($a, -6, 4); # "Perl"
(记住,在双引号内,\n
表示单个换行符。)
你还可以使用 substr()
来更改字符串,将新值赋给它的一部分。一个有用的技巧是使用长度为零来 插入 字符到字符串中
$a = "Welcome to Java!\n";
substr($a, 11, 4) = "Perl"; # $a is now "Welcome to Perl!\n";
substr($a, 7, 3) = ""; # ... "Welcome Perl!\n";
substr($a, 0, 0) = "Hello. "; # ... "Hello. Welcome Perl!\n";
接下来,让我们看看 split()
。这个函数将字符串拆分并返回一个包含各个部分的列表。通常,split()
函数接受两个参数:用于拆分字符串的正则表达式以及你想要拆分的字符串。(我们将在下一篇文章中详细讨论正则表达式;目前,我们只使用一个空格。注意正则表达式的特殊语法:/ /
。)你拆分的字符不会出现在任何列表元素中。
$a = "Hello. Welcome Perl!\n";
@a = split(/ /, $a); # Three items: "Hello.", "Welcome", "Perl!\n"
你也可以指定第三个参数:将项目放入列表中的最大数量。拆分将在列表包含那么多项目时停止
$a = "Hello. Welcome Perl!\n";
@a = split(/ /, $a, 2); # Two items: "Hello.", "Welcome Perl!\n";
当然,你可以拆分的,你也可以 join()
。join()
函数接受一个字符串列表,并使用指定的字符串将它们连接在一起,该字符串可以是空字符串
@a = ("Hello.", "Welcome", "Perl!\n");
$a = join(' ', @a); # "Hello. Welcome Perl!\n";
$b = join(' and ', @a); # "Hello. and Welcome and Perl!\n";
$c = join('', @a); # "Hello.WelcomePerl!\n";
文件句柄
关于字符串我们就说到这里。现在让我们看看文件——毕竟,如果你不能在关键的地方进行字符串操作,字符串操作又有什么用呢?
要从文件中读取或写入数据,你必须先打开它。当你打开一个文件时,Perl会询问操作系统该文件是否可访问——如果你正在尝试读取它(或者如果你正在尝试创建一个新文件,那么它是否可以被创建),以及你是否拥有进行所需操作的必要文件权限?如果你可以使用该文件,操作系统将为你准备它,Perl会给你一个文件句柄。
你可以使用open()
函数来请求Perl为你创建一个文件句柄,该函数接受两个参数:你想要创建的文件句柄和你要操作的文件。首先,我们将专注于读取文件。以下语句使用文件句柄LOGFILE
打开文件log.txt
open (LOGFILE, "log.txt");
打开一个文件涉及许多幕后任务,Perl和操作系统会共同执行,例如检查你想要打开的文件实际上是否存在(或者如果你正在尝试创建一个新文件,那么它是否被创建),以及确保你被允许操作该文件(例如,你是否拥有必要的文件权限)。Perl会为你做所有这些事情,所以通常你不需要担心。
一旦你打开了文件以供读取,你可以通过使用<>
构造来检索其中的行。在尖括号内放置你的文件句柄名称。返回的内容取决于你想要获取的内容:在标量上下文(这是一种更技术性的说法,“如果你将其分配给标量”)中,你将检索文件的下一行,但如果你正在寻找列表,你将得到文件中所有剩余行的列表。(一个常见的技巧是使用for $lines (<FH>)
来检索文件中的所有行——这里的for
意味着你要求一个列表。)
当然,你可以关闭你已打开的文件句柄。你并不总是必须这样做,因为Perl足够聪明,知道在程序结束时或当你尝试重用现有的文件句柄时关闭文件句柄。尽管如此,使用close
语句是一个好主意。这不仅会使你的代码更易读,而且你的操作系统对一次可以打开的文件数量有限制,每个打开的文件句柄都会占用宝贵的内存。
以下是一个简单的程序,它将显示文件log.txt
的内容,并假设文件的第一行是其标题
open (LOGFILE, "log.txt") or die "I couldn't get at log.txt";
# We'll discuss the "or die" in a moment.
$title = <LOGFILE>;
print "Report Title: $title";
for $line (<LOGFILE>) {
print $line;
}
close LOGFILE;
写入文件
当你写入文件时,也会使用open()
。有两种方式可以打开文件进行写入:覆盖和追加。当你以覆盖模式打开文件时,你会擦除它之前包含的内容。在追加模式下,你将新数据附加到现有文件的末尾,而不会擦除任何已有的内容。
要表示你想要一个用于写入的文件句柄,你可以在想要使用的文件名前放置一个>
字符。这将以覆盖模式打开文件。要将其以追加模式打开,请使用两个>
字符。
open (OVERWRITE, ">overwrite.txt") or die "$! error trying to overwrite";
# The original contents are gone, wave goodbye.
open (APPEND, ">>append.txt") or die "$! error trying to append";
# Original contents still there, we're adding to the end of the file
一旦我们的文件句柄已打开,我们可以使用谦逊的print
语句来写入它。指定你想要写入的文件句柄以及你想要写入的值列表
print OVERWRITE "This is the new content.\n";
print APPEND "We're adding to the end here.\n", "And here too.\n";
自由生活或死去!
您可能已经注意到,我们的大部分open()
语句后面都跟着or die "some sort of message"
。这是因为我们生活在一个不完美的世界,程序并不总是按照我们的意愿运行。一个open()
调用可能会失败;也许您正在尝试写入不允许写入的文件,或者您正在尝试读取一个不存在的文件。在Perl中,您可以通过使用or
和and
来防止这些问题。
一系列用or
分隔的语句将一直执行,直到遇到一个成功的语句或返回一个真值。这条代码要么以覆盖模式成功打开OUTPUT
,要么导致Perl退出。
open (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 VITAL, ">vitalreport.txt";
如果这个open()
调用失败(例如,vitalreport.txt
属于另一个用户,该用户没有给您写入权限),您直到有人查看文件后才会知道,他们可能会想知道为什么重要报告没有写入。想象一下,如果“那个人”是您的老板,在您的年度绩效评估前一天,这是多么令人高兴的事情。当您使用or die
时,您可以避免所有这些。
open VITAL, ">vitalreport.txt" or die "Can't write vital report: $!";
您不必怀疑程序是否已经写入了您的关键报告,您将立即得到一个错误消息,该消息会告诉您出了什么问题,以及错误发生在程序的哪一行。
您可以使用or
来测试不仅仅是文件操作。
($pie eq 'apple') or ($pie eq 'cherry') or ($pie eq 'blueberry')
or print "But I wanted apple, cherry, or blueberry!\n";
在这个序列中,如果您有一个合适的pie,Perl会跳过链中的其余部分。一旦有一个语句工作,其余的都会被忽略。and
运算符做相反的事情:它评估您的语句链,但在一个语句不工作的时候停止。
open (LOG, "log.file") and print "Logfile is open!\n";
这条语句只有在open()
成功时才会显示“Logfile is open!”,您明白为什么吗?
子程序
到目前为止,我们的Perl程序只是一系列语句。如果您编写的是非常小的程序,这是可以接受的,但随着您需求的增长,您会发现这很有限。这就是为什么大多数现代编程语言允许您定义自己的函数;在Perl中,我们称它们为子程序。
子程序是用sub
关键字定义的,并为您的程序添加了新的功能。当您想要使用这个新功能时,您可以通过名称来调用它。例如,这里是一个名为boo
的子程序的定义。
sub boo {
print "Boo!\n";
}
boo(); # Eek!
(旧版本的Perl要求在调用子程序时在名称前加上&
字符。您不再需要这样做,但如果您在其他人的Perl代码中看到类似&boo
的代码,那就是原因。)
子程序很有用,因为它们允许您将程序分解成小的、可重用的块。如果您需要在程序的四个不同地方分析字符串,编写一个&analyze_string
子程序并在四次调用它要容易得多。这样,当您对字符串分析例程进行改进时,您只需要在一个地方进行,而不是四个地方。
Perl 内置函数可以接收参数并返回值,同样的,你的子程序也可以这样做。每次调用子程序时,传递给它的任何参数都会放在特殊的数组 @_
中。你也可以通过使用 return
关键字来返回单个值或一个列表。
sub multiply {
my (@ops) = @_;
return $ops[0] * $ops[1];
}
for $i (1 .. 10) {
print "$i squared is ", multiply($i, $i), "\n";
}
为什么我们使用了 my
关键字?这表示变量是子程序私有的,因此我们程序中其他地方使用的 @ops
数组的现有值不会被覆盖。这意味着你可以避免程序中的一类难以追踪的bug。你不必一定要使用 my
,但你也无需在敲钉子时砸到手指。这都是一些好主意。
你还可以使用 my
在子程序中设置局部变量,而无需立即分配值。这可以用于循环索引或临时变量。
sub annoy {
my ($i, $j);
for $i (1 .. 100) {
$j .= "Is this annoying yet?\n";
}
print $j;
}
如果你没有明确使用 return
语句,子程序会返回最后一个语句的结果。这种隐式返回值有时很有用,但它会降低程序的可读性。记住,你阅读代码的次数会比编写代码的次数多得多!
整合一切
在第一篇文章的末尾,我们有一个简单的利息计算器。现在让我们通过将利息表写入文件而不是屏幕来使它更有趣。我们还将代码拆分成子程序,以便更容易阅读和维护。
#!/usr/local/bin/perl -w
# compound_interest_file.pl - the miracle of compound interest, part 2
# First, we'll set up the variables we want to use.
$outfile = "interest.txt"; # This is the filename of our report.
$nest_egg = 10000; # $nest_egg is our starting amount
$year = 2000; # This is the starting year for our table.
$duration = 10; # How many years are we saving up?
$apr = 9.5; # This is our annual percentage rate.
&open_report;
&print_headers;
&interest_report($nest_egg, $year, $duration, $apr);
&report_footer;
sub open_report {
open (REPORT, ">$outfile") or die "Can't open report: $!";
}
sub print_headers {
# Print the headers for our report.
print REPORT "Year", "\t", "Balance", "\t", "Interest", "\t",
"New balance", "\n";
}
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 ($nest_egg, $year, $duration, $apr) = @_;
# We have two local variables, so we'll use my to declare them here.
my ($i, $line);
# Calculate interest for each year.
for $i (1 .. $duration) {
$year++;
$interest = &calculate_interest($nest_egg, $apr);
$line = join("\t", $year, $nest_egg, $interest,
$nest_egg + $interest) . "\n";
print REPORT $line;
$nest_egg += $interest;
}
}
sub report_footer {
print REPORT "\n Our original assumptions:\n";
print REPORT " Nest egg: $nest_egg\n";
print REPORT " Number of years: $duration\n";
print REPORT " Interest rate: $apr\n";
close REPORT;
}
请注意,当你将程序拆分成子程序时,程序逻辑会变得更加清晰。一个编写成小且命名良好的子程序的好品质是它几乎变成了 自我文档化 的。看看我们程序中的这四行
open_report;
print_headers;
interest_report($nest_egg, $year, $duration, $apr);
report_footer;
这样的代码在你六个月后回来需要了解它时非常有价值——你更愿意花时间阅读整个程序来找出它做什么,还是阅读四行就能告诉你程序 1) 打开一个报告文件,2) 打印一些标题,3) 生成一个利息报告,4) 打印报告页脚?
你还会注意到我们在 interest_report
和 calculate_interest
子程序中使用了 my
来设置局部变量。主程序中的 $nest_egg
的值从未改变。这在报告的末尾很有用,当时我们输出包含原始假设的页脚。由于我们在 report_footer
中从未指定局部 $nest_egg
,我们使用全局值。
试试看!
在本文中,我们探讨了文件(文件句柄、open()
、close()
和 <>
)、字符串操作(substr()
、split()
和 join()
)和子程序。这里有一对练习——同样,一个是简单的,一个是复杂的。
你有一个名为
dictionary.txt
的文件,其中包含词典定义,每行一个,格式为 “word space definition””。(这里是一个示例。)编写一个程序,可以从命令行查找单词。提示:
@ARGV
是一个特殊的数组,它包含你的命令行参数,你需要使用split()
的三个参数形式。)尝试增强它,以便你的词典也可以包含具有多个定义的单词,格式为 “word space definition:alternate definition:alternate definition, etc…”
。编写一个 Apache 日志分析器。你可以在 http://www.w3.org/Daemon/User/Config/Logging.html 找到常见日志格式的简要描述。你的分析器应该计算每个 URL 的请求总数、每个状态码的结果总数和输出字节的总量。
标签
反馈
这篇文章有什么问题?请帮助我们通过在GitHub上创建一个issue或pull request来解决。