【问题标题】:Perl XML::LibXML $node->findnodes($xpath) finds nodes it shouldn'tPerl XML::LibXML $node->findnodes($xpath) 找到不应该的节点
【发布时间】:2012-08-10 21:46:13
【问题描述】:

这是一些我遇到问题的代码,我处理了一些 XML,并在 OO 类的一个方法中从文档中重复的几个节点中的每个节点中提取一个元素。每个节点的子树中应该只有一个这样的元素,但我的代码获取所有元素,就好像它对整个文档进行操作一样。

因为我只希望得到 oine 元素,所以我只使用数组的第零个元素,这会导致我的函数输出错误的值(文档中的所有项目都相同)

这里有一些说明问题的简化代码

$ cat t4.pl
#!/usr/bin/perl
use strict;
use warnings;
use XML::LibXML;

my $xml = <<EndXML;
<Envelope>
  <Body>
    <Reply>
      <List>
        <Item>
          <Id>8b9a</Id>
          <Message>
            <Response>
              <Identifier>55D</Identifier>
            </Response>
          </Message>
        </Item>
        <Item>
          <Id>5350</Id>
          <Message>
            <Response>
              <Identifier>56D</Identifier>
            </Response>
          </Message>
        </Item>
      </List>
    </Reply>
  </Body>
</Envelope>
EndXML

my $foo = Foo->new();

my $parser = XML::LibXML->new();
my $doc    = $parser->parse_string( $xml );
my @list   = $doc->getElementsByTagName( 'Item' );

for my $item ( @list ) {

    my $id = get( $item, 'Id' );
    my @messages = $item->getElementsByLocalName( 'Message' );

    for my $message ( @messages ) {

        my @children = $message->getChildNodes();

        for my $child ( @children ) {

            my $name = $child->nodeName;

            if ( $name eq 'Response' ) {
                print "child is a Response\n";
                $foo->do( $child, $id );
            }
            elsif ( $name eq 'text' ) {

                # ignore whitespace between elements
            }
            else {
                print "child name is '$name'\n";
            }
        }    # child
    }    # Message
}    # Item

# ..............................................

sub get {
    my ( $node, $name ) = @_;

    my $value   = "(Element $name not found)";
    my @targets = $node->getElementsByTagName( $name );

    if ( @targets ) {
        my $target = $targets[0];
        $value = $target->textContent;
    }

    return $value;
}

# ..............................................

package Foo;

sub new {
    my $self = {};
    bless $self;
    return $self;
}

sub do {
    my $self = shift;
    my ( $node, $id ) = @_;

    print '-' x 70, "\n", ' ' x 12, $node->toString( 1 ), "\n", '-' x 70, "\n";

    my @identifiers = $node->findnodes( '//Identifier' );
    print "do() found ", scalar @identifiers, " Identifiers\n";

    print "$id, ", $identifiers[0]->textContent, "\n\n";
}

这是输出

$ perl t4.pl
child is a Response
----------------------------------------------------------------------
            <Response>
              <Identifier>55D</Identifier>
            </Response>
----------------------------------------------------------------------
do() found 2 Identifiers
8b9a, 55D

child is a Response
----------------------------------------------------------------------
            <Response>
              <Identifier>56D</Identifier>
            </Response>
----------------------------------------------------------------------
do() found 2 Identifiers
5350, 55D

我期待

do() found 1 Identifiers

我期待最后一行是

5350, 56D

由于平台问题,我正在使用旧版本的 XML::LibXML。

问:是后期版本存在问题还是我做错了什么?

【问题讨论】:

    标签: xml perl xpath xml-libxml


    【解决方案1】:

    来自documentation of XPath 1.0

    //para 选择文档根

    的所有para后代

    (强调我自己的)。所以你的电话

    $node->findnodes( '//Identifier' )
    

    忽略上下文节点 $node 并在文档中的任何位置搜索所有 Identifier 元素

    要获取上下文节点的所有Identifier 后代,您必须添加一个点,如下所示

    $node->findnodes('.//Identifier');
    

    但由于$node 始终是Response 元素并且IdentifierResponse 的直接子元素,因此您可以编写

    $node->findnodes('Identifier');
    



    写这篇文章你似乎有点受不了了。我知道您已将代码缩减为示例,但是您真的需要单独的包吗?明智地应用 XPath 可以做很多事情。

    最明显的变化是您不需要循环遍历所有个孩子 - 您可以简单地挑选出您感兴趣的孩子。

    这段重构的代码可能值得一读

    use strict;
    use warnings;
    
    use XML::LibXML;
    
    my $parser = XML::LibXML->new;
    my $doc    = $parser->parse_fh(*DATA);
    
    for my $item ( $doc->findnodes('//Item') ) {
    
        print "\n";
    
        my ($id) = $item->findvalue('Id');
        printf "Item Id: %s\n", $item->findvalue('Id');
    
        my @messages = $item->findnodes('Message');
    
        for my $message (@messages) {
            my ($response) = $message->findnodes('Response');
            printf "Response Identifier: %s\n", $response->findvalue('Identifier');
        }
    }
    
    __DATA__
    <Envelope>
      <Body>
        <Reply>
          <List>
            <Item>
              <Id>8b9a</Id>
              <Message>
                <Response>
                  <Identifier>55D</Identifier>
                </Response>
              </Message>
            </Item>
            <Item>
              <Id>5350</Id>
              <Message>
                <Response>
                  <Identifier>56D</Identifier>
                </Response>
              </Message>
            </Item>
          </List>
        </Reply>
      </Body>
    </Envelope>
    

    输出

    Item Id: 8b9a
    Response Identifier: 55D
    
    Item Id: 5350
    Response Identifier: 56D
    

    【讨论】:

    • 谢谢,我会研究你的代码,但我的示例是从一个包实际上位于一个单独的模块中的示例中删除的,该模块也被其他几个程序“使用”。 XML 来自 SOAP 事务,比我展示的要复杂得多,并且处理必须处理许多其他类型的消息内容。
    • 所有这些都可能是真的,但是只看你的 get 子例程,当它是一个 XML::LibXML::Document 方法时,你在 XML::LibXML::Element 项目上调用 getElementsByTagName,以及整个 @ 987654338@ 似乎等同于 $node-&gt;findvalue($name)。我建议您对 LibXML 提供的有限 XPath 子集更加熟悉,并至少阅读 XML::LibXML::Document::Nodes::Element POD 文档
    【解决方案2】:

    我对代码的质量没有意见,但是在我使用XML::LibXML 之前学会了使用XML::DOM,我倾向于使用一些DOM 语法。我一直在努力改掉这个习惯:)。
    我提到这一点的原因是因为我看到您使用了 -&gt;item(0) 的等效项来从节点列表中获取第一个位置,就像在 DOM 中一样。
    XML::LibXML 支持使用 -&gt;item() 但从 cpan 我可以看到 xpath 创建的节点列表从 1 开始,而不是像 DOM 那样从 0 开始。我很确定,如果您将代码保持原样并寻找第一个数组位置而不是第 0 个,您将得到您想要的结果。
    不清楚的是为什么-&gt;item(0) 会像我的测试一样为您提供最后一个结果(它是否可能从数组值偏移,以便您实际上返回第 -1 个数组值)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-11-05
      • 1970-01-01
      • 2014-03-24
      • 2015-10-25
      • 2018-05-31
      • 2020-08-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多