【问题标题】:How to get everything between two HTML tags? (with XPath?)如何获取两个 HTML 标签之间的所有内容? (使用 XPath?)
【发布时间】:2012-02-15 13:26:16
【问题描述】:

编辑:我添加了一个适用于这种情况的解决方案。


我想从页面中提取一个表,并且我想(可能)使用 DOMDocument 和 XPath 来执行此操作。但如果你有更好的主意,请告诉我。

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

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

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


最后,我想要标签(在本例中为标签)和标签之间的所有内容。所以所有的 HTML,不仅仅是值(例如,不仅仅是“价值”,而是“价值”)。还有一个“捕获”...

  • 该表中包含其他表。因此,如果您只搜索表格的结尾(“标签”),您可能会得到错误的标签。
  • 开始标签有一个您可以识别它的类 (classname = '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>

【问题讨论】:

  • 是的,使用 DOMDocument,就像这里的拆分/合并 XML 文件示例 stackoverflow.com/questions/8602503/copy-xml-attributes-php/…
  • 使用 XPath 语句,例如“//table[@class='schedule']”或“//table[3]”。
  • 然后呢?你能举个例子吗?因为我就是想不通:S 我一直在努力寻找整个晚上......
  • 我在您提供的 html 中的任何地方都没有看到字符串“schedule”。你想要的输出到底是什么?您使用的术语不准确(“标签”、“元素”、“html 不是值”等),因此我们无法理解您的问题。
  • @FrancisAvila:我修改了我的问题。请记住,我是荷兰人,而不是 php 专家。哦,也看看我的解决方案:)

标签: php xpath screen-scraping


【解决方案1】:

首先,请注意 XPath 是基于 XML Infopath 的——一种没有“起始标签”和“结束标签”但只有 节点的 XML 模型>

因此,不应期望 XPath 表达式选择“标签”——它会选择 节点

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

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

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

在 XPath 1.0(我假设您正在使用)中,这并不容易。解决方案是使用 Kayessian (by @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>

【讨论】:

  • 我现在提供了我想要显示的源代码。请注意,我想显示所有的 html。
  • @SuperSpy:这根本不是格式良好的 XML——您需要清理它以使其成为格式良好的 XML。 XPath 对格式良好的 XML 文档进行操作。
  • 我无法格式化,而且不是 xml。它是另一个网站的来源。看看我的解决方案。 (虽然没有 DomDoc 或 XPath..)
【解决方案2】:

不要使用正则表达式(或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.
}

【讨论】:

    【解决方案3】:

    如果你有这样的格式良好的 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();
    ?>
    

    【讨论】:

    • 这似乎不起作用。我已经努力让它工作了......我已经编辑了我的帖子。也许你现在可以更好地帮助我。
    • @SuperSpy,我不确定什么不起作用,或者您期望什么输出。上面的例子提取了一个包裹在一个外部表中的内部表,这不就是你想要做的吗?
    • 我已经更新了我的问题并且我有一个解决方案(尽管没有 XPath)。
    【解决方案4】:

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

    功能:

    // **********************************************************************************
    // 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));
    
    } 
    

    【讨论】:

    • 您不应该在 HTML 上使用字符串操作。面对可选的结束标签或错误的标记,这将很快失败。这比人们通常做的要聪明一点,但它仍然是危险的和不必要的,因为DOMDocument 会为你做所有的硬解析!
    • @FrancisAvila:好吧,告诉我如何获得相同的结果。因为我真的做不到。甚至在一些教程之后也没有。
    • @SuperSpy:您可以使用 XML-Tidy 之类的工具将文档转换为 XML,或者您可以使用允许在 HTML 文档上评估类似 XPath 的表达式的工具——例如 Html Agility Pack,或者 Chris Lovett 的 SGML 阅读器(这个也可以很容易地用于将 HTML 转换为 XML 文档)。
    • @SuperSpy 我已经添加了一个答案,see here。 @Dimitre,在 PHP 上,DOMDocument 可以使用 loadHTML* 方法解析 HTML(它使用底层的 libxml2 html 解析器),html5lib 可以使用 HTML5 解析器生成 DOMDocument。一旦您拥有DOMDocument,您就可以针对它发出 XPath 查询。如果您对 PHP 环境不太熟悉,仅供参考。
    • @FrancisAvila:谢谢,我对 PHP 一无所知。加载的 HTML 文档上的 XPath 支持是否完全符合 XPath 或是否存在偏差/限制?
    猜你喜欢
    • 2015-07-07
    • 2012-08-15
    • 2021-10-09
    • 2021-03-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多