Pull to refresh

Comments 70

Последний мой поисковый запрос практически дословно повторяет заголовок и теги.
Спасибо мирозданию за беспроводную передачу мыслей и автору за статью и направление в котором копать.
Для PHP есть PHPQuery, она поддерживает как xpath, так и селекторы в стиле jquery. Это если кто-то пишет парсеры на php.

Насчет админки — все понятно, тоже с подобной структурой работаю, только у меня еще правила бывают вложенными. Т.е. товары из разных категорий имеют разные правила для определения своих свойств. Это полезно, к примеру, когда парсишь базу автомобилей, и там встречается множество модификаций моделей авто, и их практически нереально сопоставлять без знания, какая же у авто модель. То же самое с базой недвижимости по мелким городкам Европы — гораздо проще сопоставить город, если уже известны страна и регион.
C PHPQuery у меня были пробемы с памятью. Приходится на каждой итерации очищать, но все равно порой парсер по несколько гигабайт оперативки отжирал. Но в целом весьма удобная штука, конечно.
Интересно, что же вы с ним такое делали, что аж «несколько гигабайт»? Почти уверен, что проблема не в самом phpquery, т.к. я массово обрабатывал огромные объёмы сложных страниц, и всё занимало очень скромыне объёмы, не выходя за несколько десятков мегабайт.
в python есть библиотека grab, очень советую.
> Да, это может сказаться на производительности, но не сильно. Универсальность кода – важнее.

Насколько я понимаю, оптимизация xpath мало на что влияет, т.к. основные ресурсы тратятся на построение DOM-дерева.
Да, построение дерева очень затратная процедура в сравнении с запросами, особенно если потом нужно выполнить всего пару XPATH.
Правда периодически встречаются сайты у которых в структуре дерева не за что «зацепиться» — т.е. куча вложенных тэгов без классов и id, например страница с описанием товара на hobbyking. В этом случае XPATH запрос будет сильно сложнее, а когда их много — разница чувствуется. Хотя это и не критично, если мы не парсим весь facebook или google.
Учитывая мощность XPath всегда есть за что цепляться. Я бывает цепляюсь за соседние узлы не имеющие отношения к характеристикам товара, но являющиеся характерными и уже от них можно построить путь. Возможность выбора узла по позиции в коллекции узлов, манипуляции с безымянными тестовыми узлами и куча других фишек XPath-а позволяют, имхо, вытащить любую инфу со страницы.
Да, вы правы, способов «зацепиться» очень много, просто когда мы цепляемся к свойствам типа class='чего-то' или id='кто-то' — то шанс что эти свойства не изменяться очень велик, а значит не придется думать об исправлениях.
В случае же хватания за свойства типа
contains(@style, 'width: 100%')
или «третья нода после второго div'а шириной 113px' — шансы что придется этот код править сильно возрастают.
можно за текст цепляться, подниматься на ноду выше и выбирать то что нужно. что-то вроде
//div[@id="data"]//td[contains(text(), "Цена")]/../td[2]/text()

как верно заметили в комменте выше, всегда можно зацепиться. Сайты с табличной верской или без классов и идов сейчас редкость.
Для сомневающихся в выборе паяльника — смотрите в сторону питоновского Scrapy. Не самая простая штука, но после изучения ощущаешь себя темным магом. В проектах работает более года без сбоев.
Не затронут вопрос постраничной навигации в списке товаров. Тут я бы советовал писать XPath-и работающие со строкой навигации и не использовать передачу известных параметров через url. Потому что в одном магазине page=XX означает номер страницы, а в других отступ от начала.

Я для себя делал парсер который знает стартовую страницу от которой начинается каталог и рекурсивно бежит по списку категорий. После того как дерево категорий построено парсер пробегается по каждому разделу и, если нужно, постранично находит ссылки на подробное описание товаров, после чего уже можно получить товар с подробными характеристиками. Подстригающие тут опасности — не у всех каталогов есть кнопка «далее», на сайтах бывают ошибки когда ссылка ведущая на следующую страницу на самом деле ведет на первую страницу. Общая рекомендация тут — на каждой странице всегда есть отметка о текущей страниц, так что вытаскивать номер текущей страницы и сравнивать его в ожидаемым. Если не совпало, что-то пошло не так.

В целом рекомендую максимально работать с тем, что видно через браузер. Т.е. через XPath вытягивать данные видные конечному пользователю.
Да, упустил этот момент. Для прохода по страницам я предпочитаю искать ссылку «next», ну и смотреть на url страницы чтобы не парсить повторно (вместо номера страницы).
Кроме страничной навигации есть ведь еще и подгрузка ajax'ом — но это легко отлавливается fiddler'ом и эмулируется.

На счет последнего утверждения не соглашусь. Очень часто (особенно у буржуев) в разделе head, в метаданных очень много полезной информации которую очень удобно парсить. Это делается для того, чтобы поисковики правильно определяли тип контента, т.е. его осмысливали. Самый часто встречаемый пример это адрес: пользователю показывается в виде одной строки, а в метаданных разделено на поля: address line1, city, region ну и т.д.
Ну я в первую очередь говорил в контексте топика — парсинг магазинов. А там как правило в первую очередь стоит ориентироваться на то, что видит пользователь. Обычно эти части координатным изменения не подвергаются (зачастую слишком много там подвязанно JS-а, его менять магазину себе дороже).
Кстати, я правильно понимаю, что указанный парсер не поддерживает JavaScript и не сможет вытянуть каталог страницы которого сформированы на JS-е?
Не поддерживает, он просто парсит текст который ему подсунут считая что это HTML.
Если нужно исполнение javascript'а — то можно использовать различные обертки над браузерными движками. Естественно производительно при этом катастрофически упадет.
Не упадет, если разделять механизм парсинга и получения страницы.

Я для себя систему разделил на 2 части: краулер и парсер. Задача клаулера тупо получить html (для случаев с JS он может использовать phantomjs) и положить его в кэш. А вот парсер работает с html и ни чего сам не качает. Его задача сходить в кэш, взять страницу и вытащить из неё данные. Схема очень удобная и гибкая.
Вы просто перенесли вычислительную нагрузку из парсера в паука. Сама нагрузка от этого не изменилась.
Хотя в вашем случае легко масштабировать при увеличении нагрузки.
Боюсь нахватать минусов, но на мой взгляд проще пользоваться регулярками для таких задач. Попробую пояснить, почему я так считаю:
1) При грамотном построении регулярных выражений можно минимизировать проблемы с изменением сайтов.
2) Регулярные выражения, насколько я знаю, работают относительно быстро. В любом случае они будут быстрее, чем построение DOM и затем его анализ. Быстрота, само собой, подразумевает так же и ресурсозатраты. Плюс на регулярках построен, собственно, механизм создания DOM-дерева.
3) Если сделать некую последовательность выборок по выражению, то можно сделть довольно гибкую систему. Например, сначала мы находим все div'ы товарами, а потом уже внутри них ищем то, что нужно (в простейшем случае).
4) Связано с пунктом под номером 2. Не нужно использовать никаких сторонних библиотек. Достаточно знать, как составляются регулярные выражения.

В итоге время разработки увеличится, но если задача выходит за рамки разового использования, то это будет целесообразным, так как поддерживать это дело будет проще (по крайней мере тому, что это написал).
Поддерживать точно сложнее. И багов будет туча трудноуловимых, так как регулярки человеку значительно труднее воспринимать.
Да вот хрен его знает. В свое время писал паукана, который по сайту ходил и смотрел, изменился ли он. Причем единственным способом понять, работает ли он правильно или нет был метод тыка. Короче, доработка занимала минуты, зато на саму разработку я потратил дотаточно много времени. Мне было проще сделать его с регулярками, так как с DOM работать куда муторнее. Но это не совсем задача анализа данных, а нечто более простое.
В общем, чтобы мне делать какие-то однозначные выводы, нужно больше опыта в различных задачах такого плана. Поэтому я и начал прошлый свой комментарий со слов «на мой взгляд» и вообще был осторожен в формулировках.
суть в том, что в случае XPATH запросов — небольшие изменения сайта будут проигнорированы и данные будут выбраны корректно, регулярки приходится либо чаще править, либо писать более универсальные, но менее читабельные.
Но в принципе в Вашей логике тоже есть истина, просто инструмент нужно выбирать в зависимости от задач.
Уже более года разрабатываю парсеры, чуть ли не каждый день. С регулярками я бы удавился. Не вижу смысла спорить, попробуйте плотно заняться парсингом, а потом рассказывайте, какие регулярки хорошие ;-)
Поддерживаю, еще добавлю про пресловутый «индусский код». Сутя по тому что я видел, индусы код пишут по принципу — сейчас работает и ладно, поэтому никто не заморачивается — трудно будет поддерживать или нет, и более оптимальные пути не ищет. Наверное поэтому у них парсинг сайтов выполняется регулярками (других способов я не встречал). хотя сами регулярки составлены вполне корректно — поддерживать это — жутко, как правило проще переписать,
почему так критично? все зависит от сайта, где то хороши регулярки, а где то обход по дереву, универсального парсера всего подряд не бывает :)
я правда не знаю как дело обстоит в php, может там и есть чудо-библиотеки, которые переваривают всё, но, исходя из собственного опыта написания парсеров на java (так уж сложилось что приходится их встраивать в текущие приложения тоже на java и сменить язык никак нельзя), что их в DOM разобрать проблемно очень.
В python использую lxml библиотеку — переваривает все сайты. Я не говорю, что мол она справится со 100% сайтами, но это факт, что в работе я regular expression использую только уже когда надо что-то напарсить внутри HTML выдранного из DOM-дерева

Я не грю, что xpath это панацея, иногда да — юзаю regular expression, когда хочется очень быстро проверить что-то, не строя DOM-дерева.
В PHP в DOM руками ни чего разбираться не нужно. Просто берем и тупо скармливаем документ процессору. На получившийся объект-DOM применяем XPath выражение. На выхлопе получаем коллекцию узлов. Все что требуется — тупо написать нужное выражение.

Я вот себе написал парсер, где базовый класса как раз и выполняет всю эту рутинную требуху. А дальше пишутся потомки в которых просто перечисляется требуемый набор XPath-ей. Каждый класс потомок это каталог какого либо сайта. Добавление нового сайта для парсинга занимает от часа до восьми. А дальше такой парсер продолжает корректно работать даже если на сайте сильно поменяли разметку.
Что заладили-то — регулярки, не регулярки. В парсинге сайтов все методы хороши. Вот вам пример:

selector = HtmlXPathSelector(response)
selector.select(".//table[@class='pricetable']/tr[1]/td[2]/text()").re("(\d+)\s*([^0-9 ]+)")


или вот так я парсил e-mail, который генерился javascript'ом (в целях защиты от парсинга):

contact_email = safe_list_get(selector.select(".//table[@width='90']//script/text()").re('document.write\((.*)\)'), 0)
if contact_email:
    contact_email = re.match("<a.*>(.*)</a>", common.eval_js(contact_email.encode("utf-8"), common.js_context)).group(1)
if contact_email and common.re_patterns["email"].match(contact_email):
    contact["Email"] = contact_email


Да, код не самый красивый (как и сайт, который парсился), но идея, думаю, ясна — для поиска элемента в DOM используем селекторы (XPath или CSS-подобные); для более детального разбора (или когда селекторы слишком жёстко завязываются на структуру сайта) — используем регулярки и всё остальное, что только в голову взбредёт.
«попробуйте плотно заняться парсингом, а потом рассказывайте, какие регулярки хорошие»
С 99года регулярно распарсиваю сайты регулярками. )))
Причем регулярно, а не раз от раза. ))
Сначала на Перле (тогда альтернативы регуляркам не было), потом на РНР. Когда важно быстродействие ничего лучше не придумаешь.
Давайте в студию код парсинга какого-нить сайта, интересно посмотреть.
Да ничего интреснее банального preg_match вы там не увидите. ))
Тем более, что механизм универсальный (ну почти :)), в выражения подставляются ограничители начала/конца искомой строки: цены, названия, производителя и т.д. Потом все раскладывается по SQL-табличкам, структура почти такая же как у автора поста. Зачем изобретать велосипеды, все товарные сайты похожи друг на друга, что китайские, что российские.
У регулярок есть один плюс — им все равно на структуру и валидность документа. Но по сравнению с простотой и удобством XPATH плюс сомнительный. Многие уже отметились тут, что xpath в чистом виде очень и очень удобен. Писал долго на регулярках, перешел на xpath и доволен более чем. А если еще перед xpath прогнать документ через tidy, то парсинг становится сплошным удовольствием.
1) Слишком сложная получается регулярка. Ибо регулярка работает на слишком «низком» уровне, она работает с самим тестом. XPath же работает на уровне логической связи данных. Я не говоря уже о бОльшей читабельности XPath. К примеру, если я знаю, что нужная мне характеристика находится в элемента с классом product у которого дочерний элемент с классом property_1, то достаточно написать:
//*[class=«product»]//*[class=«property_1»]
и не нужно заморачиваться, как много между этими элементами других элементов, какие это элементы сами по себе и каким образом их в дереве вообще искать (все на себя берет XPath процессор). Получается очень устойчивый к изменениям на сайте механизм.

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

2) Да, быстро. По сравнению с обработкой через DOM где-то раз в 10 быстрее (в PHP). Памяти тоже потребляют сильно меньше.
Если не ошибаюсь, не все парсеры построены не ругялярках. В любом случае там только регулярками не обходится.

3) На практике получается слишком сложно. А гибкая система уже создана — XPath процессор.

4) Как это не нужно сторонних либ? Регулярка, как и XPath, абстракция. А реализацию на себе берет та или иная библиотека. Не говоря уже о том, что есть несколько диалектов регулярок. Общераспространенный перловый не являются единственными.

Я ничуть не хочу умалить важность регулярок. Полученные XPath-ем данные порой приходится прогонять через регулярки. Просто для работы с логической структурой html/xml Xpath подходит именно простотой разработки и сопровождения, а регулярки помогает там, где нужно работать просто со структурой текста как такового.
у вас еще достаточно простая структура.
я использую более сложную:
— параметры могут объединены в группы
— группы и сами параметры зависят от категории товаров
статья несколько упрощенная, про категории я даже не упоминал, а вот группировки параметров у меня действительно нет — как-то пока не было необходимости.
я старался делать карточки товаров у нас примерно похожими на те, что в маркете, да и удобней так для пользователей, в случае когда у товара несколько десятков параметров, разбить их на группы.
Можно посмотреть что у Вас получилось?
для ruby: nokogiri, mechanize.
… второе можно на логику натравлять, а не dom строить c селекторами и xpath.
А вы как-то делали совмещение одинаковых товаров с разными названиями? Или у вас фактически каждый отпарсенный товар является отдельной сущностью без какой-либо попытки их совместить?
Пока совмещения нет. Хотя в БД для этого все предусмотрено.
Проблема в том, что китайцы вообще не заморачиваются понятиями «производитель» и «модель». Точнее сказать они про это знают, но пользуются очень и очень редко.
Каким образом предусмотрено? Некая табличка которая идентификаторы аналогов будет содержать?

Я так понимаю, что раз китайцы не парятся на тему производителя и марки, то сравнивать придется по названиям фактически… Это сравнение каждой записи с каждой. Ох и много же придется запросов сделать :))

Или есть какие-то иные мысли на эту тему?
Можно опираться не только на название, но и набор характеристик. Если у двух товаров характеристики одинаковые, то уже не важно, какой там на нем шильдик, лейб, бренд.
Да, это в целом понятно. Но данные могут сильно отличаться даже после нормализации. У какого-то товара могут просто отсутствовать определенные характеристики, у какого-то товара характеристики могут быть абсолютно одинаковые (при разном производителе и внешнем виде), где-то присутствуют разные правила округления в размерах. Особенно все усугубляется, если данные из разных источников. То есть просто так проверка в лоб может и не сработать.
Но в рамках одного каталога (источника) эти правила более менее одинаковы. Поэтому частично нормализацию можно делать уже на стадии парсинга. Поэтому добавляя новый источник я просто учитываю эти особенности.
Нет, все проще — есть понятие «оригинальный товар» — если товар является клоном чего-то у него указан id «оригинала».
Если бы можно было все определить по названию или модели — я бы это уже давно сделал, к сожалению поиск «клонов» не тривиальная задача.
Не знаю как авто, а лично я при парсинге после получения товара и его характеристик провожу нормализацию. Есть словарь брендов, вендоров (в том числе написанные с ошибками), моделей и марок и исходя из него нормализатор приводит название к нормальному виду. После чего ищет, есть ли такой товар уже в базе. Если есть, все нормально, сливает загруженный каталог с текущим, если не смог, ставим флаг о необходимости проверки и дальше уже человек решает куда его нужно поместить.
А словарь брендов / вендоров составлялся вручную на основе реальных данных? Много времени ушло поди?
Я бы сказал «условно вручную». И да, на основе реальных данных, других вариантов тут нет, имхо.

А условно вот почему. Добавили новый парсер, он спарсил сайт. На выходе получили полный каталог сайта. Нормализовали его и попытались слить с текущим. Товары которые не смогли влить в текущий каталог уходит на обработку оператору, тот сопоставляет с товаром в текущем каталоге или создает новый. После сохранения мы знаем в том числе вендора и если его нет в словаре, то добавляем, если это какой-то новый вариант написания вендора, то добавляем его в список алиасов. Таким образом набор этих словарей пополняется автоматом. Так что лично у меня времени это требует нисколько. Кроме того мне проще, я тяну каталоги рунета и европы, а там проще, чем с китайцами.
Класс! Хотел бы я посмотреть на эту махину ))
Сколько у вас там сейчас записей в базе? :-)
мне больше интересно сколько операторов работает, по крайней мере по началу — дофигу работы.
Чуть ниже как раз об этом речь.

Кстати, а на чем у вас построен UI?
Админка это приложение на WPF, а сайты: asp.net+mssql на сервере, и jquery с шаблонами на клиенте.
Понятно, спасибо за интересную информацию! :-)
На самом деле очень немного. Под пару тысяч. Просто опять же повторюсь, у меня в основном европа, одежда. Там старые проверенные бренды и нет такого зоопарка как у азиатов. Но если что я спокоен, используемая система позволит и азиатов парсить без того, что бы мне как программисту приходилось подключаться к работе.
Очень здорово! Но раз речь о паре тысяч, то оператор наверно один легко управляется со всем? Может статью напишете тоже интересную о вашем опыте? Я бы с удовольствием почитал.
Да, оператор один. Основной фронт работы на него ложится при подключении нового источника. Через пару выгрузок все возможные сочетания и косяки собраны и оператору остается только выборочно контролировать на сколько каталоги корректно слились, а так же решать в какой раздел текущего каталога пихать вновь появившиеся разделы источника.

В планах не только написание статьи, но и релиз парсера в виде SaaS сервиса. Причем в финальной версии я хочу даже от написания XPath уйти предоставив это делать оператору через веб GUI. Оператор понятное дело ни чего про XPath не знает и знать не должен. В общем стремлюсь к максимальной степени автоматизации, что бы программист к работе подключался только в исключительных случаях.
Отлично! Буду ждать с большим нетерпением! :-)
Как я понимаю оператор будет делать примерно то же что и при импорте из веба в Excel? т.е. будете подсвечивать ему данные которые можно распарсить.
Если этот вопрос будет отражен в статье — можете тут не отвечать, подожду статью.
Нет, не так. Оператору открывается страница оригинального сайта, к примеру, с карточкой товара. Firebug-ом думаю пользоваться приходилось, там есть кнопка «Щелкнуть на элементе страницы для анализа». У оператора будет аналогичная кнопка через которую он будет выделять, к примеру, характеристики товара. Через JS можно будет получить полный XPath от корня и дальше в парсе использовать его. Да, полученный XPath будет более «хрупок», чем выражение надписанное опытным программистом. Но тут есть два важный аспекта. Можно на основании сравнения группы XPath выражений вывести более устойчивые к изменения выражения, т.е. близкие к тому, что пишет программер руками. Даже если таковых в автоматическом режиме вычислить не получиться, то оператор простой еще раз зайдет на нужные страницы и выделить нужные данные и мы снова получим корректные выражения без привлечения программера. Абсолютно тупо экономический расчет. Время оператора дешевле, чем время разработчика и структура разметки сайта меняется не каждый день. Поэтому целесообразнее раз в несколько недель заставить оператора переопределить правила парсинга чем сдергивать квалифицированного разработчика на такую, по сути, тривиальную и рутинную задачу.

Как реализовать данную идею я уже достаточно давно прикинул. Уверен, что это вполне реально. И реализую в ближайших пару месяцев.
Если кому интересно — тут делал бенчмарк различных HTML парсеров habrahabr.ru/post/163979/

Насчет
div[contains(@class,’productInfo’)]

это отстой. Любой нормальный движок XPath позволяет писать собственные функции.
Пример python lxml

from lxml import etree

def xpath_class_matches(context, *args):
    """XPath extension function. Return number of node class names matches.

    <div class="c1 c2 c3 c4"/>
    //div[class-matches('c1', 'c2, 'c5') == 2]
    """
    class_attr = context.context_node.attrib.get("class", None)
    matches = 0
    if class_attr:
        classes_ = class_attr.split(" ")
        for class_ in classes_:
            if class_ in args:
                matches += 1
    return matches

ns = etree.FunctionNamespace(None)
ns['class-matches'] = xpath_class_matches

tree = etree.HTML(html_string)
tree.xpath("//div[class-matches('productInfo')]")

И такой запрос гораздо честнее отработает — не захватит div class="smth-productInfoSmth".

 
div[@id=’productInfo’]//h1 — не знал, что так можно. Писал всегда descendant::h1.

К слову, // или descendant:: оси заметно сказываются на производительности, хоть и очень удобные.
не знал, что так можно. Писал всегда descendant::h1.

Можно. Это просто сокращенная форма записи для descendant-or-self.

В свою очередь как не подумал в сторону:
Любой нормальный движок XPath позволяет писать собственные функции.

а ведь действительно! Проблема "не захватит div class=«smth-productInfoSmth»." имеет место быть без использования функций. Респект!
Спасибо!
Теперь знаю про кастомные функции в XPATH.
Тем, кто занимается подобными задачами, советую присмотреться к convextra.com
Это онлайн сервис, который умеет парсить любые повторяющие структуры (каталоги товаров, доски объявлений, форумы и т д) без привязки к верстке вообще.
Вот смотрите сюда: goodzer.com

Парсит пол-миллиона магазинов влет и вытаскивает 2.5 миллиарда товаров. Неплохо так.
Посмотрите парсер сайтов и мониторинг цен конкурентов xmldatafeed.com — задача не только в парсинге цен с сайтов, а так же в создании аналитической системы для помощи в принятия решения!
Sign up to leave a comment.

Articles