Perl XML2JSON:如何保留XML元素顺序?

时间:2016-05-16 14:38:53

标签: json perl xml-parsing

我有一个XML格式的配置文件。我需要解析XML并转换为JSON。我可以用perl的XML2JSON模块转换它。但问题是,它不是维护XML元素的顺序。我严格需要元素,否则我无法配置

我的XML文件是这样的。我必须配置一个IP地址并将该IP设置为某个路由的网关。

<Config>
<ip>
    <address>1.1.1.1</address>
    <netmask>255.255.255.0</netmask>
</ip>
<route>
    <network>20.20.20.0</network>
    <netmask>55.255.255.0</netmask>
    <gateway>1.1.1.1</gateway>
</route>
</Config>

这是我转换为JSON的perl代码

my $file = 'config.xml';
use Data::Dumper;
open my $fh, '<',$file or die;
$/ = undef;
my $data = <$fh>;
my $XML = $data;
my $XML2JSON = XML::XML2JSON->new();
my $Obj = $XML2JSON->xml2obj($XML);
print Dumper($Obj);

我得到的输出是,

$VAR1 = {'Config' => {'route' => {'netmask' => {'$t' => '55.255.255.0'},'gateway' => {'$t' => '1.1.1.1'},'network' => {'$t' => '20.20.20.0'}},'ip' => {'netmask' => {'$t' =>                        '255.255.255.0'},'address' => {'$t' => '1.1.1.1'}}},'@encoding' => 'UTF-8','@version' => '1.0'};

我有一个脚本,它读取json对象并配置.. 但它失败了,因为它首先尝试将网关IP地址设置为尚未配置IP地址的路由,然后添加然后添加IP地址。

我严格要求先键入 ip ,然后路由才能正确配置而不会出错。像这样,我有许多依赖关系,其中键的顺序是必须的。

有什么方法可以解决这个问题吗?我尝试了几乎所有XML解析模块,如XML :: Simple,Twig :: XML,XML :: Parser。但没有任何帮助......

2 个答案:

答案 0 :(得分:2)

这是我一起攻击的程序,它使用XML::Parser来解析一些XML数据并以相同的顺序生成等效的JSON。它忽略任何属性,处理指令等,并要求每个XML元素必须包含子元素列表或文本节点。混合文本和元素将不起作用,并且 未被检查 ,除了程序将试图取消引用字符串

它旨在成为一个框架,供您根据需要进行增强,但与您在问题中显示的XML数据一样正常工作

use strict;
use warnings 'all';

use XML::Parser;


my $parser = XML::Parser->new(Handlers => {
    Start => \&start_tag,
    End   => \&end_tag,
    Char  => \&text,
});

my $struct;
my @stack;

$parser->parsefile('config.xml');

print_json($struct->[1]);


sub start_tag {
    my $expat = shift;
    my ($tag, %attr) = @_;

    my $elem = [ $tag => [] ];
    if ( $struct ) {
        my $content = $stack[-1][1];
        push @{ $content }, $elem;
    }
    else {
        $struct = $elem;
    }
    push @stack, $elem;
}


sub end_tag {
    my $expat = shift;
    my ($elem) = @_;
    die "$elem <=> $stack[-1][0]" unless $stack[-1][0] eq $elem;
    for my $content ( $stack[-1][1] ) {
        $content = "@$content" unless grep ref, @$content;
    }
    pop @stack;
}


sub text {
    my $expat = shift;
    my ($string) = @_;
    return unless $string =~ /\S/;
    $string =~ s/\A\s+//;
    $string =~ s/\s+\z//;
    push @{ $stack[-1][1] }, $string;
}


sub print_json {
    my ($data, $indent, $comma) = (@_, 0, '');

    print "{\n";

    for my $i ( 0 .. $#$data ) {

        # Note that $data, $indent and $comma are overridden here
        # to reflect the inner context
        #
        my $elem = $data->[$i];
        my $comma = $i < $#$data ? ',' : '';
        my ($tag, $data) = @$elem;
        my $indent = $indent + 1;

        printf qq{%s"%s" : }, '  ' x $indent, $tag;

        if ( ref $data ) {
            print_json($data, $indent, $comma);
        }
        else {
            printf qq{"%s"%s\n}, $data, $comma;
        }
    }

    # $indent and $comma (and $data) are restored here
    #
    printf "%s}%s\n", '  ' x $indent, $comma;
}

输出

{
  "ip" : {
    "address" : "1.1.1.1",
    "netmask" : "255.255.255.0"
  },
  "route" : {
    "network" : "20.20.20.0",
    "netmask" : "55.255.255.0",
    "gateway" : "1.1.1.1"
  }
}

答案 1 :(得分:1)

问题与XML解析没什么关系,但因为perl哈希值没有排序。所以当你写'&#39;一些JSON ......它可以是任何订单。

避免这种情况的方法是将排序函数应用于JSON。

您可以使用sort_by明确排序:

来执行此操作
#!/usr/bin/env perl
use strict;
use warnings;

use XML::Twig; 
use JSON::PP; 

use Data::Dumper;

sub order_nodes {
   my %rank_of = ( ip => 0, route => 1, address => 2, network => 3, netmask => 4, gateway => 5 ); 
   print "$JSON::PP::a <=> $JSON::PP::b\n";
   return $rank_of{$JSON::PP::a} <=> $rank_of{$JSON::PP::b};
}

my $twig = XML::Twig -> parse (\*DATA); 

my $json = JSON::PP -> new;
$json ->sort_by ( \&order_nodes );
print $json -> encode( $twig -> simplify );

__DATA__
<Config>
<ip>
    <address>1.1.1.1</address>
    <netmask>255.255.255.0</netmask>
</ip>
<route>
    <network>20.20.20.0</network>
    <netmask>55.255.255.0</netmask>
    <gateway>1.1.1.1</gateway>
</route>
</Config>

在某些情况下,设置canonical会有所帮助,因为它会将排序设置为词法顺序。 (并且意味着您的JSON输出将始终如一地排序)。这并不适用于您的情况。

您可以通过XML::Twig表达式或xpathtwig_handlers构建节点排序。我快速地试了一下,但是在弄清楚你是怎么告诉他的时候,我有点不知所措。如何根据获得address/netmask然后network/netmask/gateway计算排序。

作为一个简单的例子,您可以:

my $count = 0; 
foreach my $node ( $twig -> get_xpath ( './*' ) ) {
    $rank_of{$node->tag} = $count++ unless $rank_of{$node->tag};    
}

print Dumper \%rank_of; 

这将确保iproute始终是正确的方法。但是它不会对子键进行排序。

这实际上变得有点复杂,因为你需要递归......然后决定如何处理碰撞事故&#39; (例如netmask - address之前,但与network相比,它是如何排序的。

或者:

my $count = 0;
foreach my $node ( $twig->get_xpath('.//*') ) {
   $rank_of{ $node->tag } = $count++ unless $rank_of{ $node->tag };
}

这将遍历所有节点,并将它们按顺序排列。它不起作用,因为netmask出现在两个节中。

你得到:

{"ip":{"address":"1.1.1.1","netmask":"255.255.255.0"},"route":{"netmask":"55.255.255.0","network":"20.20.20.0","gateway":"1.1.1.1"}}

我无法找到折叠两个列表的巧妙方法。