Как получить все между двумя тегами HTML? (с XPath?)

EDIT: Я добавил решение, которое работает в этом случае.


Я хочу извлечь таблицу со страницы, и я хочу сделать это (возможно) с помощью DOMDocument и XPath. Скажите мне, если у вас есть идея лучше.

Моя первая попытка была (очевидно, ошибочной, потому что она получит первый тег закрытия таблицы):

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

Я жесткий, это может быть разрешено с DOMDocument и / или xpath


В конце концов, я хочу все, что есть между тегами (в данном случае, тегами), и тегами. Таким образом, все HTML, а не только значения (например, не только «Value», но и «Value»). И есть один «улов» …

  • В таблице есть и другие таблицы. Поэтому, если вы просто ищете конец таблицы («тег»), вы получаете, вероятно, неправильный тег.
  • У открытого тега есть класс, с которым вы можете его идентифицировать (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> 

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

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

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

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

В XPath 2.0 это можно сделать удобно, когда стандартный оператор пересекается .

В XPath 1.0 (который, как я полагаю, вы используете) это не так просто. Решение состоит в том, чтобы использовать формулу Kayessian (by @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> 

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

Выведите узлы (в обертке 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(); ?> 

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