使用模板工具包创建数据输出文件

目录

更复杂的示例

生成XML

多种格式

介绍模板工具包

有许多Perl模块被普遍认为是完成某些任务的正确选择。如果您没有使用DBI访问数据库,没有使用LWP模块从WWW获取数据,或者没有使用XML::Parser或其子类解析XML,那么您就有可能被礼貌的Perl社会所排斥。

我相信2000年出现了另一个“必备”的Perl模块——模板工具包。我不认为我独自持有这种观点,因为模板工具包在上个夏天Perl会议上赢得了“最佳新模块”奖。模板工具包的2.0版(朋友们称之为TT2)最近已发布到CPAN。

TT2由Andy Wardley设计并编写,<[email protected]>。它源于Andy之前的一个模板模块Text::Metatext,以Fred Brooks“计划扔掉一个”的最佳方式诞生;目的是成为最有用(或者至少是最常用的)Perl模板系统。

TT2提供了一种方式,可以从一个固定模板文本文件(模板)中嵌入可变数据。这种用法的一个明显例子是创建动态网页,这也是TT2受到大量关注的地方。在这篇文章中,我希望演示TT2在非Web应用中也同样有用。

使用模板工具包

让我们看看如何使用TT2处理一个简单的数据文件。TT2是一个面向对象的Perl模块。下载它并将其以通常方式安装到CPAN后,在您的程序中使用它就像在代码中放入以下行一样简单:

    use Template;

    my $tt = Template->new;

。构造函数new接受一些可选参数,这些参数在模块的大量手册页中有文档说明,但为了这篇文章的目的,我们将尽可能保持简单。

要处理模板,您可以像这样调用process方法:

    $tt->process('my_template', \%data)
      || die $tt->error;

process方法接受两个参数,第一个是要处理的模板文件名(在本例中为my_template),第二个是包含您想在模板中使用的数据项的哈希引用。如果处理模板出现任何错误,程序将带着一个(希望)有用的错误消息退出。

那么%data中可以放入什么类型的东西呢?答案是几乎任何东西。以下是一个展示关于英格兰超级联赛足球队的示例。

    my @teams = ({ name => 'Man Utd',
                   played => 16,
                   won => 12,
                   drawn => 3,
                   lost => 1 },
                 { name => 'Bradford',
                   played => 16,
                   won => 2,
                   drawn => 5,
                   lost => 9 });

    my %data = ( name => 'English Premier League',
                 season => '2000/01',
                 teams => \@teams );

这创建了三个可以访问的数据项,分别称为nameseasonteams。请注意,teams是一个复杂的数据结构。

以下是我们可以用来处理这些数据的模板。

    League Standings

    League Name: [% name %]
    Season     : [% season %]

    Teams:
    [% FOREACH team = teams -%]
    [% team.name %] [% team.played -%] 
     [% team.won %] [% team.drawn %] [% team.lost %]
    [% END %]

使用此数据运行此模板会得到以下输出

    League Standings

    League Name: English Premier League
    Season     : 2000/01

    Teams:
    Man Utd 16 12 3 1
    Bradford 16 2 5 9

希望模板的语法足够简单,易于理解。以下是一些需要注意的点。

  • 模板处理指令使用一种简单的语言,这种语言不是Perl。
  • %data的键已变成模板中数据变量的名称。
  • 模板处理指令由[%%]序列包围。
  • 如果这些标签被替换为[%- -%],则取消前一个或后一个换行符。
  • FOREACH循环中,teams列表的每个元素依次分配给临时变量team
  • 分配给team变量的每个项都是一个Perl哈希。哈希中的单个值使用点符号访问。

这些观点中,可能第一个和最后一个最为重要。第一个观点强调了数据获取逻辑与展示逻辑的分离。创建展示模板的人不需要了解Perl,他们只需要知道将传递到模板中的数据项。

最后一个观点展示了TT2如何保护模板设计师免受数据结构实现的干扰。传递给模板处理器的数据对象可以是标量、数组、哈希、对象甚至是子程序。模板处理器将正确解释你的数据,并“做正确的事”返回正确的值。在这个例子中,每个队伍都是一个哈希,但在一个更大的系统中,每个队伍可能是一个对象,在这种情况下,nameplayed等将是访问底层对象属性的访问器方法。由于模板处理器会意识到需要调用方法而不是访问哈希值,因此模板不需要做任何更改。

一个更复杂的例子

英格兰足球联赛的统计数据通常以比我们上面使用的稍微复杂一些的格式呈现。一组完整的统计数据将显示一个队伍赢得、输掉或平局的比赛数量,该队得分和失分,以及因此该队获得的积分。队伍赢得一场比赛获得三分,平局获得一分。当队伍积分相同时,它们将通过净胜球来区分,即该队进球数减去对方进球数。为了使事情更加复杂,赢得、平局和输掉的比赛以及进球数通常是按照主客场分开计算的。

因此,如果你有一个列出队伍名称以及按主客场分开计算的赢得、平局和输掉的赛事以及进球数(总共11个数据项)的数据源,你可以计算所有其他项目(净胜球、获得积分和联赛排名)。让我们看看这样一个文件,但我们将只关注前三名队伍。它看起来可能像这样

  Man Utd,7,1,0,26,4,5,2,1,15,6
  Arsenal,7,1,0,17,4,2,3,3,7,9
  Leicester,4,3,1,10,8,4,2,2,7,4

一个简单的脚本来读取这些数据到一个哈希数组可能看起来是这样的(我已经简化了数据列的名称 - w、d和l分别表示赢得、平局和输掉的赛事;f和a表示得分和失分;数据项名称前的h和a表示是否是主场或客场统计数据)

  my @cols = qw(name hw hd hl hf ha aw ad al af aa);

  my @teams;
  while (<>) {
    chomp;

    my %team;

    @team{@cols} = split /,/;

    push @teams, \%team;
  }

然后我们可以再次遍历队伍,并计算所有派生的数据项

  foreach (@teams) {
    $_->{w} = $_->{hw} + $_->{aw};
    $_->{d} = $_->{hd} + $_->{ad};
    $_->{l} = $_->{hl} + $_->{al};

    $_->{pl} = $_->{w} + $_->{d} + $_->{l};


    $_->{f} = $_->{hf} + $_->{af};
    $_->{a} = $_->{ha} + $_->{aa};

    $_->{gd} = $_->{f} - $_->{a};

    $_->{pt} = (3 * $_->{w}) + $_->{d};
  }

然后生成一个按降序排列的列表

  @teams 
    = sort { $b->{pt} <=> $b->{pt}
             || $b->{gd} <=> $a->{gd} } @teams;

最后添加联赛排名数据项

  $teams[$_]->{pos} = $_ + 1 
    foreach 0 .. $#teams;

在将所有数据拉入内部数据结构后,我们可以开始使用我们的模板生成输出。一个创建包含主客场统计数据的CSV文件的模板可能看起来像这样

  [% FOREACH team = teams -%]
  [% team.pos %],[% team.name %],[% team.pl %],[% team.hw %],
  [%- team.hd %],[% team.hl %],[% team.hf %],[% team.ha %],
  [%- team.aw %],[% team.ad %],[% team.al %],[% team.af %],
  [%- team.aa %],[% team.gd %],[% team.pt %]
  [%- END %]

并按如下方式处理它

  $tt->process('split.tt', { teams => \@teams }, 'split.csv')
    || die $tt->error;

生成以下输出

  1,Man Utd,16,7,1,0,26,4,5,2,1,15,6,31,39
  2,Arsenal,16,7,1,0,17,4,2,3,3,7,9,11,31
  3,Leicester,16,4,3,1,10,8,4,2,2,7,4,5,29

请注意,我们向process引入了第三个参数。如果缺少此参数,TT2将输出发送到STDOUT。如果此参数是标量,则它被视为写入输出的文件名。此参数还可以是(包括其他事项)文件句柄或实现print方法的对象的引用。

如果我们对主客场游戏的区分不感兴趣,那么我们可以使用一个更简单的模板,如下所示

  [% FOREACH team = teams -%]
  [% team.pos %],[% team.name %],[% team.pl %],[% team.w %],
  [%- team.d %],[% team.l %],[% team.f %],[% team.a %],
  [%- team.aa %],[% team.gd %],[% team.pt %]
  [% END -%]

这将产生以下输出

  1,Man Utd,16,12,3,1,41,10,6,31,39
  2,Arsenal,16,9,4,3,24,13,9,11,31
  3,Leicester,16,8,5,3,17,12,4,5,29

生成XML

这开始展现出TT2的一些强大功能和灵活性,但你可能会想,用代码中的foreach循环和几个print语句也能轻松实现相同的效果。当然,这是真的;但这是因为我故意选择了简单的例子来解释这些概念。如果我们想生成包含数据的XML文件呢?还有,如果我们(正如我之前提到的)的联赛数据存储在一个对象中呢?那么代码看起来会更容易,因为我们之前编写的大部分代码都将隐藏在FootballLeague.pm中。

  use FootballLeague;
  use Template;

  my $league = FootballLeague->new(name => 'English Premier');

  my $tt = Template->new;

  $tt->process('league_xml.tt', { league => $league })
    || die $tt->error;

league_xml.tt模板可能看起来像这样

  <?xml version="1.0"?>
  <!DOCTYPE LEAGUE SYSTEM "league.dtd">

  <league name="[% league.name %]" season="[% league.season %]">
  [% FOREACH team = league.teams -%]
    <team name="[% team.name %]"
          pos="[% team.pos %]"
          played="[% team.pl %]"
          goal_diff="[% team.gd %]"
          points="[% team.pt %]">
       <stats type="home">
              win="[% team.hw %]"
              draw="[%- team.hd %]"
              lose="[% team.hl %]"
              for="[% team.hf %]"
              against="[% team.ha %]" />
       <stats type="away">
              win="[% team.aw %]"
              draw="[%- team.ad %]"
              lose="[% team.al %]"
              for="[% team.af %]"
              against="[% team.aa %]" />
    </team>
  [% END -%]
  </league>

请注意,因为我们已经将整个对象传递给了process,所以我们需要在模板变量上添加一个额外的间接层——现在一切都是league变量的组成部分。除此之外,模板中的其他内容与我们之前使用的非常相似。现在,team.name调用的是一个访问器函数而不是执行哈希查找,但对于我们的模板设计者来说,所有这些都是透明的。

多种格式

作为一个最终的例子,让我们假设我们需要以多种格式创建我们的足球联赛表格。也许我们将这些数据传递给其他人,他们不能都使用相同的格式。一些用户需要CSV文件,而另一些用户需要XML。一些用户需要将数据分割成主场比赛和客场比赛,而另一些用户只需要总数。因此,我们总共需要四个不同的模板,但好消息是它们可以使用相同的数据对象。脚本需要做的只是确定所需的模板并处理它。

  use FootballLeague;
  use Template;

  my ($name, $type, $stats) = @_;

  my $league = FootballLeague->new(name => $name);

  my $tt = Template->new;

  $tt->process("league_${type}_$stats.tt", 
               { league => $league }
               "league_$stats.$type")
    || die $tt->error;

例如,以这种方式调用此脚本

  league.pl 'English Premier' xml split

这将处理名为league_xml_split.tt的模板,并将结果放入名为league_split.xml的文件中。

这开始展现出模板工具的真实力量。如果我们后来想添加另一种文件格式——比如我们想创建联赛表格HTML页面或者甚至是LaTeX文档——我们只需要创建适当的模板,并按照现有的命名约定命名它。我们不需要对代码进行任何修改。

我希望你现在能明白为什么模板工具正迅速成为许多人Perl安装的必备部分。

标签

大卫·克罗斯

大卫·克罗斯是一位经验丰富的Perl程序员,在开发创新和高效的软件解决方案方面拥有丰富的专业知识。他在Perl社区中有很强的影响力,为社区贡献了许多模块,并通过演讲和工作坊分享了他的知识。在编程之外,大卫喜欢深入研究开源项目并与志同道合的爱好者合作。他讨厌写个人简介,所以他把这项工作交给了ChatGPT。

浏览他的文章

反馈

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