跳转至

Perl进阶

前言

  • Perl版本:v5.14

第1章 简介

1.1 获取帮助的方式

1.2 strict和warnings

  • 所有代码都应该打开strictwarnings,以规范编写的perl代码,如:

    #!/usr/local/bin/perl
    
    use strict;
    use warnings;
    

1.3 程序版本

  • 告知程序版本可以避免后续使用新版本的Perl时,会因为新加入的特性导致程序无法正常工作。
  • 方法:
    • use 5.014
    • use v5.14.2
    • use 5.14.2

1.4 书单

  • 有助于编写可复用代码:《Perl Best Practices》- Damian Conway

第2章 使用模块

2.1 CPAN

2.2. 阅读模块手册

使用perldoc来阅读模块文档,Unix则用man也可以,如:

perldoc File::Basename

# On Unix
man File::Basename

2.3 功能接口

  • 加载模块:

    use File::Basename;
    
    • 这样会把模块的默认符号加载到当前脚本中。
    • 注意:如果当前脚本中已经有该符号则会被覆盖。
  • 选择需要导入的内容,仅导入fileparsebasename

    # fool way
    use File::Basename ('fileparse', 'basename');
    
    # good way
    use File::Basename qw( fileparse basename );
    
    # full namespace
    my $dirname = File::Basename::dirname($some_path);
    
    use File::Basename (); # no import
    
    • 空列表特指不导入任何符号。

2.4 面向对象的接口

use File::Spec;

my $filespec = File::Spec->catfile( $homedir{homqyy}, 'web_docs', 'photos', 'USS_Minnow.gif');

2.5 核心模块内容

  • Module::CoreList是一个数据结构和接口库,该模块把很多关于Perl 5版本的模块历史信息聚合在一起,并且提供一个可编程的方式以访问它们:

    use Module::CoreList;
    
    # 查看在perlv5.01400中的CPAN版本
    print $Module::CoreList::version{5.01400}{CPAN};
    
  • 也可以在bash中直接运行命令corelist% corelist Module::Build

2.6 通过CPAN安装模块

  • 自动安装:

    • 使用perl自带的cpan工具安装:% cpan Perl::Critic
    • 使用cpanp(CPAN Plus):% cpanp -i Perl::Tidy
    • 使用cpanm(CPAN Minus):% cpanm DBI WWW::Mechanize
      • 零配置,轻量级的CPAN客户端
  • 手动安装:

    • Makefile.PL:
      1. 下载perl模块包:% wget <URL> (该URL可以从CPAN站点中获取)
      2. 解压perl模块包:% tar -xzf <MODULE.tar.gz>
      3. 进入模块目录:% cd <MODULE>
      4. % perl Makefile.PL
        • 可以用INSTALL_BASE参数来指定安装的路径:perl Makefile.PL INSTALL_BASE=/home/homqyy/
      5. % make
      6. % make test
      7. % make install
    • Build.PL
      1. 下载perl模块包:% wget <URL> (该URL可以从CPAN站点中获取)
      2. 解压perl模块包:% tar -xzf <MODULE.tar.gz>
      3. 进入模块目录:% cd <MODULE>
      4. % perl Build.PL
        • 可以用--install_base参数来指定安装路径:% perl Build.PL --install_base /home/homqyy/
      5. % perl Build
      6. % perl Build test
      7. % perl Build install

2.7 搜索路径

  • perl是通过@INC数组里的路径去搜索模块的,可以通过以下两种方式获取@INC的值:

    1. % perl -V
    2. % perl -le "print for @INC
  • 程序中添加指定路径:

    BEGIN {
        unshift @INC, '/home/homqyy/lib';
    }
    
    use lib '/home/homqyy/lib'; # good way. Action the same to above example
    
    use constant LIB_DIR => '/home/homqyy/lib'; # set a constant
    use lib LIB_DIR;
    
    • 推荐使用use lib,它是在编译执行,行为等效于首个例子:将参数给的路径加入到@INC数组的开头。
    • use constant可以设置常量,也是在编译时运行。
  • 上述的指定路径是硬编码,不具备可移植性,我们可以利用FinBin模块来实现更通用的添加路径:

    use FindBin qw($Bin); # $Bin is path of the current script
    use lib $Bin; # join path of the script to @INC
    use lib "$Bin/lib"; #join "$Bin/lib" to @INC
    use lib "$Bin/../lib"; #join "$Bin/../lib" to @INC
    

2.8 在程序外部设置搜索路径

  • 使用环境变量 PERL5LIB% export PER5LIB=/home/homqyy/lib:/usr/local/lib/perl5
  • 设立 PERL5LIB 环境变量的目的是为了给非管理员用户也能够扩展Perl的安装路径,如果管理员想增加额外的安装目录,只需要重新编译并安装Perl即可。
  • 也可以在程序运行的使用通过 -I 选项来扩展安装路径:% perl -I/home/homqyy/lib test.pl

2.9 local::lib

  • 在没有管理员权限的时候,我们需要有个便携的安装路径以及自动找到路径的方法,这时候就可以使用 local::lib

  • 安装

    • 该模块还不是核心模块,需要用 cpan 下载:% cpan local::lib
  • 查看提供的环境变量:% perl -Mlocal::lib

  • 使用其安装模块:

    • 对于 cpan% cpan -I Set::Crossproduct
    • 对于 cpanm% cpanm --local-lib HTML::Parser
  • 在脚本中自动将安装的路径加载到 @INC 中:

    use local::lib;
    
  • 自定义路径:

    % perl -Mlocal::lib='~/perlstuff'
    

第3章 中级基础

  • 常见列表操作符:
    • print:打印
    • sort:正序排列
    • reverse:反序排列
    • push:添加元素

3.1 使用grep过滤列表

  • grep会测试列表中的每个值,如果测试结果为真(标量上下文),则将值返回:

    my @input_numbers = (1, 2, 4, 8, 10, 15, 20, 51, 60);
    
    # 1. compare express
    my @bigger_then_10 = grep $_ > 10, @input_numbers; # filter number of greater than 10
    my $bigger_than_10_num = grep $_ > 10, @input_numbers; # count number of greater than 10
    
    # 2. pattern
    my @end_in_0 = grep /4$/, @input_numbers; # filter number of end in 4
    
    # 3. function
    my @odd_digit_sum = grep digit_sum_is_odd($_), @input_numbers;
    
    sub digit_sum_is_odd {
        my $input = shift;
        my @digits = split //, $input; # Assume no nondigit characters
        my $sum;
        $sum += $_ for @digits;
        $sum % 2;
    }
    
    # 4. block of code
    
    my @odd_digit_sum = grep {
        my $input = $_;
        my @digits = split //, $input; # Assume no nondigit characters
        my $sum;
        $sum += $_ for @digits;
        $sum % 2;
    } @input_numbers;
    
    # 5. optimization
    
    my @odd_digit_sum = grep {
        my $sum;
        $sum += $_ for split //;
        $sum % 2;
    } @input_numbers;
    
    • 这里的 $_ 是列表中的每个元素值,而且是别名,即:如果修改了 $_ 值,则原值也将被修改。
    • 代码快实际上是一个**匿名子例程**。
    • 将**示例3**用**示例4**的代码块代替时,有两处需要变更:
      • 不再使用入参,而是$_;
      • 不使用return

3.2 使用map转换列表

  • 功能是将列表中的元素转换成另一个(列表上下文)。与grep一样,支持**表达式**和**代码块**。

    my @input_numbers = (1, 2, 4, 8, 16, 32, 64);
    my @result = map $_ + 100, @input_numbers; # @result = (101, 102, 104, 108, 116, 132, 164)
    
  • 因为是列表上下文,因此一个元素可以产生多个结果:

    my @input_numbers = (1, 2, 4, 8, 16, 32, 64);
    my @result = map { $_, 3 * $_ } @input_numbers; # @result = (1, 3, 2, 6, 4, 12, 8, 24, 16, 48, 32, 96, 64, 192)
    
  • 列表转hash:当列表成对时,可以将其转成hash,列表会被按'Key-Value'解析:

    my %hash = @result;
    
    # 或则直接用 map
    my %hash = map { $_, 3 * $_ } @input_numbers;
    
    • 有时候我们不关心键值,只关心是否有键存在,这时候可以如此:

      my %hash = map { $_, 1 } @castaways;
      
      my $person = 'Gilligan';
      
      if ( $hash{$person} )
      {
          print "$person is a castaway.\n";
      }
      
  • 可以返回一个空列表 () 来表示此次结果不记录到列表中,即删除列表中的某个元素

    my @result = map {
        my @digits = split //, $_;
        if ($digits[-1] == 4)
        {
            @digits;
        }
        else
        {
            # 不处理非4结尾的数据
            ();
        }
    } @input_numbers;
    
    • 因此,利用此特性我们可以用 map 来代替 grep

3.3 使用eval捕获错误

  • 使用 eval 来捕获错误,避免程序因为出错直接崩溃。如果捕获到错误,则 $@ 会有值,反之则为空。最常见的用法就是在 eval之后立刻判断 $@ 的值:

    eval { $average = $total / $count };
    print "Continuing after error: $@" if $@;
    
  • eval 语句块后的分号是必须的,因为它是一个术语,语句块是真实的语句块,而不是像ifwhile

  • eval 语句块中可以包含 my 等任意语句。
  • eval 语句块有类似子例程的返回值(最后一行表达式求值,或者之前通过 return 返回的值)。如果块中代码运行失败,在标量上下文中返回 undef ,在列表上下文中返回空列表 ()

    my $average = eval { $total / $count };
    
  • eval 语句块不能捕获最严重的错误:使perl自己中断的错误。

  • 可以使用 Try::Tiny 来处理复杂的异常:

    use Try::Tiny;
    my $average = try { $total / $count } catch { "NaN" };
    

3.4 使用eval动态编译代码

  • 这是 eval 的第二种形式,它的参数是**字符串表达式**而不是**代码块**:

    foreach my $operator ( qw(+ - * /) )
    {
        my $result = eval "2 $operator 2";
        print "2 $operator 2 is $result\n";
    }
    

3.5 使用do语句块

  • do 将一组语句汇聚成单个表达式,其执行结果为最后一个表达式的值。do 非常适合创建一个操作的作用域:

    my $file_contents = do {
        local $/;
        local @ARGV = ( $filename );
        <>
    };
    
  • do还支持**字符串参数**的形式:

    do $filename;
    
    1. do 语句查找文件并读取该文件,然后切换内容为 eval 语句块的字符串形式,以执行它。
    2. 因此 do 将忽视文件中的任何错误,程序将继续执行。

3.6 require

  • 除了 use 也可以用 require 来加载模块,实际 use 等效于以下代码:

    BEGIN {
        require List::Util;
        List::Util->import(...);
    }
    
    • 但是 require 实际上是在运行程序时执行的,不过 require 可以记录所加载的文件,避免重复加载同样的文件。

第4章 引用简介

  • 这里的引用,效果类似指针,但与指针不同的是这里指向的是整个数组,而不是首个元素。

4.1 在多个数组上完成相同的任务

  • 示例:为了海上安全,每个登上Minnow的人需要有一套救生用具、一瓶防晒霜、一个水壶和一件雨衣:

    sub check_required_items
    {
        my $who = shift;
        my %whos_items = map { $_, 1 } @_; # the rest are the person's items
    
        my @required = qw(preserver sunscreen water_bottle jacket);
    
        for my $item (@required)
        {
            unless ( $whos_items{$item} )
            {
                # not found in list?
                print "$who is missing $item.\n";
            }
        }
    }
    
    my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
    my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
    check_required_items('skipper', @skipper);
    check_required_items('professor', @professor);
    
  • 上述示例在将数组的值传递给了方法check_required_items,如果值大量的话势必会造成大规模的复制数据,浪费空间并损耗性能。

4.2 Perl图形结构(PeGS)

  • 该图形由**Joseph Hall**开发,用图形可以方便的解释Perl,图形内容不做记录。

4.3 数组引用

  • 取引用

    my $ref_to_skipper = \@skipper;
    
    my $second_fef_to_skipper = $reference_to_skipper;
    
  • 可以将引用的地址传递给函数,这样我们需要的话还能直接修改其值,如同C语言的指针一样

    check_required_items("The Skipper", \@skipper);
    
    sub check_required_items
    {
        my $who = shift;
        my $items = shift; # 获取引用值
    
        my %whose_items = map { $_, 1 } @$items; # 解引用
        my @required = qw(preserver sunscreen water_bottle jacket);
    
        for my $item (@required)
        {
            unless ( $whose_items{ $item } ) # 没有在列表中发现该工具
            {
                print "$who is missing $item.\n";
                push @missing, $item;
            }
        }
    
        if (@missing)
        {
            print "Adding @missing to @$items for $who.\n";
            push @$items, @missing; # 修改引用项的内容:向里面增加缺失的工具
        }
    }
    
  • 解引用

    • 用括号来解引用,应@表明以数组的方式解引用:@{ $itmes }
    • 如果引用项是简单的$+裸字,则可以省略掉中间的括号:@$items

4.4 嵌套的数据结构

  • 假设我们有如下嵌套结构:

    my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
    my @skipper_with_name = ('Skipper' => \@skipper);
    
    my @all_with_names = {
        \@skipper_with_name,
        # ...其他元素
    }
    
  • 运用4.3知识,@{ $all_with_names[0] } 进行解引用可以得到带有两个元素的数组 @skipper_with_name 的引用。接着可以用如下方式获取名字和其携带的工具:

    my $who = ${ $all_with_names[0] }[0]
    my $skipper = ${ $all_with_names[0] }[1]
    
    # 或则用一个循环来遍历所有人
    
    for my $person (@all_with_names)
    {
        my $who = $$person[0];
        my $provisions_reference = $$person[1];
    
        check_required_items($who, $provisions_reference);
    }
    

4.5 用箭头简化嵌套元素的引用

  • 学过C语言的都知道->具有解引用的效果,而Perl也支持类似的方式:
    • ${ $all_with_names[0] }[0] 等效于 $all_with_names[0]->[0]
    • ${ $all_with_names[0] }[0][1] 等效于 $all_with_names[0]->[0][1]

4.6 散列的引用

  • 与数组一样,只是散列解引用时用的是%{}

    # 创建一个散列结构
    my %gilligan_info = {
        name        => 'Gilligan',
        hat         => 'White',
        shirt       => 'Red',
        position    => 'First Mate',
    };
    
    my $hash_ref = \%gilligan_info; # 引用散列
    
    # 获取名称
    $name = ${ $hash_ref }{'name'}; # 带括号的形式
    $name1 = $$hash_ref{'name'} # 不带括号的形式
    $name2 = $hash_ref->{'name'} # `->`形式
    
    # 重获散列结构
    my @keys = keys %{ $hash_ref }
    

4.7 数组与散列的嵌套引用

  • 结合4.5和4.6即可,比如:

    my %gilligan_info = {
        name        => 'Gilligan',
        hat         => 'White',
        shirt       => 'Red',
        position    => 'First Mate',
    };
    
    my %skipper_info = {
        name        => 'Skipper',
        hat         => 'Black',
        shirt       => 'Blue',
        position    => 'Captain',
    };
    
    my @crew = (\%gilligan_info, \%skipper_info); # 数组元素引用了散列
    
  • 可以如下使用各值:

    • 选用一个我喜欢的方式:$crew[0]->{'name'}

4.8 检查引用类型

  • 使用 ref 来检查引用类型:

    use Carp qw(croak);
    
    sub show_hash
    {
        my $hash_ref = shift;
        my $ref_type = ref $hash_ref;
    
        croak "I expected a hash reference!"
            unless $ref_type eq 'HASH';
    
        while (my ($key, $value) = each %$hash_ref )
        {
            print "$key => $value\n";
    
            # ...其他
        }
    }
    
    # 测试:成功
    &show_hash({'name' => 'successful'});
    
    # 测试:失败
    &show_hash(['failed']);
    
  • 但是 ref 返回的是引用类型的字符串,如果有一个表现类似于散列引用的对象,程序将运行失败,因为字符串是不同的。,可以使用Scalar::Util模块的reftype代替:

    use Carp qw(croak);
    
    sub show_hash
    {
        my $hash_ref = shift;
        my $ref_type = ref $hash_ref;
    
        croak "I expected a hash reference!"
            unless $ref_type eq 'HASH';
    
        foreach my $key ( %$hash_ref )
        {
            # ...其他
        }
    }
    
  • 也可以用eval来检查:

    sub is_hash_ref
    {
        my $hash_ref = shift;
    
        return eval { keys %$ref_type; 1 };
    }
    

第5章 引用和作用域

  • Perl通过“引用计数”的来实现“自动回收垃圾”的机制。即,一块数据仅当引用计数为0时被销毁,且被销毁的数据空间通常并不会返还给操作系统,而是保留给下一次需要空间的数据使用。

  • 每创建一个数据的时候,引用计数值初始为1。

5.1 循环引用造成内存泄露

  • 示例:

    {
        my @data1 = qw(one won);
        my @data2 = qw(two too to);
    
        # 创建循环引用
        push @data2, \@data1; # @data2引用@data1,'qw(one won)'的引用数变成2
        push @data1, \@data2; # @data1引用@data2,'qw(two too to)'的引用数变成2
    }
    
    # 由于@data1和@data2超出作用域,因此引用计数分别减1,但是引用值仍旧不为0,内存泄露!
    
  • 使用引用计数在循环引用的情况下无法正常处理,因为它的引用计数将永远不为0:如例子,@data1@data2结束生命周期后,两个列表的引用计数都还为1。

  • 因此,我们必须谨防创建循环引用,或则在不得不这样做的时候,在变量超出作用于之前打断“环”:

    {
        my @data1 = qw(one won);
        my @data2 = qw(two too to);
    
        # 创建循环引用
        push @data2, \@data1; # @data2引用@data1,'qw(one won)'的引用数变成2
        push @data1, \@data2; # @data1引用@data2,'qw(two too to)'的引用数变成2
    
        # 打破环
        @data1 = (); # 解除对@data2的引用,'qw(one won)'引用数减1,剩下1
        @data2 = (); # 解除对@data2的引用,'qw(two too to)'引用数减1,剩下1
    }
    
    # 由于@data1和@data2超出作用于,因此引用计数从1减为0,回收数据空间
    

5.2 匿名数组和散列

  • 匿名数组使用[]创建,匿名散列由{}创建:

    # 匿名数组
    my $array_ref = ['one', 'two'];
    
    # 匿名散列
    my $hash_ref = {
        one => '1',
        two => '2',
    };
    
  • 由于匿名散列与代码块有冲突,因此我们可以在左括号前加入一个+来显示的告诉Perl这是一个匿名散列,在左括号后面加入一个;来显示表示是一个代码块:

    +{
        'one' => 1,
        'two' => 2,
    } # 这是一个匿名散列
    
    {;
        push @array, '1';
    } # 这是一个代码块
    

5.3 自动带入

  • 如果没有给变量(或者访问数组或者散列中的单个元素)赋值,Perl将自动创建代码过程假定存在的引用类型。

  • 示例:

    my $not_yet; # 未定义的变量
    @$not_yet = (1, 2, 3); # 由于我们将$not_yet当成了数组的引用使用,因此Perl自动作了'$not_yet = []'的操作
    
    my $not_yet2; # 未定义的变量
    %$not_yet2 = (
        'one' => 1,
        'two' => 2,
    ); # 由于我们将$not_yet当成了散列的引用使用,因此Perl自动作了'$not_yet = {}'的操作
    
    # 多层赋值
    
    my $top;
    $top->[2]->[4] = 'lee-lou'; # 同样的,这里将沿途自动创建对应的匿名数组
    

第6章 操作复杂的数据结构

6.1 使用调试器

  • 在运行程序时添加-d参数来启动调试模式,类似于C程序的gdb% perl -d ./test.pl
  • 使用方法可以查阅手册perldebug,或则在通过-d启动后输入h来查看帮助。

6.2 使用 Data::Dumper 模块查看复杂数据

  • 该模块提供了一个基本方法,将Perl的数据结构显示为Perl代码:
  • 输入文件为:
  • 结果如下所示:
$VAR1 = {
          'thurston.howell.hut' => {
                                     'lovey.howell.hut' => 1250
                                   },
          'professor.hut' => {
                               'lovey.howell.hut' => 1360,
                               'gilligan.crew.hut' => 1250
                             },
          'ginger.girl.hut' => {
                                 'maryann.girl.hut' => 199,
                                 'professor.hut' => 1218
                               }
        };
  • 该模块目的是用于后续重建数据结构使用,因此输出结构就是一段完成的Perl代码

  • 其他转储程序

    • Data::Dump

      use Data::Dump qw(dump);
      dump( \%total_bytes );
      
    • Data::Printer

      use Data::Printer;
      p( %total_bytes );
      

6.4 数据编组

  • 数据编组:利用Data::Dumper可以将复杂的数据结构转化为字节流,这样可以供另一个程序使用。
  • 因为Data::Dumper输出的符号将变成普通的**VAR**符号,这样会影响阅读,因此可以利用Dump接口来实现符号的定义:

    print Data::Dumper->Dump(
            [\@data1, \@data2],
            [qw(*data1, *data2)]
        );
    
  • 更适合编组的模块 Storable:原因是其生成的更短小并且易于处理的文件:

    • 要求:必须把所有数据放入到一个引用中
  • 使用方法1:内存

  • 使用方法2:文件
  • 浅复制和深复制:平时使用的my @d1 = @d2是浅拷贝,而Storable提供了深拷贝的方法:my @d1 = @{ dclone \@d2 }

  • YAML模块:通过该模块可以让被Data::Dumper编组后的数据可读性更强

  • JSON模块:提供了将数据结构与JSON格式间相互转换的方法

第7章 对子例程的引用

7.1 引用子例程

  • 与数组和散列引用一样,也是用\进行引用,比如:

    my $ref_to_greeter = \&skipper_greets; # '&'是函数
    
  • 解引用也是有3种:

    # 1 大括号
    &{ $ref_to_greeter } ('Gilligan');
    
    # 2 只由简单的符号和裸字时可以省略大括号
    &$ref_to_greeter('Gilligan');
    
    # 3 用'->'
    $ref_to_greeter->('Gilligan');
    
  • 匿名子例程,格式为:sub { ...body of subroutine };,结果是创建了一个匿名函数的引用,比如:

    my $ginger = sub {
        my $person = shift;
        print "Ginger: (in a sultry voice) Well hello, $person!\n"
    };
    
    $ginger->('Skipper');
    
  • 回调函数:通过传递一个函数的引用形成回调,比如:

    use File::Find;
    
    my @starting_directories = qw(.);
    
    find(
        sub
        {
            print "$File::Find::name found\n";
        }, # 回调函数,每搜索到一个结果都会被调用
        @starting_directories, # 搜索的目录
    );
    

7.2 闭包

  • 在Perl术语中,闭包是一个包含已经超出作用域的词法变量的子例程:

    use File::Find;
    
    sub create_find_callback_that_counts
    {
        my $count = 0;
        return sub { print ++$count, ": $File::Find::name\n" }; # 因为匿名子例程引用了'$count',因此'$count'的引用数为2
    }
    
    my $callback = create_find_callback_that_counts();
    find($callback, '.'); # '$count'的引用数仍为1,不会被销毁回收
    
  • 返回多个子例程:

    use File::Find;
    
    sub create_find_callbacks_that_sum_the_size {
        my $total_size = 0;
        return (sub { $total_size += -s if -f }, sub { return $total_size });
    }
    
    my ($count_em, $get_results) = create_find_callbacks_that_sum_the_size();
    find($count_em, '.');
    
    my $total_size = &$get_results();
    print "total size of . is $total_size\n";
    
  • 也可以通过参数来初始化**闭包变量**

  • 命名子例程也可以作为闭包函数,但是闭包变量的位置很重要:

    • 首先,my $countdown = 10这句赋值语句有两层含义:
      1. 在编译阶段,将$countdown翻译为词法变量,并且值都为undef
      2. 在运行阶段,给$countdown赋值为10
    • 因此,以下代码将不能正常工作:

      sub count_down
      {
          $countdown--; # 此时$countdown还未定义
      };
      
      my $countdown = 10;
      
      count_down();
      count_down();
      
      print $countdown;
      
    • 解决方案:

      1. $countdown提前放置:

        my $countdown = 10;
        sub count_down
        {
            $countdown--;
        };
        
        count_down();
        count_down();
        
        print $countdown;
        
      2. 将闭包变量和定义都放到BEGIN中测试失败):

        BEGIN {
            my $countdown = 10;
            sub count_down { $countdown-- };
        }
        
        count_down();
        count_down();
        
        print $countdown;
        
      3. 使用state变量:在Perl v5.10开始引入了此功能,类似于C语言的静态变量(static声明的):

        use v5.10;
        
        sub add_to_tab {
            my $castaway = shift;
            state @castaways = qw(Ginger Mary Ann Gilligan); # 编译错误
            state %tab = map { $_, 0 } @castaways; # 编译错误
            $tab{$castaway}++;
        
            print "$castaway: $tab{$castaway}\n";
        }
        
        add_to_tab('Ann');
        add_to_tab('Ann');
        
  • 转储闭包:使用Data::Dump::Streamer模块可以将代码引用和闭包的内容展示出来

    use Data::Dump::Streamer;
    
    my @luxuries = qw(Diamonds Furs Caviar);
    
    my $hash = {
        gilligan        => sub { say 'Howdy Skipper!' },
        Skipper         => sub { say 'Gilligan!!!!' },
        'Mr. Howell'    => sub { say 'Money money money!' },
        Ginger          => sub { say $luxuries[rand @luxuries] },
    };
    
    Dump $hash;
    

第8章 文件句柄引用

  • 在 Perl v5.6 及后续版本,open支持打开匿名的临时文件:

    # 文件名设置为'undef'
    open my $fh, '+>', undef
        or die "Could not open temp file: $!";
    

8.1 typeglob

  • 在旧版本上,使用符号表(是typeglob,书籍翻译成符号表有点不好理解,因为还有个symbol table)来传递文件句柄:

    open LOG_FH, '>>', 'castaways.log'
        or die "Could not open castaways.log: $!";
    
    log_message(*LOG_FH, 'The Globetrotters are stranded with us!');
    
    sub log_message
    {
        local *FH = shift; # 包变量不允许使用词法变量'my',这里使用'local'
        print FH @_, "\n";
    }
    

8.2 标量

  • 从Perl v5.6开始,open能够用标量来存储句柄了,前提是该变量的值必须是undef
  • 建议在文件句柄部分加上大括号,以显示声明我们的意图
  • 当标量超出作用域后Perl将自动关闭对应的文件句柄,可以不显示的关闭
  • 如果想在老版本中使用标量,则可通过模块IO::Scalar来实现。
  • 示例:

    open my $log_fh, '>>', 'castaways.log'
        or die "Could not open castaways.log: $!";
    
    print {$log_fh} "We have no bananas today!\n";
    
    while (<>)
    {
        print {$log_fh}; # 用花括号包裹文件句柄
    }
    

8.3 指向字符串

  • 从Perl v5.6开始,能够以文件句柄的形式打开一个标量而不是文件:

    open my $string_fh, '>', \ my $string; # 这里必须是标量的引用
    print {$string_fh} "1234\n";
    
    print "string: $string";
    
  • 可以利用此特性和钻石操作符结合来处理多行字符串:

    my $multiline_string = "data1\ndata2\ndata3\n";
    open my $string_fh, '<', \ $multiline_string;
    while (<$string_fh>)
    {
        print;
    }
    

8.4 IO::Handle

  • 将文件句柄以对象的形式使用:

    use IO::Handle;
    
    open my $fh, '>', $filename or die '...';
    
    $fh->print('Coconut headphones');
    $fh->close;
    

8.5 IO::File

  • 使用该模块以一个更友好的方式来使用文件句柄:

    use IO::File;
    
    # 打开一个文件:与C语言的fopen类似
    my $read_fh = IO::File->new('castaways.log', 'r');
    my $append_fh = IO::File->new('castaways.log', O_WRONLY|O_APPEND);
    
    # 创建临时文件
    my $temp_fh = IO::File->new_tmpfile;
    

8.6 IO::Tee

  • 多路通道输出模块,该模块功能等效于shell工具:tee

8.7 IO::Pipe

  • 使用该模块来执行命令:

    use IO::Pipe;
    
    my $pipe = IO::Pipe->new;
    
    $pipe->reader("$^X -V"); # $^X is the current perl executable
    
    while (<$pipe>)
    {
        print "Read: $_";
    }
    
    $pipe->close;
    
    # TODO: writer有点问题,一直在运行中
    
    # my $w_pipe = IO::Pipe->new;
    
    # $w_pipe->writer("perl");
    
    # print $w_pipe "print 'ok!'";
    
    # $w_pipe->close;
    

8.8 IO::Null

  • 可以利用此模块进行调试输出的设置:

    use IO::Null;
    
    my $Debug = 1;
    
    my $debug_fh = $Debug ? *STDOUT : IO::Null->new;
    
    $debug_fh->print("Hey, the radio's not working!");
    
    undef $Debug;
    

8.9 IO::Dir

  • 用该模块去操作目录

9 正则表达式引用

  • 预编译操作符:qr//
  • 如果用单引号'作为分隔符(qr''),则Perl解释器就不会做任何双引号插入操作:qr'$var'
  • 正则表达式选项:
    • 可以用3种方式添加选项(flags):
      • 在匹配或替换操作符最后一个分隔符后面添加:m/pattern/flagss/pattern/flags
      • 在qr后面添加:qr/pattern/flags
      • 在模式本身中指定:?flags:pattern
        • 能够在模式中的flag前面追加一个-号表明要移除某个修饰符:qr/abc(?x-i:G i l l i g a n)def/i,使用了x,移除了i
  • 正则表达式可以以变量的形式使用,通过qr//操作符来创造引用:

    use experimental qw(smartmatch);
    
    my $string = 'Gilligan test';
    my $regex = qr/Gilligan/;
    
    print "variable of regex\n" if $string =~ m/$regex/;
    
    # 也可以直接绑定,不用使用 m//
    print "normal regex\n" if $string =~ $regex;
    print "smartmatch\n" if $string ~~ $regex;
    
    $string =~ s/$regex/Skipper/;
    
    print "after modify: $string\n";
    
    use experimental qw(smartmatch);
    use feature say;
    
    my @patterns = (
        qr/(?:Willie )?Gilligan/,
        qr/Mary Ann/,
        qr/Ginger/,
        qr/(?:The )?Professor/,
        qr/Skipper/,
        qr/Mrs?\. Howell/,
    );
    
    my $name = 'Ginger';
    
    # 手动遍历
    foreach my $pattern (@patterns)
    {
        if ($name =~ $pattern)
        {
            say "Manual Match!";
            last;
        }
    }
    
    # 智能匹配将遍历数组中的每个元素
    say "Smart Match!" if $name ~~ @patterns;
    
  • 当在一个更大的模式中引用正则表达式时,正则的引用其相当于一个原子(原理是qr操作的pattern会自动加上非捕获圆括号(?:)my $poor_people = qr/$r1|$r2/;

  • **RFC 1738**规定了URL格式,其翻译结果如下:

    my $alpha       = qr/[a-z]/;
    my $digit       = qr/\d/;
    my $alphadigit  = qr/(?i:$alpha|$digit)/;
    my $safe        = qr/[\$_.+-]/;
    my $extra       = qr/[!*'\(\),]/;
    my $national    = qr/[{}|\\^~\[\]`]/;
    my $reserved    = qr|[;/?:@&=]|;
    my $hex         = qr/(?i:$digit|[A-F])/;
    my $escape      = qr/%$hex$hex/;
    my $unreserved  = qr/$alpha|$digit|$safe|$extra/;
    my $uchar       = qr/$unreserved|$escape/;
    my $xchar       = qr/$unreserved|$reserved|$escape/;
    my $ucharplus   = qr/(?:$uchar|[;?&=])*/;
    my $digits      = qr/(?:$digit){1,}/;
    
    my $hsegment    = qr/ucharplus;/;
    my $hpath       = qr|$hsegment(?:/$hsegment)*|;
    my $search      = $ucharplus;
    my $scheme      = qr|(?i:https?://)|;
    my $port        = qr/$digits/;
    my $password    = $ucharplus;
    my $user        = $ucharplus;
    
    my $toplevel    = qr/$alpha|$alpha(?:$alphadigit|-)*$alphadigit/;
    my $domainlabel = qr/$alphadigit|$alphadigit(?:$alphadigit|-)*$alphadigit/;
    my $hostname    = qr/(?:$domainlabel\.)*$toplevel/;
    my $hostnumber  = qr/$digits\.$digits\.$digits\.$digits/;
    my $host        = qr/$hostname|$hostnumber/;
    my $hostport    = qr/$host(?::$port)?/;
    my $login       = qr/(?:$user(?::$password)\@)?/;
    
    my $urlpath     = qr/(?:(?:$xchar)*)/;
    
  • 使用Regexp::Common模块来直接获取某个pattern。

  • egexp::Assemble模块帮助我们建立高效的择一匹配

  • List::Util模块中的first函数功能类似grep,但是它只要成功命中一次就停止继续匹配。

    my ( $math ) = fist { $name =~ $patterns{$_} } keys %patterns;
    

第10章 使用的引用技巧

10.1 施瓦茨变换

  • 一个高效的排序结构:

    my @output_data =
        map     { EXTRACTION },  # 提取
        sort    { COMPARISON }   # 比较,如果是哈希形式的话,可以如此使用:{ $a->{xxx} cmp $b->{xxx} }
        map     [ CONSTRUCTION ],# 构造,也支持哈希的形式:map { CONSTRUCTION }
    
    • 因为map操作符和sort操作符是从右到左执行的,所以我们应该由下而上地读这个结构。
  • 示例

    my @names =
        map $_->[0],
        sort { $b->[1] <=> $a->[1] }
        map [ $_, ask_monkey_about($_) ],
        @castaways
    
    map $_->{VALUE},
    sort {
        $a->{LOWER} cmp $b->{LOWER} or
        $a->{ID}    <=> $b->{ID}    or
        $a->{NAME}  AND $b->{NAME} }
    map {
        VALUE => $_,
        LOWER => lc,
        ID    => get_id($_),
        NAME  => get_name($_),
    },
    @input_data;
    

10.2 递归定义的数据

在递归算法的不同分支上拥有多个基线条件是很常见的。没有基线条件的递归算法将是无限循环。

递归子例程有一个调用它本身的分支用于处理部分任务,以及一个不调用它本身的分支用于处理基线条件。

注意

类似Perl的动态语言无法自动将“尾递归”转为循环,因为再一次调用子例程之前,子例程定义可能改变。

use Data::Dumper;

sub data_for_path {
    my $path = shift;

    if (-f $path or -l $path) # files or symbolic links
    { 
        return undef;
    }

    if (-d $path)
    {
        my %directory;
        opendir PATH, $path or die "Cannot opendir $path: $!";
        my @names = readdir PATH;
        closedir PATH;

        for my $name (@names)
        {
            next if $name eq '.' or $name eq '..';
            $directory{$name} = data_for_path("$path/$name");
        }

        return \%directory;
    }

    warn "$path is neither a file nor a directory\n";
    return undef;
}

print Dumper(&data_for_path('../'));

10.3 避免递归

  • 迭代取代递归的模板:

    sub iterative_solution {
        my ($start) = @_;
    
        my $data = {};
        my @queue = ( [$start, $data] );
    
        while ( my $next = shift @queue )
        {
            # ... process current element ...
            # ... add new things to @queue ...
        }
    
        return $data;
    }
    
    • 在模板中,@queue数组中的每项都携带希望处理的全部元素,其中每个元素都是匿名数组。此时start是需要处理的项,并且$data是用于存储结果的引用。
  • 深度优先解决方案:FIFO

    • 优势:能够在任意喜欢的层级很轻易地停留。
  • 广度优先解决方案:LIFO


第11章 构建更大型的程序

11.1 基本概念

  • 函数获取参数的方法:

    • my $arg = shift:作者更喜欢这种
    • (my $arg) = @_:与lua风格相似
  • .pm扩展名是“Perl模块”的意思

11.2 嵌入代码

  • eval嵌入代码:eval $code_string; die $@ if $@;

    • 类似bash下的resourceeval可以调用作用域下的任何词法变量
  • do嵌入代码:do 'Navigation.pm'; die $@ if $@;

    • 导入的代码作用域在do自己里面,因此类似my等语句并不会影响主程序。
    • 不会搜索模块目录,因此需要提供绝对路径或相对路径。
  • require嵌入代码:追踪文件,可以避免重复

    • 导入文件中的任何语法错误都将终止程序,所以不再需要很多die $@ if $@语句;
    • 文件中的最后一个求值表达式必须返回一个真值,因此require语句才能知道该文件正确导入
      • 由于这个特点,用于require的文件在末尾都需要加个神秘的1

11.3 命名空间

  • 命名空间可以避免符号冲突。
  • 对于.pm的文件,在文件开头增加命名空间:package Navigation;
  • 命名规则与变量一样,包名应当以一个大写字母开头(来源于perlmodlib文档)
  • 包名也可以由::(双冒号)分隔多个名称:Name1::Name2
  • 主程序的包名为main

  • Package有作用域:

    • 代码块:

      package Navigation;
      
      { # start scope block
          package main; # now in package main
      
          sub turn_toward_heading { # main::turn_toward_heading
              ... code here ...
          }
      } # end scope block
      
      # back to package Navigation
      
      sub turn_toward_heading { # Navigation::turn_toward_heading
          ... code here ...
      }
      
    • Perl v5.12后支持包语句块:

      package Navigation {
          ... code here ...
      }
      
  • 无论当前包如何定义,有些名称或变量总在main包中:

    • 名称:ARGV, ARGVOUT, ENV, INC, SIG, STDERR, STDIN, STDOUT
    • 标点符号变量:$_, $2
  • 设置包版本的方法:

    • 设置$VERSION的值:our $VERSION = '0.01'
    • 在v5.12版本后可以用package指定:package Navigation 0.01

第12章 创建你自己的发行版本

  • 构建方法有两种:
    • Makefil.PL:老的,基于make,使用ExtUtils::Maker构建
    • Build.PL:新的,存Perl工具,使用Module::Build构建

12.1 构建工具

  • h2xs
  • Module::Starter
    • 创建模板:Module::Starter::Plugin
  • Dist::Zilla:这个模块不但可以自动创建发行版,而且在我们修改发行版中的文件后,它还知道如何更新发行包。

12.2 Build.PL

  1. 创建构建框架:% module-starter --module=Animal --author="yourName" --email="yourEmail" --verbose --mb
  2. 创建构建脚本: % perl Build.PL
  3. 开始构建:% ./Build
  4. 执行测试:% ./Build test
  5. 发行前检测一下内容是否有遗漏:% ./Build disttest
  6. 发行版本:% ./Build dist

12.3 Makefile.PL

  1. 创建构建框架:% module-starter --module=Animal --author="homqyy" --email="youEmail" --verbose --builder="ExtUtils::Makemaker"
  2. 创建构建脚本: % perl Makefile.PL
  3. 开始构建:% make
  4. 执行测试:% make test
  5. 发行前检测一下内容是否有遗漏:% make disttest
  6. 发行版本:% make dist

12.3 添加额外的模块

  1. 安装插件:Module::Starter::AddModule
  2. 添加额外的模块:module-starter --moudle=addon_module --dist=.

12.4 目录文件介绍

  • MANIFEST:记录检查的结果
  • META.*文件描述了发行版本的信息,其中客户端最关系_require相关字段。

12.5 文档

  • pod语法:perldoc pod
  • 检测格式:podchecker
  • 查看或产生文档:
    • 查看:perldoc module.pm
    • 产生文档:pod2html, pod2man, pod2text, pod2usage

12.5.1 段落

  • 标题:=head n

    # 1级标题
    =head1 NAME
    
    # 2级标题
    =head2 DESCRIPTION
    
    # 3级标题
    =head3 Functions
    
    # 返回代码模式
    =cut
    
  • 有序列表:

    # 指明缩进空格数n:over n
    =over 4
    
    =item 1. Gilligan
    
    =item 2. Skipper
    
    =item 3. Ginger
    
    # 结束列表
    =back
    
  • 无序列表

    # 指明缩进空格数n:over n
    =over 4
    
    =item * Gilligan
    
    =item * Skipper
    
    =item * Ginger
    
    # 结束列表
    =back
    
  • 文本:

    # 文本可以直接添加,无需任何标记
    =head1 SYNOPSIS
    
    # 直接书写的文本会被自动换行
    Quick summary of what the module does.
    
    Perhaps a little code snippet.
    
    # 如果不换行可以开启“逐字段落”:以空格开始的文本
        use Animal;
        my $foo = Animal->new();
    
    =cut
    

12.5.2 文本格式

  • 每一种含格式都以一个大写字母开始,并且用<>括住所需的内容
    • B<粗体文本>
    • C<代码文本>
    • E<实体文本>
    • I<斜体文本>
    • L<链接文本>
  • 根据需要,可以增加<>的个数,只要成对就行:B<<< 粗体文本 >>>

  • 使用utf8:

    =encoding utf8
    
    文本内容
    

第13章 对象简介

  • 面向对象编程(OOP)
  • 对于Perl来说,仅当程序超过1000行时(经验值),OOP的溢出才能显露出来
  • OOP书籍:
    • 《Object Oriented Perl》Damian Conway(Manning出版社)

13.1 调用方法

  • Class->method(@args)

    • 这种调用方式,会隐式的添加类名作为首个参数,等效于Class::method(Class, @args)
    # 包名
    use Cow;
    
    Cow->speek;
    
    # 变量
    
    my $beast = 'Cow';
    
    $beast->speek;
    
  • Class::Method('Class', @args)

    • 该调用方法与Class->method(@args)等效

13.2 继承

  • 示例1:

    package Cow;
    
    use Animal;
    
    our @ISA = qw(Animal); # 这里表明继承了'Animal'这个类
    
    sub sound { "mooo" }; # 这里重载了'Animal->sound'方法
    
  • 示例2(等效示例1):

    use v5.10.1;
    
    package Cow;
    
    # 等效于 'use Animal;' 和 'ousr @ISA = qw(Animal);'
    use parent qw(Animal);
    
    sub sound { "mooo" };
    
  • 使用类的方式调用函数时,Perl的调用过程为:

    1. 构建参数列表
    2. 先尝试查找Class::method
    3. @ISA中按序找,比如:$ISA[0]::method$ISA[1]::method、...
    4. 调用找到的方法,并将1中保存的参数列表传入
      • 首个参数是类名
  • @ISA注意事项:

    • @ISA中查找都是递归的,深度优先,并且从左到右进行。

13.3 调用父类方法

  • 直接调用(不提倡):

    package Mouse;
    use parent qw(Animal);
    
    sub sound { 'squeak' };
    
    sub speak {
        my $class = shift;
        $class->Animal::speak; # 直接调用,这里无法保证Animal一定有speak,所以不是好方法。
        print "[but you can barely hear it!]\n";
    }
    
  • SUPER调用(提倡)

    package Mouse;
    use parent qw(Animal);
    
    sub sound { 'squeak' };
    
    sub speak {
        my $class = shift;
        $class->SUPER::speak; # 通过SUPER调用,会自动递归的找
        print "[but you can barely hear it!]\n";
    }
    

第14章 测试简介

  • 测试模块:
    • 基本模块:Test::More
    • 其他模块:Test::*
  • 测试文档:Test::Turorial

  • 声明测试数量

    # 1. 在开头写明测试数量
    use Test::More;
    plan tests => 1;
    
    # ... 
    
    # 2. 或则在末尾声明测试结束
    done_testing();
    
  • 测试的艺术:

    • 我们需要测试代码运行中断的情况,以及代码正常工作的情况。
    • 需要测试边界和中间情况。
    • 如果某种情况应当抛出异常,我们也要确保测试不会有不良的副作用:传递额外的参数或则多余的参数,或则没有传递足够的参数,搞混命名参数的大小写。
  • 处理浮点数:Test::Number::Delta

  • 测试有两种模式,通过以下两个环境变量区分:

    • RELEASE_TESTING:作者自行的测试,为发行前的准备
    • AUTOMATED_TESTING:自动测试,在用户侧进行的测试
  • 模块编译检查:在BEGIN中使用use_ok()

    #!perl -T
    
    use Test::More;
    plan tests => 1;
    
    BEGIN {
        use_ok( 'Animal' ) || print "Bail out!\n";
    }
    
  • 由于开启了“污染”检查模式(perl -T),因此PERL5LIB这个环境变量会被忽略,需要自行指定搜索路径:

    • 使用-I指定:perl -Iblib/lib -T t/00-load.t
    • 使用blib模块搜索:perl -Mblib -T t/00-load.t
  • TODO标注那些期望测试失败的用例,类似于备忘,该用例失败后不会作为失败处理。其中,$TODO作为测试的标签:

    TODO: {
        local $TODO = "Need to replace the boilerplate text";
    
        ...
    }
    
  • 测试Pod:当安装了Test::PosTest::Pos::Coverage时,./Build test会对Pod进行测试。

  • 测量测试覆盖率:

    1. 安装模块Devel::Cover
    2. 执行% ./Build testcover来进行覆盖率测量
    3. 输出报告:% cover

第15章 带数据的对象


第x章 环境变量汇总

  • PERL5LIB:设置搜索路径
    • Linux可用 : 分隔多个搜索路径
    • Windows可用 ; 分隔多个搜索路径

第x章 模块汇总

  • Cwd > 提供了获取当前路径的方法

  • Data::Dumper > 数据编组:将Perl的数据结构转为Perl代码(字节流)

  • Data::Dump

  • Data::Printer

  • File::Basename > 处理路径

  • File::Spec > 类似于File::Basename,但是是面向对象的。

  • File::Find > 提供一个可移植的方式高效的遍历给定文件系统的层次结构

  • List::Unit > > - first: 与grep用法一样,只是匹配一次成功就返回

  • Math::BigInt > 能够处理超出Perl本身范围的数字

  • 构建工具

    • h2xs
    • Module::Starter
    • Module::Starter::Plugin > 创建模板
    • Dist::Zilla > 这个模块不但可以自动创建发行版,而且在我们修改发行版中的文件后,它还知道如何更新发行包。
  • Module::Starter > 一个好用的构建发行版本的模块,支持插件

  • Regexp::Common > > - Abigail,Perl的一位正则表达式大事,将大部分复杂的模式放入一个模块中 > - 该模块使用了tie,详情可以查看perltie文档

  • Regexp::Assemble > 该模块帮助建立高效的择一匹配

  • Spreadsheet::WriteExcel > 创建并写入Excel数据

  • HTTP::SimpleLinkChecker > URL检测

  • Try::Tiny > 异常处理模块

  • Storable > 数据编组:将Perl的数据结构转为二进制流,并且提供了深拷贝

  • IO::Handle > > - Pler实际上使用该模块实现文件句柄操作,因此,文件句柄标量实际上是IO::Handler模块的对象。 > - 自 Perl v5.14 之后,不必显示加载 IO::Handler模块

  • IO::File > 该模块是IO::Handle模块用于操作文件的子集。属于标准发型版本。

  • IO::Scalar > 如果使用的Perl是古老的版本,会出现不支持标量引用文件句柄的情况,这时候可以用该模块来支持此功能

  • IO::Pipe > 该模块是IO::Handle模块的前端,只要提供一条命令,就自动处理forkexec命令,有点类似于C语言的popen

  • IO::Null > 创建一个空文件句柄,等效于/dev/null

  • IO::Interactive > 返回一个文件句柄,当对该句柄进行写操作的时候,如果调用的程序是daemon则不输出,反之则输出到屏幕

  • IO::Dir

    • 自 v5.6起,该模块称为Perl标准发行版的一部分,其将对目录的操作打成包,便于使用。

第x章 问题汇总

  • localmy的区别: > local在后续调用的子例程中可以使用,而my则不行,其余的都一样: > >
    $v = 999; # global variable
    
    sub output
    {
        my $name = shift;
        print "$name value: $v\n";
    }
    
    sub call_local
    {
        local $v = shift;
        output('local');
    }
    
    sub call_my
    {
        my $v = shift;
        output('my');
    }
    
    call_my(1);
    call_local(2);
    

评论