Я пишу общий проводник 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.
Нет, не существует одного выражения XPath 1.0, которое возвращает то, что вы хотите. Во-первых, потому что XPath 1.0 не позволяет перебирать промежуточные результаты, а во-вторых, потому что последовательность элементов определяется как набор узлов — в котором не может быть дубликатов.
Я вижу два возможных решения вашей проблемы. Либо написать код PHP, который
a
узлы, например с выражением как //a
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]
Так что я не уверен, как вы пытаетесь использовать это, но я бы попробовал что-то вроде:
$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 …