1

初心者の Perl プログラマー。単純な xml 文字列をタブ区切りのテキスト ファイルに変換しようとしています。XML::Parser (および XML::Twig/Simple、さらには XSLT) の使用に苦労しましたが、主要なデータ部分を列見出しにする方法がわかりませんでした。

次に、XSLT を使用してそれを実行しようとしましたが、要素間のセパレーターを取得する方法がわかりません (その場合、分割や結合を使用するだけでしょうか?) しかし、それらはすべて 1 つの文字列で一緒に実行されます。

列見出しを手動で手動で印刷しました。テンプレートでそれを行う簡単な方法はありますか?

私が見た同様の質問ですが、ファイルにセパレーターが追加されているのを確認できませんでした。XML からタブ区切りテキスト へ XML をタブ区切りテキスト ファイルに変換するための XSLT の変更

質問:

  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;  

私の出力は、どの要素も分離されていない、すべて 1 つのものです。

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 変換には冗長すぎます。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 文字セットには、少なくとも LF および CR 文字が含まれています。

編集:さらなる説明

\参照演算子なので\@columns、配列を指す配列参照@columnsです。

このmap関数は、コード ブロックとリストを受け取ります。ループのようforeachに、リスト内の値ごとにこのブロックを実行します。各反復で、$_変数は現在の要素に設定されます。foreachループとは異なり、map値のリストを返します。これにより、変換に適しています。たとえば、いくつかの数値を 2 倍にするには:

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

DOM ノードのfindvalueメソッドは、このノードのコンテキストで XPath 式を適用し、見つかった要素のテキスト値を返します。XPath 式./fooは と同等でfoo、 という子要素を検索しますfoo。変数を使用して$_、列名/タグ名を示します。したがって、マップ式

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

列のリストをテキスト値のリストに変換します。特にファイル パスの表記に慣れている場合は、「このSMS ( )のタグ名を持つ./foo直下の子 ( ) をください」という意味がよりよく伝わると思うので、XPath 式にこの形式を使用しました。/foo.

演算子は、[ ... ]内部のリストから配列参照を作成する方法です。たとえば[1, 2, 3]、のショートカットです

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

\(オペレーターにもう一度注意してください)。

于 2013-08-10T00:36:50.133 に答える