1

新手 Perl 程序员,试图将简单的 xml 字符串转换为制表符分隔的文本文件。我在使用 XML::Parser(和 XML::Twig/Simple 甚至 XSLT)时遇到了困难,但我不知道如何让主要数据部分成为列标题。

然后我开始尝试用 XSLT 来做,但我不知道如何在元素之间获取分隔符——(然后我会使用 split 和/或 join?)但它们都只是在一个字符串中一起运行。

我只是手动手动打印列标题。有没有一种简单的方法可以用模板做到这一点?

我查看了类似的问题,但看不到任何分隔符被添加到我的文件中。XML 到制表符分隔的文本 修改 XSLT 以将 XML 转换为制表符分隔的文本文件

问题:

  1. 一般来说,最简单的方法是什么,我什至应该使用 XSLT(我一直在努力理解)。

  2. 我该如何解决以下问题?

看起来我已经很接近了,但只需要在 XSLT 输出字符串中添加一个分隔符,这样我就可以将它拆分,然后在我的输出中将它与“\t”连接到制表符分隔的文本文件中。??

这是我的 XML(来自 Twilio 的 SMS 日志):

  <?xml version="1.0" encoding="UTF-8"?>
  <TwilioResponse>
     <SMSMessages end="49" firstpageuri="/2010-04-01/Accounts/ACcbaa0/SMS/Messages?Page=0&amp;PageSize=50" lastpageuri="/2010-04-01/Accounts/ACcbaa/SMS/Messages?Page=54&amp;PageSize=50" nextpageuri="/2010-04-01/Accounts/ACcbaa0103c/SMS/Messages?Page=1&amp;PageSize=50&amp;AfterSid=SMc20cf7" numpages="55" page="0" pagesize="50" previouspageuri="" start="0" total="2703" uri="/2010-04-01/Accounts/ACcbaa0103cf/SMS/Messages">
        <SMSMessage>
           <Sid>SMe24eb108b7eb6a3b</Sid>
           <DateCreated>Fri, 09 Aug 2013 00:07:59 +0000</DateCreated>
           <DateUpdated>Fri, 09 Aug 2013 00:07:59 +0000</DateUpdated>
           <DateSent>Fri, 09 Aug 2013 00:07:59 +0000</DateSent>
           <AccountSid>ACcbaa0103c4141e5cd754042cb424d4ff</AccountSid>
           <To>+14444444444</To>
           <From>+15555555555</From>
           <Body>Hi there!</Body>
           <Status>sent</Status>
           <Direction>outbound-api</Direction>
           <Price>-0.01000</Price>
           <PriceUnit>USD</PriceUnit>
           <ApiVersion>2010-04-01</ApiVersion>
           <Uri>/2010-04-01/Accounts/ACcbaa01/SMS/Messages/SMe24eb108b</Uri>
        </SMSMessage>
        <SMSMessage>
            ... etc. ...
        </SMSMessage>
     </SMSMessages>
  </TwilioResponse>

这是我尝试使用的 XSLT:

   <?xml version="1.0" encoding="ISO-8859-1"?>
   <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs">
   <xsl:template match="//TwilioResponse">
   <xsl:for-each select="SMSMessage">
       <xsl:value-of select="Sid"/>
       <!-- I tried all these, too: &#x20   &#x9;  even &#xA;   -->
       <xsl:text>&#09;</xsl:text>
       <!-- I also tried this from another SO question -->
       <xsl:if test="position() != last()">, </xsl:if>
       <xsl:value-of select="DateCreated"/>
       <xsl:text>&#x9;</xsl:text>
       <xsl:value-of select="DateUpdated"/>
       <xsl:text>&#09;</xsl:text>
       <xsl:value-of select="DateSent"/>
       <xsl:text>&#xA;</xsl:text>
       <xsl:value-of select="AccountSid"/>
       <xsl:text>&#09;</xsl:text>
       <xsl:text>&#xA;</xsl:text>
       <xsl:text>&#x20;</xsl:text>
       <xsl:text>&#x9;</xsl:text>
       <xsl:value-of select="To"/>
       <xsl:text>&#x9;</xsl:text>
       <xsl:value-of select="From"/>
       <xsl:text>&#x9;</xsl:text>
       <xsl:value-of select="Body"/>
       <xsl:text>&#x9;</xsl:text>
       <xsl:value-of select="Status"/>
       <xsl:text>&#x9;</xsl:text>
       <xsl:value-of select="Direction"/>
       <xsl:text>&#x9;</xsl:text>
       <xsl:value-of select="Price"/>
       <xsl:text>&#x9;</xsl:text>
       <xsl:value-of select="PriceUnit"/>
       <xsl:text>&#x9;</xsl:text>
       <xsl:value-of select="ApiVersion"/>
       <xsl:text>&#x9;</xsl:text>
       <xsl:value-of select="Uri"/>
       <!-- I tried both of these: line feed char -->
       <xsl:text>&#xA;</xsl:text>
       <xsl:text>&#10;</xsl:text>
     </xsl:for-each>
   </xsl:template>
 </xsl:stylesheet>

这是我的 Perl 代码的相关部分:

use XML::XSLT;

my $logs = $twilio -> GET ('SMS/Messages');
my $string = $logs->{content};

my $xsl = 'xsl.txt';
my $xslt = XML::XSLT->new ($xsl);
$xslt->transform ($string);
my $xsltToString = $xslt->toString;

    print $xsltToString;

my $columnHeadings = "Sid\tDateCreated\tDateUpdated\tDateSent\tAccountSid\tTo\tFrom\tBody\tStatus\tDirection\tPrice\tPriceUnit\tApiVersion\tUri\n";

open(my $fh, '>', 'textfile.txt') || die("Unable to open file. $!");
    print $fh  $columnHeadings;
    foreach my $k (@split) {
        print $fh join("\t", $xsltToString) . "\t";
    }       
        #print $fh split("\t", $val). "\t"; ;
close($fh);
$xslt->dispose();


# P.S. I'm sure there's a better way to check and see how many lines were saved.

my $xmllines = 0;
open $fh, '<', 'textfile.txt' or die "Could not open file. $!";
   while (<$fh>) {
      $xmllines++;
   }
print ("\n" . $xmllines . " lines saved to tab-delimited logs textfile. \n");   
close $fh;  

我的输出是一回事,任何元素之间都没有分离。

4

2 回答 2

4

这是一个使用XML::Twig的示例:

#!/usr/bin/env perl

use strict;
use warnings;

use Const::Fast;
use Text::CSV;
use XML::Twig;

run({
    csv => Text::CSV->new({
        always_quote => 1,
        binary => 1,
    }),
    in_fh => \*DATA,
    out_fh => \*STDOUT,
    wanted_fields => [
        qw(
            Sid
            DateCreated
            DateUpdated
            DateSent
            AccountSid
            To
            From
            Body
            Status
            Direction
            Price
            PriceUnit
            ApiVersion
            Uri
        )
    ],
});

sub run {
    my $args = shift;
    my $twig = XML::Twig->new(
        twig_roots => {
            SMSMessage => sub { print_csv($args, @_) },
        }
    );
    $twig->parse($args->{in_fh});
}

sub print_csv {
    my $args = shift;
    my $twig = shift;
    my $elt = shift;
    my %fields = map { $_->name, $_->text } $elt->children;

    my $csv = $args->{csv};
    my $wanted = $args->{wanted_fields};
    $csv->combine(@fields{ @{$args->{wanted_fields}} });

    print { $args->{out_fh} } $csv->string, "\n";
    $twig->purge;
    return;
}

__DATA__
<?xml version="1.0" encoding="UTF-8"?>
  <TwilioResponse>
     <SMSMessages end="49" firstpageuri="/2010-04-01/Accounts/ACcbaa0/SMS/Messages?Page=0&amp;PageSize=50" lastpageuri="/2010-04-01/Accounts/ACcbaa/SMS/Messages?Page=54&amp;PageSize=50" nextpageuri="/2010-04-01/Accounts/ACcbaa0103c/SMS/Messages?Page=1&amp;PageSize=50&amp;AfterSid=SMc20cf7" numpages="55" page="0" pagesize="50" previouspageuri="" start="0" total="2703" uri="/2010-04-01/Accounts/ACcbaa0103cf/SMS/Messages">
        <SMSMessage>
           <Sid>SMe24eb108b7eb6a3b</Sid>
           <DateCreated>Fri, 09 Aug 2013 00:07:59 +0000</DateCreated>
           <DateUpdated>Fri, 09 Aug 2013 00:07:59 +0000</DateUpdated>
           <DateSent>Fri, 09 Aug 2013 00:07:59 +0000</DateSent>
           <AccountSid>ACcbaa0103c4141e5cd754042cb424d4ff</AccountSid>
           <To>+14444444444</To>
           <From>+15555555555</From>
           <Body>Hi there!</Body>
           <Status>sent</Status>
           <Direction>outbound-api</Direction>
           <Price>-0.01000</Price>
           <PriceUnit>USD</PriceUnit>
           <ApiVersion>2010-04-01</ApiVersion>
           <Uri>/2010-04-01/Accounts/ACcbaa01/SMS/Messages/SMe24eb108b</Uri>
        </SMSMessage>
        <SMSMessage>
            ... etc. ...
        </SMSMessage>
     </SMSMessages>
  </TwilioResponse>
于 2013-08-10T13:26:05.203 回答
3

我认为 XSLT 是解决这个问题的错误工具:它非常适合 XML→XML 转换,但对于这种 XML→CSV 转换来说太冗长了。XML::LibXML我们可以使用 Perl 的模块或类似的东西来解析 XML 并应用 XPath 查询,并将Text::CSV数据发送到文件,而不是应用 XSLT 样式。

use strict; use warnings;
use autodie;
use XML::LibXML;
use Text::CSV;

# Parse the XML
my $xml = XML::LibXML->load_xml(string => ...);

# Prepare the CSV
open my $csv_fh, ">:utf8", "textfile.csv";
my $csv = Text::CSV->new({
  binary => 1,
  eol => "\n",
  # sep_char => "\t", # for tab separation. Default is comma
  # quote_space => 0, # makes tab seperated data look better.
});

my @columns = qw/
  Sid
  DateCreated  DateUpdated  DateSent
  AccountSid
  To  From  Body
  Status
  Direction
  Price  PriceUnit
  ApiVersion
  Uri
/;

$csv->print($csv_fh, \@columns);  # print the header

# loop through all messages. Note that `print` wants an arrayref.
for my $sms ($xml->findnodes('//SMSMessage')) {
  $csv->print($csv_fh, [ map { $sms->findvalue("./$_") } @columns ]);
}

输出:

Sid,DateCreated,DateUpdated,DateSent,AccountSid,To,From,Body,Status,Direction,Price,PriceUnit,ApiVersion,Uri
SMe24eb108b7eb6a3b,"Fri, 09 Aug 2013 00:07:59 +0000","Fri, 09 Aug 2013 00:07:59 +0000","Fri, 09 Aug 2013 00:07:59 +0000",ACcbaa0103c4141e5cd754042cb424d4ff,+14444444444,+15555555555,"Hi there!",sent,outbound-api,-0.01000,USD,2010-04-01,/2010-04-01/Accounts/ACcbaa01/SMS/Messages/SMe24eb108b
,,,,,,,,,,,,,

或制表符分隔的版本:

Sid     DateCreated     DateUpdated     DateSent        AccountSid      To      From    Body   Status   Direction       Price   PriceUnit       ApiVersion      Uri
SMe24eb108b7eb6a3b      Fri, 09 Aug 2013 00:07:59 +0000 Fri, 09 Aug 2013 00:07:59 +0000 Fri, 09 Aug 2013 00:07:59 +0000 ACcbaa0103c4141e5cd754042cb424d4ff      +14444444444    +15555555555   Hi there!        sent    outbound-api    -0.01000        USD     2010-04-01      /2010-04-01/Accounts/ACcbaa01/SMS/Messages/SMe24eb108b

(最后一行不显示)

请注意,使用带有任何分隔符字符的 CSV 可能是个坏主意:当消息包含换行符或制表符时会发生什么?基本的GSM 03.38 字符集至少包括 LF 和 CR 字符。

编辑:进一步解释

\是引用运算符,因此是\@columns指向数组的数组引用@columns

map函数接受一段代码和一个列表。像foreach循环一样,它为列表中的每个值执行此块。在每次迭代中,$_变量都设置为当前元素。与foreach循环不同,它map返回一个值列表。这使它适合转换。例如将一些数字加倍:

my @doubles = map { $_ * 2 } 1 .. 5; #=> 2, 4, 6, 8, 10

DOM 节点的findvalue方法在该节点的上下文中应用 XPath 表达式并返回找到的元素的文本值。XPath 表达式./foo等效于foo,并搜索名为 的子元素foo。我们使用$_变量来表示列名/标签名。所以地图表达式

map { $sms->findvalue("./$_") } @columns

将列列表转换为文本值列表。我使用./fooXPath 表达式的形式是因为我认为它更好地传达了“给我一个带有SMS ( )/标记名称的直接子代 () foo”的含义,尤其是在习惯于文件路径的符号时。.

[ ... ]运算符是一种从内部列表创建数组引用的方法。例如[1, 2, 3]是一个快捷方式

  my @temp = (1, 2, 3);
  \@temp;

(再次注意\操作员)。

于 2013-08-10T00:36:50.133 回答