Search code examples
xmlperlxml-simple

XML::Simple convert xml to perl hash warning "non-unique value"


I have a well formed XML that I'm trying to convert to a hash with perl module XML::Simple. There are some section in this file which cannot be parsed correctly. Is there any way (or a workaround) to get the xml parsed correctly and get the desired result?

D:\tmp>perl parse_dns2.pl dns_problem_public.xml
Warning: <dns_entry> element has non-unique value in 'domain' key attribute: 
0 at parse_dns2.pl line 9.
Warning: <dns_entry> element has non-unique value in 'domain' key attribute: 
example.com at parse_dns2.pl line 9.
Warning: <dns_entry> element has non-unique value in 'domain' key attribute: 
test.com at parse_dns2.pl line 9.
$VAR1 = {
  'dns_timeout' => '20',
  'local_dns' => {
    'dns_entry' => {
      '0' => {
        'content' => '192.168.120.32'
      },
      'domain.example.com' => {
        'content' => '172.16.113.13'
      },
      'example.com' => {
        'content' => '172.16.113.13'
      },
      'test.com' => {
        'content' => '172.17.0.113'
      }
    }
  }
};

My code is strightforward:

#!/usr/bin/perl
use strict;
use warnings;
use diagnostics;
use XML::Simple;
use Data::Dumper;

my $ref = XMLin(
    $ARGV[0],
    ForceArray => ['dns_entry'],
    KeyAttr    => { 'dns_entry' => 'priority' },
    KeyAttr    => { 'dns_entry' => 'domain' },
    ForceContent => 0
);

print Dumper $ref;

The xml file (the relevant section) contain attributes which I need to use as a key:

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE config SYSTEM "config.dtd"> 
<dns>
    <local_dns>
        <dns_entry priority="0">192.168.120.31</dns_entry>
        <dns_entry priority="0">192.168.120.32</dns_entry>
        <dns_entry domain="example.com">172.16.103.20</dns_entry>
        <dns_entry domain="example.com">172.16.113.13</dns_entry>
        <dns_entry domain="test.com">172.17.0.111</dns_entry>
        <dns_entry domain="test.com">172.17.0.113</dns_entry>
        <dns_entry domain="domain.example.com">172.16.103.20</dns_entry>
        <dns_entry domain="domain.example.com">172.16.113.13</dns_entry>
    </local_dns>
    <dns_timeout>20</dns_timeout>
</dns>

The first problem that XML::Simple cannot accept similar elements with the same attributes (although different values). And the second problem is that I can use only one attribute as a key attribute in the same XML block.

Desired result:

$VAR1 = {
  'local_dns' => {
    'dns_entry' => {
      'domain' => {
        'domain.example.com' => {
          'content' => [
            '172.16.113.20',
            '172.16.113.13'
          ]
        },
        'example.com' => {
          'content' => [
            '172.16.113.20',
            '172.16.113.13'
          ]
        },
        'test.com' => {
          'content' => [
            '172.17.0.111',
            '172.17.0.111'
          ]
        }
      },
      'priority' => {
        '0' => {
          'content' => [
            '192.168.120.31',
            '192.168.120.32'
          ]
        }
      }
    }
  },
  'dns_timeout' => '20'
};

Solution

  • Nodes can;'t have multiple contents, so some transformation is needed.

    You should take this opportunity to avoid using the most complicated of the XML parsers out there. It's so hard to use its own documentation advises against using it.

    Here's an XML::LibXML solution:

    use XML::LibXML qw( );
    
    my $doc = XML::LibXML->new->parse_file('dns.xml');
    
    my %data;
    {
       $data{dns_timeout} = $doc->findvalue('/dns/dns_timeout/text()');
    
       for my $dns_entry_node ($doc->findnodes('/dns/local_dns/dns_entry')) {
          my $addr = $dns_entry_node->textContent();
    
          if (defined( my $priority = $dns_entry_node->getAttribute('priority') )) {
             push @{ $data{local_dns}{dns_entry}{priority}{$priority} }, $addr;
          }
    
          if (defined( my $domain = $dns_entry_node->getAttribute('domain') )) {
             push @{ $data{local_dns}{dns_entry}{domain}{$domain} }, $addr;
          }
       }
    }