【问题标题】:Convert XML like format to CSV in Perl在 Perl 中将类似 XML 的格式转换为 CSV
【发布时间】:2021-02-15 16:58:09
【问题描述】:

所以,我有一个看起来像这样的文件:

random stuff in the beginning...

 <component>
   <name>bob</name>
   <age>7</age>
   <country>Great_Britain</country>
 </component>

 <component>
   <name>bob_secondbob</name>
   <age>7</age>
   <country>Great_Britain</country>
 </component>

 <component>
   <name>bam</name>
   <age>7</age>
   <country>Great_Britain</country>
 </component>

等等……

而且,我只想将其保存为 CSV,如下所示:

name,age,country
bob,7,Great_Britain
bob_secondbob,7,Great_Britain
bam,7,Great_Britain

所以我想知道我该怎么做呢?

所以我当前的代码有 bobbam 这样的词,所以我一直在寻找它并使用 sed 来获取值:

grep -A4 "<component>" $file | grep -A4 "<name>$bob.*</name>" | grep "<name>" 
grep -A4 "<component>" $file | grep -A4 "<name>$bob.*</name>" | grep "<age>"
grep -A4 "<component>" $file | grep -A4 "<name>$bob.*</name>" | grep "<country>"

etc...

其中变量$bob"bob"

但问题是bob.* 有 2 个实例,我不知道如何将它们分开,所以我可以将它们打印出来......

我查看了 XML 模块,但这个文件不完全是 XML,所以我不能使用它...

bam 这样通过 grep 可以很容易地打印出来,但是如果有多个 bob.* 实例我需要它们,我知道如何正确打印它,因为 grep 会返回多个结果。

有什么建议可以解决这个问题吗?

【问题讨论】:

  • “Grep”可能不是要走的路。我建议使用具有良好 XML 库的语言编写一个简短的脚本或程序。 Perl(您的标签之一)符合要求。我会 1) 获取 Perl LibXML,2) 解析你的 XML 文件,一次是一个“”元素,3) 写你的 .CSV,一次一行。
  • "这个文件不是完全的 XML" -- (1) 这究竟是什么意思?它有多远? (2) 如果它不能用一个库来完成,那么你就只能使用正则表达式了,你所展示的内容并不难解析。当然,问题在于将来或其他文件中的任何微小更改都可能需要您重写正则表达式(或者可能会悄悄引入错误)
  • 如果您的文件实际上是 XML,则需要存在“随机内容”,以便您的示例数据是有效的 XML。
  • @Shawn 我认为他们没有完全有 XML,通过标题和文本中的声明。但我同意最好制作正确的XML并使用库:)
  • 我会逐行读取文件,根据需要复制或修改每一行以创建格式良好的 XML 输出;然后使用 XSLT 或 XQuery 等标准技术处理 XML。

标签: xml csv perl grep


【解决方案1】:

明确指出“文件不是完全的 XML”并且不能使用 XML 库。无赖:(

然后使用正则表达式对其进行解析。请记住,必须始终关注输入文件以查看其格式是否发生了变化;即使是最小的更改也很容易导致正则表达式失效,从而导致程序崩溃,或者更糟糕的是,导致一个安静的错误。

显示的格式很容易解析。这是一个基本步骤,为任何标签及其值解析类似 XML 的 component 部分,然后按所需顺序打印给定的一组实际标签。

use warnings;
use strict;
use feature 'say';

my $section_name = 'component';   # XML-like section to parse
my @tags = qw(name age country);  # given tags and their order

my (%record, $in_XML);

while (<>) {
    if    (/^\s*<$section_name>\s*$/)   { $in_XML = 1 }
    elsif (/^\s*<\/$section_name>\s*$/) { $in_XML = 0 } 
    
    if ( $in_XML and m{<([^<]+)> ([^<]+) </\g{1}>}x ) { 
        push @{$record{$1}}, $2; 
    }   
}

# Print out CSV-style output, with given tags
say join ',', @tags;
for my $i (0..$#{$record{$tags[0]}}) { 
    say join ',', map { $record{$_}->[$i] } @tags;
}

对标签做了一些假设。一些重要的:每个标签对都在一行上;所有标签名称都是唯一的。如果这些不符合代码需要调整,可以做什么但需要一些工作。

除了匹配类似 XML 的开始和结束标记对 &lt;tagname&gt;...&lt;/tagname&gt; 之外,我还在 component 部分中添加了处理 is 时的标志。在if 条件内测试标志允许在XML 之外进行其他处理,否则我们可以在if 条件之前有next if not $in_XML;。如果文本中其他地方不可能出现意外的类似 XML 的标记对,那么整个业务可能是不必要的。

请注意,不必必须指定和使用@tags,但可以打印文件中的标签,即my @tags = keys %record,如果这是可以接受的并且如果订单没有'没关系。

请添加测试这些标签及其值是否确实符合人们的期望。现实的输入文件往往偶尔会丢失或意外的部分。


如果可能,最好纠正“不是完全 XML”(使其成为 XML)并使用库。

【讨论】:

  • 您可以按照您已经在做的方式提取 XML,然后使用解析器。
  • @ikegami 是的,我考虑过(这是我最初使用$in_XML 标志的原因)。我这样做是因为我不知道他们的数据以及它对 XML 的影响程度如何,在这种情况下,零碎的 XML 处理可能会造成混淆(或者对于一些轻微的 XML 违规行为不起作用)。现在有你正确的 XML 方式(还有更多好东西:) 所以一切都很好:)
【解决方案2】:

输入数据顺序读取,遇到&lt;component&gt;表示数据跟随。

现在我们开始解析数据,直到我们将 &lt;/component&gt; 读入哈希,然后将结果推送到数组中。

处理完整个文件后,将散列的键作为header 输出,并通过, 连接每个元素的数据散列值。

use strict;
use warnings;
use feature 'say';

my @data;
my @fields = qw/name age country/;

while( <DATA> ) {
    if( /<component>/ ) {
        my $component;
        while( <DATA> ) {
            last if /<\/component>/;
            $component->{$1} = $2 if /^\s+<(.+?)>(.*?)</;
        }
        push @data, $component;
    }
}

say join ',', @fields;
say join ',', $_->@{@fields} for @data;

__DATA__


 <component>
   <name>bob</name>
   <age>7</age>
   <country>Great_Britain</country>
 </component>

 <component>
   <name>bob_secondbob</name>
   <age>7</age>
   <country>Great_Britain</country>
 </component>

 <component>
   <name>bam</name>
   <age>7</age>
   <country>Great_Britain</country>
 </component>

match range operatornamed capture groups 的第二个变体

use strict;
use warnings;
use feature 'say';

my(@data,%component);
my @fields = qw/name age country/;

while( <DATA> ) {
    chomp;
    if( /<component>/../<\/component>/ ) {
        $component{$+{tag}} = $+{val} if m!<(?<tag>.+?)>(?<val>.*?)</\g{tag}>!;
        if( /<\/component>/ ) {
            my %hash = %component;
            push @data, \%hash;
            %component = ();        
        }
    }
}

say join ',', @fields;
say join ',', $_->@{@fields} for @data;

__DATA__


 <component>
   <name>bob</name>
   <age>7</age>
   <country>Great_Britain</country>
 </component>

 <component>
   <name>bob_secondbob</name>
   <age>7</age>
   <country>Great_Britain</country>
 </component>

 <component>
   <name>bam</name>
   <age>7</age>
   <country>Great_Britain</country>
 </component>

输出

name,age,country
bob,7,Great_Britain
bob_secondbob,7,Great_Britain
bam,7,Great_Britain

【讨论】:

    【解决方案3】:

    您仍应使用适当的 XML 解析器。它将更快更简单不易出错更易读更易维护 em> 等等。只需从文件中提取 XML 位并从那里使用常规方法。

    出于同样的原因,请使用适当的 CSV 生成器,而不是推出自己的。

    use Text::CSV_XS qw( );
    use XML::LibXML  qw( );
    
    my $xml_parser = XML::LibXML->new()
    my $csv = Text::CSV_XS->new({ auto_diag => 2, binary => 1 });
    
    $csv->say(\*STDOUT, [qw( name age country )]);
    
    my $in_xml = 0;
    my $xml;
    while (
       $in_xml ||= /<component\b/;
       $xml .= $_ if $in_xml;
       if (/<\/component\b/) {
          my $doc = $xml_parser->parse_string($xml);
          my $name    = $doc->findvalue("/component/name");
          my $age     = $doc->findvalue("/component/age");
          my $country = $doc->findvalue("/component/country");
          $csv->say(\*STDOUT, [ $name, $age, $country ]);
    
          $in_xml = 0;
          $xml = undef;
       }
    }
    

    解析 XML 的另一种方法:

    my $doc = $xml_parser->parse_string($xml);
    my %rec;
    $rec{ $_->nodeName } = $_->textContent()
       for $doc->findnodes("/component/*");
    
    $csv->say(\*STDOUT, [ @rec{qw( name age country )} ]);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-11-30
      • 2021-04-29
      • 1970-01-01
      相关资源
      最近更新 更多