【问题标题】:Retrieving data from siblings to node that match将数据从兄弟节点检索到匹配的节点
【发布时间】:2012-12-02 22:42:50
【问题描述】:

我正在使用 SimpleXML 遍历一个 xml 文档。我有一个带有 id 的数组($ids),我正在检查 XML(工作表/表/行/单元格/数据)中是否存在匹配项。如果匹配,我希望能够从以下两个兄弟姐妹那里获取数据,但我不知道如何。

来自 php:

// $ids <---- array('8', '53', '38')

foreach ($thePositions->Worksheet->Table->Row as $row) {

    if($row->Cell->Data == true) {

        for ($i = 0; $i < count($ids); $i++) {
            foreach($row->Cell->Data as $data) {

                if ($data == $ids[$i]) {
                    echo 'match!';

                    /* 
                       Tried $siblings = $data->xpath('preceding-sibling::* | following-sibling::*');
                       but doesn't seem to work in this case.
                    */
                }
            }
        }
    }
}

xml:

<?xml version="1.0"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:o="urn:schemas-microsoft-com:office:office"
 xmlns:x="urn:schemas-microsoft-com:office:excel"
 xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:html="http://www.w3.org/TR/REC-html40">
 <DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">
  <LastAuthor>Herpa Derp </LastAuthor>
  <Created>2012-09-25T13:44:01Z</Created>
  <LastSaved>2012-09-25T13:48:24Z</LastSaved>
  <Version>14.0</Version>
 </DocumentProperties>
 <OfficeDocumentSettings xmlns="urn:schemas-microsoft-com:office:office">
  <AllowPNG/>
 </OfficeDocumentSettings>
 <ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">
  <WindowHeight>14060</WindowHeight>
  <WindowWidth>25040</WindowWidth>
  <WindowTopX>25540</WindowTopX>
  <WindowTopY>4100</WindowTopY>
  <Date1904/>
  <ProtectStructure>False</ProtectStructure>
  <ProtectWindows>False</ProtectWindows>
 </ExcelWorkbook>
 <Styles>
  <Style ss:ID="Default" ss:Name="Normal">
   <Alignment ss:Vertical="Bottom"/>
   <Borders/>
   <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="12" ss:Color="#000000"/>
   <Interior/>
   <NumberFormat/>
   <Protection/>
  </Style>
  <Style ss:ID="s62">
   <Font ss:FontName="Courier" ss:Color="#000000"/>
  </Style>
 </Styles>
 <Worksheet ss:Name="Workbook1.csv">
  <Table ss:ExpandedColumnCount="5" ss:ExpandedRowCount="79" x:FullColumns="1"
   x:FullRows="1" ss:DefaultColumnWidth="65" ss:DefaultRowHeight="15">
   <Column ss:Index="2" ss:AutoFitWidth="0" ss:Width="43"/>
   <Column ss:AutoFitWidth="0" ss:Width="113"/>
   <Column ss:Index="5" ss:AutoFitWidth="0" ss:Width="220"/>
   <Row ss:Index="6">
    <Cell ss:Index="3" ss:StyleID="s62"/>
   </Row>
   <Row>
    <Cell ss:Index="3" ss:StyleID="s62"/>
   </Row>
   <Row>
    <Cell ss:Index="3" ss:StyleID="s62"/>
   </Row>
   <Row>
    <Cell ss:Index="2"><Data ss:Type="String">id</Data></Cell>
    <Cell ss:StyleID="s62"><Data ss:Type="String">latitude</Data></Cell>
    <Cell><Data ss:Type="String">longitude</Data></Cell>
   </Row>
   <Row>
    <Cell ss:Index="2"><Data ss:Type="Number">8</Data></Cell>
    <Cell ss:StyleID="s62"><Data ss:Type="Number">57.4999</Data></Cell>    // to be saved to $latutude
    <Cell><Data ss:Type="Number">15.8280</Data></Cell>    // to be saved to $longitude
   </Row>
   <Row>
    <Cell ss:Index="2"><Data ss:Type="Number">38</Data></Cell>
    <Cell><Data ss:Type="Number">56.5659</Data></Cell>
    <Cell><Data ss:Type="Number">16.1380</Data></Cell>
   </Row>

【问题讨论】:

  • XML 命名空间定义怎么样
  • 什么不起作用?结果太多?没有结果?如果你想要 2 个下一个兄弟姐妹,你可以像 //Row[Cell/Data[. = '8' or . = '53' or . = '38']]/following-sibling::*[position() &lt;= 2] 这样在 XPath 中完全运行它
  • @BeniBela 如果它解决了提出的问题,为什么这是评论而不是答案?
  • @IMSoP:因为我们不知道他的问题到底是什么。他说,他想要下面的兄弟姐妹,但评论也读到前面的兄弟姐妹......如果有命名空间问题,它就行不通了......
  • @BeniBela 这个问题包括一个相当清晰的例子,你可以根据它测试你的代码,对我来说似乎很清楚。

标签: php xml xpath simplexml siblings


【解决方案1】:

要求兄弟姐妹不起作用的原因是&lt;Data&gt; 元素不是兄弟姐妹;它们更像是表亲——相邻 &lt;Cell&gt; 元素的子代。

出于同样的原因,您不应该使用foreach($row-&gt;Cell-&gt;Data as $data),因为这相当于foreach($row-&gt;Cell[0]-&gt;Data as $data),即查看第一个&lt;Cell&gt; 节点的所有&lt;Data&gt; 子节点。因为在&lt;Cell&gt; 中只会有一个&lt;Data&gt; 元素,您不妨写$data = $row-&gt;Cell[0]-&gt;Data - 在这种情况下很好,因为您要查找的值位于行的开头。

你真正需要做的是循环&lt;Cell&gt;s:foreach($row-&gt;Cell as $cell) { $data = $cell-&gt;Data; /* ... */ }

然后,您有几个选项可用于查找相邻单元格,包括 XPath。一种更“PHP-ish”的方式是使用数组索引(兄弟在 SimpleXML 循环/数组访问中以数字方式索引):

foreach($row->Cell as $cell_index => $cell)
{
    $data = $cell->Data;
    if ($data == $ids[$i])
    {
        // Tip: always cast SimpleXML objects to string when you're done with their magic XMLiness
        $latitudes[$i] = (string)$row->Cell[ $cell_index + 1 ]->Data;
        $longitudes[$i] = (string)$row->Cell[ $cell_index + 2 ]->Data;
    }
}

或者,您可以依赖您的 ID 始终位于第一列,而 lat 和 long 位于接下来的两列(毕竟这是一个电子表格!)并完全避免内部循环:

if ( $row->Cell[0]->Data == $ids[$i] )
{
    $latitudes[$i] = (string)$row->Cell[1]->Data;
    $longitudes[$i] = (string)$row->Cell[2]->Data;
}

【讨论】:

  • 非常感谢您的详细解释。我知道我的代码存在问题。我已经实现了您的代码(尝试了两个“版本”),但是我想知道如何解决一个问题,那就是通过 $ids 中的 id 进行迭代。我尝试将代码放在 for 循环中,这意味着我对每个 $row 的所有 Cell 节点进行迭代,因为在 $ids 中还有数据需要迭代。这可行,但需要很长时间(超过 60 秒,以便 PHP 解析器显示错误消息)。如何以更好、更快的方式做到这一点?
  • @holyredbeard 绝对可以加快将问题代码中的for ($i = 0; $i &lt; count($ids); $i++) 更改为for ($i = 0, $c = count($ids); $i &lt; $c; $i++) 的速度(如果每次迭代都会执行$i &lt; count($ids) 计数)。这个例子假设 lat 和 lng 是 $cell_index + 1$cell_index + 2 - 所以首先检查有多少个单元格,如果剩下的处理不超过 3 个或不超过 2 个(当前处理的除外),然后中断并转到另一行。
  • 对不起,我的意思是如果总共少于 3 个或除当前一个之外少于 2 个
  • @holyredbeard 如其他答案之一所述,您可以使用in_arrayarray_search。您还可以切换循环,因此在每一行内,您依次检查每个 ID。如果 ID 是唯一的,您可以在找到它们时将它们从列表中删除;一旦你没有剩余,你可以使用 break 停止查找(使用例如 break 3 如果你最终有 3 个嵌套循环)。
  • @holyredbeard 另请参阅我的附加答案,如果要搜索的 ID 很多,应该会更有效。
【解决方案2】:

在这种 XML 的情况下,单元格的顺序始终相同,因此可以按如下方式完成:

$ids = array('8', '53', '38');
foreach ($xml->Worksheet->Table->Row as $row) {
    $children = $row->children();
    if (count($children) == 3 && in_array(((string) $children[0]->Data), $ids)) {
        echo 'lat: ' . $children[1]->Data . ' lng: ' . $children[2]->Data . "\n";
    }
}

【讨论】:

    【解决方案3】:

    您可以完全在 XPath 中完成,无需任何循环,例如:

    //Row[Cell/Data[. = '8' or . = '53' or . = '38']]/following-sibling::*[position() <= 2]
    

    在任何数据元素中搜索具有 id 的所有行,然后获取接下来的两个兄弟。

    或者

    //Row[Cell[1]/Data[. = '8' or . = '53' or . = '38']]/following-sibling::*[position() <= 2]
    

    如果确定 id 总是在第一个单元格中。 (这也可以防止由于 id 与经度/经度相同而导致的错误)

    或者

    //Row[Cell[@ss:Index = "2"]/Data[. = '8' or . = '53' or . = '38']]/following-sibling::*[position() <= 2]
    

    如果 id 在索引为 2 的单元格中。

    但在所有情况下,您都需要正确初始化命名空间

    【讨论】:

    • 有趣的方法。不过我有几个问题。 1. 在您的代码中,您明确查找 id 的 8、53 和 38,但它可以是 $ids 中的任何 id(1 个或多个)。我可以将变量直接放在 XPath 代码中吗? 2. 我想将 id 及其位置保存在一个多维数组中,你的代码怎么能做到这一点?提前致谢!
    • 1:在 XPath 2 中可以,我不知道您是否可以直接在 XPath 1 中这样做。但是您可以动态构造查询". = '".implode("' or . = '",$ids)."'" 2:XPath 不支持多维数组。您可以从查询中删除以下兄弟姐妹以获取所有匹配的行,然后使用 dom 函数在 php 中获取他们的下一个兄弟姐妹
    【解决方案4】:

    如果您有很多要匹配的 ID,另一种方法是根据 ID 创建所有行的“散列”,然后查看该散列而不是循环搜索匹配项。

    // Initialise an empty array to use as the hash
    $rows_by_id = array();
    
    // Loop over all the rows in the spreadsheet
    foreach ($thePositions->Worksheet->Table->Row as $row) {
        // Skip rows with less than 3 cells
        if ( count($row->Cell) < 3 ) {
            continue;
        }
    
        // Take the ID from the first cell on the row
        $id = (string)$row->Cell[0]->Data;
    
        // Add this row to the hash, identified by it's ID
        $rows_by_id[$id] = array(
            'latitude'  => (string)$row->Cell[1]->Data,
            'longitude' => (string)$row->Cell[2]->Data
        );
    
        // Or if your IDs are not unique, and you want all matches:
        // $rows_by_id[$id][] = array( ... )
    }
    
    foreach ( $ids  as $required_id ) {
        // Retrieve the results from the hash
        if ( isset($rows_by_id[$required_id]) ) { 
            $matched_row = $rows_by_id[$required_id];
    
            echo "ID $required_id has latitude {$matched_row['latitude']} and longitude {$matched_row['longitude']}.\n";
        }
        else {
            echo "ID $required_id was not matched. :(\n";
        }
    
        // If you have non-unique matches, you'll need something like this:
        // $all_matched_rows = $rows_by_id[$required_id]; ... foreach ( $all_matched_rows as $matched_row )
    }
    

    【讨论】:

    • $required_id 没有行时,您会收到通知 - 您应该检查它是否存在 ($matched_row = isset($rows_by_id[$required_id]) ? $_rows_by_id[$required_id] : null;)
    • @lupatus 好点。我倾向于在关闭通知的情况下运行,因为我发现将它们全部修复实际上会导致更丑陋的代码而没有什么好处。在这种情况下,if ( isset($rows_by_id[$required_id]) ) { $matched_row = $rows_by_id[$required_id]; ... } 可能有意义。
    • (答案已编辑以包含isset 和行宽检查)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多