Почему мой запрос XPath (очистка таблиц HTML) работает только в Firebug, а не в приложении, которое я разрабатываю?
Это предназначено для обеспечения канонических вопросов и ответов для всех подобных (но слишком специфических вопросов, чтобы быть близким целевым кандидатом), появляющихся один или два раза в неделю.
Я разрабатываю приложение, которое должно анализировать веб-сайт с таблицами в нем. Поскольку получение выражения XPath для очистки веб-страниц является скучной и подверженной ошибкам работой, я хотел бы использовать для этого функцию извлечения XPath в Firebug (или аналогичные инструменты в других браузерах).
Пример ввода выглядит так:
<!-- snip -->
<table id="example">
<tr>
<th>Example Cell</th>
<th>Another one</th>
</tr>
<tr>
<td>foobar</td>
<td>42</td>
</tr>
</table>
<!-- snip -->
Я хочу извлечь первую ячейку данных ("foobar"). Firebug предлагает выражение XPath
//table[@id="example"]/tbody/tr[2]/td[1]
который отлично работает в любых плагинах XPath Tester, но не в моем собственном приложении (результатов не найдено). Если я урезать запрос //table[@id]
опять работает.
Что не так?
2 ответа
Проблема: ДОМ требует <tbody/>
Теги
Firebug, Chrome Developer Tool, функции XPath в JavaScript и другие работают на DOM, а не на основном исходном коде HTML.
DOM для HTML требует, чтобы все строки таблицы не содержались в верхнем колонтитуле таблицы (<thead/>
, <tfoot/>
) включены в теги тела таблицы <tbody/>
, Таким образом, браузеры добавляют этот тег, если он отсутствует при разборе (X)HTML. Например, документация Microsoft DOM гласит
tbody
элемент предоставляется для всех таблиц, даже если таблица явно не определяетtbody
элемент.
В другом ответе на stackru есть подробное объяснение.
С другой стороны, HTML не обязательно требует использования этого тега:
TBODY
Начальный тег всегда требуется, за исключением случаев, когда таблица содержит только одно тело таблицы и не содержит ни заголовка, ни секции таблицы.
Большинство процессоров XPath работают на необработанном XML
Исключая JavaScript, большинство процессоров XPath работают на необработанном XML, а не на DOM, поэтому не добавляют <tbody/>
теги. Кроме того, библиотеки синтаксического анализатора HTML, такие как tag-soup и htmltidy, выводят только XHTML, а не "DOM-HTML".
Это распространенная проблема, размещенная в Stackru для PHP, Ruby, Python, Java, C#, Google Docs (Spreadsheets) и многих других. Selenium работает внутри браузера и работает на DOM - так что это не влияет!
Воспроизведение вопроса
Сравните источник, показанный Firebug (или Chrome Dev Tools), с тем, который вы получили, щелкнув правой кнопкой мыши и выбрав "Показать источник страницы" (или как там он называется в ваших браузерах) - или с помощью curl http://your.example.org
в командной строке. Последнее, вероятно, не будет содержать <tbody/>
элементы (они используются редко), Firebug всегда будет их показывать.
Решение 1: Удалить /tbody
Шаг оси
Проверьте, действительно ли таблица, в которой вы застряли, не содержит <tbody/>
элемент (см. последний абзац). Если это произойдет, у вас, вероятно, есть другая проблема.
Теперь удалите /tbody
шаг оси, поэтому ваш запрос будет выглядеть
//table[@id="example"]/tr[2]/td[1]
Решение 2: Пропустить <tbody/>
Теги
Это довольно грязное решение, и оно может дать сбой для вложенных таблиц (может перейти во внутренние таблицы). Я бы рекомендовал это делать в очень редких случаях.
Заменить /tbody
шаг оси на шаг потомка или себя:
//table[@id="example"]//tr[2]/td[1]
Решение 3. Разрешить ввод как с, так и без <tbody/>
Теги
Если вы заранее не уверены, что ваша таблица или используется запрос как в "HTML-источнике", так и в контексте DOM; и не хотят / не могут использовать взлом из решения 2, предоставить альтернативный запрос (для XPath 1.0) или использовать "необязательный" шаг оси (XPath 2.0 и выше).
- XPath 1.0:
//table[@id="example"]/tr[2]/td[1] | //table[@id="example"]/tbody/tr[2]/td[1]
- XPath 2.0:
//table[@id="example"]/(tbody, .)/tr[2]/td[1]
Просто наткнулся на ту же проблему. Я почти написал рекурсивную функцию для проверки каждого тега tbody, если он существует, и таким образом прошел через домен, потом я вспомнил, что знаю регулярное выражение.:)
Перед разбором получите html в виде строки. Вставить пропущенный <tbody>
а также </tbody>
теги с регулярным выражением, а затем загрузить его обратно в ваш объект DOMDocument.
Йенс Эрат дает хорошее объяснение, но вот
Решение 4. Убедитесь, что источник HTML всегда имеет <tbody>
теги с регулярным выражением
JavaScript
var html = '<html><table><tr><td>foo</td><td>bar</td></tr></table></html>';
html.replace(/(<table([^>]+)?>([^<>]+)?)(?!<tbody([^>]+)?>)/g,"$1<tbody>").replace(/(<(?!(\/tbody))([^>]+)?>)(<\/table([^>]+)?>)/g,"$1</tbody>$4");
PHP
$html = $dom->saveHTML();
$html = preg_replace(array('/(<table([^>]+)?>([^<>]+)?)(?!<tbody([^>]+)?>)/','/(<(?!(\/tbody))([^>]+)?>)(<\/table([^>]+)?>)/'),array('$1<tbody>','$1</tbody>$4'),$html);
$dom->loadHTML($html);
Просто регулярное выражение:
matches `<table>` tag with whatever else junk inside the tag and between this and the next tag if the next tag is NOT `<tbody>` also with stuff inside the tag
/(<table([^>]+)?>([^<>]+)?)(?!<tbody([^>]+)?>)/
replace with
$1<tbody>
the $1 referencing the captured `<table>` tag with contents.
Do the same for the closing tag like this:
/(<(?!(\/tbody))([^>]+)?>)(<\/table([^>]+)?>)/
replace with
$1</tbody>$4
Таким образом, дом всегда будет иметь <tbody>
теги, где это необходимо.