Почему мой запрос 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> теги, где это необходимо.

Другие вопросы по тегам