XPath для програміста, що працює з парсерами, це один з must-have. Він дозволяє більш гнучко (в порівнянні з CSS селекторами) описувати локації всередині HTML/XML документа. Якщо ви ще не знайомі з ним, то ось гарний туторіал.
А в цій статті ми поговоримо про деякі помилки, які допускають користувачі, що вже знайомі з XPath. Для демонстрації прикладів ми будемо використовувати Scrapy Selector API
Не використовуйте contains(.//text(), "search text")
, використовуйте contains(., "search text")
А тепер розкажу чому: .//text()
повертає набір текстових елементів, node-set. А коли node-set перетворюється в текстовий рядок (що й відбувається при передаванні його в функції, що працює з рядками), результат повернеться лише для першого елементу.
>>> from scrapy import Selector
>>> sel = Selector(text='<a href="#">Click here to go to the <strong>Next Page</strong></a>')
>>> xp = lambda x: sel.xpath(x).extract() # let's type this only once
>>> xp('//web.archive.org/web/20230402095329/https://a//text()') # take a peek at the node-set
[u'Click here to go to the ', u'Next Page']
>>> xp('string(//a//text())') # convert it to a string
[u'Click here to go to the ']
А коли в стрічку конвертується одна нода, то повертається і її текст, і її вкладених тегів.
>>> xp('//a[1]') # selects the first a node
[u'<a href="#">Click here to go to the <strong>Next Page</strong></a>']
>>> xp('string(//a[1])') # converts it to string
[u'Click here to go to the Next Page']
Підсумуємо:
# Добре
>>> xp("//a[contains(., 'Next Page')]")
[u'<a href="#">Click here to go to the <strong>Next Page</strong></a>']
# Погано
>>> xp("//a[contains(.//text(), 'Next Page')]")
[]
# Добре
>>> xp("substring-after(//a, 'Next ')")
[u'Page']
# Погано
>>> xp("substring-after(//a//text(), 'Next ')")
[u'']
Детальніше тут.
Розумійте різницю між //node[1]
та (//node)[1]
//node[1]
вибирає кожну першу ноду серед вкладених в батьківську.
(//node)[1]
шукає всі ноди в документі, а потім повертає першу.
>>> from scrapy import Selector
>>> sel=Selector(text="""
....: <ul class="list">
....: <li>1</li>
....: <li>2</li>
....: <li>3</li>
....: </ul>
....: <ul class="list">
....: <li>4</li>
....: <li>5</li>
....: <li>6</li>
....: </ul>""")
>>> xp = lambda x: sel.xpath(x).extract()
>>> xp("//li[1]")
[u'<li>1</li>', u'<li>4</li>']
>>> xp("(//li)[1]")
[u'<li>1</li>']
>>> xp("//ul/li[1]")
[u'<li>1</li>', u'<li>4</li>']
>>> xp("(//ul/li)[1]")
[u'<li>1</li>']
Також
//a[starts-with(@href, '#')][1]
отримує набір локальних посилань-якорів, що вкладені безпосередньо в батьківську ноду і повертає кожну першу.
(//a[starts-with(@href, '#')])[1]
отримує набір всіх нодів, що вкладені в батьківську (на всіх рівнях) і повертає першу.
Будьте уважні при пошуку за класом
Найкращий спосіб знайти якийсь елемент за класом, використовуючи XPath, це:
*[contains(concat(' ', normalize-space(@class), ' '), ' someclass ')]
Багатослівно. Але давайте розглянемо на прикладах:
>>> sel = Selector(text='<p class="content-author">Someone</p><p class="content text-wrap">Some content</p>')
>>> xp = lambda x: sel.xpath(x).extract()
# Не працює, бо в елемента більше одного класу
>>> xp("//*[@class='content']")
[]
# Отримуємо не те, що нам потрібно
>>> xp("//*[contains(@class,'content')]")
[u'<p class=""content-author"">Someone</p>']
# А ось так нормально
>>> xp("//*[contains(concat(' ', normalize-space(@class), ' '), ' content ')]")
[u'<p class="content text-wrap">Some content</p>']
Також, для пошуку за класом не гріх використати й CSS селектори.
>>> sel.css(".content").extract()
[u'<p class="content text-wrap">Some content</p>']
>>> sel.css('.content').xpath('@class').extract()
[u'content text-wrap']
Більше доків по Scrapy селекторах тут.
Розберіться зі всіма роздільниками
Якщо ви тільки поверхнево ознайомились з XPath, то я рекомендую перш за все гарно ознайомитися з роздільниками, щоб не писати власні велосипеди. Гарний туторіал.
Також слід розуміти різницю між following та following-sibling, це часто плутає новачків і більш досвідчених програмістів теж. Те ж саме й з preceding та preceding-sibling, з ancestor та parent.
Сніппет для отримання текстового контенту
Невеличкий сніпет, що отримує текст сторінки, ігноруючи <script>
та <style>
, а також ноди, що містять в собі лише пробіли:
1//*[not(self::script or self::style)]/text()[normalize-space(.)]
А в вас є ще якісь хитрощі при роботі з XPath? Пишіть в коментарі.
Ще немає коментарів