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

时间:2012-01-21 04:14:11

标签: php xpath screen-scraping

编辑:我已添加了适用于此案例的解决方案。


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

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

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

我很难,这可能是用DOMDocument和/或xpath解决的......


最后,我希望标签之间的所有内容(在本例中为标签)和标签都是自己的。所有HTML,不仅仅是价值(例如,不只是&#39;价值&#39;但是&#39;价值&#39;)。并且有一个&#39; catch&#39; ...

  • 表中有其他表格。因此,如果您只是搜索表格的末尾(&#39;标记&#39;),您可能会收到错误的标记。
  • 开场标记有一个类,您可以使用它来识别它(classname =&#39; schedule&#39;)。

这可能吗?

这是我想要从另一个网站提取的(简化的)源代码:(我还想显示html标签,而不仅仅是值,所以整个表格带有类&#39; schedule&#39; )

<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>

4 个答案:

答案 0 :(得分:8)

首先,请注意XPath基于XML Infopath--一种XML模型,其中没有“起始标记”和“结束标记”,只有节点

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

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

  

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

在XPath 2.0中,可以使用标准运算符 intersect 方便地完成此操作。

在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>

选择了start元素
//table[@class = 'target']
         //td[. = 'Starting Node']

结束元素由

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

要获得所有想要的节点,我们将以下两组相交

  1. 包含start元素和所有后续元素的集合(我们将此名称命名为$vFollowing)。

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

  3. 分别通过以下XPath表达式选择

    $ vFollowing:

    $vStartNode | $vStartNode/following::*
    

    $ vPreceding:

    $vEndNode | $vEndNode/preceding::*
    

    现在我们只需在节点集$vFollowing$vPreceding上应用Kayessian公式:

           $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>
    

答案 1 :(得分:1)

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

答案 2 :(得分:0)

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

答案 3 :(得分:-3)

这得到了整个表格。但它可以修改为让它抓住另一个标签。这是一个非常具体的解决方案,只能在特定情况下使用。如果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));

}