如何比较来自不同 XML 文档的某些值?

How do I compare certain values from different XML documents?

我想用 Perl 编写代码来比较两个 XML 文件。

一点点历史…… 使用 API 文档(获取请求),我从 Web Service1 获取数据 1,从 Service2 获取数据 2。它们以 XML 格式显示,但不相同。

我应该比较这些文件中的两个元素(deviceName 和 ipAddress),如果它们在两个文件中相同,它应该是一条消息“WebService1 已经包含 DeviceName "Switch1"”。如果没有 - 我会提出 POST 请求并在 WebService1/WebService2.

中添加此设备

你能给我建议吗,我应该使用什么模块,我应该如何开始这个比较?

例如 (file1)

   <?xml version="1.0" ?>
   <queryResponse last="34" first="0" count="35" type="Devices" responseType="listEntityInstances" requestUrl="https://hostname/webacs/api/v1/data/Devices?.full=true" rootUrl="https://hostname/webacs/api/v1/data">
      <entity dtoType="devicesDTO" type="Devices" url="https://hostname/webacs/api/v1/data/Devices/201">
         <devicesDTO displayName="201201" id="201">
           <clearedAlarms>0</clearedAlarms>
           <collectionDetail></collectionDetail>
           <collectionTime></collectionTime>
           <creationTime></creationTime>
           <criticalAlarms>0</criticalAlarms>
           <deviceId>205571</deviceId>
           <deviceName>NEW-SW5</deviceName>
           <deviceType>Cisco Switch</deviceType>
           <informationAlarms>0</informationAlarms>
           <ipAddress>10.66.12.128</ipAddress>
         <location></location>
           <majorAlarms>0</majorAlarms>
           <managementStatus></managementStatus>
              <manufacturerPartNrs>
                  <manufacturerPartNr></manufacturerPartNr>
              </manufacturerPartNrs>
              <minorAlarms>0</minorAlarms>
              <productFamily></productFamily>
              <reachability>Reachable</reachability>
              <softwareType>IOS</softwareType>
              <softwareVersion>12.1(22)</softwareVersion>
              <warningAlarms>0</warningAlarms>
         </devicesDTO>
      </entity>
   </queryResponse>

文件 2

  <?xml version="1.0" encoding="utf-8" standalone="yes"?>
  <ns3:networkdevice name="NEW-SW5" id="9a6ef750-2620-11e4-81be-b83861d71f95" xmlns:ns2="ers.ise.cisco.com" xmlns:ns3="network.ers.ise.cisco.com">
  <link type="application/xml" href="https://hostname:9060/ers/config/networkdevice/123456" rel="self"/>
       <authenticationSettings>
          <enableKeyWrap>false</enableKeyWrap>
          <keyInputFormat>ASCII</keyInputFormat>
          <networkProtocol>RADIUS</networkProtocol>
          <radiusSharedSecret>******</radiusSharedSecret>
       </authenticationSettings>
       <NetworkDeviceIPList>
         <NetworkDeviceIP>
            <ipaddress>10.66.12.128</ipaddress>
            <mask>21</mask>
         </NetworkDeviceIP>
       </NetworkDeviceIPList>
       <NetworkDeviceGroupList>
         <NetworkDeviceGroup>Location#All Locations</NetworkDeviceGroup>
         <NetworkDeviceGroup>Device Type#All Device Types</NetworkDeviceGroup>
   </NetworkDeviceGroupList>
  </ns3:networkdevice>

有一些特别之处:在文件 1 中,我的标签名为:deviceName、ipAddress,它们是 elements.
在 file2 中,我们有一个属性(因为它位于主要元素 ns3:networkdevice 中,它被称为 name ,它从 file1 响应我们的 deviceName )和另一个元素称为 ipaddress(文件 1 中的 ipAddress)

这不是一项从头开始编写的简单任务。你应该使用 XML::Compare

您可以使用 XML::Twig 来解析这两个响应。他们每个人都需要一个单独的解析器。

对于第一个,您需要选择 <deviceName><ipAddress> 这两个标签。匹配元素的简单 twig_handler for each of them that access the text 属性 就足够了。

这些处理程序可能很复杂,但在我们的例子中,处理单个值的代码参考就足够了。我们知道每个值只有一次出现,所以我们可以直接将它们都赋值给各自的词法变量。

use strict;
use warnings;
use XML::Twig;

my ($device_name, $ip_address);
XML::Twig->new(
    twig_handlers => {
        deviceName => sub { $device_name = $_->text },
        ipAddress => sub { $ip_address = $_->text },
    }
)->parse(\*DATA);

say $device_name;
say $ip_address;

__DATA__
<?xml version="1.0" ?>
<queryResponse last="34" first="0" count="35" type="Devices" responseType="listEntityInstances" requestUrl="https://hostname/webacs/api/v1/data/Devices?.full=true" rootUrl="https://hostname/webacs/api/v1/data">
   <entity dtoType="devicesDTO" type="Devices" url="https://hostname/webacs/api/v1/data/Devices/201">
      <devicesDTO displayName="201201" id="201">
        <clearedAlarms>0</clearedAlarms>
        <collectionDetail></collectionDetail>
        <collectionTime></collectionTime>
        <creationTime></creationTime>
        <criticalAlarms>0</criticalAlarms>
        <deviceId>205571</deviceId>
        <deviceName>NEW-SW5</deviceName>
        <deviceType>Cisco Switch</deviceType>
        <informationAlarms>0</informationAlarms>
        <ipAddress>10.66.12.128</ipAddress>
      <location></location>
        <majorAlarms>0</majorAlarms>
        <managementStatus></managementStatus>
           <manufacturerPartNrs>
               <manufacturerPartNr></manufacturerPartNr>
           </manufacturerPartNrs>
           <minorAlarms>0</minorAlarms>
           <productFamily></productFamily>
           <reachability>Reachable</reachability>
           <softwareType>IOS</softwareType>
           <softwareVersion>12.1(22)</softwareVersion>
           <warningAlarms>0</warningAlarms>
      </devicesDTO>
   </entity>
</queryResponse>

对于第二个,您需要使用 att() 来获取其中一个元素的 name 属性,但这也很简单。

use strict;
use warnings;
use XML::Twig;

my ($device_name, $ip_address);
XML::Twig->new(
    twig_handlers => {
        'ns3:networkdevice' => sub { $device_name = $_->att('name') },
        ipaddress => sub { $ip_address = $_->text },
    }
)->parse(\*DATA);

say $device_name;
say $ip_address;
__DATA__
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<ns3:networkdevice name="NEW-SW5" id="9a6ef750-2620-11e4-81be-b83861d71f95" xmlns:ns2="ers.ise.cisco.com" xmlns:ns3="network.ers.ise.cisco.com">
<link type="application/xml" href="https://hostname:9060/ers/config/networkdevice/123456" rel="self"/>
     <authenticationSettings>
        <enableKeyWrap>false</enableKeyWrap>
        <keyInputFormat>ASCII</keyInputFormat>
        <networkProtocol>RADIUS</networkProtocol>
        <radiusSharedSecret>******</radiusSharedSecret>
     </authenticationSettings>
     <NetworkDeviceIPList>
       <NetworkDeviceIP>
          <ipaddress>10.66.12.128</ipaddress>
          <mask>21</mask>
       </NetworkDeviceIP>
     </NetworkDeviceIPList>
     <NetworkDeviceGroupList>
       <NetworkDeviceGroup>Location#All Locations</NetworkDeviceGroup>
       <NetworkDeviceGroup>Device Type#All Device Types</NetworkDeviceGroup>
 </NetworkDeviceGroupList>
</ns3:networkdevice>

现在你已经拥有了这两个,你可以将它们结合起来。我建议为它们中的每一个创建一个函数,传入响应 XML 并使它们 return 成为 $device_name$ip_address.

use strict;
use warnings;
use XML::Twig;

sub parse_response_1 {
    my $xml = shift;

    my ( $device_name, $ip_address );
    XML::Twig->new(
        twig_handlers => {
            deviceName => sub { $device_name = $_->text },
            ipAddress  => sub { $ip_address  = $_->text },
        }
    )->parse($xml);

    return $device_name, $ip_address;
}

sub parse_response_2 {
    my $xml = shift;

    my ( $device_name, $ip_address );
    XML::Twig->new(
        twig_handlers => {
            'ns3:networkdevice' => sub { $device_name = $_->att('name') },
            ipaddress           => sub { $ip_address  = $_->text },
        }
    )->parse($xml);

    return $device_name, $ip_address;
}

当然我的名字parse_response_1parse_response_2不是最好的选择。不要使用数字,而是使用 return 编辑响应的服务名称。

有了这两个函数,我们现在可以准确地检索我们想要的信息。剩下的就是检查它们了。

sub check {
    my ( $response_1, $response_2 ) = @_;

    my ( $device_name_1, $ip_address_1 ) = parse_response_1($response_1);
    my ( $device_name_2, $ip_address_2 ) = parse_response_2($response_2);

    return $device_name_1 eq $device_name_2 && $ip_address_1 eq $ip_address_2;
}

同样,变量的名称可能会更好。现在你只需要用你的两个响应 XML 调用它,它会 return 一个真值,或者不是。

use XML::Simple;
use Data::Dumper;

my $file1_ref = XMLin("./file1");
my $file2_ref = XMLin("./file2");

if($file2_ref->{NetworkDeviceIPList}->{NetworkDeviceIP}->{ipaddress} eq $file1_ref->{entity}->{devicesDTO}->{ipAddress} && $file2_ref->{name} eq $file1_ref->{entity}->{devicesDTO}->{deviceName}) {
  print "WebService1 already contains DeviceName \"".$file2_ref->{name}."\"\n";
} else {
  # POST request and add this device in WebService1/WebService2
  # Code here ....                                                                                                                                                                                                                                                              
}

您可以将调用转换为方法,我强烈建议您围绕转换添加和评估并检查错误,以防返回的 XML 有问题

首先请注意,关于两个 XML 文件是 "the same" 的含义,目前还没有普遍的共识。例如,每个人都同意忽略开始和结束标记中的空格,属性周围的单引号和双引号之间的区别无关紧要,属性可以按任何顺序排列;但对于如何处理注释、元素标签之间的空格、命名空间前缀和许多其他细节,要求各不相同。

另一个要求不同的领域是当文件被认为不同时您需要什么信息。有些机制只会给你一个是或否的答案,不会帮助你找到差异。

这会导致可能存在通用解决方案,但它们可能并不总能满足您的特定要求。

因此,如果您准备编写几百行代码,那么编写自己的比较器并不是一个荒谬的想法。

但是如果您能在 Perl 环境中找到 运行 的示例,您可以考虑两个现成的解决方案:

  • XML canonicalizers:规范化两个文档,然后在二进制级别比较结果。

  • XPath 2.0: 提供函数 deep-equal() 来比较两个节点(包括文档节点)

很像 simbaque I'd use XML::Twig,尽管我处理它的方式略有不同 - 我提供这个是为了比较 - 而不是使用 twig_handlers - 我称之为强大的和有用的技术,但特别适合增量解析更大的 XML - 使用 get_xpath 在 XML 中查找基于 xpath 的引用的东西可能提供替代方案。

#!/usr/bin/env perl
use strict;
use warnings;

use XML::Twig;

my $xml1 = XML::Twig->new->parsefile('test1a.xml');
my $xml2 = XML::Twig->new->parsefile('test1b.xml');

if ( $xml1->get_xpath( '//deviceName', 0 )->text 
  eq $xml2->root->att('name') )
{
   print "Name matches\n";
}

if ( $xml1->get_xpath( '//ipAddress', 0 )->text 
  eq $xml2->get_xpath( '//ipaddress', 0 )->text )
{
   print "IP matches\n";
}

我们将两个文件解析成一个XML::Twig对象,然后使用get_xpath查找节点位置。 // 表示树中的任何地方, 0 指的是哪个实例(例如第一个,唯一)。

理想情况下,我们可能会做一些 xpath 字符串来直接比较 - 我们不能在这里,因为 'name' 属性是根节点的属性(也是 XML::Twig xpath引擎是你不能直接select属性内容)。

但是 XML::LibXML - 功能更全面,代价是学习曲线有点陡峭。我一般不会使用它 但在这种特定情况下,它可以将 xpath 表达式处理为 select 根节点的属性。

所以会是这样的:

#!/usr/bin/env perl
use strict;
use warnings;

use XML::LibXML;

my %compare = (
   '//deviceName' => '//@name',
   '//ipAddress'  => '//ipaddress'
);

my $search1 = XML::LibXML::XPathContext->new(
                 XML::LibXML->load_xml( location => 'test1a.xml' ) );
my $search2 = XML::LibXML::XPathContext->new(
                 XML::LibXML->load_xml( location => 'test1b.xml' ) );

foreach my $key ( keys %compare ) {
   my $first  = $search1->find($key);
   my $second = $search2->find( $compare{$key} );

   print "$key = $first\n";
   print "$compare{$key} = $second\n";
   print "Matches found\n" if $first eq $second;
}