我在递归调用中使用引用时遇到问题.
我想要完成的是根据相应元素中不同节点的最大数量来描述XML文档 – 事先不知道任何节点元素名称.
考虑这个文件:
<Data>
<Record>
<SAMPLE>
<TITLE>Superior Title</TITLE>
<SUBTITLE>Sub Title</SUBTITLE>
<AUTH>
<FNAME>John</FNAME>
<disPLAY>No</disPLAY>
</AUTH>
<AUTH>
<FNAME>Jane</FNAME>
<disPLAY>No</disPLAY>
</AUTH>
<ABSTRACT/>
</SAMPLE>
</Record>
<Record>
<SAMPLE>
<TITLE>Interesting Title</TITLE>
<AUTH>
<FNAME>John</FNAME>
<disPLAY>No</disPLAY>
</AUTH>
<ABSTRACT/>
</SAMPLE>
<SAMPLE>
<TITLE>Another Title</TITLE>
<AUTH>
<FNAME>Jane</FNAME>
<disPLAY>No</disPLAY>
</AUTH>
<ABSTRACT/>
</SAMPLE>
</Record>
</Data>
您可以看到Record有1个或2个SAMPLE节点,SAMPLE有1个或2个AUTH节点.我正在尝试生成一个数组,该数组将根据相应节点内不同节点的最大数量来描述文档的结构.
所以我试图得到这样的结果:
$result = [
"Data" => [
"max_count" => 1,
"elements" => [
"Record" => [
"max_count" => 2,
"elements" => [
"SAMPLE" => [
"max_count" => 2,
"elements" => [
"TITLE" => [
"max_count" => 1
],
"SUBTITLE" => [
"max_count" => 1
],
"AUTH" => [
"max_count" => 2,
"elements" => [
"FNAME" => [
"max_count" => 1
],
"disPLAY" => [
"max_count" => 1
]
]
],
"ABSTRACT" => [
"max_count" => 1
]
]
]
]
]
]
]
];
为了保持我的一点理智,我使用sabre/xml来完成解析XML的工作.
private function countArrayElements(&$array, &$result){
// get collection of subnodes
foreach ($array as $node){
$name = $this->stripNamespace($node['name']);
// get count of distinct subnodes
if (empty($result[$name])){
$result[$name]["max_count"] = 1;
} else {
$result[$name]["max_count"]++;
}
if (is_array($node['value'])){
$this->countArrayElements($node['value'], $result[$name]["elements"]);
}
}
}
所以我的理由是我也可以通过引用传递数组并进行比较,这适用于前两个节点,但不知何故在后续节点上重置,这导致AUTH节点的计数仅为1.
private function countArrayElements(&$array, &$prevIoUs){
// get collection of subnodes
foreach ($array as $node){
$name = $this->stripNamespace($node['name']);
// get count of distinct subnodes
if (empty($result[$name]["max_count"])){
$result[$name]["max_count"] = 1;
} else {
$result[$name]["max_count"]++;
}
// recurse
if (is_array($node['value'])){
$result[$name]["elements"] = $this->countArrayElements(
$node['value'],
$result[$name]["elements"]
);
}
// compare prevIoUs max
if (!empty($prevIoUs[$name]["max_count"])){
$result[$name]["max_count"] = max(
$prevIoUs[$name]["max_count"],
$result[$name]["max_count"]
);
}
}
return $result;
}
我意识到这是一个非常复杂的问题,它只是一个更大的项目的一小部分,所以我试图尽可能地分解这个MCVE,我还准备了a special repository这些文件完成了一个PHPunit测试.
解决方法:
虽然你的解决方案有效,并且非常有效,因为它在O(n * k)时间内运行(其中n是树中节点的数量,k是顶点的数量),我想我会提出一个替代解决方案,它不依赖于数组或引用,而且更通用化,不仅适用于XML,也适用于任何DOM树.该解决方案也可在O(n * k)时间内运行,因此效率也很高.唯一的区别是您可以使用generator中的值而无需先构建整个阵列.
建模问题
我理解这个问题的最简单方法是将其建模为图形.如果我们对文档进行建模,那么我们获得的是级别和顶点.
如此有效,这使我们能够分而治之,将问题分解为两个截然不同的步骤.
>将给定垂直的基本子节点名称计算为总和(verticies)
>在水平(水平)上找到集合总和的最大值
这意味着如果我们在这棵树上进行水平顺序遍历,我们应该能够轻松地将节点名称的基数作为所有垂直的最大总和.
换句话说,获得每个节点的不同子节点名称存在基数问题.然后是找到整个级别的最大总和的问题.
最小,完整,可验证,自包含的示例
因此,为了提供一个最小,完整,可验证和自包含的示例,我将依赖于扩展PHP的DOMDocument而不是您在示例中使用的第三方XML库.
It’s probably worth noting that this code is not backwards compatible with PHP 5, (because of the use of
yield from
), so you must use PHP 7 for this implementation to work.
首先,我将在DOMDocument中实现一个函数,它允许我们使用generator以级别顺序迭代DOM树.
class SpecialDOM extends DOMDocument {
public function level(DOMNode $node = null, $level = 0, $ignore = ["#text"]) {
if (!$node) {
$node = $this;
}
$stack = [];
if ($node->hasChildNodes()) {
foreach($node->childNodes as $child) {
if (!in_array($child->nodeName, $ignore, true)) {
$stack[] = $child;
}
}
}
if ($stack) {
yield $level => $stack;
foreach($stack as $node) {
yield from $this->level($node, $level + 1, $ignore);
}
}
}
}
函数本身的机制实际上非常简单.它不依赖于传递数组或使用引用,而是使用DOMDocument对象本身来构建给定节点中所有子节点的堆栈.然后它可以立即产生整个堆栈.这是关卡部分.此时,我们依靠递归从该堆栈中的每个元素产生下一级别的任何其他节点.
这是一个非常简单的XML文档,用于演示这是多么简单.
$xml = <<<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<Data>
<Record>
<SAMPLE>Some Sample</SAMPLE>
</Record>
<Note>
<SAMPLE>Some Sample</SAMPLE>
</Note>
<Record>
<SAMPLE>Sample 1</SAMPLE>
<SAMPLE>Sample 2</SAMPLE>
</Record>
</Data>
XML;
$dom = new SpecialDOM;
$dom->loadXML($xml);
foreach($dom->level() as $level => $stack) {
echo "- Level $level\n";
foreach($stack as $item => $node) {
echo "$item => $node->nodeName\n";
}
}
输出将如下所示.
- Level 0 0 => Data - Level 1 0 => Record 1 => Note 2 => Record - Level 2 0 => SAMPLE - Level 2 0 => SAMPLE - Level 2 0 => SAMPLE 1 => SAMPLE
所以至少现在我们有一种方法可以知道节点在什么级别以及它在该级别上出现的顺序,这对我们打算做的事情很有用.
现在,构建嵌套数组的想法实际上不需要获得max_count所寻求的基数.因为我们已经可以从DOM树访问节点本身.这意味着我们知道在每次迭代时循环内部包含哪些元素.我们不必一次生成整个数组来开始探索它.我们可以在级别顺序执行此操作,这实际上非常酷,因为这意味着您可以构建一个平面数组以获取每条记录的max_count.
让我演示一下这是如何工作的.
$max = [];
foreach($dom->level() as $level => $stack) {
$sum = [];
foreach($stack as $item => $node) {
$name = $node->nodeName;
// the sum
if (!isset($sum[$name])) {
$sum[$name] = 1;
} else {
$sum[$name]++;
}
// the maximum
if (!isset($max[$level][$name])) {
$max[$level][$name] = 1;
} else {
$max[$level][$name] = max($sum[$name], $max[$level][$name]);
}
}
}
var_dump($max);
我们得到的输出看起来像这样.
array(3) { [0]=> array(1) { ["Data"]=> int(1) } [1]=> array(2) { ["Record"]=> int(2) ["Note"]=> int(1) } [2]=> array(1) { ["SAMPLE"]=> int(2) } }
这证明我们可以在不需要引用或复杂嵌套数组的情况下计算max_count.当您消除PHP数组的单向映射语义时,它也更容易包围.
概要
array(5) { [0]=> array(1) { ["Data"]=> int(1) } [1]=> array(1) { ["Record"]=> int(2) } [2]=> array(1) { ["SAMPLE"]=> int(2) } [3]=> array(4) { ["TITLE"]=> int(1) ["SUBTITLE"]=> int(1) ["AUTH"]=> int(2) ["ABSTRACT"]=> int(1) } [4]=> array(2) { ["FNAME"]=> int(1) ["disPLAY"]=> int(1) } }
这与每个子数组的max_count相同.
> 0级
>数据=> max_count 1
> 1级
>记录=> max_count 2
> 2级
> SAMPLE => max_count 2
> 3级
> TITLE => max_count 1
> SUBTITLE => max_count 1
> AUTH => max_count 2
> ABSTRACT => max_count 1
>第4级
> FNAME => max_count 1
> disPLAY => max_count 1
要在整个循环中获取任何这些节点的元素,只需查看$node-> childNodes,因为您已经拥有了树(因此无需引用).
您需要将元素嵌套到数组中的唯一原因是因为PHP数组的键必须是唯一的,因为您使用节点名作为键,这需要嵌套以获取树的较低级别并仍然构造max_count的值正确.所以这是一个数据结构问题,我通过避免在数据结构之后对解决方案进行建模来解决它.