【问题标题】:Perl XML to tab delimited text file with XSLT (or not)使用 XSLT(或不使用)将 Perl XML 转换为制表符分隔的文本文件
【发布时间】:2013-08-09 23:01:09
【问题描述】:

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

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

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

我查看了类似的问题,但没有看到任何分隔符被添加到我的文件中。 XML to Tab delimited Text Modifying a XSLT for converting XML to tab delimited text file

问题:

  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;  

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

【问题讨论】:

  • 如果我运行 XML::Simple 来解析您的 XML,我会得到一个很好的、难以理解的小数据结构,其中应该包含您要创建制表符分隔文件的数据。有什么问题?
  • perl -MXML::Simple -MData::Dumper -lwe"print Dumper XMLin(shift)" yourfile.xml
  • 如果您在编辑器等中查看您的输出文件,您不会看到制表符分隔符间距,但它已经到位..
  • @TLP 也许我应该在我这样做时发布我正在尝试的内容。我想我会的,b / c我想知道为什么我也无法将它变成我想要的东西。只是按照您想要的顺序写出特定的哈希引用吗?谢谢!
  • @hwnd - 啊!但是那么excel不应该可以导入吗?

标签: xml perl xslt


【解决方案1】:

这里是一个使用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>

【讨论】:

    【解决方案2】:

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

    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 charset 至少包含 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
    

    将列列表转换为文本值列表。我对 XPath 表达式使用了 ./foo 形式,因为我认为它更好地传达了“给我一个直接的孩子 (/) 的意思,标签名称为 foothis SMS (@987654347 @)”,尤其是习惯于文件路径的符号时。

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

    的快捷方式
      my @temp = (1, 2, 3);
      \@temp;
    

    (再次注意\ 运算符)。

    【讨论】:

    • 太棒了-谢谢! 1. 回复:$csv->print($csv_fh, \@columns) 中的 \@columns; # 打印标题。这是 perl 的事情还是我在文档中查找 Text::CSV? 2.什么是“./$_”?我想我大部分时间都理解 $_ ,但是 ./ 在这里添加了什么? 3. [ ] 是否围绕 [ map { $sms->findvalue("./$_") } @columns ] 以便无论“它”是什么,都成为数组引用?
    • @BradFallon 我更新了更多解释。 TL;DR: (1) 这个print 方法记录在Text::CSV 中。它需要一个数组引用,我们使用\ 运算符生成它。 (2) 不必要的 XPath 语法。 (3) 是的。
    • 很好的答案,但您可以简单地写$sms-&gt;findvalue($_),而不是$sms-&gt;findvalue("./$_")
    猜你喜欢
    • 1970-01-01
    • 2011-06-22
    • 1970-01-01
    • 1970-01-01
    • 2023-03-21
    • 2011-03-04
    • 1970-01-01
    • 1970-01-01
    • 2019-04-11
    相关资源
    最近更新 更多