Как получить все между двумя тегами HTML? (с XPath?) - PullRequest
1 голос
/ 21 января 2012

РЕДАКТИРОВАТЬ: Я добавил решение, которое работает в этом случае.


Я хочу извлечь таблицу со страницы, и я хочу сделать это (вероятно)с 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, а не только значения,итак вся таблица с классом 'расписание')

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

8 голосов
/ 21 января 2012

Прежде всего, обратите внимание, что XPath основан на XML Infopath - модели XML, в которой нет «начального тега» и «конечного тега», а есть только узлы

Поэтому не следует ожидать, что выражение XPath выберет «теги» - оно выбирает узлов .

Учитывая этот факт, я интерпретируювопрос как:

Я хочу получить набор всех элементов, которые находятся между данным «начальным» элементом и заданным «конечным элементом», включая начальный и конечный элементы.

В XPath 2.0 это удобно сделать с помощью стандартного оператора пересечь .

В XPath 1.0 (который я предполагаю, что вы используете) это не так просто.Решение состоит в том, чтобы использовать формулу Кейса (по @Michael Kay) для пересечения наборов узлов :

Пересечение двух наборов узлов: $ns1 и $ns2 выбирается путем оценки следующегоВыражение XPath:

$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. Набор, состоящий из начального элемента и всех следующих элементов (назовем это $vFollowing).

  2. Набор, состоящий из конечного элемента и всех предыдущих элементов (мы называем это $vPreceding).

Они выбираются, соответственно, следующими выражениями XPath :

$ vFollowing:

$vStartNode | $vStartNode/following::*

$ vPreceding:

$vEndNode | $vEndNode/preceding::*

Теперь мы можем просто применить Кейсианформула для наборов узлов $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>
1 голос
/ 22 января 2012

Не используйте регулярные выражения (или 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.
}
0 голосов
/ 22 января 2012

Это получает всю таблицу. Но это может быть изменено, чтобы позволить ему захватить другой тег. Это вполне конкретное решение, которое может использоваться только при определенных обстоятельствах. Разрывы, если комментарии 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));

} 
0 голосов
/ 21 января 2012

Если у вас хорошо сформированный 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>

Вывести узлы (в xml-оболочке) с этим кодом pho

<?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();
?>
...