Как стать автором
Обновить

Комментарии 33

Уж слишком базовый пример: парсинг одной страницы без перехода по ссылкам. В реальной жизни такой usecase встречается не часто. Так что автор правильно рекомендует mechanize в конце статьи.
В ближайшее время планирую перевод следующей статьи этого автора, там рассказывается про использование mechanize.
Расскажите о решении каких сложностей вы хотели бы услышать. Возможно, смогу рассказать.
Самая большая сложность — сайты, на которых dom генерируется средствами javascript. Для таких целей пришлось использовать casperjs (который использует headless браузер phantomjs). А хотелось бы такие проблемы решать на ruby.
Мы используем PhantomJS из Ruby. С помощью Capybara. Наверное, это немного извращение, но работает.
Тоже использую Capybara, но с headless webkit (gem capybara-webkit).
Не помню, почему не PhantomJS не стал использовать.
selenium есть, который рубями управляется, но он имеет некоторое количество ограничений и неудобств, с фантомом проще работать
Переход по ссылкам — не самая лучшая идея, если вы собрались всерьез и надолго парсить сайт. Преимущественно вы знаете на какой странице находятся нужный данные и как можно сконструировать ссылку на нее. Каждый переход добавляет один HTTP запрос, а это не самое быстрое занятие. Возможно, если вы изучаете теорию алгоритмов, константы можно игнорировать, но в реальном мире миллион и два миллиона запросов — это две большие разницы. Конечно, если сайт очень хитрый и обфусцирует свои ссылки, ничего не поделаешь.
Не задумывался, вот тогда такой вопрос. Вот недавний пример из моей практики: сайт — каталог. Воск ссылки на две разные турбины из каталога: раз и два. Как видите, url состоит из:
  • Засекреченного номера, идентифицирующего марку автомобиля
  • oem артикул турбины
  • артикул турбины

Мне надо собрать информацию по всем турбинам на сайте. Я же правильно понимаю, что обход сайта по ссылкам — единственное решение?

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

Могу посоветовать также хороший цикл статей по этой теме, там пошире немного: ruby.bastardsbook.com/chapters/html-parsing/
от себя добавлю — механиз нужен в принципе только если на сайте есть логин. А так нокогири вполне себе «голый» отлично со всем справляется. А вот если нужно эмулировать браузер то да, механиз.
Не то, чтобы нужен, но с ним удобнее из-за наличия cookie jar. Залогиниться можно и с помощью Net::HTTP. Отправляем POST запрос с логином, паролем и, возможно, CSRF токеном; запоминаем возвращенный заголовок Set-Cookie. При этом, если нет CSRF, можно еще и сэкономить один запрос.
ну, если говорить о том что Вам хочется этот процесс оптимизировать — да, возможно такой подход во многих конкретных ситуациях оправдан. Но когда не хочется заморачиваться то 1 гем с простым интерфейсом самое то. Как в статье и продемонстрировано — простота. Создал обьект, и через простые методы обращаешься к нему а за кулисами он делает что-нибудь полезное и увы ресурсоемкое возможно.

По сему механиз вполне себе «когда не хочешь заморачиваться», особенно если скрапинг нужен на 1 фазе работы а не далее. Типа 1 раз срабатывание механиза, засунем в рельсы куда нибудь его в seeds.rb этот код или в миграцию, 1 раз отработаем начальное заполнение и норм )
Предлагаю ещё задуматься о nokogumbo, как о замене nokogiri.
А что это за PHP-style в первом куске кода из «Обработка html»?

Мне кажется, что имеет смысл сначала узнать про map-reduce, а потом писать статьи. А то не дай бог попадется джуниору какому — он так и будет людей кошмарить вот таким: `title_el.children.each { |c| c.remove if c.name == 'span' }` вместо `select`.
В защиту автора, статья — перевод. А на junior'ов есть code review. Как напишут так и перепишут.
К переводчику — вопросов нет, кроме «а почему бы не дать тут примечанием „как правильно“».

Потому, что программирование — это французская поэзия, а не математика. Здесь нет правильно и неправильно. Вот, например, как нравится мне (на коленке, я понимаю, что можно много где улучшить). Но я уверен, что мой подход не понравится сторонникам функционального программирования, например. Кстати, по время рефакторинга заметил, что `remove` автор делает потому, что ему нужно потом получить текст parent node без span'ов. Я заменил это на dup, чтобы вызов метода не разрушал состояние объекта.

class Showing
  attr_accessor :element

  def initialize(element)
    self.element = element
  end

  def id
    element['id'].split('_').last.to_i
  end

  def tags
    tag_links.map(&:text).map(&:strip)
  end

  def title
    title_link.dup.touch { |title| title.children.reject! { |c| c.name == 'span' } }.text.strip
  end

  def dates
    element.at_css('.start_and_pricing').inner_html.split('<br>').map(&:strip).map(&DateTime.method(:parse))
  end

  def description
    element.at_css('.copy').text.gsub('[more...]', '').strip
  end

  def to_h
    {
      id: id,
      title: title,
      tags: tags,
      dates: dates,
      description: description
    }
  end

  private

  def tag_links
    element.css('.tags a')
  end

  def title_link
    element.at_css('h1 a')
  end
end

showings = doc.css('.showing').map(&Showing.method(:initialize)).map(&:to_h)
def to_h
   Hash[%w(id title tags dates description).map { |w| [w.to_sym, "#{w}"] }]
end

:)
Спасибо, но я же не претендовал на идеальный код, только хотел показать объектно-ориентированный способ. Кстати, ваш код не заработает: вы нигде не вызываете метод.

  def to_h
    attributes = %i(id title tags dates description)
    attributes.zip(attributes.map(&method(:public_send))).to_h
  end
Ой, да, пардон. Да дело же не в идеальном коде, на самом деле. Дело в том, что в “руководствах” имеет смысл сразу демонстрировать более-менее приемлемые практики. Ваш код легко читаем, внятен и укладывается в принятые стили программирования в руби. Код из статьи — ужасен.
в php есть array_filter, который аналогичен рубишному .select, так что не могу назвать это php-style.
array_filter медленнее, чем foreach, почти вдвое.
спасибо. а можно пруфлинк?
А код на php медленнее чем на c++ и что?
В большинстве случаев более понятный код важнее чем производительность.
соглашусь
Так, да не так. Язык, как обычный разговорный, так и язык программирования, семантическими конструкциями провоцирует говорить/писать определенным образом. Тривиальный map-reduce, будучи записан с использованием php-синтаксиса, превращается в малочитаемую кашу.

Помимо производительности, разная нотация (`array_map` первым параметром хочет callback, `array_reduce` — уже наоборот), недоступность ключей без извращений типа `array_map(function($el) use($arr) { next($arr); ...` и прочие палки в колесах рано или поздно научат не выпендриваться и звать `foreach`.

Точно так же, как перевод Пруста или Манна на английский подчистую уничтожает авторский стиль.
То что вы написали верно, но именно задача фильтрации выглядит изящнее при array_filter, нежели foreach.

P.S. не хочу пост про прекрасный Ruby портить кодом на php, но можете сами написать реализацию для фильтрации по полю и посмотреть.
Только до тех пор, пока не нужен фильтр inplace. Или пока это не map-reduce. В общем, пока это фильтр из учебника.
Добавлю для страждущих github.com/chriskite/anemone — очень прелестный Ruby инструмент для обхода сайта.
Мы делаем на базе .net (sql, azure, asp.net) парсинг сайтов и мониторинг цен конкурентов xmldatafeed.com — помучались изрядно, но в итоге работает стабильно. Самое сложное — разбор html оказался.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.