11 декабря 2010 в 16:40

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

Ruby*
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.
Алексей Кузнецов @Kasheftin
карма
39,0
рейтинг 0,0
Пользователь
Похожие публикации
Самое читаемое Разработка

Комментарии (74)

  • –35
    Как вы заебали, ловцы готового, парсеры-шмайсеры. Я понимаю, что это может применяться и в аналитике. Но сиди и анализируй, с хера выкладывать в паблик инструменты для мошенников-пионеров?
    «В древности в таких случаях мы строили свой велосипед» в древности, блять, стрелялись из-за воровства.
    • –7
      Следуя вашей логике (элементарный пример):
      Чтобы узнавать курс доллара вы предлагаете отправлять своих посетителей на сайт ЦБ РФ или предлагаете им отправить SMS на номер *** с текстом "?USD"? :)
      • +4
        Для общих вещей всегда есть стандартные методы выгрузки. Как для валют: export.rbc.ru/.
        Я тоже против статьи в таком виде, нужно описывать приемы программирования, а не стимулировать воровство контента.
        • +2
          Не всегда. Найдите стандартный метод выгрузки курсов валют для национального банка Украины.
          • –9
            Украины… Вы бы еще про Замбию спросили.
            • +1
              А на Украину и Замбию проблемы программирования не распространяются?
        • 0
          В некоторых случаях это не воровство, а распространение контента (как привели ранее про курсы валют)

          можно и другие плюсы найти…
          пример моего сотрудника: чел захотел купить машину именно марки М и до суммы С — он написал парсер нескольких сайтов с объявлениями и постоянно получал свежую информацию о ценах интересующей его модели… — чем плохо?

          Статья полезная! Спасибо автору!
    • –3
      оо да, набежали хомячки-борцы за справедливость, половина небось сидит на пиратской винде и юзает пиратский софт, но спарсить несколько кб байт данных это преступление века.
      • +2
        У меня лицензионная Windows и купленный софт. У меня есть сайт. Написано over 900 уникальных авторских материалов. У меня крадут. Я тоже против.
        Технически я понимаю как могу защитится, но это будет во вред пользователям, скорости и поисковому продвижению. У автора комментария, вероятно, есть конкуренты. У вас, видимо, нет. Рад за вас.
        • +2
          Автор не предлагает воровать, он предлагает парсить.

          Если у вас сервис уникальный, а у него воруют весь его функционал, то:
          1) можно настучать поисковикам
          2) несложно защититься. (что пойдет на пользу здоровью ресурса)

          Если информацию с ресурса парсят с другой целью (недвижимость, тендеры, объявления, новости и т.д.), то что в этом плохого? Предпочитаете пользоваться каталогами для поиска информации? Или перебирать 30 страниц выдачи, с целью найти искомое? Гугль с яшей ничем не отличаются от ваших «злодеев». Они, суки вообще только и делают, что парсят, и ничяго, им еще за это и приплачивают.

          Ваш ресурс кому полезен? Гуглу, людям или вам? Почему википедия не волнуется за свой контент? У них больше 900 по тыщи знаков

          Пардонте за моветон, веткой камментов навеяно.

          P.S. Я не предлагаю удариться в альтуризм, или кричать «Information must be free». Просто, как мне кажется, хороший контент своего хозяина не потеряет. Так или иначе.
          • 0
            Википедия не волнуется за свой контент, потому что она на нем не зарабатывает.

            Ваш комментарий из цикла «Почему в банке охрана, вот я был в супермаркете, там охраны нет».
            • +2
              1) т.е. если я выложу статью, и не поставлю баннера — я могу быть спокоен? )))))
              2) в супермаркете охраны не меньше, чем в банках, а в крупных — больше.
              3) вы каммент читали (весь?)
          • 0
            Насчет настучать поисковикам — не понимаю, причем здесь эта статья. Любой контент, который находится или может находится в индексе поисковиков, в 10 раз легче парсится без всякого ватира. Поисковики не разбирают js, ajax, поэтому там обычный html. Ватир нужен, когда парсятся специальные страницы со сложными динамическими данными. Статистики, цены. Настолько динамические, что поисковикам нет смысла их рассматривать.
            • 0
              гм. Я отвечал на комментарий, и про стучание поисковикам именно в этом ключе упомянул. Так сказать боролся с ветряными мельницами )))

              С ватиром все понятно, и большое спасибо за пост. Из-за незнания, что такое mechanize потерял два дня, и еще два-три часа, чтобы под него переделать граббер. Теперь и про ватир знаю. Спасибо.

              P.S. Надеюсь, что проблем с asp-шными viewstate у него нет )).
    • –1
      Ого, оказывается сколько тут тебя заминусовало. Мой ответ постигнет наверняка та же участь. Видимо у многих до сих пор считается нормальным воровать контент. Даже когда им дают удобные средства этого избежать. Их не пугают даже сложности, как говорит автор, парсинга «очередного отмороженного сайта».
    • +11
      Парсинг бывает мелким и безобидным, а бывает действительно убыточным для владельца контента.

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

      Но если нагло украден основной контент сайта, то тут, конечно, надо подавать с суд.

      Бывают и такие случаи, что и владелец не против, чтобы у него брали информацию, но ему в лом писать API.

      В общем, суть в том, что парсинг — это технология. Как и торренты. А как ее применять — это уже дело каждого.
    • +2
      На том же GameSpot есть нужный мне список десятков тысяч видео игр, но я же не вор, пожалуй бездарно потрачу несколько месяцев на добавление информации вручную, чтобы прийти к тому же результату!
    • 0
      Но, данная информация полезна не только ворующим, но и защищающимся.
    • –3
      че за бред пля, сгинь
      • 0
        Успокойся :)
  • +5
    Если преимущества у watir перед selenium?
    • 0
      Простите, читать: «Есть ли преимущества у watir перед selenium?»
      • +5
        так и прочитал :)
    • +1
      Я зашел сюда прочитать или написать этот комментарий.
    • 0
      coz this is RUBY
      • 0
        selenium тоже ruby
    • 0
      Мне кажется, что селениум мощнее, но объемнее.
    • 0
      из того что бросилось в глаза, и пожалуй самое стоящее — здесь не нужны xpath
    • 0
      практически нет. считается только что ватир больше заточен под ИЕ а селениум под фф
  • +2
    >>сам сайт грузился в iframe, а скрипт извне отправлял dom-дерево ифрейма обратно на сервер.
    можете это место подробнее написать? Вроде нельзя же доставать dom iframe-а
  • +3
    Замените регулярки на hpricot
    • +1
      nokogiri уж тогда
      • 0
        а какая разница?
        • 0
          nokogiri быстрее раз в 10
  • +3
    Отличный инструмент, попробую в использовании.

    Только 1 совет, под конец, когда достали результаты, проще и удобнее будет использовать Nokogiri для парсинга.
    • 0
      теперь я знаю, как звучит японская пила, спасибо )
      • +2
        парсер nokogori:
        • 0
          Выглядит устрашающе, с таким нужно еще поучиться пользоваться :)
  • +3
    Несколько замечаний по коду чтобы приблизить его к ruby-way:

    while true do… end меняем на:
    loop do .. end


    # в строке
    if (next_link.exists?) then next_link.click else break end
    # лучше использовать тернарный оператор:
    next_link.exists? ? next_link.click : break


    # слишком избыточный однострочный if, и скобки лишние тут
    if (div.class_name != "module") then next end
    # вот так намного лучше:
    next  if div.class_name != "module"


    А еще блоки кода лучше заключать в тег
    <source lang="ruby"></source>
    • +1
      Большое спасибо. Руби совсем не знаю. Поставил его вместе с watir-ом чтобы изучить последний.
    • +2
      это как просить заменить if ($exists) {click()}else{break} на $exists?click():break;
      куда подевались сторонники читаемости кода? /ирония
      • 0
        ах да, наверное просто они такие топики не читают )
      • 0
        ну тут больше на любителя, так-то они оба правильные

        лично мне второй вариант больше нравится, зачем эти лишние скобки и if/else?
        • 0
          Тернарный оператор по смыслу больше подходит, когда важно возвращаемое значение (и в нем неожиданно будет увидеть код с побочным эффектом)

          if/else же не имеет возвращаемого значения, и в него уже стоит писать код с побочным эффектом.
          • 0
            В Ruby if-else имеет возвращаемое значение.

            >> a = if 5 > 3 then 2 else -2 end
            => 2
            • 0
              А, извиняюсь, в таком случае if/else эквивалентен ?:

              Но всё же использовать if в качестве замены ?: не вижу смысла — код это не сокращает, а читаемость снижает (так как в стандартных языках: C/C++/Java if/else таким образом не используют)
  • 0
    а есть подобное для Python?
    • 0
      Не знаю есть ли такое, но есть лучше. seleniumhq.org/
      • 0
        после опыта selenium+.net/perl и watir (на реальных задачах)
        мой выбор — watir причем ruby тут составляет важную часть
        очень КРАСИВАЯ связка, приятная в использовании
        и сильно менее многословная.

        ну а у watir есть кучу его реализаций, watin — .net, watij — java

        каждому свое.
      • 0
        У вас есть опыт работы с селениумумом?

        Расскажите, плиз, по нему след. моменты:

        1) можно ли задавать для каждого отдельного потока браузера свою проксю?
        2) можно ли там сохранять картинки?
        3) сколько примерно одновременно получается через селениум запускать ФФ?
    • +3
      использовал Scrapy
      scrapy.org/

      just for fun делал отчет кто и сколько по времени и в какие часы из моих друзей и закладок сидит вконтакте…
      кто хочет попробовать-поучиться, но не знает, что бы ему пропарсить — вот довольно занятная и смешная вещь) для хранения можно использовать nosql)

      местами даже полезная, если вдруг вы кого-то ищите из фрилансеров, которые на вас работают, а он прячется от вас и затягивает сроки — добавляете его вконтакте, ставите вот такого шпиона, и он вам показывает когда тот человек бывает, подкараулить его со статусом online и отругать становится проще) ну и в некоторых других случаях тоже помогает)

      потом было прикольно смотреть на графики… жаль, мне надоело и я забил на этот проект)
      также парсю сайтики типа entropiatracker.com и другие достаточно простые через крон раз в минуту)

      какие-то очень сложные вещи — даже не знаю, не пробовал… но я думаю, что особых проблем быть не должно…

      что касается использования парсеров для воровства контента — мне бы хотелось чтобы алгоритмы поисковиков были оптимизированы таким образом, чтобы этот процесс был невыгодным и риски попасть под штрафные санкции был бы выше прибыли от использования ворованного контента…

      однако парсеры как класс программ нужны и очень полезны для сбора интересных отчётов и решения сложных задач типа при добавлении какого-то объекта на наш сайт, добавить такие же объекты в базы других сайтов :-) не все сайты предоставляют API и не все хотят предоставлять это API…
  • 0
    а такая штука как mechanize уже не работает для парсинга данных? вроде она для этих целей и создавалась
    p.s. while true do тоже смутило, получается выйти из цикла не получится никогда…
    • 0
      Там break стоит в цикле
    • +1
      Mechanize не отрабатывает JS, в этом я и фишка у Watir, что запуск от браузера страницы со всеми делами.
  • +1
    возможно ли заюзать подобное в сборе отельных данных для anthil ;) с учетом большого количества запросов. не проверял на том глючном сайте с яваскриптом?
    • +1
      holidayinn.com? отлично парсится. можно было и его в качестве примера привести. а вот анты были больно дохлые, их можно было использовать разве что как прокси-сервера, без всякой обработки сливать все на антхил.
  • +3
    «You can't parse [X]HTML with regex…» bit.ly/866KiU
    • 0
      Забыли чут дальше процитировать:
      «Every time you attempt to parse HTML with regular expressions, the unholy child weeps the blood of virgins, and Russian hackers pwn your webapp».

      Хорошее у него сравнение :)
      • 0
        Там вообще весь пост классный, так что стоит сходить по ссылке, да.
      • 0
        и русские хакеры крадут ваш сайт? Че они так про нас…
        • +1
          Это к вопросу о штампах в сознании западного обывателя, далекого от политики.
          Вроде бы разумный человек, хорошо мыслит, начитан. А все равно где-то в глубине сознания сидит картинка типа «Downloading mp3, you are downloading communism»: то есть русские — это какие-то злобные монстры независимо от их политического строя.
          Любопытный сигнал — думаю, у нас касательно их не меньше штампов и стереотипов.
  • –3
    node.js?
  • 0
    Кстати, с Watir давно уже пора сделать нормальный anonym proxy сервис, а то у моего тайского провайдера WinBB половина сайтов закрыто министерством культуры и печати :(
  • +2
    >>Но так делать не следует. Каждая команда watir-а к элементам dom-дерева — это лишний запрос к браузеру.

    конечно, он будет тормозить, попробуйте такое

    require «rubygems»
    require «firewatir»

    ff = FireWatir::Firefox.new
    ff.goto(«www.travelocity.com/Hotels»)

    t_now = Time.now
    t_tomorrow=t_now + (60 * 60 * 24)

    ff.text_field(:id,«HO_to»).set(«New York, NY, USA»)
    ff.text_field(:id,«HO_fromdate»).set(t_now.strftime("%m/%d/%Y"))
    ff.text_field(:id,«HO_todate»).set(t_tomorrow.strftime("%m/%d/%Y"))
    ff.form(:name,«formHO»).submit

    do_f=true
    while do_f do
    ff.wait_until{ff.div(:id,«resultsList»).div(:class,«module»).exists?}

    puts «page: #{ff.div(:id,'resultcontrol-top').strong(:text,/\d+/).text}»

    ff.div(:id,«resultsList»).divs.each do |t|
    next if (t.class_name != «module»)
    name=t.h2(:class,'property-name').links[2].text
    price=t.h3(:class,'price')
    price=price.span(:class,'newprice') if price.span(:class,'newprice').exist?
    puts "#{name} -> #{price.text}"
    end

    if (ff.link(:class,'gwt-Anchor next').exist?)
    ff.link(:class,'gwt-Anchor next').click
    else
    do_f=false
    end

    end
  • 0
    Добавьте в пост ссылку «как откатить руби до 1.8.6»
    panthersoftware.com/articles/view/6/ruby-on-rails-development-on-ubuntu-9-04-jaunty-using-ruby-1-8-6
  • –1
    Подскажите а есть ли аналогичные средства для PHP? Вожможно ли приспособить для этого node.js?
  • 0
    Вы знаете как через watir сохранить картинку в файл?
    Через image.save() у меня не получается. Браузер открывает диалоговое окно для сохранения и на этом все прекращается. Как сделать чтобы он автоматом сохранял картинку?
    • 0
      ie = Watir::IE.new
      ie.goto «yandex.ru»
      ie.image(:src, 'http://img.yandex.net/i/www/logo.png').save(«d:\2.png»)

      — вот пример кода. Браузер вызывает диалог сохранения картинки, и там вбито в названии файла «logo», а не мой путь. И автоматом он картинку не сохраняет.
    • 0
      И еще пару вопросов по watir появилось :)

      1) Можно ли для запуска браузера указывать прокси?

      2) Сколько одновременно браузеров на среднем компьютере получается запускать через watir? И какой вариант для большей многопоточности оптимальнее: ie или firefox?
      • 0
        1. В настройках ff проставьте настройки соединения, будет вам через прокси.
        • 0
          а как сделать чтобы в 10 потоках одновременно было 10 разных проксей?
    • 0
      Я бы не рассматривал watir как панацею от всего. Распарсили url картинки — открыли сокет, закачали.
      • 0
        картинки бывают динамические, которые без правильных кук и прочих хедеров не скачать через сокет, надо качать их в сессии запроса страницы
        • 0
          Если вопрос про cookie, то с ними идет работа через ff.document.cookie. Естественно, их нужно добавить в запрос.
          С трудом могу представить систему, где картинки зашиты в js код и грузятся через какой-нибудь data: uri sprites. В остальных случаях это все же ссылки на файлы, и для их загрузки ватир не нужен.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.