Pull to refresh

Watir: простой парсинг сложных сайтов

Reading time4 min
Views51K
imageКаждый, кто пишет парсеры, знает, что можно распарсить сто сайтов, а на сто-первом застрять на несколько дней. Структура очередного отмороженного сайта может быть сколь угодно сложной, и, когда дело касается сжатых 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 class="module">. Ждем, пока они не появятся. Замечу, что некоторые команды, например 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.h2(: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.h3(: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(/<h3[^<>]*class\s*=\s*["']?price["']?[^<>]*>([\s\S]*?)<\/h3[^<>]*>/)<br> data["price"] = m[1] unless m.nil?<br>end<br>
Советую применять watir только там, где это необходимо. Заполнение и сабмит форм, ожидание, пока браузер не выполнит js код, и затем — получение финального html-кода. Да, доступ к значениям элементов через watir кажется надежнее, чем парсинг потока кода без dom-структуры. Чтобы вытащить внутренность некоторого дива, внутри которого могут быть другие дивы, нужно написать сложночитаемое регулярное выражение. Но все равно это гораздо быстрее. Если таких дивов много, самое простое решение — несложной рекурсивной функцией разбить весь код по уровням вложенности тегов. Я писал такую штуку в одном своем классе на php.
Tags:
Hubs:
+51
Comments74

Articles

Change theme settings