Я пишу общий проводник 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
halfer 30 Янв 2015 в 21:38

2 ответа

Лучший ответ

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

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

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

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

< Сильный > Stylesheet

<?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
Mathias Müller 30 Янв 2015 в 22:11

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

$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
}

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

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

2
halfer 1 Фев 2015 в 13:49