PHP - 通过遍历 n 个无界元素获取 xml 值

PHP - Fetching xml values with looping over n unbounded element

我正在获取一些 xml 并将其转换为类似于下面的 csv。一些记录有额外的 n 个(无界)元素(“EntityEvents”)。我怎样才能同时获取它们并将它们写入第二个 (mm) csv 文件?

这是我的结构:

XML 文件:

<abc:ABCData xmlns:abc="http://www.abc-example.com" xmlns:xyz="http:/www.xyz-example.com">
<abc:ABCRecords>
  <abc:ABCRecord>
    <abc:ABC>5EXZX4LPK</abc:ABC>
    <abc:Entity>
      <abc:Name>Bornheim</abc:Name>
      <abc:EntityEvents>
        <abc:EntityEvent>
          <abc:EntityEventType>TypeA</abc:EntityEventType>
          <abc:EntityEventName>EventA</abc:EntityEventName> 
        </abc:EntityEvent>
      </abc:EntityEvents>    
    </abc:Entity>
  </abc:ABCRecord>
  <abc:ABCRecord>
    <abc:ABC>5967007LI</abc:ABC>
    <abc:Entity>
      <abc:Name>MOON BANK</abc:Name>
      <abc:EntityEvents>
        <abc:EntityEvent>
          <abc:EntityEventType>TypeB</abc:EntityEventType>
          <abc:EntityEventName>EventB</abc:EntityEventName>         
        </abc:EntityEvent>
        <abc:EntityEvent>
          <abc:EntityEventType>TypeC</abc:EntityEventType>
          <abc:EntityEventName>EventC</abc:EntityEventName>         
        </abc:EntityEvent>
      </abc:EntityEvents>                   
    </abc:Entity>
  </abc:ABCRecord>
  <abc:ABCRecord>
    <abc:ABC>2792340TZ</abc:ABC>
    <abc:Entity>
      <abc:Name>SUN BANK</abc:Name>
      <abc:EntityEvents>
        <abc:EntityEvent>
          <abc:EntityEventType>TypeD</abc:EntityEventType>
          <abc:EntityEventName>EventD</abc:EntityEventName>         
        </abc:EntityEvent>
        <abc:EntityEvent>
          <abc:EntityEventType>TypeF</abc:EntityEventType>
          <abc:EntityEventName>EventF</abc:EntityEventName>         
        </abc:EntityEvent>
        <abc:EntityEvent>
          <abc:EntityEventType>TypeG</abc:EntityEventType>
          <abc:EntityEventName>EventG</abc:EntityEventName>         
        </abc:EntityEvent>
      </abc:EntityEvents>                   
    </abc:Entity>
  </abc:ABCRecord>   
</abc:ABCRecords>
</abc:ABCData>

PHP 文件:

<?php

$reader = new XMLReader();
$reader->open('php://stdin');

$output = fopen('php://stdout', 'w');
fputcsv($output, ['id', 'name']);

$xmlns = [
  'abc' => 'http://www.abc-example.com'
];

$dom   = new DOMDocument;
$xpath = new DOMXpath($dom);
foreach ($xmlns as $prefix => $namespaceURI) {
  $xpath->registerNamespace($prefix, $namespaceURI);
}

while (
  $reader->read() && 
  (
    $reader->localName !== 'ABCRecord' || 
    $reader->namespaceURI !== $xmlns['abc']
  )
) {
  continue;
}

while ($reader->localName === 'ABCRecord') {
  if ($reader->namespaceURI === 'http://www.abc-example.com') {
    $node = $reader->expand($dom);
    fputcsv(
      $output, 
      [
        $xpath->evaluate('string(abc:ABC)', $node),
        $xpath->evaluate('string(abc:Entity/abc:Name)', $node)
      ]
    );
  }

  $reader->next('ABCRecord');
}     

输出 1 (CSV):

5EXZX4LPK,Bornheim
5967007LI,"MOON BANK"
2792340TZ,"SUN BANK"  

所需的输出 2 (CSV):

5EXZX4LPK,TypeA,EventA
5967007LI,TypeB,EventB
5967007LI,TypeC,EventC
2792340TZ,TypeD,EventD
2792340TZ,TypeE,EventE
2792340TZ,TypeF,EventF  

我怎样才能做到这一点?我想将它们写到一个单独的文件中,但我对如何实现这一点持开放态度。我也愿意分两步完成,这意味着在一个单独的 php 文件中。

使用单个 XPath 表达式和标记名称来填充数组。构建一个 csv 对象应该是微不足道的。

$document = new DOMDocument();
$document->loadXML($xml);
$xpath = new DOMXpath($document);

$csv1 = [];
$csv2 = [];
foreach ($xpath->evaluate("(//abc:ABCRecord/abc:ABC | //abc:Entity/abc:Name) | (//abc:ABCRecord/abc:ABC | //abc:ABCRecord//abc:EntityEvent)") as $ele) {
  if($ele -> localName == 'ABC'){
    $n = $ele->nodeValue;
  }
  if($ele -> localName == 'Name'){
    $csv1[] = $n . ','. $ele -> nodeValue;
  }else if($ele -> localName == 'EntityEvent'){
    $csv2[] = $n . ','. $ele -> nodeValue;
  }
}
var_dump($csv1);
var_dump($csv2);

结果

array(3) {
  [0]=>
  string(18) "5EXZX4LPK,Bornheim"
  [1]=>
  string(19) "5967007LI,MOON BANK"
  [2]=>
  string(18) "2792340TZ,SUN BANK"
}
array(6) {
  [0]=>
  string(16) "5EXZX4LPK,EventA"
  [1]=>
  string(16) "5967007LI,EventB"
  [2]=>
  string(16) "5967007LI,EventC"
  [3]=>
  string(16) "2792340TZ,EventD"
  [4]=>
  string(16) "2792340TZ,EventF"
  [5]=>
  string(16) "2792340TZ,EventG"
}

打开一个辅助文件句柄。然后把节点展开成DOM后,用表达式把events取出来,写到第二个文件里。

//...
$node = $reader->expand($dom);
// store the identifier
$identifier = $xpath->evaluate('string(abc:ABC)', $node);
fputcsv(
  $output, 
  [
    $identifier,
    $xpath->evaluate('string(abc:Entity/abc:Name)', $node)
  ]
);
// iterate the EntityEvent elements
foreach ($xpath->evaluate('abc:Entity/abc:EntityEvents/abc:EntityEvent', $node) as $event) {
  fputcsv(
    $detailOutput, 
    [
      $identifier,
      $xpath->evaluate('string(abc:EntityEventType)', $event),
      $xpath->evaluate('string(abc:EntityEventName)', $event)
    ]
  ); 
}
//...

您问题中的代码在 XMLReader 中实现了第一个节点列表迭代,以避免将整个文档加载到内存中。在 XMLReader::expand() 之后你得到了一个 DOM 节点。

用 Xpath 读取 DOM 始终是两者之一。基本位置路径 return 是一个节点列表(示例:ancestor/parent/child)。结果将始终是一个列表,如果表达式不匹配,它将是一个空列表。 Xpath 表达式可以变得更加复杂 - 它们允许条件、嵌套和替代项。

如果您需要单个值,您可以使用 Xpath 函数转换位置路径(示例:string(ancestor/parent/child))。 string()number() 之类的函数会将节点列表中的第一个值或 return 转换为默认值。如果表达式本身不匹配,string() 将 return 一个空字符串。其他方法或运算符的使用也可能导致类型转换(例如:count(ancestor/parent/child) > 0)。

但是,如果您可以使用 DOM methods/properties 从当前节点读取值,我建议您这样做。在这种情况下,Xpath 是不必要的开销。

// fetch and iterate nodes
foreach ($xpath->evaluate($expression, $contextNode) as $childNode) {
  var_dump(
    // reading an attribute 
    $childNode->getAttribute('attribute-one'),
    // the node name (without the namespace prefix)
    $childNode->localName,
    // using Xpath for nested data
    $xpath->evaluate('string(child)', $childNode)
  );
}

我会做一些不同的事情:

$targets = $xpath->query("//abc:ABCRecord");
foreach ($targets as $target) {
  $id = $xpath->query('.//abc:ABC', $target)[0]->nodeValue;
  $events = $xpath->query('.//abc:EntityEvent', $target);
  foreach ($events as $event) {
    $type = $xpath->query('.//abc:EntityEventType', $event)[0]->nodeValue;
    $name = $xpath->query('.//abc:EntityEventName', $event)[0]->nodeValue;
    fputcsv(
      $output,
      [
        $id,
        $type,
        $name
      ]
    );
  }
  
}