18 марта 2015 в 17:52

Продвинутый парсинг веб-сайтов с Mechanize перевод tutorial

В продолжение темы парсинга сайтов на Ruby, я решил перевести следующую статью этого же автора.

В предыдущей записи я описал основы — введение в веб парсинг на Ruby. В конце поста, я упомянул инструмент Mechanize, который используется для продвинутого парсинга.

Данная статья объясняет как делать продвинутый парсинг веб-сайтов с использованием Mechanize, который, в свою очередь, позволяет делать отличную обработку HTML, работая над Nokogiri.

Парсинг обзоров с Pitchfork


Mechanize из коробки предоставляет инструменты, которые позволяют заполнять поля в формах, переходить по ссылкам и учитывать файл robots.txt. В данной записи, я покажу как это использовать для получения последних обзоров с сайта Pitchfork.

Парсить аккуратно
Вы всегда должны парсить аккуратно. Прочитайте статью Is scraping legal? из блога ScraperWiki для ознакомления с обсуждениями на эту тему.


Отзывы разделены на несколько страниц, поэтому, мы не можем просто взять одну страницу и разобрать её с помощью Nokogiri. Здесь то нам и понадобится Mechanize с его способностью кликать на ссылки и переходить по ним на другие страницы.

Установка


Вначале нужно установить сам Mechanize и его зависимости через Rubygems.

$ gem install mechanize


Можно приступить к написанию нашего парсера. Создадим файл scraper.rb и добавим в него некоторые require. Это укажет на зависимости, которые необходимы для нашего скрипта. date и json это части стандартной библиотеки ruby, так что дополнительно устанавливать их нет необходимости.

require 'mechanize'
require 'date'
require 'json'


Теперь мы можем начать использовать Mechanize. Первое, что нужно сделать, это создать новый экземпляр класса Mechanize (agent) и использовать его, чтобы скачать страницу (page).

agent = Mehanize.new
page  = agent.get("http://pitchfork.com/reviews/albums/")


Находим ссылки на обзоры


Теперь мы можем использовать объект page, чтобы найти ссылки на обзоры.
Mehanize позволяет использовать метод .links_with, который, как следует из названия, находит ссылки с указанными атрибутами. Здесь мы ищем ссылки, которые соответствуют регулярному выражению.

Это вернет массив ссылок, но нам нужны только ссылки на обзоры, не пагинация. Чтобы удалить ненужное мы можем вызвать .reject и отбросить ссылки, похожие на пагинацию.

review_links = page.links_with(href: %r{^/reviews/albums/\w+})

review_links = review_links.reject do |link|
  parent_classes = link.node.parent['class'].split
  parent_classes.any? { |p| %w[next-container page-number].include?(p) }
end


В показательных целях и чтобы не нагружать сервера Pitchfork, мы будем брать ссылки только на первые 4 обзора.

review_links = review_links[0...4]


Обработка каждого обзора


Мы получили список ссылок и хотим обработать каждую в отдельности, для этого мы будем использовать метод .map и возвращать хеш после каждой итерации.

Объект page имеет метод .search, который делегируется методу .search Nokogiri. Это означает, что мы можем использовать CSS селектор как аргумент для .serach и он вернет массив совпавших элементов.

Сначала мы возьмем метаданные обзора, используя CSS селектор #main .review-meta .info, а затем будем искать внутри review_meta элемента кусочки информации, которая нам нужна.

reviews = review_links.map do |link|
  review = link.click
  review_meta = review.search('#main .review-meta .info')
  artist = review_meta.search('h1')[0].text
  album = review_meta.search('h2')[0].text
  label, year = review_meta.search('h3')[0].text.split(';').map(&:strip)
  reviewer = review_meta.search('h4 address')[0].text
  review_date = Date.parse(review_meta.search('.pub-date')[0].text)
  score = review_meta.search('.score').text.to_f
  {
    artist: artist,
    album: album,
    label: label,
    year: year,
    reviewer: reviewer,
    review_date: review_date,
    score: score
  }
end


Теперь мы имеем массив хешей с обзорами, который мы можем, например, вывести в JSON формате.

puts JSON.pretty_generate(reviews)


Все вместе


Скрипт полностью:

require 'mechanize'
require 'date'
require 'json'

agent = Mechanize.new
page = agent.get("http://pitchfork.com/reviews/albums/")

review_links = page.links_with(href: %r{^/reviews/albums/\w+})

review_links = review_links.reject do |link|
  parent_classes = link.node.parent['class'].split
  parent_classes.any? { |p| %w[next-container page-number].include?(p) }
end

review_links = review_links[0...4]

reviews = review_links.map do |link|
  review = link.click
  review_meta = review.search('#main .review-meta .info')
  artist = review_meta.search('h1')[0].text
  album = review_meta.search('h2')[0].text
  label, year = review_meta.search('h3')[0].text.split(';').map(&:strip)
  reviewer = review_meta.search('h4 address')[0].text
  review_date = Date.parse(review_meta.search('.pub-date')[0].text)
  score = review_meta.search('.score').text.to_f
  {
    artist: artist,
    album: album,
    label: label,
    year: year,
    reviewer: reviewer,
    review_date: review_date,
    score: score
  }
end

puts JSON.pretty_generate(reviews)


Сохранив этот код в нашем файле scraper.rb и запустив его командой:

$ ruby scraper.rb


Мы получим, что-то похожее на это:

[
  {
    "artist": "Viet Cong",
    "album": "Viet Cong",
    "label": "Jagjaguwar",
    "year": "2015",
    "reviewer": "Ian Cohen",
    "review_date": "2015-01-22",
    "score": 8.5
  },
  {
    "artist": "Lupe Fiasco",
    "album": "Tetsuo & Youth",
    "label": "Atlantic / 1st and 15th",
    "year": "2015",
    "reviewer": "Jayson Greene",
    "review_date": "2015-01-22",
    "score": 7.2
  },
  {
    "artist": "The Go-Betweens",
    "album": "G Stands for Go-Betweens: Volume 1, 1978-1984",
    "label": "Domino",
    "year": "2015",
    "reviewer": "Douglas Wolk",
    "review_date": "2015-01-22",
    "score": 8.2
  },
  {
    "artist": "The Sidekicks",
    "album": "Runners in the Nerved World",
    "label": "Epitaph",
    "year": "2015",
    "reviewer": "Ian Cohen",
    "review_date": "2015-01-22",
    "score": 7.4
  }
]


Если хотите, вы можете перенаправить эти данные в файл.

$ ruby scraper.rb > reviews.json


Заключение


Это только вершина возможностей Mechanize. В этой статье я даже не коснулся способности Mechanize заполнять и отправлять формы. Если вам это интересно, то я рекомендую почитать руководство Mechanize и примеры использования.

Много людей в комментариях к предыдущему посту сказали, что я должен был просто использовать Mechanize. Хотя я согласен, что Mechanize является отличным инструментом, тот пример, который я привел в первой записи на эту тему был простым, и использование в нем Mechanize, как мне кажется, является излишним.

Однако, учитывая способности Mechanize, я начинаю думать, что даже для простых задач парсинга, зачастую, будет лучше использовать именно его.

Все статьи серии:
Автор оригинала: Chris Mytton
Антон Рябов @tonymadbrain
карма
4,2
рейтинг 0,0
Системный администратор
Самое читаемое Разработка

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

  • +1
    Такое простое действие можно сделать без дополнительной библиотеки. Достаточно выбрать массив ссылок через Nokogiri и пройтись по нему.
    • 0
      А в это просто действие входить использование какой-то другой библиотеки помимо Net::HTTP для скачивания? а то код усложняется…
      • 0
        Я не говорил, что код усложняется. Автор статьи использует эту библиотеку не по назначению.

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