07.04.2024: Эта статья изначально была опубликована на habr.com и была просмотрена там 31 тысячу раз. Но теперь я решил перенести её в свой блог.
От переводчика: Оригинал статьи Advanced web scraping with Mechanize
Обзор
В предыдущей записи я описал основы - введение в веб парсинг на 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, я начинаю думать, что даже для простых задач парсинга, зачастую, будет лучше использовать именно его.
Все статьи серии:
- Веб-парсинг на Ruby
- Продвинутый парсинг веб-сайтов с Mechanize (вы здесь)
- Использование morph.io для веб-парсинга