УДАЛЕНИЕ ВЕБ-СТРАНИЦ

Веб-скрапинг стал проще с Ruby

Изучите основы веб-парсинга с Nokogiri Gem

В качестве примера предположим, что мы хотим увидеть ведущих авторов на данном носителе, которые пишут под данным именем тега, например Ruby on Rails?

Для этого нет API https://github.com/Medium/medium-api-docs

Посмотрим, что можно сделать быстро. Позже мы постараемся использовать эти данные в более читаемом формате.

Во-первых, давайте разберемся с основами. Чтобы очистить информацию, нам нужно знать, по какому URL мы хотим попасть.

Следовательно, URL был https://medium.com/tag/ruby-on-rails/latest, и нам нужен список всех ведущих авторов.

Настроить скребок

Предварительные требования:

  • Ruby 2.0.0 или новее
  • Самоцветы open-uri nokogiri pry table-print

Давайте создадим файл medium_scraper.rb

touch medium_scraper.rb

и сохраните его с помощью следующего кода:

# medium_scraper.rb
unless ARGV[0]
  puts "Please enter desired url"
  puts "Help: ruby medium_scraper.rb <tag>"
  exit
end

Давайте запустим ruby medium_scraper.rb, и вы должны увидеть подсказку:

❯ ruby medium_scraper.rb
Please enter desired url
Help: ruby medium_scraper.rb <url>

На следующем этапе мы добавим возможность открывать ссылки с помощью open-uri и читать HTML с помощью nokogiri.

# medium_scraper.rb
unless ARGV[0]
  puts "Please enter desired tag"
  puts "Help: ruby medium_scraper.rb <tag>"
  exit
end

begin
  BASE_URL  = "https://medium.com/"
  url       = BASE_URL + "tag/#{ARGV[0].strip}"
  uri       = URI.open(url)
  doc       = Nokogiri::HTML(uri) # Fetch and parse HTML document

  puts doc.inspect

rescue => e
  puts "An error occurred."
  puts e
  retry
end

Давайте запустим его еще раз ruby medium_scraper.rb ruby-on-rails, и мы увидим, что в нем есть проанализированные данные.

❯ ruby medium_scraper.rb ruby-on-rails
#<Nokogiri::HTML4::Document:0xeed4 name="document" children=[#<Nokogiri::XML::DTD:0x208 name="html">, #<Nokogiri::XML::Element:0xeec0 name="html" attributes=[#<Nokogiri:

Извлеките некоторые данные

Прежде чем мы добьемся желаемых результатов, давайте воспользуемся pry, чтобы поиграть с нашим doc объектом.

# medium_scraper.rb
+ require 'pry'
+ binding.pry
puts doc.inspec

давай снова бежим ruby medium_scraper.rb ruby-on-rails

Мы видим, что он возвращает Nokogiri документ, теперь нам нужно выполнить поиск Top Writers, но если вы посмотрите на классы HTML, они не декларативны, поэтому мы не можем извлечь их, выполняя поиск только по именам классов.

Отображение тега p Top Writers

Давайте посмотрим, какие данные у нас есть вокруг тега <p>, и мы можем получить родительские элементы <p> Top Writers </p>

p = doc.css('p')
p.size
=> 63

Если мы проверим наш первый элемент p, вы увидите, что у нас есть p.children[0].text

p.first
=> #(Element:0x230 {
  name = "p",
  attributes = [ #(Attr:0x208 { name = "class", value = "ay b eu ba bb" })],
  children = [ #(Text "12.5K")]
  })

Мы можем получить доступ к text методу всех дочерних элементов, но в нашем случае всегда есть один дочерний элемент.

p.first.children.first
=> #(Text "12.5K")
p.first.children.first.text
=> "12.5K"
p.first.children.text
=> "12.5K"

получим тег p с текстом Top Writers

p_heading  = p.map{ |p|  p if p.children.text == 'Top Writers' }
=> [nil,
 nil,
 nil,
 nil,
 nil, .....
 #(Element:0x13ec {
   name = "p",
   attributes = [ #(Attr:0x13c4 { name = "class", value = "ay b dp dq dr ds dt" })],
   children = [ #(Text "Top Writers")]
   }),
 nil,..]

Мы видим, что он сопоставил единственный тег Top Writers, но теперь у нас так много nils, давайте избавимся от nils и получим только первый результат.

p_heading = p.map{ |p| p if p.children.text == 'Top Writers' }.compact.first
=> #(Element:0x13ec {
  name = "p",
  attributes = [ #(Attr:0x13c4 { name = "class", value = "ay b dp dq dr ds dt" })],
  children = [ #(Text "Top Writers")]
  })

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

# 3 level up, second child
p_heading.parent.parent.parent.children[1]

Теперь мы можем видеть результаты профиля

Выбор тега p Top Writers с помощью CSS-селекторов

Другой подход может заключаться в использовании селекторов CSS, выберите div, в котором p и p содержат текст «Top Writers».

p_heading = doc.search("div:has(> p:contains('Top Writers'))").first
# 2 level up, second child
div_writers = p_heading.parent.parent.children[1]

И мы можем получить тот же результат

Выбор профиля, ссылки и имени

На картинке ниже мы видим, что после 3 div мы хотим получить a детали тега

# 3 cascading divs and last a
content = div_writers.search('div > div > div > a')
content.size
=> 5

Теперь у нас есть 5 элементов в div_writers, и каждый a имеет атрибут href со ссылкой на профиль, а имя Writer находится в первом child h2 text

Извлечь ссылки теперь можно с помощью:

content.map{|e| e.attr('href') }

Имя и биография:

content.map{|e| e.children.map(&:text) }

Давайте объединим все это в результаты с немного другим подходом, мы будем использовать массив [] и вставим каждый результат хеширования.

results = content.inject([]) do |result, ele|
  result << {
    profile: ele.attr('href'),
    name: ele.search('h2').text,
    bio: ele.search('p').text
  }
end

Если вы хотите получить немного лучшие результаты на консоли, используйте table_print gem, а затем вы можете просто использовать tp results для создания такой красивой таблицы.

Что дальше?

Здесь мы можем расширить сценарий разными способами. Мы можем извлечь данные и преобразовать их в CSV и отправить их в Google Таблицы или Airtable, и мы даже можем обрабатывать несколько аргументов тегов fromARGV[0], например ruby medium_scraper.rb ruby-on-rails ruby programming

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

Хотели бы вы поделиться своим подходом к парсингу с помощью Ruby? Комментарий ниже, если он у вас есть!

Использованная литература: