你需要了解的Perl

简介

在深入探讨mod_perl编程的细节之前,回顾一些重要的Perl基础知识可能是个不错的想法。当你开始为mod_perl编程时,你会发现这些知识非常有价值。我会从纯Perl笔记开始,逐步过渡到解释为mod_perl编程的特定之处,展示可能陷入的陷阱,并解释一些对我们中的一些人来说显而易见但对其他人来说可能不是的问题。

使用全局变量以及在模块/包之间共享它们

当你以结构化的方式编写应用程序,使用perl包时,这会很有帮助,但正如你可能知道的,一旦开始使用包,在各个包之间共享变量就变得困难得多。配置包可以作为想要其变量可以从其他模块访问的包的好例子。

当然,使用面向对象(OO)编程是提供访问变量的最佳方式。但如果你还没有准备好使用OO技术,那么你仍然可以从中受益于我将要讨论的技术。

使变量全局

当你第一次在你的代码中写入$x时,你创建了一个(包)全局变量。它在你的程序中的任何地方都是可见的,尽管如果在除了声明它的包之外的包中使用(默认为main::),那么它必须使用其完全限定的名称来引用,除非你已经使用import()导入了这个变量。这只有在没有使用strict祈使句的情况下才会工作;但如果你想在mod_perl下运行脚本,那么使用这个祈使句是很重要的。

启用strict祈使句时使变量全局

首先,使用

  use strict;

然后使用

 use vars qw($scalar %hash @array);

这将在当前包中声明命名变量为包全局变量。它们可以在同一文件和包中使用它们的非限定名称进行引用;在不同的文件/包中使用它们的完全限定名称。

从perl5.6开始,你可以使用our运算符代替

  our ($scalar, %hash, @array);

如果你想在包之间共享包全局变量,那么这里是你能做的事情。

使用Exporter.pm共享全局变量

假设你想要在模块之间共享CGI.pm对象(我将使用$q)。例如,你创建它在script.pl中,但你希望它在My::HTML中可见。首先,你使$q成为全局的。

  script.pl:
  ----------------
  use vars qw($q);
  use CGI;
  use lib qw(.); 
  use My::HTML qw($q); # My/HTML.pm is in the same dir as script.pl
  $q = CGI->new;
  My::HTML::printmyheader();

注意我已经从My::HTML中导入了$q。并且My::HTML导出了$q

  My/HTML.pm
  ----------------
  package My::HTML;
  use strict;

  BEGIN {
    use Exporter ();

    @My::HTML::ISA         = qw(Exporter);
    @My::HTML::EXPORT      = qw();
    @My::HTML::EXPORT_OK   = qw($q);

  }
  use vars qw($q);
  sub printmyheader{
    # Whatever you want to do with $q... e.g.
    print $q->header();
  }
  1;

因此,$qMy::HTML包和script.pl之间共享。如果对象在My::HTML中创建但在script.pl中使用,它也会反之亦然。这是一个真正的共享,因为如果你在script.pl中更改$q,那么它也会在My::HTML中更改。

如果你需要在超过两个包之间共享$q怎么办?例如,你希望My::Doc也共享$q

你让My::HTML保持不变,并将script.pl修改为包含

 use My::Doc qw($q);

然后,将我在My::HTML中使用的相同的Exporter代码添加到My::Doc中,以便它也导出$q

一个可能的陷阱是,当你想在My::HTMLscript.pl中同时使用My::Doc时。只有当你添加

  use My::Doc qw($q);

将变量导入到 My::HTML 中时,$q 将会被共享。否则 My::Doc 将不再共享 $q。为了清晰起见,以下是代码:

  script.pl:
  ----------------
  use vars qw($q);
  use CGI;
  use lib qw(.); 
  use My::HTML qw($q); # My/HTML.pm is in the same dir as script.pl
  use My::Doc  qw($q); # Ditto
  $q = new CGI;

  My::HTML::printmyheader();  

  My/HTML.pm
  ----------------
  package My::HTML;
  use strict;

  BEGIN {
    use Exporter ();
    @My::HTML::ISA         = qw(Exporter);
    @My::HTML::EXPORT      = qw();
    @My::HTML::EXPORT_OK   = qw($q);
  }
  use vars     qw($q);
  use My::Doc  qw($q);
  sub printmyheader{
    # Whatever you want to do with $q... e.g.
    print $q->header();
    My::Doc::printtitle('Guide');
  }
  1;  

  My/Doc.pm
  ----------------
  package My::Doc;
  use strict;

  BEGIN {
    use Exporter ();
    @My::Doc::ISA         = qw(Exporter);
    @My::Doc::EXPORT      = qw();
    @My::Doc::EXPORT_OK   = qw($q);
  }
  use vars qw($q);
  sub printtitle{
    my $title = shift || 'None';
    print $q->h1($title);
  }
  1;

使用 Perl 的别名功能共享全局变量

正如标题所说,您可以在不使用 Exporter.pm 的情况下将变量导入到脚本或模块中。我发现将所有配置变量放在一个模块 My::Config 中非常有用。但这样我就必须导出所有变量才能在其他模块中使用它们,这有两个缺点:用额外的标记污染其他包的命名空间,增加了内存需求;以及跟踪配置模块应该导出哪些变量、导入哪些变量的开销,这对于某些特定包来说可能很麻烦。我通过将所有变量保存在一个散列 %c 中并导出这个散列来解决此问题。以下是一个 My::Config 的例子:

  package My::Config;
  use strict;
  use vars qw(%c);
  %c = (
    # All the configs go here
    scalar_var => 5,

    array_var  => [qw(foo bar)],
    hash_var   => {
                   foo => 'Foo',
                   bar => 'BARRR',
                  },
  );
  1;

现在在想要使用配置变量的包中,我必须使用完全限定的名称,如 $My::Config::test,这我不喜欢,或者像上一节所述那样导入它们。但是,嘿,由于我只需要处理一个变量,我可以使事情更加简单,并节省加载 Exporter.pm 包的开销。我将使用 Perl 的别名功能来导出并保存按键

  package My::HTML;
  use strict;
  use lib qw(.);
    # Global Configuration now aliased to global %c
  use My::Config (); # My/Config.pm in the same dir as script.pl
  use vars qw(%c);
  *c = \%My::Config::c;

    # Now you can access the variables from the My::Config
  print $c{scalar_var};
  print $c{array_var}[0];
  print $c{hash_var}{foo};
O'Reilly 开源大会 -- 7月22-26日,加州圣地亚哥。

从研究前沿到企业核心

mod_perl 2.0,下一代 Stas Bekman 将在即将到来的 O'Reilly 开源大会(7月22-26日,圣地亚哥)的演讲中概述 mod_perl 2.0 中的新功能以及未来计划的其他内容。

当然,当您像上面描述的那样使用它时,$c 是全局的,如果您更改它,它将影响您已别名为 $My::Config::c 的任何其他包。

请注意,别名既可以与全局变量也可以与 local() 变量一起使用 - 您不能写

  my *c = \%My::Config::c; # ERROR!

这是一个错误。但是您可以写

  local *c = \%My::Config::c;

有关别名的更多信息,请参阅 Camel 书的第二版,第 51-52 页。

使用非硬编码的配置模块名称

您已经看到了如何使用配置模块进行配置集中化以及轻松访问存储在此模块中的信息。然而,存在一个鸡生蛋的问题:如何让您的其他模块知道这个文件的名称?硬编码名称是脆弱的 - 如果您只有一个项目,那么这应该没问题。如果您有更多使用不同配置的项目,并且希望重用它们的代码,那么您将不得不找到所有硬编码名称的实例并将它们替换。

另一个解决方案可能是给配置模块同一个名称,如 My::Config,但将不同副本放在不同的位置。但这在 mod_perl 中不会工作,因为命名空间冲突。您不能加载使用相同名称的不同模块;只有第一个会被加载。

幸运的是,还有一个解决方案可以使我们更灵活。PerlSetVar 出现来拯救。就像环境变量一样,您可以为服务器设置全局 Perl 变量,这些变量可以从任何模块和脚本中检索。这些语句放置在 httpd.conf 文件中。例如

  PerlSetVar FooBaseDir       /home/httpd/foo
  PerlSetVar FooConfigModule  Foo::Config

现在我将 require() 上述配置将使用的文件。

  PerlRequire /home/httpd/perl/startup.pl

startup.pl 中,我可能会有以下代码:

    # retrieve the configuration module path
  use Apache:
  my $s             = Apache->server;
  my $base_dir      = $s->dir_config('FooBaseDir')      || '';
  my $config_module = $s->dir_config('FooConfigModule') || '';
  die "FooBaseDir and FooConfigModule aren't set in httpd.conf" 
    unless $base_dir and $config_module;

    # build the real path to the config module
  my $path = "$base_dir/$config_module";
  $path =~ s|::|/|;
  $path .= ".pm";
    # I have something like "/home/httpd/foo/Foo/Config.pm"
    # now I can pull in the configuration module
  require $path;

现在我知道模块名称并且它已加载,因此例如,如果我要使用此模块中存储的一些变量来打开数据库连接,那么我将做

  Apache::DBI->connect_on_init
  ("DBI:mysql:${$config_module.'::DB_NAME'}::${$config_module.'::SERVER'}",
   ${$config_module.'::USER'},
   ${$config_module.'::USER_PASSWD'},
   {
    PrintError => 1, # warn() on errors
    RaiseError => 0, # don't die on error
    AutoCommit => 1, # commit executes immediately
   }
  );

其中像

  ${$config_module.'::USER'}

在我的例子中实际上是

  $Foo::Config::USER

如果您想在运行时从您的代码中访问这些变量,而不是访问服务器对象 $c,那么请使用请求对象 $r

  my $r = shift;
  my $base_dir      = $r->dir_config('FooBaseDir')      || '';
  my $config_module = $r->dir_config('FooConfigModule') || '';

特殊Perl变量的作用域

现在让我们谈谈特殊Perl变量。

$|(缓冲)、$^T(脚本的开始时间)、$^W(警告模式)、$/(输入记录分隔符)、$\(输出记录分隔符)等特殊Perl变量都是真正的全局变量;它们不属于任何特定的包(甚至不包括main::)并且在整个程序中普遍可用。这意味着,如果您更改它们,那么在程序中的任何地方都会发生更改;此外,您不能使用my()来限制它们的范围。然而,您可以使用local()化它们,这意味着您应用的所有更改都只会在包含作用域的末尾持续。在mod_perl的情况下,如果子服务器通常不会退出,那么如果您的脚本中修改了一个全局变量,那么它将影响该进程生命周期的其余部分,并将影响由该进程执行的任何脚本。因此,推荐使用local()化这些变量;我认为甚至可以说是强制性的。

我将演示输入记录分隔符变量的情况。如果您取消定义此变量,那么如果您有足够的内存,菱形运算符(读取行)将一次将整个文件吸入。记住这一点,您绝对不应该编写如下示例的代码。

  $/ = undef; # BAD!
  open IN, "file" ....
    # slurp it all into a variable
  $all_the_file = <IN>;

正确的方法是在更改特殊变量之前使用local()关键字,如下所示

  local $/ = undef; 
  open IN, "file" ....
    # slurp it all inside a variable
  $all_the_file = <IN>;

但是有一个陷阱。local()将更改的值传播到其下面的代码。修改后的值将有效,直到脚本终止,除非在脚本的其他地方再次更改。

一种更简洁的方法是将受修改变量影响的整个代码块包裹在一个块中,如下所示

  {
    local $/ = undef; 
    open IN, "file" ....
      # slurp it all inside a variable
    $all_the_file = <IN>;
  }

这样,当Perl离开该块时,它会恢复$/变量的原始值,您不必在其他程序部分的代码中担心其值在这里被更改。

请注意,如果您在设置全局变量并在包含块内调用子例程之后调用子例程,则全局变量将以其新值在子例程中可见。

编译的正则表达式

最后,我想谈谈许多人已经陷入的陷阱。让我们谈谈mod_perl下的正则表达式使用。

当使用包含插值Perl变量的正则表达式时,如果已知变量(或变量)在程序执行期间不会更改,一个标准的优化技术是在正则表达式模式中添加/o修饰符。这指示编译器在脚本的生命周期内仅构建一次内部表,而不是每次执行模式时。

  my $pat = '^foo$'; # likely to be input from an HTML form field
  foreach( @list ) {
    print if /$pat/o;
  }

本系列之前的文章

在没有超级用户权限的情况下安装mod_perl

30分钟学会mod_perl

为什么要使用mod_perl?

这通常在列表循环或使用grep()map()运算符时是一个很大的胜利。

然而,在长时间运行的mod_perl脚本中,变量可能会在每次调用时更改,这可能会引起问题。新httpd子进程的第一个调用将编译正则表达式并正确执行搜索。但是,该子进程后续的所有使用将继续匹配原始模式,而不管模式应该依赖的当前Perl变量的内容如何。您的脚本似乎出现了故障。

该问题有两个解决方案

第一个是使用eval q//,强制代码每次都被评估。只需确保eval块覆盖整个处理循环,而不仅仅是模式匹配本身。

上述代码片段将被重写为

  my $pat = '^foo$';
  eval q{
    foreach( @list ) {
      print if /$pat/o;
    }
  }

只是说

  foreach( @list ) {
    eval q{ print if /$pat/o; };
  }

意味着我重新编译了列表中每个元素的正则表达式,即使正则表达式本身没有变化。

如果您需要在代码的某个部分中使用多个模式匹配运算符,可以使用这种方法。如果该部分仅包含一个运算符(无论是 m// 还是 s///),则可以依靠空模式的重用最后看到的模式属性。这导致了第二种解决方案,它还消除了对 eval 的使用。

上述代码片段变为

  my $pat = '^foo$';
  "something" =~ /$pat/; # dummy match (MUST NOT FAIL!)
  foreach( @list ) {
    print if //;
  }

唯一的问题是启动正则表达式引擎的占位符匹配必须绝对、肯定成功,否则模式将不会被缓存,而 // 将匹配一切。如果您不能依靠固定文本来确保匹配成功,那么您有两种可能性。

如果您可以保证模式变量不包含元字符(如 *, +, ^, $…),则可以使用占位符匹配

  $pat =~ /\Q$pat\E/; # guaranteed if no meta-characters present

如果有可能模式包含元字符,则应按如下方式搜索模式或不可搜索的 \377 字符

  "\377" =~ /$pat|^\377$/; # guaranteed if meta-characters present

另一种方法

这取决于您应用的 regex 的复杂性。一种常见的用法是,在反复“匹配一组模式中的任意一个”时,编译后的 regex 通常更有效。

可能使用辅助例程更容易记住。这里有一个稍微修改自 Jeffery Friedl 在其著作《精通正则表达式》中的示例。

  #####################################################
  # Build_MatchMany_Function
  # -- Input:  list of patterns
  # -- Output: A code ref which matches its $_[0]
  #            against ANY of the patterns given in the
  #            "Input", efficiently.
  #
  sub Build_MatchMany_Function {
    my @R = @_;
    my $expr = join '||', map { "\$_[0] =~ m/\$R[$_]/o" } ( 0..$#R );
    my $matchsub = eval "sub { $expr }";
    die "Failed in building regex @R: $@" if $@;
    $matchsub;
  }

示例用法

  @some_browsers = qw(Mozilla Lynx MSIE AmigaVoyager lwp libwww);
  $Known_Browser=Build_MatchMany_Function(@some_browsers);

  while (<ACCESS_LOG>) {
    # ...
    $browser = get_browser_field($_);
    if ( ! &$Known_Browser($browser) ) {
      print STDERR "Unknown Browser: $browser\n";
    }
    # ...
  }

在下一篇文章中,我将介绍一些与 mod_perl 编程直接相关的 Perl 基础知识。

参考资料

  • Jeffrey E. Friedl 著作《精通正则表达式》。

  • Randal L. Schwartz 著作《学习 Perl》(也称为“羊驼”一书,以书封上的羊驼图案命名)。

  • L.Wall、T. Christiansen 和 J.Orwant 著作《Perl 编程》(也称为“骆驼”一书,以书封上的骆驼图案命名)。

  • Exporterperlreperlvarperlmodperlmodlib 手册页。

标签

反馈

这篇文章有什么问题吗?请在 GitHub 上打开一个问题或拉取请求,帮助我们。