如何获取两个 HTML 标签之间的所有内容?(用XPath?


How to get everything between two HTML tags? (with XPath?)

编辑:我添加了一个在这种情况下有效的解决方案。


我想

从页面中提取一个表,我想(可能(使用 DOMDocument 和 XPath 来做到这一点。但如果你有更好的主意,告诉我。

我的第一次尝试是这样的(显然是错误的,因为它会得到第一个关闭表标签(:

<?php 
    $tableStart = strpos($source, '<table class="schedule"');
    $tableEnd   = strpos($source, '</table>', $tableStart);
    $rawTable   = substr($source, $tableStart, ($tableEnd - $tableStart));
?>

我很难,这可能可以通过 DOMDocument 和/或 xpath 来解决......


最后,我想要标签(在本例中为标签(和标签本身之间的所有内容。所以所有的HTML,而不仅仅是值(例如,不仅仅是"值",而是"值"(。还有一个"陷阱"...

  • 该表中有其他表。因此,如果您只是搜索表的末尾(' tag'(,您可能会得到错误的标签。
  • 开始标签有一个类,你可以用它来识别它(类名='schedule'(。

这可能吗?

这是我想从另一个网站提取的(简化的(源部分:(我还想显示 html 标签,而不仅仅是值,所以整个表都带有类"schedule"(

<table class="schedule">
    <table class="annoying nested table">
        Lots of table rows, etc.
    </table> <-- The problematic tag...
    <table class="annoying nested table">
        Lots of table rows, etc.
    </table> <-- The problematic tag...
    <table class="annoying nested table">
        Lots of table rows, etc.
    </table> <-- a problematic tag...
    This could even be variable content. =O =S
</table>

首先,请注意XPath是基于XML Infopath的 - 一个XML模型,其中没有"开始标记"和"结束标记",只有节点

因此,不应期望 XPath 表达式选择"标记"——它选择节点

考虑到这一事实,我将这个问题解释为:

我想获取给定"开始"之间的所有元素的集合 元素和给定的"结束元素",包括开始和结束元素。

在 XPath 2.0 中,这可以通过标准运算符相交方便地完成。

在XPath 1.0(我假设您正在使用(中,这并不容易。解决方案是使用 Kayessian(通过 @Michael Kay(公式进行节点集交集

通过计算以下 XPath 表达式来选择两个节点集的交集:$ns1$ns2

$ns1[count(.|$ns2) = count($ns2)]

假设我们有以下 XML 文档(因为您从未提供过(:

<html>
    <body>
        <table>
            <tr valign="top">
                <td>
                    <table class="target">
                        <tr>
                            <td>Other Node</td>
                            <td>Other Node</td>
                            <td>Starting Node</td>
                            <td>Inner Node</td>
                            <td>Inner Node</td>
                            <td>Inner Node</td>
                            <td>Ending Node</td>
                            <td>Other Node</td>
                            <td>Other Node</td>
                            <td>Other Node</td>
                        </tr>
                    </table>
                </td>
            </tr>
        </table>
    </body>
</html>

起始元素由以下方式选择

//table[@class = 'target']
         //td[. = 'Starting Node']

结束元素由以下方法选择

//table[@class = 'target']
         //td[. = Ending Node']

为了获得所有想要的节点,我们将以下两个集合相交

  1. 由开始元素和所有后续元素组成的集合(我们将其命名为$vFollowing(。

  2. 由结束元素和所有前面元素组成的集合(我们将其命名为$vPreceding(。

它们分别由以下 XPath 表达式选择

$vFollowing:

$vStartNode | $vStartNode/following::*

$vPreceding:

$vEndNode | $vEndNode/preceding::*

现在我们可以简单地将 Kayessian 公式应用于节点集$vFollowing$vPreceding

       $vFollowing
          [count(.|$vPreceding)
          =
           count($vPreceding)
          ]

剩下的就是用它们各自的表达式替换所有变量。

基于 XSLT 的验证

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>
 <xsl:variable name="vStartNode" select=
 "//table[@class = 'target']//td[. = 'Starting Node']"/>
 <xsl:variable name="vEndNode" select=
 "//table[@class = 'target']//td[. = 'Ending Node']"/>
 <xsl:variable name="vFollowing" select=
 "$vStartNode | $vStartNode/following::*"/>
 <xsl:variable name="vPreceding" select=
 "$vEndNode | $vEndNode/preceding::*"/>
 <xsl:template match="/">
      <xsl:copy-of select=
          "$vFollowing
              [count(.|$vPreceding)
              =
               count($vPreceding)
              ]"/>
 </xsl:template>
</xsl:stylesheet>

当应用于上面的 XML 文档时,将计算 XPath 表达式,并输出所需的、正确的结果选择节点集

<td>Starting Node</td>
<td>Inner Node</td>
<td>Inner Node</td>
<td>Inner Node</td>
<td>Ending Node</td>

不要使用正则表达式(或strpos...(来解析 HTML!

这个问题对你来说很困难的部分原因是你在思考"标签"而不是"节点"或"元素"。标记是序列化的项目。(HTML 具有可选的结束标记。节点是实际的数据结构。DOMDocument没有"标签",只有以正确的树结构排列的"节点"。

以下是使用 XPath 获取表的方法:

// This is a simple solution, but only works if the value of "class" attribute is exactly "schedule"
// $xpath = '//table[@class="schedule"]';
// This is what you want. It is equivalent to the "table.schedule" css selector:
$xpath = "//table[contains(concat(' ',normalize-space(@class),' '),' schedule ')]";
$d = new DOMDocument();
$d->loadHTMLFile('http://example.org');
$xp = new DOMXPath($d);
$tables = $xp->query($xpath);
foreach ($tables as $table) {
    $table; // this is a DOMElement of a table with class="schedule"; It includes all nodes which are children of it.
}

如果你有像这样的格式良好的HTML

<html>
<body>
    <table>
        <tr valign='top'>
            <td>
                <table class='inner'>
                    <tr><td>Inner Table</td></tr>
                </table>
            </td>
            <td>
                <table class='second inner'>
                    <tr><td>Second  Inner</td></tr>
                </table>
            </td>
        </tr>
    </table>
</body>
</html>

使用此pho代码输出节点(在xml包装器中(

<?php
    $xml = new DOMDocument();
    $strFileName = "t.xml";
    $xml->load($strFileName);
    $xmlCopy = new DOMDocument();
    $xmlCopy->loadXML( "<xml/>" ); 
    $xpath = new domxpath( $xml );
    $strXPath = "//table[@class='inner']";
    $elements = $xpath->query( $strXPath, $xml );
    foreach( $elements as $element ) {
        $ndTemp = $xmlCopy->importNode( $element, true );
        $xmlCopy->documentElement->appendChild( $ndTemp );
    }
    echo $xmlCopy->saveXML();
?>

这将获取整个表。但是可以对其进行修改以使其获取另一个标签。这是一个非常特定于案例的解决方案,只能在特定情况下使用。如果 html、php 或 css 注释包含开始或结束标记,则中断。请谨慎使用。

功能:

// **********************************************************************************
// Gets a whole html tag with its contents.
//  - Source should be a well formatted html string (get it with file_get_contents or cURL)
//  - You CAN provide a custom startTag with in it e.g. an id or something else (<table style='border:0;')
//    This is recommended if it is not the only p/table/h2/etc. tag in the script.
//  - Ignores closing tags if there is an opening tag of the same sort you provided. Got it?
function getTagWithContents($source, $tag, $customStartTag = false)
{
    $startTag = '<'.$tag;
    $endTag   = '</'.$tag.'>';
    $startTagLength = strlen($startTag);
    $endTagLength   = strlen($endTag);
//      ***************************** 
    if ($customStartTag)
        $gotStartTag = strpos($source, $customStartTag);
    else
        $gotStartTag = strpos($source, $startTag);
    // Can't find it?
    if (!$gotStartTag)
        return false;       
    else
    {
//      ***************************** 
        // This is the hard part: finding the correct closing tag position.
        // <table class="schedule">
        //     <table>
        //     </table> <-- Not this one
        // </table> <-- But this one
        $foundIt          = false;
        $locationInScript = $gotStartTag;
        $startPosition    = $gotStartTag;
        // Checks if there is an opening tag before the start tag.
        while ($foundIt == false)
        {
            $gotAnotherStart = strpos($source, $startTag, $locationInScript + $startTagLength);
            $endPosition        = strpos($source, $endTag,   $locationInScript + $endTagLength);
            // If it can find another opening tag before the closing tag, skip that closing tag.
            if ($gotAnotherStart && $gotAnotherStart < $endPosition)
            {               
                $locationInScript = $endPosition;
            }
            else
            {
                $foundIt  = true;
                $endPosition = $endPosition + $endTagLength;
            }
        }
//      ***************************** 
        // cut the piece from its source and return it.
        return substr($source, $startPosition, ($endPosition - $startPosition));
    } 
}

功能的应用:

$gotTable = getTagWithContents($tableData, 'table', '<table class="schedule"');
if (!$gotTable)
{
    $error = 'Faild to log in or to get the tag';
}
else
{
    //Do something you want to do with it, e.g. display it or clean it...
    $cleanTable = preg_replace('|href=''(.*)''|', '', $gotTable);
    $cleanTable = preg_replace('|TITLE="(.*)"|', '', $cleanTable);
}

上面你可以找到我问题的最终解决方案。在旧解决方案下面,我从中制作了一个通用功能。

旧解决方案:

// Try to find the table and remember its starting position. Check for succes.
// No success means the user is not logged in.
$gotTableStart = strpos($source, '<table class="schedule"');
if (!$gotTableStart)
{
    $err = 'Can''t find the table start';
}
else
{
//      ***************************** 
    // This is the hard part: finding the closing tag.
    $foundIt          = false;
    $locationInScript = $gotTableStart;
    $tableStart       = $gotTableStart;
    while ($foundIt == false)
    {
        $innerTablePos = strpos($source, '<table', $locationInScript + 6);
        $tableEnd      = strpos($source, '</table>', $locationInScript + 7);
        // If it can find '<table' before '</table>' skip that closing tag.
        if ($innerTablePos != false && $innerTablePos < $tableEnd)
        {               
            $locationInScript = $tableEnd;
        }
        else
        {
            $foundIt  = true;
            $tableEnd = $tableEnd + 8;
        }
    }
//      ***************************** 
    // Clear the table from links and popups...
    $rawTable   = substr($tableData, $tableStart, ($tableEnd - $tableStart));
}