Как получить текст родительского элемента относительно определенных найденных узлов HTML?

Я пишу общий проводник HTML, который может выполнять список операций, таких как посещение страницы, поиск таблицы, поиск строк, сохранение данных и т. Д. Он использует Goutte / Guzzle внутренне и, таким образом, может использовать селекторы CSS и XPath. У меня есть интересная проблема, касающаяся выбора нового набора результатов относительно существующего набора результатов.

Посмотрите на этот демонстрационный HTML:

    <h2>Burrowing</h2>
<ul>
<li>
<a href="/jobs/junior-mole">Junior Mole</a>
</li>
<li>
<a href="/jobs/head-of-badger-partnerships">Head of Badger Partnerships</a>
</li>
<li>
<a href="/jobs/trainee-worm">Trainee Worm</a>
</li>
</ul>

<h2>Tree Surgery</h2>
<ul>
<li>
<a href="/jobs/senior-woodpecker">Senior Woodpecker</a>
</li>
<li>
<a href="/jobs/owl-supervisor">Owl Supervisor</a>
</li>
</ul>

<h2>Grass maintenance</h2>
<ul>
<li>
<a href="/jobs/trainee-sheep">Trainee sheep</a>
</li>
<li>
<a href="/jobs/sheep-shearer">Sheep shearer</a>
</li>
</ul>

<h2>Aerial supervision</h2>
<ul>
<li>
<a href="/jobs/head-magpie-ops">Head of Magpie Operations</a>
</li>
</ul>

Я запускаю этот CSS-запрос, чтобы получить роли в ссылках (это правильно получает восемь элементов):

ul li a

Для каждого я хотел бы получить категорию, которая является <h2> непосредственно предшествующий <ul> в каждом случае. Теперь я могу сделать это с помощью абсолютного селектора CSS:

h2

Однако это дает четыре результата, так что я не знаю, какая категория (h2) идет с какой работой (ссылка). Мне нужно получить восемь результатов: три лота первой категории, два вторых, два третьего и один четвертый, поэтому каждая категория отображается на каждую роль.

Я подумал, понадобится ли мне родительский селектор для этого, поэтому я переключился с CSS на XPath и сначала попробовал это, чтобы каждый h2 имел следующий элемент списка:

//h2[(following-sibling::ul)[1]/li/a]

Это находит h2s, имеющие указанную родительскую структуру, но снова возвращается с четырьмя результатами — ничего хорошего.

Следующая попытка:

//ul/li[../preceding-sibling::h2[1]]

Он получает правильное количество результатов (на основе получения элемента списка с непосредственно предшествующим заголовком), но получает текст ссылки, а не текст категории.

Я думал о создании цикла — я знаю, что у меня есть восемь результатов, поэтому я мог бы сделать это (X — это циклическая переменная с инжекцией от 1 до 8). Это работает, но я считаю добавление ручного цикла довольно неэффективным — я стараюсь придерживаться общих правил:

//li[X]/../preceding-sibling::h2[1]

Существует ли операция XPath, которая может вернуть требуемые результаты? Во избежание сомнений я ищу следующее (или просто текстовые элементы будут в порядке):

<h2>Burrowing</h2>
<h2>Burrowing</h2>
<h2>Burrowing</h2>
<h2>Tree Surgery</h2>
<h2>Tree Surgery</h2>
<h2>Grass maintenance</h2>
<h2>Grass maintenance</h2>
<h2>Aerial supervision</h2>

CSS тоже подойдет, но я предполагаю, что это невозможно, потому что у CSS нет родительского оператора (в любом случае, Goutte просто конвертирует селекторы CSS в селекторы XPath).

Поскольку я нахожусь на PHP (5.5), я считаю, что я должен придерживаться XPath 1.0.

4

Решение

Нет, не существует одного выражения XPath 1.0, которое возвращает то, что вы хотите. Во-первых, потому что XPath 1.0 не позволяет перебирать промежуточные результаты, а во-вторых, потому что последовательность элементов определяется как набор узлов — в котором не может быть дубликатов.

Я вижу два возможных решения вашей проблемы. Либо написать код PHP, который

  • сначала извлекает все релевантные a узлы, например с выражением как //a
  • применяет второе выражение XPath к каждому из них по очереди: preceding::h2[1]

Вы должны написать этот код PHP самостоятельно, учитывая мои плохие навыки в этом. Но я могу предложить альтернативу: вы также можете использовать преобразование XSLT 1.0, есть процессоры XSLT 1.0 в PHP.

стилей

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes" />

<xsl:template match="/">
<xsl:for-each select="//a">
<xsl:copy-of select="preceding::h2[1]"/>
</xsl:for-each>
</xsl:template>

</xsl:transform>

Применительно к вашему входу (после добавления корневого элемента), результат

<h2>Burrowing</h2>
<h2>Burrowing</h2>
<h2>Burrowing</h2>
<h2>Tree Surgery</h2>
<h2>Tree Surgery</h2>
<h2>Grass maintenance</h2>
<h2>Grass maintenance</h2>
<h2>Aerial supervision</h2>

Попробуйте онлайн Вот. Кстати, если вы заинтересованы в том, как сделать это с XPath 2.0, используя for, как вы упомянули в комментарии, смотрите эта версия вместо:

for $a in //a return $a/preceding::h2[1]
2

Другие решения

Так что я не уверен, как вы пытаетесь использовать это, но я бы попробовал что-то вроде:

$links = $cralwer->filter('ul li a');
foreach ($links as $link) {
// do stuff with the link
// ...
// get the H2
$header = $link->parents()->filter('ul[../preceding-sibling::h2]');
// do stuff with the header
}

Обратите внимание, что это не проверено, и я пришел к выводу, глядя на Symfony\Component\DomCrawler API напрямую, но я думаю, что это должно работать на основе этого (если у меня нет XPath неправильно — но если я сделаю это, вам будет довольно легко разобраться).

Вы также можете использовать Symfony\Component\DomCrawler::each и делать это внутри замыкания вместо того, чтобы делать foreach …

1