Php

Парсинг html xpath php: Примеры xpath-запросов к html / Хабр

Содержание

XPath примеры — шпаргалка для разбора страниц ♦ Программисту РФ

Автор: Igor
Kirsanov

Примеры использования xpath из практики парсинга информации с сайтов. Приведены участки кода xpath.

 

Получить текст заголовока h2


//h2/text()

 

Получить текст заголовока с классом produnctName


//h2[@class="produnctName"]/text()

 

Получить значение определенного span по классу


//span[@class="price"]

 

Получить значение атрибута title у кнопки с классом  addtocart_button


//input[@class="addtocart_button"]/@title

 

Текст ссылки


//a/text()

 

Получить url значение атрибута href ссылки


//a/@href

 

Изображение src


//img/@src

 

Изображение сразу за определенным элементом в DOM, ось following


//h2[@class="produnctName"]//following::div/img/@src

 

Изображение в 4 div по счету


//div[4]/img/@src

 

XPath (XML Path Language) — язык запросов к элементам XML-документа. Разработан для организации доступа к частям документа XML в файлах трансформации XSLT и является стандартом консорциума W3C. XPath призван реализовать навигацию по DOM в XML.

XML имеет древовидную структуру. У элемента дерева всегда существуют потомки и предки, кроме корневого элемента, у которого предков нет, а также тупиковых элементов (листьев дерева), у которых нет потомков.

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

Функции над множествами узлов

  • * — обозначает любое имя или набор символов по указанной оси, например: * — любой дочерний узел; @* — любой атрибут.
  • $name — обращение к переменной, где name — имя переменной или параметра.
  • [] — дополнительные условия выборки или, что то же самое, предикат шага адресации. Должен содержать логическое значение. Если содержит числовое, считается что это порядковый номер узла, что эквивалентно приписыванию перед этим числом выражения «position()=»
  • {} — если применяется внутри тега другого языка (например HTML), то XSLT процессор рассматривает содержимое фигурных скобок как XPath.
  • / — определяет уровень дерева, то есть разделяет шаги адресации
  • | — объединяет результат. То есть, можно написать несколько путей разбора через знак | и в результат такого выражения войдёт всё, что будет найдено любым из этих путей.

Возвращает все узлы. Вместо этой функции часто используют заменитель ‘*’, но, в отличие от звездочки, функция node() возвращает и текстовые узлы.

Возвращает набор текстовых узлов;

Возвращает множество из одного элемента, который является текущим. Если мы делаем обработку множества с условиями, то единственным способом дотянуться из этого условия до текущего элемента будет данная функция.

Возвращает позицию элемента в множестве. Корректно работает только в цикле <xsl:for-each/>

Возвращает номер последнего элемента в множестве. Корректно работает только в цикле <xsl:for-each/>

Возвращает количество элементов в node-set.

Возвращает полное имя первого тега в множестве.

  • string namespace-uri(node-set?)

Возвращает ссылку на url определяющий пространство имён.

  • string local-name(node-set?)

Возвращает имя первого тега в множестве, без пространства имён.

Находит элемент с уникальным идентификатором

Оси — это база языка XPath. Для некоторых осей существуют сокращённые обозначения.

  • ancestor:: — Возвращает множество предков.
  • ancestor-or-self:: — Возвращает множество предков и текущий элемент.
  • attribute:: — Возвращает множество атрибутов текущего элемента. Это обращение можно заменить на «@»
  • child:: — Возвращает множество потомков на один уровень ниже. Это название сокращается полностью, то есть его можно вовсе опускать.
  • descendant:: — Возвращает полное множество потомков (то есть, как ближайших потомков, так и всех их потомков).
  • descendant-or-self:: — Возвращает полное множество потомков и текущий элемент. Выражение «/descendant-or-self::node()/» можно сокращать до «//». С помощью этой оси, например, можно вторым шагом организовать отбор элементов с любого узла, а не только с корневого: достаточно первым шагом взять всех потомков корневого. Например, путь «//span» отберёт все узлы span документа, независимо от их положения в иерархии, взглянув как на имя корневого, так и на имена всех его дочерних элементов, на всю глубину их вложенности.
  • following:: — Возвращает необработанное множество, ниже текущего элемента.
  • following-sibling:: — Возвращает множество элементов на том же уровне, следующих за текущим.
  • namespace:: — Возвращает множество, имеющее пространство имён (то есть присутствует атрибут xmlns).
  • parent:: — Возвращает предка на один уровень назад. Это обращение можно заменить на «..»
  • preceding:: — Возвращает множество обработанных элементов исключая множество предков.
  • preceding-sibling:: — Возвращает множество элементов на том же уровне, предшествующих текущему.
  • self:: — Возвращает текущий элемент. Это обращение можно заменить на «.»

Как я html-парсер на php писал, и что из этого вышло. Вводная часть / Хабр

Привет.

Сегодня я хочу рассказать, как написать html парсер, а также с какими проблемами я столкнулся, разрабатывая подобный парсер на php. А проблем было много. И в первой части я расскажу о проектировании парсера, и о возникших проблемах, ведь html парсер отличается от парсера привычных всем языков программирования.

Введение

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

Здесь и далее в статье я буду называть документ, содержащий html просто «Документ».

Dom дерево, находящееся в элементе, будет называться «Подмассив».

Что должен делать парсер?

Давайте сначала определимся, что должен делать парсер, чтобы в будущем отталкиваться от этого при разработке. А именно, парсер должен:

  • Проектировать dom-дерево на основе документа
  • Если есть ошибки в документе, то он должен их решать
  • Находить элементы в dom-дереве
  • Находить children элементы
  • Находить текст

Это самый простой список того, что должен уметь парсер. По-хорошему, он еще должен отправлять информацию об ошибках, если таковые были найдены в исходном документе.

Впрочем, это мелочи. Основного функционала вполне хватит, чтобы поломать голову пару ночей напролет.

Но тут есть проблема, с которой я столкнулся сразу же: Html — это не просто язык, это язык гипертекста. У такого языка свой синтаксис, и обычный парсер не подойдет.

Разделяй и властвуй

Для начала, нужно разделить работу парсера на два этапа:

  • Отделение обычного текста от тегов
  • Сортировка всех полученных тегов в dom дерево

Это что касается непосредственно парсинга документа. Про поиск элементов я буду говорить чуть позже далее в этой главе.

Для описания первого этапа я нарисовал схему, которая наглядно показывает, как обрабатываются данные на первом этапе:

Я решил опустить все мелкие детали. Например, как отличить, что после открывающего «<» идет тег, а не текст? Об этом я расскажу в следующих частях. Пока что этого вполне хватит.

Также тут стоит уточнить. Логично, что в документе помимо тегов есть еще и текст. Говоря простым языком, если парсер найдет открывающий тег и если в нем будет текст, он запишет его после открывающего тега в виде отдельного тега. Такой тег будет считаться как одиночный и не будет участвовать в дальнейшей работе парсера.

Ну и второй этап. Самый сложный с точки зрения проектирования, и самый простой на первый взгляд с точки зрения понимания:

В данном случаи уровень означает уровень рекурсии. То есть если парсер нашел открывающий тег, он вызывает самого себя, «входит на уровень ниже», и так будет продолжаться до тех пор, пока не будет найден закрывающий тег. В этом случаи рекурсия выдает результат, «Выходит на уровень выше». Но, как обстоят дела с одиночными тегами? Такие теги считаются рекурсией ни как открывающие, ни как закрывающие. Они просто переходят в dom «Как есть».

В итоге у нас получится что-то вроде этого:

	[0] => Array
	(
	[is_closing] =>
	[is_singleton] =>
	[pointer] => 215
	[tag] => div
	[0] => Array //открывается подмассив
		(
		[0] => Array
		(
			[is_closing] =>
			[is_singleton] =>
			[pointer] => 238
			[tag] => div
			[id] => Array
			(
			[0] => tjojo
			)
				[0] => Array //открывается подмассив
					(
					[0] => Array //Текст записывается в виде отдельного тега
					(
						[tag] => __TEXT
						[0] => Привет!
					)
					[1] => Array
					(
					[is_closing] => 1
					[is_singleton] =>
					[pointer] => 268
					[tag] => div
					)
				)
			)
		)
	)

Что там насчет поиска элементов?

А теперь давайте поговорим про поиск элементов. Но тут не все так однозначно, как можно подумать. Сначала стоит разобраться, по каким критериям мы ищем элементы. Тут все просто, мы ищем их по тем же критериям, как это делает Javascript: теги, классы и идентификаторы. Но тут проблема. Дело в том, что тег может быть только один, а вот классов и идентификаторов у одного элемента — множество, либо вообще не быть. Поэтому, поиск элемента по тегу будет отличаться от поиска по классу или идентификатору. Я нарисовал схему поиска по тегу, но не волнуйтесь: поиск по классу или идентификатору не особо отличаются.

Немного уточнений. Под исходным значением я имел в виду название тега, «div» например. Также, если элемент не равен исходному значению, но у него есть подмассив с подходящим элементом, в результат запишется именно подходящий элемент с его подмассивом, если таковой существует.

Стоит также сказать, что у парсера будет функция, позволяющая искать определенный элемент в документе. Это заметно ускорит производительность парсера, что позволит ему выполняться быстрее. Можно будет, например, взять только первый найденный элемент, или пятый, как вы захотите. Согласитесь, в таком случаи парсеру будет гораздо проще искать элементы.

Поиск children элементов

Хорошо, с поиском элементов разобрались, а как насчет children элементов? Тут тоже все просто: наш парсер будет брать все вложенные подмассивы найденных до этого элементов, если таковые существуют. Если таковых нет, парсер выведет пустой результат и пойдет дальше:

Поиск текста

Тут говорить особо не о чем. Парсер просто будет брать весь полученный текст из подмассива и выводить его.

Ошибки

Документ может содержать ошибки, с которыми наш скрипт должен успешно справляться, либо, если ошибка критическая, выводить ее на экран. Тут будет приведен список всех возможных ошибок, о которых, в будущем, мы будем говорить:

  • Символ «>» не был найден

    Такая ошибка будет возникать в том случаи, если парсер дошел до конца документа и не нашел закрывающего символа «>».
  • Неизвестное значение атрибута

    Данная ошибка сигнализирует о том, что была проведена попытка передачи значения атрибуту когда закрывающий тег был найден.

    <tag some =><!--И что там написано? А никто не знает, как и парсер-->

  • Ошибка html синтаксиса

    Данная ошибка возникает в двух случаях: Либо у атрибута тега в названии есть «<«, либо если знак «=» ставится дважды, хотя значение еще не было передано.

    <tag some = ='something'><!--Случайная ошибка, с кем не бывает-->
    <tag <some ='something'><!--И что это? Тег там, где должен быть атрибут? Непорядок-->

  • Слишком много открывающих тегов

    Данная ошибка часто встречается на сайтах, и говорит она о том, что открывающих тегов больше, чем закрывающих.

    <div>
    <div id = ='wefwe'>
    Привет!
    </div>
    <!--И куда делся </div>?-->
    


    Данная ошибка не является критической и будет решаться парсером.

  • Слишком много закрывающих тегов

    То же самое, что и прошлая ошибка, только наоборот.

    <div id = ='wefwe'>
    Привет!
    </div>
    </div><!--И что ты собрался закрывать?-->
    


    Данная ошибка также не является критической.

  • Children элемент не найден

    В этом случаи парсер просто будет выводить пустой массив.

Script, style и комментарии

В парсере теги script и style будут сразу же пропускаться, поскольку я не вижу смысл их записывать. С комментариями ситуация другая. Если вы захотите из записывать, то вы сможете включить отдельную функцию скрипта, и тогда он будет их записывать. Комментарии будут записываться точно так же как и текст, то есть как отдельный тег.

Заключение

Эту статью скорее нужно считать небольшим экскурсом в тему парсеров html. Я ее написал для тех, кто задумывается над написанием своего парсера, либо для тех, кому просто интересно. Поверьте, это действительно весело!

Данная статья является первой вводной частью. В следующих частях этого цикла уже будет участвовать непосредственно код, и будет меньше картинок с алгоритмами(что прекрасно, потому что рисовать я их не умею). Stay tuned!

Как использовать Python и Xpath для поиска данных в html

Платформы онлайн-обучения и соревнований по Kaggle обычно предоставляют вам полный (и чистый) набор данных. На практике, первый шаг проекта машинного обучения — получить в свои руки необходимые данные. Очистка веб-страниц или извлечение данных с веб-сайтов является одним из инструментов для достижения этой цели.

XPath может использоваться для анализа содержимого с веб-сайта. Он расшифровывается как «XML Path Language». Для просмотра веб-страниц нас интересует XPath, поскольку он может использоваться для анализа HTML.

Веб-сайты используют HTML для отображения контента, который вы видите на веб-странице. HTML — это язык разметки, который использует «теги» для определения того, как веб-сайт просматривается в вашем браузере. Компоненты документа HTML, который мы ищем, называются «узлами». Выражения XPath работают путем определения «пути» для навигации по HTML-сайту и выбора нужных вам узлов.

Чтобы представить основы XPath, мы рассмотрим, как мы можем использовать его для получения цитат из «Звездных войн» с этого сайта. Могут помочь некоторые базовые знания HTML, но знать его не обязательно.

Краткое примечание об ответственном анализе и отказе от ответственности. Не следует пытаться получать данные с сайтов, которые не допускают очистку. Вы можете проверить «robots.txt» сайта, чтобы увидеть его политику в отношении очистки веб-страниц. Цель этой статьи — помочь облегчить обычно ручное исследование и сбор данных. Не пытайтесь получить доступ к данным, на которые у вас нет полномочий. Я не являюсь владельцем сайта или данных, представленных здесь — он просто используется в качестве примера.

Получение HTML сайта

Прежде чем мы сможем написать какие-либо запросы XPath, нам нужно получить HTML-документ целевого веб-сайта. Для этого мы просто напишем пару строк, которые извлекают HTML-код с сайта и сохраняют его на нашем компьютере.

# getting the data
import requests
from urllib.request import urlopen
from lxml import etree
# get html from site and write to local file
url = 'https://www.starwars.com/news/15-star-wars-quotes-to-use-in-everyday-life'
headers = {'Content-Type': 'text/html',}
response = requests.get(url, headers=headers)
html = response.text
with open ('star_wars_html', 'w') as f:
    f.write(html)
    
# read local html file and set up lxml html parser
local = 'insert_browser_file_path_here'
response = urlopen(local)
htmlparser = etree.HTMLParser()
tree = etree.parse(response, htmlparser)

Здесь мы использовали «запросы», чтобы получить копию HTML сайта и записали ее в локальный файл «star_wars_html». Затем вам нужно открыть этот файл в браузере и скопировать увиденную там ссылку в переменную «local».

Для анализа HTML с помощью XPath мы будем использовать модуль lxml для Python. Мы создали «дерево», которое позволит нам создавать запросы XPath и получать нужные данные из сохраненного HTML-документа.

Скриншот сайта цитат Звездных войн

Нашей целью будет получить все цитаты и текст под каждой цитатой.

Введение в написание выражений XPath

Тестирование и объяснение простого выражения

tree.xpath('//p/strong/text()')

Мы будем помещать все наши выражения в функцию «tree.xpath». В данном случае выражение «//p/strong/text()». Вы также можете проверить эти выражения в вашем браузере. Просто перейдите к инструменту инспектора (щелкните правой кнопкой мыши и «проверить» в Chrome) и нажмите CMD + F. Вы можете ввести свое выражение XPath в появившейся строке поиска.

Чтобы найти HTML-код, соответствующий той части сайта, которую вы хотите, нажмите кнопку мыши в левом верхнем углу панели «Проверка», а затем наведите курсор мыши на нужную строку. Это выделит связанный HTML на вашей панели. Здесь мы видим, что каждая «цитата» находится внутри тега strong, который находится внутри тега р.

Скриншот сайта цитат Звездных войн

Теперь давайте разберем «//p/strong/text()»:

  1. // — это означает поиск по всему HTML-документу (начните с корня сайта и найдите все, что соответствует искомому выражению)
  2. p — это HTML-тег, который содержит другой тег с текстом, который мы ищем.
  3. strong — это HTML-тег, который на самом деле содержит текст, который мы ищем
  4. text() — это получение текстового узла.

Результат этого выражения выглядит так:

Ура! Мы написали наше первое выражение XPath и получили список цитат с сайта!

Фильтрация для получения контента по ключевому слову

Скриншот сайта цитаты из «Звездных войн».

Давайте попробуем получить текст под каждой цитатой. Используя инструмент проверки, мы видим, что он попадает под тег «p». Тем не менее, нет никаких других отличительных особенностей, основанных на HTML.

В этом случае нам повезло, потому что все эти тексты содержат ключевое слово. Все они начинаются со слова «use». Чтобы захватить их, мы напишем следующее выражение:

tree.xpath('//p[contains(text(),"Use")]/text()')

Давайте разберем, что мы видим:

  1. [] — вы можете видеть, что часть выражения заключена в квадратные скобки. Это называется «предикатом» и используется для фильтрации узлов на основе критериев, указанных внутри него. В этом случае он будет отфильтровывать узлы, которые обычно выводит «//p».
  2. contains() — это ищет первый аргумент для случаев, когда второй аргумент присутствует. В этом случае мы ищем весь текст с надписью «use».

Это выражение дает нам список всего текста в тегах «p», который содержит слово «use».

Фильтрация нежелательного контента

На первом скриншоте видно, что мы также получили «\xao» в списке результатов. Чтобы убедиться, что мы не получаем экземпляры этого, мы напишем это выражение:

tree.xpath('//p/strong[not(contains(text(),"\xa0"))]/text()')

Здесь мы используем один новый фрагмент синтаксиса XPath:

  1. not() — принимает указанное логическое условие и возвращает значение «False».

Эта модификация позволяет нашему начальному выражению XPath отфильтровывать весь текст из узлов, возвращаемых «//p/strong» с «\xao», что дает нам чистый список.

Получение узлов, которые начинаются с ключевого слова

Допустим, мы хотели получить ссылки на все изображения, использованные в посте.

Скриншот сайта цитаты из «Звездных войн».

Здесь все изображения находятся в теге «img». На сайте есть другие изображения, поэтому, чтобы получить только те, которые связаны с каждой цитатой, нам нужно получить те, которые имеют значение класса, начинающееся с «alignnone».

Для этого мы пишем это выражение:

tree.xpath('//img[starts-with(@class, "alignnone")]/@src')

Что нового здесь:

  1. starts-with — работает так же, как «contains», за исключением того, что на этот раз мы смотрим на начало узлов, указанных первым аргументом, для подстроки, предоставленной вторым аргументом.
  2. @ — это как выбрать «атрибут» с помощью XPath. Атрибуты представляют собой фрагменты HTML после открывающего тега, который изменяет функцию исходного элемента.

С помощью этого выражения мы хотим найти атрибуты @class тегов «img», чтобы увидеть, есть ли у них «alignnone» в начале. Затем мы хотим вернуть атрибут «@src», который содержит URL-адрес изображения. Выражение возвращает список URL-адресов изображений, которые нам нужны.

Получение узлов на основе отношений

Теперь скажите, что мы хотим получить все метаданные для этой статьи, такие как дата публикации и категория.

Здесь мы видим, что дата, заголовок, подзаголовок и категория попадают под тег «header» с классом «article-header». Другая особенность XPath позволяет вам перемещаться по HTML, основываясь на «осях» или на том, как соотносятся теги. Мы уже сделали это с помощью «//p/strong». Здесь «strong» является прямым потомком «р».

Но что, если мы хотим получить все виды тегов-потомков, а не только тот, который мы указываем?

Мы будем использовать это выражение:

tree.xpath('//header[@class="article header"]/descendant::node()/text()')

Две новые вещи здесь:

  1. [@something=“something” ] — здесь вместо того, чтобы искать значение атрибута, которое содержит или начинается с чего-то, мы прямо указываем в предикате, что хотим точное совпадение.
  2. descendant::node() — позволяет нам искать все узлы, которые идут после предыдущего тега.

В этом случае выражение позволяет нам получить текст, связанный с тегами p, h2 и h3, которые идут после начального тега заголовка. Это дает нам нужные метаданные статьи.

Получение узлов на основе индекса

Теперь давайте попробуем разместить соответствующие статьи в нижней части страницы.

Снимок экрана сайта цитат Звездных войн

Мы видим, что ссылки на каждый связанный пост находятся в теге «a» под тегом «li» с классом «related-post». Мы попробуем это выражение:

tree.xpath('//li[@class="related-post"]/a/@href')

Мы захватили все ссылки, найдя значения «@href» в HTML, но похоже, что мы получаем дублированные данные. Выражения XPath будут захватывать каждый узел, который соответствует вашим заданным условиям, поэтому он захватывает оба значения «@href», связанные с нашим целевым элементом «li».

Чтобы исправить это, мы напишем это выражение XPath:

tree.xpath('//li[@class="related-post"]/a[1]/@href')

Все, что мы здесь сделали, это добавили значение индекса «[1]». Это изменяет выражение XPath, чтобы выбрать только первый экземпляр «a», который он находит внутри элемента «li». В отличие от Python, индекс начинается с «1» при использовании выражений XPath, поэтому не пытайтесь писать «[0]», когда вам нужен первый элемент.

XPath обладает гораздо большей функциональностью, чем описанная здесь. Вы можете проверить их полную документацию здесь и взглянуть на эту шпаргалку с devhints.io.

Введение в XPath (часть 2). Основы построения запросов — Блог вебразработчика

Где при тестировании может понадобиться XPath, а также обзор инструментальных средств для отладки XPath-запросов можно найти в предыдущей статье.
Что же представляет из себя xpath-запрос?

XPath (XML Path Language) — язык запросов к элементам XML или XHTML документа. XML имеет древовидную структуру. В документе всегда имеется корневой элемент. У элемента дерева всегда существуют предки (исключение — корневой элемент, у которого предков нет) и могут существовать потомки. Каждый элемент дерева находится на определенном уровне вложенности. У элементов на одном уровне бывают предыдущие и следующие за ним элементы. Строка XPath — это фактически путь к элементу в дереве, где каждый уровень разделяется косой чертой «/». В результате обработки выражения XPath получается объект, который может быть:

  1. набор узлов (node-set) — неупорядоченный набор узлов без дубликатов
  2. булево значение (boolean) — true или false
  3. число (number) — число с плавающей точкой
  4. строка (string) — последовательность UCS символов

Небольшой примерчик для старта:
Результатом выполнения следующего XPath запроса будет узел <span>:

 /html/body/*/span
<html>
 <body>
    <div>
      <span>span внутри div</span>
    </div>
    <div></div>
    <span>некоторый текст</span>
  </body>
</html>

Следующий запрос вернет несколько элементов (div#first и div#second):

 /html/body/div
<html>
 <body>
    <div>
      <span>span внутри div</span>
    </div> <div></div>
    <span>некоторый текст</span>
  </body>
</html>
  • * — обозначает любое имя или набор символов
  • / — определяет уровень дерева

Если в запросе первым символом стоит «/» , то путь адресации считается абсолютным (то есть от корня документа). Корень документа всегда является контекстом по умолчанию. Контекст — это текущий полученный узел или набор узлов, относительно которых рассчитывается следующий шаг.

Оси

Оси это основа запросов XPath и их обязательная часть.

  • ancestor:: — возвращает множество предков.
  • ancestor-or-self:: — возвращает множество предков и текущий элемент.
  • attribute:: — возвращает множество атрибутов текущего элемента.
  • child:: — возвращает множество потомков на один уровень ниже.
  • descendant:: — возвращает полное множество потомков.
  • descendant-or-self:: — возвращает полное множество потомков и текущий элемент.
  • following:: — возвращает необработанное множество, ниже текущего элемента.
  • following-sibling:: — возвращает множество элементов на том же уровне, следующих за текущим.
  • namespace:: — возвращает множество имеющее пространство имён (то есть присутствует атрибут xmlns).
  • parent:: — возвращает предка на один уровень назад.
  • preceding:: — возвращает множество обработанных элементов исключая множество предков.
  • preceding-sibling:: — возвращает множество элементов на том же уровне, предшествующих текущему.
  • self:: — возвращает текущий элемент.

Для наиболее часто используемых осей существуют сокращения:

  • attribute:: — можно заменить на «@»
  • child:: — часто просто опускают
  • descendant:: — можно заменить на «.//»
  • parent:: — можно заменить на «..»
  • self:: — можно заменить на «.»

Для приведенного выше примера

/html/body/*/span

полный синтаксис будет иметь вид

 /child::html/child::body/child::*/child::span

Чаще xpath-запрос начинают с «.//» или «//», это делает путь к элементу относительным. Символы ".//" в начале запроса возвращают полное множество потомков, которые являются дочерними для корня документа, т.е. все элементы на текущей странице. В данном случае точку в начале запроса можно опустить, потому что корневой элемент уже является контекстом.

Первый пример можно переписать так:

.//div/span
<html>
 <body>
    <div>
      <span>span внутри div</span>
    </div>
    <div></div>
    <span>некоторый текст</span>
  </body>
</html>

В то время как следующий xpath-запрос вернет оба элемента span:

.//span
<html>
 <body>
    <div>
      <span>span внутри div</span>
    </div>
    <div></div>
    <span>некоторый текст</span>
  </body>
</html>

Важную роль в построение запросов играют предикаты — это необязательная часть, заключаемая в квадратные скобки, в которой могут содержаться оси, условия проверки, функции и операторы. В ходе обработки предиката из полученного на шаге набора узлов исключаются узлы, не прошедшие условие проверки. Например, найдем элемент span, находящийся внутри любого элемента с id = «first»

.//*[@id="first"]/span
<html>
 <body>
    <div>
      <span>span внутри div</span>
    </div>
    <div></div>
    <span>некоторый текст</span>
  </body>
</html>

Примеры использования осей при построении запросов:

1. following-sibling::


.//*[@id="first"]/following-sibling::span
                 или
.//*[@id="second"]/following-sibling::*[1]
<html>
 <body>
    <div>
      <span>span внутри div</span>
    </div>
    <div></div>
    <span>некоторый текст</span>
  </body>
</html>


2. parent::

.//span[1]/..
<html>
 <body>
    <div>
      <span>span внутри div</span>
    </div>
    <div></div>
    <span>некоторый текст</span>
  </body>
</html>


Синтаксис XPath

Для выбора узлов и наборов узлов в XML документе XPath использует выражения путей. Узел выбирается следуя по заданному пути или по, так называемым, шагам.

Пример XML документа

Для демонстрации синтаксиса XPath будет использоваться следующий XML документ:


<?xml version="1.0" encoding="UTF-8"?>
<messages>
   <note>
      <heading  date="10/01/2008">Напоминание</heading>
      <body>Отправить письмо!</body>
   </note>
   <note>
      <heading  date="11/01/2008">Re: Напоминание</heading>
      <body>Письмо отправлено</body>
   </note>
</messages>

Выбор узлов

Чтобы выбрать узлы в XML документе, XPath использует выражения пути. Узел выбирается следуя по заданному пути. Наиболее полезные выражения пути:

ВыражениеРезультат
имя_узлаВыбирает все узлы с именем «имя_узла»
/Выбирает от корневого узла
//Выбирает узлы от текущего узла, соответствующего выбору, независимо от их местонахождения
.Выбирает текущий узел
..Выбирает родителя текущего узла
@Выбирает атрибуты

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

Выражение XPathРезультат
messagesВыбирает все узлы с именем «messages»
/messagesВыбирает корневой элемент сообщений
Примечание: Если путь начинается с косой черты ( / ), то он всегда представляет абсолютный путь к элементу!
messages/noteВыбирает все элементы note, являющиеся потомками элемента messages
//noteВыбирает все элементы note независимо от того, где в документе они находятся
messages//noteВыбирает все элементы note, являющиеся потомками элемента messages независимо от того, где они находятся от элемента messages
//@dateВыбирает все атрибуты с именем date

Предикаты

Предикаты позволяют найти конкретный узел или узел с конкретным значением.

Предикаты всегда заключаются в квадратные скобки.

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

Выражение XPathРезультат
/messages/note[1]Выбирает первый элемент note, котор

Пишем php парсер сайтов с нуля

Опубликовано: 13.02.2015 12:48

Просмотров: 63707

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

Парсер на php — раз плюнуть!

Приветствую вас, наши дорогие читатели. Сегодня решил написать сложную статью про парсеры (сбор информации со сторонних ресурсов).

Скажу сразу, что вам потребуется знание основ программирования на php. В противном случае почитайте теории. Я не буду рассказывать азы, а сразу полезу показывать всё на практике.

Шаг 1 — PHP Simple HTML DOM Parser

Для парсинга сайтов мы будем использовать простецкую библиотечку под названием PHP Simple HTML DOM Parser, которую вы сможете скачать на сайте разработчика. Данный класс поможет вам работать с DOM-моделью страницы (дерево документа). Т.е. главная идея нашей будущей программы будет состоять из следующих пунктов:

  1. Скачиваем нужную страницу сайта
  2. Разбираем её по элементы (div, table, img и прочее)
  3. В соответствии с логикой получим определённые данные.

Давайте же начнём написание нашего php парсера сайтов.

Для начала подключим нашу библиотеку с помощью следующей строки кода:

include 'simple_html_dom.php';

Шаг 2 — Скачиваем страничку

На этом этапе мы смогли подключить файл к проекту и теперь пришла пора скачать страничку для парсинга.

В нашей библе есть две функции для получения удалённой страницы сайта. Вот эти функции

  1. str_get_htm() — получает в качестве параметров обычную строку. Это полезно, если вы стянули страничку с помощью CURL или метода file_get_contents. Пример использования: 
    $seo = str_get_html('<html>Привет, наш любимый читатель блога SEO-Love.ru!</html>')

     

  2. file_get_html() — здесь же мы передаём в качестве параметра какой-то url, с которого нам потребуется скачать контент. 
  3. $seo = file_get_html('http://www.site.ru/');

После скачивания каждой страницы вам требуется подчищать память, дабы парсеру было легче работать и не так сильно грузился ваш сервер. Эта функция вызовется с помощью данного кода:

$seo = file_get_html('http://www.site.ru/');
$seo->clear();

Шаг 3 — Ищем нужные элементы на странице

После получения DOM-модели мы можем приступить непосредственно к поиску нужного элемента-блока в полученном коде.

Большая часть функций поиска использует метод find(selector, [index]). Если не указывать индекс, то функция возвратит массив всех полученных элементов. В противном случае метод вернёт элемент с номером [index].

 Давайте же приведу вам первый пример. Спарсим мою страничку и найдём все картинки.

1

2

3

4

5

6

7

8

9

10

11

12

//подключили библиотеку
require_once 'simple_html_dom.php';
//скачали страничку
$page = file_get_html('http://xdan.ru');
//проверка нашли ли хотя бы 1 блок img и не пустая ли страница
if($page->innertext!='' and count($data->find('img'))){
  //для всех элементов найдём элементы img
  foreach($data->find('img') as $img){
    //выведем данный элемент
    echo $a->innertext;
  }
}

 

Если что-то пошло не так, то прошу отписаться в комментариях. Здесь очень кстати будет мой предыдущий материал Запутываем PHP-код без зазрения совести. Полезно для тех, кто программирует как ниндзя. Больше не отвлекаюсь, идём дальше.

Шаг 4 — Параметры поиска

Надеюсь все уже поняли, что в метод find() можно писать как теги (‘a’), так и id’шники (‘#id’), классы (‘.myclass’), комбинации из предыдущих элементов (‘div #id1 span .class’). Таким образом вы сможете найти любой элемент на странице.

Если метод поиска ничего не найдёт, то он возвратит пустой массив, который приведёт к конфликту. Для этого надо указывать проверку с помощью фукнции count(), которую я использовал выше в примере.

Также вы можете производить поиск по наличию атрибутов у искомого элемента. Пример:

//Найдём все изображения с шириной 300
$seo->find('img[width=300px]');
//Найдём изображения, у которых задана ширина
$seo->find('img[width]');
//Поиск по наличию нескольких классов
$seo->find('img[class=class1 class2]');//<img class="aclass1 class2"/>
//Ищем несколько тегов вместе
$seo->find('div, span, img, a');
//Поиск по вложенности.
//В div ищем все спаны, а в спанах ссылки
$html->find('div span a');

Замечу, что у каждого вложенного тега так же есть возможность поиска!

Есть много вариантов поиска по атрибутам. Перечислять не стану, для более полного руководства прошу пройти на сайт разработчиков 🙂

Обычный текст, без тегов и прочего, можно искать так find(‘text’). Комментарии аналогично find(‘comment’).

Шаг 5 — Поля элементов

Каждый найденный элемент имеет несколько структур:

  1. $seo->tag   Прочитает или запишет имя тега искомого элемента.
  2. $seo->outertext   Прочитает или запишет всю HTML-структуру элемента с ним включительно.
  3. $seo->innertext   Прочитает или запишет внутреннюю HTML-структуру элемента.
  4. $seo->plaintext   Прочитает или запишет обычный текст в элементе. Запись в данное поле ничего не поменяет, хоть возможность изменения как бы присутствует.

Примеры:

$seo = str_get_html("<div>first word <b>second word</b></div>");
echo $seo; // получим <div>first word <b>second word</b></div>, т.е. всю структуру
$div = $seo->find("div", 0);
echo $div->tag; // Вернет: "div"
echo $div->outertext; // Получим <div>first word <b>second word</b></div>
echo $div->innertext; // Получим first word <b>second word</b>
echo $div->plaintext; // Получим first word second word 

Эта возможность очень просто позволяет бегать по DOM-дереву и перебирать его в зависимости от ваших нужд.

Если вы захотите затереть какой-либо элемент из дерева, то просто обнулить значение outertext, т.е. $div->outertext = «»; Можно поэксперементировать с удалением элементов.

P.S. Я обнаружил проблему с кодировками при очистке и всяческими манипуляциями с полем innertext. Пришлось использовать outertext и затем с помощью функции strip_tags удалял ненужные теги.

Шаг 6 — Дочерние элементы

Разработчики данной библиотеки позаботились так же и о том, чтобы вам было легко перемещаться по дочерним и родительским элементам дерева. Для этого ими были любезно созданы следующие методы:

  1. $seo->children ( [int $index] )   Возвращает N-ый дочерний элемент, иначе возвращает массив, состоящий из всех дочерних элементов.
  2. $seo->parent()   Возвращает родительский элемент искомого элемента.
  3. $seo->first_child()   Возвращает первый дочерний элемент искомого элемента, или NULL, если результат пустой
  4. $seo->last_child()   Возвращает последний дочерний элемент искомого элемента, или null, если результат пустой
  5. $seo->next_sibling()   Возвращает следующий родственный элемент искомого элемента, или null, если результат пустой
  6. $seo->prev_sibling()   Возвращает предыдущий родственный элемент искомого элемента, или null, если результат пустой

 Я особо не пользовался этими возможностями, потому что они ещё ни разу не пригодились мне. Хотя один раз при разборе таблицы использовал, потому что они структурированы, что делает разбор очень простым и лёгким.

Шаг 7 — Практика

Перейдём к практике. Я решил отдать вам на растерзание одну функцию, что использовал при написании парсера текстов песен на один из своих сайтов. Пытался досконально подробно описать код. Смотрите комментарии и задавайте вопросы.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

public function parser_rock_txt() {
        $i = 0;
        $new_songs = 0;
        //номер категории, чтобы хранить в базе. У меня Рок = 1
        $category = 1;
  		//Скачиваем страничку с сайта Rock-Txt.ru
        $data = file_get_html('http://rock-txt.ru/');
        //нашли хотя бы одну ссылку на песни по буквам (проходим навигацию)
        if (count($data->find('div.a-z a'))) {
            //пробежим по всей навигации
            foreach ($data->find('div.a-z a') as $a) {
                //Выводим букву, которую парсим
                echo ('Текущая буква - ' . $a->plaintext . '<br />');
                //нашли список всех исполнителей
                $data_vocalist = file_get_html("http://rock-txt.ru" . $a->href);
                //если есть хотя бы один исполнитель
                if (count($data_vocalist->find('#dle-content div.full-news a'))) {
                    foreach ($data_vocalist->find('#dle-content div.full-news a') as $vocalist) {
                        //приводим название исполнителя к нижнему регистру
                        $vocalist->plaintext = mb_strtolower((mb_convert_encoding(($vocalist->plaintext), 'utf-8', mb_detect_encoding(($vocalist->plaintext)))), 'UTF-8');
                        //получаем id исполнителя из моей базы
                        $id_vocalist = $this->songs_model->check_vocalist(trim($this->db->escape($vocalist->plaintext)), trim($this->db->escape($this->translit($vocalist->plaintext))), $category);
                        //Нашли все песни исполнителя
                        $data_songs = file_get_html($vocalist->href);
                        //если есть хотя бы одна песня такого исполнителя - идём дальше
                        if (count($data_songs->find('#dle-content div.left-news-band a'))) {
                            foreach ($data_songs->find('#dle-content div.left-news-band a') as $songs) {
                                //Получим название песни. Удалим название исполнителя.
                                $name_song = substr(preg_replace('/\s\s+/', ' ', $songs->plaintext), strlen(trim($vocalist->plaintext)) + 1);
                                $name_song = trim($name_song);
                                //приводим название песни в нижний регистр
                                $name_song = mb_strtolower((mb_convert_encoding(($name_song), 'utf-8', mb_detect_encoding(($name_song)))), 'UTF-8');
                                //Транслитизируем название песни (моя самописная функция)
                                $name_song_translit = $this->translit($name_song);
                                //Отсекаем все пустые названия
                                if ($name_song == '' || $name_song_translit == '')
                                    continue;   
                                //Проходим по всем страницам навигации (пейджер, постраничная навигация)
                                $num_page = 0;
                                foreach ($songs->find('div.navigation a') as $num) {
                                    //если число - сравниваем, а не нашли ли мы ещё одну страницу навигации
                                    if (is_int($num->plaintext)) {
                                        if ($num->plaintext > $num_page)
                                            $num_page = $num->plaintext;
                                    }
                                }
                                echo $num_page . '<br />';
                                //загрузим текст песни
                                $text_songs = file_get_html($songs->href);
                                if (count($text_songs->find('div.full-news-full div[id] p'))) {
                                    foreach ($text_songs->find('div.full-news-full div[id] p') as $text_song) {
                                        //очищаем всякие ненужны ссылки и спаны
                                        foreach ($text_song->find('span') as $span) {
                                            $span->outertext = '';
                                        }
                                        foreach ($text_song->find('a') as $a) {
                                            $a->href = '';
                                            $a->outertext = '';
                                        }
                                        //выводим исполнителя, песню и текст
                                        echo $name_song . '<br />';
                                        echo $songs->href . '<br />';
                                        echo $text_song->outertext . '<br />';
                                        $text_song->outertext = preg_replace("/(<br[^>]*>\s*)+/i", "<br />", $text_song->outertext, 1);
                                        //вставляю в мою базу текст песни и исполнителя (самописная функция)
                                        $result = $this->songs_model->check_song(trim($this->db->escape($name_song_translit)), trim($this->db->escape($name_song)), trim($this->db->escape($id_vocalist)), trim($this->db->escape_str(preg_replace("#(:?<br />){2,}#i", "<br />", strip_tags($text_song->outertext, '<br /><br><b><strong><p>')))));
                                        //если добавили - увеличим счётчик новых песен
                                        if ($result != -1) {
                                            $new_songs++;
                                        }
                                        $i++;
                                        //выйдем, тут всякие косяки бывают
                                        break;
                                    }
                                }
                                //теперь аналогично пробегаем по остальным страницам
                                if ($num_page > 0) {
                                    $text_songs = file_get_html($songs->href . 'page/' . $num_page);
                                    if (count($text_songs->find('div.full-news-full div[id] p'))) {
                                        foreach ($text_songs->find('div.full-news-full div[id] p') as $text_song) {
                                            foreach ($text_song->find('span') as $span) {
                                                $span->outertext = '';
                                            }
                                            foreach ($text_song->find('a') as $a) {
                                                $a->href = '';
                                                $a->outertext = '';
                                            }
                                            echo $name_song . '<br />';
                                            echo $songs->href . '<br />';
                                            echo $text_song->outertext . '<br />';
                                            $text_song->outertext = preg_replace("/(<br[^>]*>\s*)+/i", "<br />", $text_song->outertext, 1);
                                            $result = $this->songs_model->check_song(trim($this->db->escape($name_song_translit)), trim($this->db->escape($name_song)), trim($this->db->escape($id_vocalist)), trim($this->db->escape_str(preg_replace("#(:?<br />){2,}#i", "<br />", strip_tags($text_song->outertext, '<br /><br><b><strong><p>')))));
                                            if ($result != -1) {
                                                $new_songs++;
                                            }
                                            $i++;
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return "<br />Парсер сайта rock-txt.ru завершён. Спарсено песен всего " . $i . ", из них новых " . $new_songs . " ";
    }

 Получилась вот такая здоровая функция, которая парсит тексты песен с сайта о роке. Написал её я за час. Спарсил 10 000 текстов песен. Думаю, что руками вы бы набивали такую базу очень и очень долго 🙂

Замечу, что в коде много самописных функций, которые используются для вставки в мой базу. Эти функции у каждого могут быть индивидуальными, так что в этом я вам не помощник. Либо обращайтесь за помощью в комментарии. Всегда буду рад помочь!

Пока что на этом всё. В следующих уроках расскажу, как можно быстро и просто спарсить кучу информации на несколько десятков сайтов. Этот кейс должен обогатить каждого!

Всего доброго! Ретвиты, лайки и репосты приветствуются!

Если статья была для Вас полезной — Поделитесь ссылкой!

Советуем почитать

Закрепленные

Понравившиеся

PHP XPath для синтаксического анализа таблицы

Переполнение стека

  1. Около
  2. Продукты

  3. Для команд
  1. Переполнение стека
    Общественные вопросы и ответы

  2. Переполнение стека для команд
    Где разработчики и технологи делятся частными знаниями с коллегами

  3. Вакансии
    Программирование и связанные с ним технические возможности карьерного роста

  4. Талант
    Нанимайте технических специалистов и создавайте свой бренд работодателя

  5. Реклама
    Обратитесь к разработчикам и технологам со всего мира

  6. О компании

.Анализ

— скрипт синтаксического анализа php xpath src

Переполнение стека

  1. Около
  2. Продукты

  3. Для команд
  1. Переполнение стека
    Общественные вопросы и ответы

  2. Переполнение стека для команд
    Где разработчики и технологи делятся частными знаниями с коллегами

  3. Вакансии
    Программирование и связанные с ним технические возможности карьерного роста

  4. Талант
    Нанимайте технических специалистов и создавайте свой бренд работодателя

  5. Реклама
    Обратитесь к разработчикам и технологам со всего мира

  6. О компании

Загрузка…

.Анализ

— исчезновение элементов PHP XPath Table

Переполнение стека

  1. Около
  2. Продукты

  3. Для команд
  1. Переполнение стека
    Общественные вопросы и ответы

  2. Переполнение стека для команд
    Где разработчики и технологи делятся частными знаниями с коллегами

  3. Вакансии
    Программирование и связанные с ним технические возможности карьерного роста

  4. Талант
    Нанимайте технических специалистов и создавайте свой бренд работодателя

  5. Реклама
    Обратитесь к разработчикам и технологам со всего мира

.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *