Perl初学者入门 - 第2部分

编者按:这个久负盛名的系列正在进行更新。您可能对更新的版本感兴趣,可在以下位置找到:

目录

本系列第1部分
本系列第3部分
本系列第4部分
本系列第5部分
本系列第6部分

比较运算符
whileuntil
字符串比较
字符串的更多乐趣
文件句柄
写入文件
自由地生活或死去!
子程序
整合一切
玩一玩!

在上一篇文章中,我们讨论了Perl的核心元素:变量(标量、数组和哈希)、数学运算符和一些基本流程控制(for语句)。现在是时候与世界互动了。

在本期文章中,我们将讨论如何切片和切块字符串,如何处理文件以及如何定义自己的函数。但是,首先,我们将讨论Perl语言的一个核心概念:条件和比较。

比较运算符

在上篇文章中,我们跳过了Perl的一个重要元素:比较运算符。像所有优秀的编程语言一样,Perl允许你提出诸如“这个数字是否大于那个数字?”或“这两个字符串是否相同?”等问题,并根据答案执行不同的操作。

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

你可以使用这些运算符与Perl的条件关键字之一(如ifunless)一起使用。这两个关键字都接受一个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等于五并覆盖了它的旧值。(在后面的文章中,我们将讨论一种方法来确保这个错误不会在运行代码中出现。)

ifunless都可以跟一个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";
    }

whileuntil

两个稍微复杂的关键字是 whileuntil。它们都接受一个条件和一段代码块,就像 ifunless 一样,但它们的行为类似于 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中,您可以通过使用orand来防止这些问题。

一系列用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_reportcalculate_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来解决。