парсер запросов / Mail.ru Group corporate blog / Habr
Чем занят отдел обработки запросов в Поиске Mail.Ru? Если одним предложением, мы пытаемся «понять» запрос, то есть осуществляем подготовку запроса к поиску, приводим его в вид, пригодный для взаимодействия с нашим индексом, ранжированием, подмесами и прочими компонентами. Если же вы хотите узнать о нашей работе подробнее — добро пожаловать под кат. В этом посте я расскажу об одной из областей нашей работы — парсере запросов.Где мы, кто мы и где здесь парсер?
На рисунке изображена структура сервиса поиска. Участки системы, в которых работаем мы, выделены розовым цветом.
Рис. 1. Наше место в поиске
Фронтенд предоставляет пользователю форму для ввода запроса, по готовности запрос пересылается на MetaSM, где попадает в Query Parser, занимающийся его разбором и классификацией. Затем запрос, обогащенный дополнительными параметрами, в виде дерева передается на бэкенды, где на основе этого дерева из индекса извлекаются соответствующие запросу данные, передаваемые на окончательную обработку ранжированию.
До того, как запрос передан в систему поиска, он обрабатывается еще двумя компонентами. Первый — саджесты, сервис, который реагирует практически на каждое нажатие клавиши, предлагая подходящие, по его мнению, варианты продолжения запроса. Второй — спеллчекер, которому запрос передается после отправки: он анализирует запрос на предмет опечаток.
Навигационная база изображена отдельно исходя из функционала; на самом деле она интегрирована в парсер запросов, но об этом — чуть ниже.
Классы и разновидности запросов
Зачем вообще нужна какая-либо классификация запроса и предварительный анализ? Причина в том, что разные классы запросов требуют разных данных, разных формул ранжирования, разных визуальных представлений результатов.
Рис. 2. Разновидности запросов
Традиционно все поисковые запросы разделяют на три класса:
● информационные
● навигационные
● транзакционные
Каждый из них, в свою очередь, распадается на более мелкие классы. Эта диаграмма не претендует на полноту, процесс сегментации всего множества запросов, по сути, фрактален — это скорее вопрос терпения и ресурсов. Здесь указаны лишь самые основные элементы.
Транзакционные запросы включают в себя поиск контента, содержащегося в интернете, и поиск объектов, находящихся «в оффлайне», т.е., прежде всего, различных товаров и услуг. Навигационные запросы — поиск явно указанных сайтов, блогов и микроблогов, личных страниц, организаций. Информационный — самый широкий в смысловом плане класс запросов, к нему относится все остальное: это новости, справочные запросы по фактам и терминам, поиск мнений и отзывов, ответы на вопросы и многое другое.
Сразу отмечу, что один запрос зачастую имеет признаки сразу нескольких классов. Поэтому классификация — это не создание древовидной структуры, в каждую ячейку которой попадает часть запроса, а скорее тэгирование, то есть наделение запроса дополнительными свойствами.
Распределение тем
Рис. 3. Частотное распределение разных классов запросов
По оценке размеров различных классов запросов видно, что малое количество классов содержит большую долю потока запросов, а за ними следует длинный хвост уменьшающихся фрагментов потока запросов. Чем уже тема, тем меньше по ней запросов и тем больше таких тем.
С практической точки зрения это значит, что мы не стремимся классифицировать абсолютно все запросы — мы скорее пытаемся отщипнуть от общего потока запросов наиболее важные. Как правило, критерием выбора класса является его частотность, но не только: это могут быть запросы, которые мы можем качественно обработать, интересно визуализировать результаты и так далее.
Архитектура парсера
Рис. 4. Архитектура парсера запросов
Парсер представляет собой очередь агентов, каждый из которых производит какое-либо более или менее атомарное действие над запросом, начиная от разбиения его на слова и другие регулярные сущности типа URL-ов и телефонных номеров, и заканчивая сложными классификаторами типа детектора цитат.
Агенты выстроены в очередь. Каждый запрос прогоняется через эту очередь, причём результаты работы каждого агента влияют на дальнейший логический путь запроса — например, запрос, классифицированный как навигационный, не попадёт на обработку детектора цитат. Сам запрос здесь представляется как некая доска объявлений, на которой каждый агент может оставить свое сообщение, прочитать сообщения других агентов и при необходимости изменить или отменить их. По завершении запрос передается дополнительным программам подмесов и, в конечном счете, отправляется на бэкенды, где все записанные агентами дополнительные параметры используются как факторы ранжирования.
Словари
Основным поставщиком данных для парсера запросов являются словари. Это огромные файлы, содержащие сгруппированные массивы строк, каждый из которых имеет определенный смысл.
Рис. 5. Словари: пример из жизни
На рисунке представлен фрагмент одного из словарей, в котором собраны слова, наиболее характерные для запросов класса «Фильмы». Каждой из этих групп слов может быть сопоставлен маркер (на рисунке он окружен зеленой рамкой). Он обозначает, что в случае нахождения в запросе такого фрагмента всему запросу будет поставлен флаг, сигнализирующий, что предметом этого запроса является кино.
В этих же словарях задаются синонимы (красная рамка на рисунке). Благодаря им, если, к примеру, в запросе есть слово “саундтрек”, то запрос будет расширен словосочетаниями “звуковая дорожка”, “саунд трек” и аббревиатурой ОST.
Таких словарей существует несколько десятков. Размер некоторых из них —сотни тысяч, даже миллионы подобных строк. На их основе происходит первичная обработка запроса, его раскрашивание, после чего работает более сложная логика.
Маркеры
Возьмем три запроса:
● Пираты Карибского моря
● глубина Карибского моря
● последствия Карибского кризиса
С точки зрения текстового поиска все они — просто строки, содержащие по три слова, в то время как с точки зрения пользователя каждый из них имеет совершенно различный смысл. Наша же цель — сообщить системе как можно больше об этих различиях.
Рис. 6. Словари и маркеры
Четыре словаря на рисунке выбраны для наглядности и не вполне соответствуют реально существующим. Эти словари содержат подходящие для подмеса факты, словарь с историческими событиями, словарь с фильмами и словарь с географическими объектами. Все слова и словосочетания запроса прогоняются через эти словари, в результате чего находятся следующие фрагменты:
● Пираты Карибского моря тэгируется как фильм, внутри которого находится какая-то география
● глубина Карибского моря – это некий объективный факт
● Последствия Карибского кризиса содержит маркер факта, о чем свидетельствует слово «последствия», и исторического события – Карибский кризис.
Рис. 7. Словари и маркеры: промежуточный результат
Последним этапом мы удаляем включенный в фильм географический объект, чтобы он не мешал выборке. Необходимость такого удаления также настраивается синтаксисом в словаре. Это называется жадность маркеров: как правило (но далеко не всегда), большое количество маркеров и включение одного в другой означает, что внутренний маркер нужно игнорировать.
Рис. 8. Словари и маркеры: итог
Виды словарей
Содержимое словарей делится на два больших класса:
● Частотные выражения-маркеры, характеризующие определенную тематику или намерение пользователя. Такие словари составляются вручную.
● Объекты (фильмы, рецепты, блюда, знаменитые личности, географические названия и т.д.).
С точки зрения наполнения словарей объекты представляют наибольший интерес. Здесь существует несколько подходов. Первый – это, опять же, составлять их вручную. Это самый качественный способ получения данных, но и самый трудоемкий.
Автоматическое создание словаря объектов – тема для отдельного поста, но если описать вкратце, то это замысловатый набор простых алгоритмов, который на основе небольшой обучающей выборки объектов заданного типа выбирает из лога все запросы, содержащие объекты того же типа, при этом разбивая каждый из этих запросов на объект и его контекст.
Необходимо отметить, что многие запросы неоднозначны: например, по запросу «скачать хоббит» совершенно невозможно сказать, что именно хочет скачать пользователь — фильм, мульфильм, книгу или игру.
Конечным этапом работы парсера запросов является передача полученной информации ранжированию. Перед этим происходит пересечение данных о запросе и о структуре и данных из индекса. Если вкратце, то для каждого слова запроса из обратного индекса извлекаются списки документов, которые затем пересекаются по правилам, заданным структурой дерева запроса. Сложность заключается в том, что у всех слов разные веса, некоторые слова могут быть сочтены необязательными, имеет значение порядок слов, их формы в запросе, расстояния между ними и другие свойства. Все полученные данные вместе со всеми флагами запроса передаются ранжированию, где становятся частью множества факторов, определяющих окончательный результат поиска.
Рис. 9. Дерево запроса: авиабилеты москва тель-авив
Цитатные запросы
Цитатные запросы представляют собой пример более сложной классификации, которую невозможно качественно сделать на основе одних словарей. Изначально при обработке цитат просто анализировался лог запроса: выявлялось, какие там есть цитаты, чем они отличаются. Основные, самые очевидные признаки цитатного запроса – это большая длина, грамматическая согласованность слов, наличие заглавных букв и знаков препинания. На основе этих признаков был реализован первый агент, который, используя вручную подобранные коэффициенты и примитивную логику if-then-else, рассчитывал вероятность того, что запрос является цитатой.
Однако когда факторов становится много, и оценивать влияние каждого из них вручную уже невозможно и даже вредно, применяются различные алгоритмы машинного включения. При выявлении цитат используется тот же код, который применяется в ранжировании. Реакцией на цитатный запрос является не только влияние этого фактора на ранжирование, но и изменение алгоритма склейки похожих документов. При ложном срабатывании классификатора, следствием чего оказывается неудачный поиск в целом, приходится делать перезапрос, в котором запрос уже более не считается цитатным. Таким образом, парсер влияет и на алгоритм поиска в целом.
Рис. 10. Цитатные запросы
Для машинного обучения применяются деревья принятия решений и методы, основанные на Марковских моделях. Более сложные методы непозволительно дороги для real-time. Поэтому кластеризация запросов или выделение объектов выносится в предварительную обработку, а не делается на лету.
To be continued
О навигационных запросах я расскажу отдельно — ждите продолжения в ближайшие дни. Кроме того, в следующих сериях я подробно остановлюсь на спеллчекере и саджестах.
Если у вас есть вопросы или полезный опыт, которым вы хотели бы поделиться, добро пожаловать в комментарии.
руководитель группы обработки поисковых запросов
Парсинг разными способами максимум результатов из индекса поисковых систем | A-Parser
Парсить максимум страниц сайта из индекса Google можно несколькими способами. Рассмотрим несколько методов:- Парсим только максимальное кол-во страниц и результатов на странице.
- Парсим все результаты через отдельную опцию “Спарсить все результаты”.
- Парсим максимальное кол-во результатов через макросы подстановок.
1. Максимальное кол-во страниц и результатов на страницах выдачи
1. Выбираем парсер SE::Google::Modern
2. Для того чтобы по запросу получить положительный ответ увеличиваем кол-во повторных обращений по нему до 100.
3. Добавляем опции Pages count (кол-во страниц в поисковой выдаче) и Links per page (кол-во результатов по запросу на странице поисковой выдачи). Устанавливаем максимальное кол-во страниц (10) и результатов на странице (100).
4. Вводим запрос в поле “Введите запрос”, например — “парсер site:a-parser.com”.
5. Добавляем фильтр для того чтобы не выводить страницы, которые не содержат нужной информации по запросу. После тестового парсинга, в результате видим большое кол-во таких ссылок:
так как нам эти ссылки в результатах не нужны, то при помощи фильтров будем пропускать эти ссылки.
Выбираем переменную результата, для которой применять фильтр, далее выбираем тип условия и указываем значение в поле “Строка”. Устанавливаем параметр чувствительности к регистру. Добавляем еще 1 фильтр и в нем выбираем тип условия “Регулярка совпадает”.
6. Формат результата и формат запроса оставляем по умолчанию
7.В итоге получаем максимальный количество результатов по одному запросу. Кол-во ссылок получилось 241.
Но Google говорит, что по этому запросу в выдаче значительно больше результатов:
Возникает вопрос: как получить их все? И об этом ниже.
2. Парсим все результаты опцией “Спарсить все результаты”
Повторяем пункты 1 — 6 из предыдущего парсинга.
7. Добавляем опцию “Спарсить все результаты”. Данная опция работает с подстановкой символов от a до z (английского алфавита), подстановка происходит в конце запроса. При этом количество подстановок подбирается автоматически, в зависимости от количества результатов в индексе по начальному запросу.
Вот реальные значения соотношения кол-ва результатов в индексе и макроса подстановок. Чем больше результатов в индексе, тем больше подстановок.
1000..30000 — {az:a:z}
30000..1000000 — {az:aa:zz}
>= 1000000 — {az:aaa:zzz}
8. Включаем опцию “Уникальность по строке” в блоке “Результаты”. Данная опция оставляет в результатах только уникальные строки, повторение строк отсеивает.
9. В итоге получаем максимальный результат по запросу “парсер site:a-parser.com” в индексе Google при помощи опции “Спарсить все”. Только теперь кол-ва результатов из выдачи поисковой системы удалось собрать в 5,37 раза больше чем в предыдущем парсинге. Кол-во ссылок получилось — 1295.
Но количество собранных результатов по-прежнему меньше, чем указано в выдаче Гугла. Поэтому можно вручную еще больше размножить запросы.
3. Парсим максимально возможное кол-во результатов через макросы подстановок
Повторяем пункты 1 — 6 из первого парсинга.
7. Добавляем в “Формат запроса” макрос подстановки, например
9. Включаем опцию “Уникальность по строке” в блоке “Результаты”.
10. В итоге получаем результат с кол-вом ссылок — 8873. Что в 36,81 раз больше чем в первом результате, и в 6,85 раз больше чем во втором результате.
Теперь кол-во собранных результатов даже больше, чем указано в выдаче Google. Это нормально, т.к. Google никогда не показывает реальное количество результатов, а собранный результат только подтверждает это.
В этой статье мы рассмотрели 3 способа парсинга ссылок из поисковой выдачи Google. Эти методы в целом применимы и для других поисковых систем. Благодаря им можно собирать максимум результатов, используя минимум запросов.
простой парсинг сложных сайтов / Habr
Каждый, кто пишет парсеры, знает, что можно распарсить сто сайтов, а на сто-первом застрять на несколько дней. Структура очередного отмороженного сайта может быть сколь угодно сложной, и, когда дело касается сжатых javascript-ов и ajax-запросов, расшифровать их и извлечь информацию с помощью обычного curl-а и регекспов становится дороже самой информации.Грубо говоря, проблема в том, что в браузере работает javascript, а на сервере его нет. Нужно либо писать интерпретатор js на одном из серверных языков (jParser и jTokenizer), либо ставить на сервер браузер, посылать в него запросы и вытаскивать итоговое dom-дерево.
В древности в таких случаях мы строили свой велосипед: на отдельной машине запускали браузер, в нем js, который постоянно стучался на сервер и получал от него задания (джобы), сам сайт грузился в iframe, а скрипт извне отправлял dom-дерево ифрейма обратно на сервер.
Сейчас появились более продвинутые средства — xulrunner (crowbar) и watir. Первый — безголовый firefox. У crowbar есть даже ff-плагин для визуального выделения нужных данных, который генерит специальный парсер-js-код, однако там не поддерживаются cookies, а допиливать неохота. Watir позиционируется разработчиками как средство отладки, но мы будем его использовать по прямому назначению и в качестве примера вытащим какие-нибудь данные с сайта travelocity.com.
Watir — это ruby gem, через который идет взаимодействие с браузером. Есть версии для разных платформ — watir, firewatir и safariwatir. Несмотря на подробный мануал по установке, у меня возникли проблемы как в винде, так и в убунте. В windows (ie6) watir не работает на ruby 1.9.1. Пришлось поставить версию 1.8.6, тогда заработало. В убунте — для того, чтобы работал FireWatir (или обычный watir через firefox), в браузер нужно поставить плагин jssh. Но версия, предлагаемая для FireWatir на странице установки не заработала с моим FireFox 3.6 на Ubuntu 10.04.
Чтобы проверить, работает у вас jssh или нет, нужно запустить firefox -jssh
, а потом послать что-нибудь на 9997 порт (telnet localhost 9997
). Если порт не открывается, либо происходит аварийное завершение работы firefox (как у меня), значит нужно собрать свой jssh, подробная инструкция о сборке находится здесь.
Начнем писать парсер отелей с travelocity.com. Для примера выберем цены комнат во всех отелях по направлению New York, NY, USA на сегодня. Будем работать с FireWatir на Ubuntu 10.4.
Запускаем браузер и грузим страницу с формой:
require "rubygems"<br>require "firewatir"<br>ff = FireWatir::Firefox.new<br>ff.goto("http://www.travelocity.com/Hotels")<br>
Заполняем форму нужными значениями и делаем submit:
ff.text_field(:id,"HO_to").val("New York, NY, USA")<br>ff.text_field(:id,"HO_fromdate").val(Time.now.strftime("%m/%d/%Y"))<br>ff.text_field(:id,"HO_todate").val(Time.tomorrow.strftime("%m/%d/%Y"))<br>ff.form(:name,"formHO").submit<br>
Ждем окончания загрузки:
ff.wait_until{ff.div(:id,"resultsList").div(:class,"module").exists?}<br>
wait_until — очень важная инструкция. При сабмите формы на сайте делается несколько редиректов, а после — ajax запрос. Нужно дождаться финальной загрузки страницы, и только ПОСЛЕ этого работать с dom-деревом. Как узнать, что страница загрузилась? Нужно посмотреть, какие элементы появляются на странице после выполнения ajax. В нашем случае после запроса к /pub/gwt/hotel/esf/hotelresultlist.gwt-rpc в resultsPage появляется несколько элементов <div>
. Ждем, пока они не появятся. Замечу, что некоторые команды, например text_field, submit, уже включают в себя wait_until, поэтому перед ними данная команда не нужна.
Теперь делаем переход по страницам:
while true do<br> ff.wait_until{ff.div(:id,"resultsList").div(:class,"module").exists?}<br> ...<br> next_link = ff.div(:id,"resultcontrol-top").link(:text,"Next")<br> if (next_link.exists?) then next_link.click else break end<br>end<br>
Там, где в коде стоит многоточие, находится непосредственное вытаскивание данных. Возникает искушение применить watir и в этом случае, к примеру, пробежать по всем дивам в resultsList такой командой:
ff.div(:id,"resultsList").divs.each.do |div|<br> if (div.class_name != "module") then next end<br> ...<br>end<br>
И из каждого дива вытащить название отеля и цену:
m = div.h3(:class,"property-name").html.match(/propertyId=(\d+)[^<>]*>([^<>]*)<\/a[^<>]*>/)<br>data["id"] = m[1] unless m.nil?<br>data["name"] = m[2] unless m.nil?<br>data["price"] = div.h4(:class,"price").text<br>
Но так делать не следует. Каждая команда watir-а к элементам dom-дерева — это лишний запрос к браузеру. У меня работает около секунды. Гораздо эффективнее за ту же секунду за раз выдернуть весь dom и мгновенно распарсить обычными регулярками:
ff.div(:id,"resultsList").html.split(/<div[^<>]*class\s*=\s*["']?module["']?[^<>]*>/).each do |str|<br>m = str.match(/<a[^<>]*propertyId=(\d+)[^<>]*>([\s\S]*?)<\/a[^<>]*>/)<br> data["id"] = m[1] unless m.nil?<br> data["name"] = m[2] unless m.nil?<br> m = str.match(/<h4[^<>]*class\s*=\s*["']?price["']?[^<>]*>([\s\S]*?)<\/h4[^<>]*>/)<br> data["price"] = m[1] unless m.nil?<br>end<br>
Советую применять watir только там, где это необходимо. Заполнение и сабмит форм, ожидание, пока браузер не выполнит js код, и затем — получение финального html-кода. Да, доступ к значениям элементов через watir кажется надежнее, чем парсинг потока кода без dom-структуры. Чтобы вытащить внутренность некоторого дива, внутри которого могут быть другие дивы, нужно написать сложночитаемое регулярное выражение. Но все равно это гораздо быстрее. Если таких дивов много, самое простое решение — несложной рекурсивной функцией разбить весь код по уровням вложенности тегов. Я писал такую штуку в одном своем классе на php.
Web Scraping с помощью python / Habr
Введение
Недавно заглянув на КиноПоиск, я обнаружила, что за долгие годы успела оставить более 1000 оценок и подумала, что было бы интересно поисследовать эти данные подробнее: менялись ли мои вкусы в кино с течением времени? есть ли годовая/недельная сезонность в активности? коррелируют ли мои оценки с рейтингом КиноПоиска, IMDb или кинокритиков?
Но прежде чем анализировать и строить красивые графики, нужно получить данные. К сожалению, многие сервисы (и КиноПоиск не исключение) не имеют публичного API, так что, приходится засучить рукава и парсить html-страницы. Именно о том, как скачать и распарсить web-cайт, я и хочу рассказать в этой статье.
В первую очередь статья предназначена для тех, кто всегда хотел разобраться с Web Scrapping, но не доходили руки или не знал с чего начать.
Off-topic: к слову, Новый Кинопоиск под капотом использует запросы, которые возвращают данные об оценках в виде JSON, так что, задача могла быть решена и другим путем.
Задача
Задача будет состоять в том, чтобы выгрузить данные о просмотренных фильмах на КиноПоиске: название фильма (русское, английское), дату и время просмотра, оценку пользователя.
На самом деле, можно разбить работу на 2 этапа:
- Этап 1: выгрузить и сохранить html-страницы
- Этап 2: распарсить html в удобный для дальнейшего анализа формат (csv, json, pandas dataframe etc.)
Инструменты
Для отправки http-запросов есть немало python-библиотек, наиболее известные urllib/urllib2 и Requests. На мой вкус Requests удобнее и лаконичнее, так что, буду использовать ее.
Также необходимо выбрать библиотеку для парсинга html, небольшой research дает следующие варианты:
- re
Регулярные выражения, конечно, нам пригодятся, но использовать только их, на мой взгляд, слишком хардкорный путь, и они немного не для этого. Были придуманы более удобные инструменты для разбора html, так что перейдем к ним. - BeatifulSoup, lxml
Это две наиболее популярные библиотеки для парсинга html и выбор одной из них, скорее, обусловлен личными предпочтениями. Более того, эти библиотеки тесно переплелись: BeautifulSoup стал использовать lxml в качестве внутреннего парсера для ускорения, а в lxml был добавлен модуль soupparser. Подробнее про плюсы и минусы этих библиотек можно почитать в обсуждении. Для сравнения подходов я буду парсить данные с помощью BeautifulSoup и используя XPath селекторы в модуле lxml.html. - scrapy
Это уже не просто библиотека, а целый open-source framework для получения данных с веб-страниц. В нем есть множество полезных функций: асинхронные запросы, возможность использовать XPath и CSS селекторы для обработки данных, удобная работа с кодировками и многое другое (подробнее можно почитать тут). Если бы моя задача была не разовой выгрузкой, а production процессом, то я бы выбрала его. В текущей постановке это overkill.
Загрузка данных
Первая попытка
Приступим к выгрузке данных. Для начала, попробуем просто получить страницу по url и сохранить в локальный файл.
import requests
user_id = 12345
url = 'http://www.kinopoisk.ru/user/%d/votes/list/ord/date/page/2/#list' % (user_id) # url для второй страницы
r = requests.get(url)
with open('test.html', 'w') as output_file:
output_file.write(r.text.encode('cp1251'))
Открываем полученный файл и видим, что все не так просто: сайт распознал в нас робота и не спешит показывать данные.
Разберемся, как работает браузер
Однако, у браузера отлично получается получать информацию с сайта. Посмотрим, как именно он отправляет запрос. Для этого воспользуемся панелью «Сеть» в «Инструментах разработчика» в браузере (я использую для этого Firebug), обычно нужный нам запрос — самый продолжительный.
Как мы видим, браузер также передает в headers UserAgent, cookie и еще ряд параметров. Для начала попробуем просто передать в header корректный UserAgent.
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:45.0) Gecko/20100101 Firefox/45.0'
}
r = requests.get(url, headers = headers)
На этот раз все получилось, теперь нам отдаются нужные данные. Стоит отметить, что иногда сайт также проверяет корректность cookie, в таком случае помогут sessions в библиотеке Requests.
Скачаем все оценки
Теперь мы умеем сохранять одну страницу с оценками. Но обычно у пользователя достаточно много оценок и нужно проитерироваться по всем страницам. Интересующий нас номер страницы легко передать непосредственно в url. Остается только вопрос: «Как понять сколько всего страниц с оценками?» Я решила эту проблему следующим образом: если указать слишком большой номер страницы, то нам вернется вот такая страница без таблицы с фильмами. Таким образом мы можем итерироваться по страницам до тех, пор пока находится блок с оценками фильмов (
<div class = "profileFilmsList">
).import requests
# establishing session
s = requests.Session()
s.headers.update({
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:45.0) Gecko/20100101 Firefox/45.0'
})
def load_user_data(user_id, page, session):
url = 'http://www.kinopoisk.ru/user/%d/votes/list/ord/date/page/%d/#list' % (user_id, page)
request = session.get(url)
return request.text
def contain_movies_data(text):
soup = BeautifulSoup(text)
film_list = soup.find('div', {'class': 'profileFilmsList'})
return film_list is not None
# loading files
page = 1
while True:
data = load_user_data(user_id, page, s)
if contain_movies_data(data):
with open('./page_%d.html' % (page), 'w') as output_file:
output_file.write(data.encode('cp1251'))
page += 1
else:
break
Парсинг
Немного про XPath
XPath — это язык запросов к xml и xhtml документов. Мы будем использовать XPath селекторы при работе с библиотекой lxml (документация). Рассмотрим небольшой пример работы с XPath
from lxml import html
test = '''
<html>
<body>
<div>
<h3 align='center'>one</h3>
<h3 align='left'>two</h3>
</div>
<h3>another tag</h3>
</body>
</html>
'''
tree = html.fromstring(test)
tree.xpath('//h3') # все h3 теги
tree.xpath('//h3[@align]') # h3 теги с атрибутом align
tree.xpath('//h3[@align="center"]') # h3 теги с атрибутом align равным "center"
div_node = tree.xpath('//div')[0] # div тег
div_node.xpath('.//h3') # все h3 теги, которые являются дочерними div ноде
Подробнее про синтаксис XPath также можно почитать на W3Schools.
Вернемся к нашей задаче
Теперь перейдем непосредственно к получению данных из html. Проще всего понять как устроена html-страница используя функцию «Инспектировать элемент» в браузере. В данном случае все довольно просто: вся таблица с оценками заключена в теге
<div class = "profileFilmsList">
. Выделим эту ноду:from bs4 import BeautifulSoup
from lxml import html
# Beautiful Soup
soup = BeautifulSoup(text)
film_list = soup.find('div', {'class': 'profileFilmsList'})
# lxml
tree = html.fromstring(text)
film_list_lxml = tree.xpath('//div[@class = "profileFilmsList"]')[0]
Каждый фильм представлен как
<div class = "item">
или <div class = "item even">
. Рассмотрим, как вытащить русское название фильма и ссылку на страницу фильма (также узнаем, как получить текст и значение атрибута).# Beatiful Soup
movie_link = item.find('div', {'class': 'nameRus'}).find('a').get('href')
movie_desc = item.find('div', {'class': 'nameRus'}).find('a').text
# lxml
movie_link = item_lxml.xpath('.//div[@class = "nameRus"]/a/@href')[0]
movie_desc = item_lxml.xpath('.//div[@class = "nameRus"]/a/text()')[0]
Еще небольшой хинт для debug’a: для того, чтобы посмотреть, что внутри выбранной ноды в BeautifulSoup можно просто распечатать ее, а в lxml воспользоваться функцией
tostring()
модуля etree.# BeatifulSoup
print item
#lxml
from lxml import etree
print etree.tostring(item_lxml)
Полный код для парсинга html-файлов под катом
def read_file(filename):
with open(filename) as input_file:
text = input_file.read()
return text
def parse_user_datafile_bs(filename):
results = []
text = read_file(filename)
soup = BeautifulSoup(text)
film_list = film_list = soup.find('div', {'class': 'profileFilmsList'})
items = film_list.find_all('div', {'class': ['item', 'item even']})
for item in items:
# getting movie_id
movie_link = item.find('div', {'class': 'nameRus'}).find('a').get('href')
movie_desc = item.find('div', {'class': 'nameRus'}).find('a').text
movie_id = re.findall('\d+', movie_link)[0]
# getting english name
name_eng = item.find('div', {'class': 'nameEng'}).text
#getting watch time
watch_datetime = item.find('div', {'class': 'date'}).text
date_watched, time_watched = re.match('(\d{2}\.\d{2}\.\d{4}), (\d{2}:\d{2})', watch_datetime).groups()
# getting user rating
user_rating = item.find('div', {'class': 'vote'}).text
if user_rating:
user_rating = int(user_rating)
results.append({
'movie_id': movie_id,
'name_eng': name_eng,
'date_watched': date_watched,
'time_watched': time_watched,
'user_rating': user_rating,
'movie_desc': movie_desc
})
return results
def parse_user_datafile_lxml(filename):
results = []
text = read_file(filename)
tree = html.fromstring(text)
film_list_lxml = tree.xpath('//div[@class = "profileFilmsList"]')[0]
items_lxml = film_list_lxml.xpath('//div[@class = "item even" or @class = "item"]')
for item_lxml in items_lxml:
# getting movie id
movie_link = item_lxml.xpath('.//div[@class = "nameRus"]/a/@href')[0]
movie_desc = item_lxml.xpath('.//div[@class = "nameRus"]/a/text()')[0]
movie_id = re.findall('\d+', movie_link)[0]
# getting english name
name_eng = item_lxml.xpath('.//div[@class = "nameEng"]/text()')[0]
# getting watch time
watch_datetime = item_lxml.xpath('.//div[@class = "date"]/text()')[0]
date_watched, time_watched = re.match('(\d{2}\.\d{2}\.\d{4}), (\d{2}:\d{2})', watch_datetime).groups()
# getting user rating
user_rating = item_lxml.xpath('.//div[@class = "vote"]/text()')
if user_rating:
user_rating = int(user_rating[0])
results.append({
'movie_id': movie_id,
'name_eng': name_eng,
'date_watched': date_watched,
'time_watched': time_watched,
'user_rating': user_rating,
'movie_desc': movie_desc
})
return results
Резюме
В результате, мы научились парсить web-сайты, познакомились с библиотеками Requests, BeautifulSoup и lxml, а также получили пригодные для дальнейшего анализа данные о просмотренных фильмах на КиноПоиске.
Полный код проекта можно найти на github’e.
UPD
Как отметили в комментариях, в контексте Web Scrapping’a могут оказаться полезны следующие темы:
- Аутентификация: зачастую для того, чтобы получить данные с сайта нужно пройти аутентификацию, в простейшем случае это просто HTTP Basic Auth: логин и пароль. Тут нам снова поможет библиотека Requests. Кроме того, широко распространена oauth3: как использовать oauth3 в python можно почитать на stackoverflow. Также в комментариях есть пример от Terras того, как пройти аутентификацию в web-форме.
- Контролы: На сайте также могут быть дополнительные web-формы (выпадающие списки, check box’ы итд). Алгоритм работы с ними примерно тот же: смотрим, что посылает браузер и отправляем эти же параметры как data в POST-запрос (Requests, stackoverflow). Также могу порекомендовать посмотреть 2й урок курса «Data Wrangling» на Udacity, где подробно рассмотрен пример scrapping сайта US Department of Transportation и посылка данных web-форм.
Конструктор запросов | A-Parser — парсер для профессионалов SEO
Конструктор запросов — позволяет разделять исходный запрос на части и\или преобразовывать запросы по определенным правилам
Возможности(top)
- Разделение запроса на части с помощью регулярного выражения или с помощью произвольного разделителя
- Замена подстроки в запросе или замена регулярным выражением
- Выделение домена\главного домена из ссылки
- Добавление знака ! перед каждым словом в запросе(необходимо для парсера
SE::Yandex::WordStat)
- Приведение запроса к верхнему\нижнему регистру
Полученные новые запросы можно использовать в Формате запроса при формировании запроса к парсеру и в Формате результата при формировании результата
Дополнительно:
- Если создать запрос $proxy то в текущем запросе будет использован именно этот прокси в обход Proxy Checker. Прокси должен быть передан в формате http://ip:port или socks://ip:port
Основные варианты использования(top)
- Выделение домена из ссылки для последующей проверки домена в
SE::Google::pR и подобных парсерах
- Использование в запросе только части исходной строки, например мы имеем файл запросов:
Мы можем разделить кейворд и уже имеющиеся данные о частотности кейворда, проверить кейворд на конкурентность и записать в файл результат в формате keyword;частотность;конкурентностьkeyword1;100000
keyword2;200000Нажмите, чтобы раскрыть…
- Автоматическая подстановка символа ! перед каждым словом в запросе для парсера
SE::Yandex::WordStat
Пример использования(top)
Проверка на PR домена и сохранение дополнительный информации из запроса
Предположим у нас есть файл с запросами в формате ссылка на страницу и через пробел описание:
Нам необходимо проверить Google PageRank домена и сохранить в результат исходную ссылку, домен, PR и исходное описание, задание будет выглядеть так:http://gofuckbiz.com/showthread.php?t=30454&page=11 Форум GoFuckBiz.com
http://forum.searchengines.ru/showthread.php?t=720568&page=10 Форум SearchEngines.ruНажмите, чтобы раскрыть…
В результате мы получим:
http://gofuckbiz.com/showthread.php?t=30454&page=11 PageRank of gofuckbiz.com: 2, Description: Форум GoFuckBiz.com
http://forum.searchengines.ru/showthread.php?t=720568&page=10 PageRank of forum.searchengines.ru: 5, Description: Форум SearchEngines.ruНажмите, чтобы раскрыть…
Группировка ключевых слов и запросов, структуризация и кластеризация семантического ядра

Автоматическая группировка ключевых слов
Группировка большого количества запросов на посадочные страницы превращается в головную боль, если заниматься этим вручную. Теперь есть аспирин — SEMParser!

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

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

Генерация ТЗ для копирайтера
Сервис определит SEO параметры текстов самых видимых конкуретнов по каждому запросу/группе запросов, предоставит отчет, поможет создать грамотное ТЗ копирайтеру с учетом LSI и быстро проверить полученный текст.

Проверка текстов на соответствие ТОПу по SEO параметрам
Инструмент «Проверка текста» разберет ваши тексты, проверит их на соответствие ТОПу по вхождениям ключевых фраз, встречаемости n-грамм и отдельных слов, а также выделит красным слова, которые не свойственны тематике запросов.

Определение «коммерческости» запросов
К продвижению разных типов запросов нужно подходить по-разному. SEMparser определит, является ли запрос коммерческим с точки зрения поисковой системы.

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

Определение геозависимости запросов
Геозависимость запроса является важным параметром, который стоит учитывать при продвижении. Мы отдаем полностью достоверные данные, взятые напрямую из поисковой системы.

Определение релевантных страниц
SEMparser определит релевантные с точки зрения поисковой системы страницы на вашем сайте для каждого запроса и для каждой группы запросов.