在 Perl 中将 XML 之类的格式转换为 CSV
Convert XML like format to CSV in Perl
所以,我有一个如下所示的文件:
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
所以我想知道我该怎么做?
所以我当前的代码有 bob
和 bam
这样的词,所以我一直在像这样 greping 并使用 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 会 return 多个结果。
关于我如何解决这个问题有什么建议吗?
明确指出“文件不完整 XML”并且 XML 库无法使用。无赖:(
然后使用正则表达式对其进行解析。请记住,必须始终关注输入文件以查看其格式是否发生了变化;即使是最小的更改也很容易使正则表达式失效,充其量会导致程序崩溃,或者更糟糕的是,会导致一个安静的错误。†
显示的格式很容易解析。这是一个基本的做法,解析 XML-like 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{}}, ;
}
}
# Print out CSV-style output, with given tags
say join ',', @tags;
for my $i (0..$#{$record{$tags[0]}}) {
say join ',', map { $record{$_}->[$i] } @tags;
}
对标签做了一些假设。一些重要的:每个 tag-pair 在一行上;所有标签名称都是唯一的。如果这些不支持代码需要调整,可以做什么但需要一些工作。
除了匹配 XML-like 打开和关闭 tag-pair、<tagname>...</tagname>
,我还添加了一个标志,用于处理 是 [=42] =] 在 component
部分中。在 if
条件内测试标志允许在 XML 之外进行其他处理,否则我们可以在 if
条件之前有 next if not $in_XML;
。如果没有机会在文本中的其他地方出现意外 XML-like tag-pair,则整个业务可能是不必要的。
请注意,不必指定和使用 @tags
,但可以打印文件中找到的标签,即 my @tags = keys %record
,如果可以接受并且顺序无关紧要。
请添加这些标签及其值是否确实符合预期的测试。真实的输入文件往往偶尔会有缺失或意外的部分。
† 最好补救“不完全XML”(让它XML) 并尽可能使用库。
输入数据是顺序读取的,一旦遇到<component>
表示后面有数据
现在我们开始解析数据,直到我们将 </component>
读入哈希,然后将结果推入数组。
处理完整个文件后,将散列的键输出为 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->{} = 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 operator and named 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
您仍应使用适当的 XML 解析器。它将更快、更简单、更少error-prone、更多可读性、更易于维护,等等。只需从文件中提取 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 )} ]);
所以,我有一个如下所示的文件:
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
所以我想知道我该怎么做?
所以我当前的代码有 bob
和 bam
这样的词,所以我一直在像这样 greping 并使用 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 会 return 多个结果。
关于我如何解决这个问题有什么建议吗?
明确指出“文件不完整 XML”并且 XML 库无法使用。无赖:(
然后使用正则表达式对其进行解析。请记住,必须始终关注输入文件以查看其格式是否发生了变化;即使是最小的更改也很容易使正则表达式失效,充其量会导致程序崩溃,或者更糟糕的是,会导致一个安静的错误。†
显示的格式很容易解析。这是一个基本的做法,解析 XML-like 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{}}, ;
}
}
# Print out CSV-style output, with given tags
say join ',', @tags;
for my $i (0..$#{$record{$tags[0]}}) {
say join ',', map { $record{$_}->[$i] } @tags;
}
对标签做了一些假设。一些重要的:每个 tag-pair 在一行上;所有标签名称都是唯一的。如果这些不支持代码需要调整,可以做什么但需要一些工作。
除了匹配 XML-like 打开和关闭 tag-pair、<tagname>...</tagname>
,我还添加了一个标志,用于处理 是 [=42] =] 在 component
部分中。在 if
条件内测试标志允许在 XML 之外进行其他处理,否则我们可以在 if
条件之前有 next if not $in_XML;
。如果没有机会在文本中的其他地方出现意外 XML-like tag-pair,则整个业务可能是不必要的。
请注意,不必指定和使用 @tags
,但可以打印文件中找到的标签,即 my @tags = keys %record
,如果可以接受并且顺序无关紧要。
请添加这些标签及其值是否确实符合预期的测试。真实的输入文件往往偶尔会有缺失或意外的部分。
† 最好补救“不完全XML”(让它XML) 并尽可能使用库。
输入数据是顺序读取的,一旦遇到<component>
表示后面有数据
现在我们开始解析数据,直到我们将 </component>
读入哈希,然后将结果推入数组。
处理完整个文件后,将散列的键输出为 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->{} = 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 operator and named 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
您仍应使用适当的 XML 解析器。它将更快、更简单、更少error-prone、更多可读性、更易于维护,等等。只需从文件中提取 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 )} ]);