Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Ruby

Создаем свой первый веб-скрэпер, часть 3

by
Length:LongLanguages:

Russian (Pусский) translation by blacktreelab (you can also view the original English article)

С возвращением в серию статей по созданию веб-скрэпера. В этом уроке я рассмотрю пример сбора данных с моего подкаст-сайта. Я расскажу подробно о том, как извлекать данные, как вспомогательные и утилитные методы выполняют свою работу, и как все кусочки головоломки собираются вместе.

Темы

  • Экстракция моего подкаста
  • Pry
  • Скрэпер
  • Вспомогательные методы
  • Пишем статьи

Экстракция моего подкаста

Теперь давайте попробуем на практике все что мы изучили. По некоторым причинам давно пора сменить дизайн моего сайта подкастов Between | Screens. Там было столько проблем, что я по утрам просыпался в холодном поту. Поэтому я решил создать новый статический сайт, построенный на Middleman, и разместить его GitHub Pages.

После настройки блога на Middleman под свои нужды, я потратил массу времени на новый дизайн. Все, что оставалось сделать, это импортировать содержимое базы данных из приложения на Sinatra, поэтому мне нужно было выбрать содержимое и перенести его на мой новый статический вебсайт.

Речи не шло о том, чтобы делать эту нудную работу вручную - без вариантов - так как я мог рассчитывать, что мои друзья Nokogiri и Mechanize сделают работу за меня. Что нас ждет впереди - это достаточно небольшая работа по экстракции данных, не слишком сложная, но предлагающая несколько интересных моментов, которые будут интересны новичкам скрэпинга.

Ниже вы найдете два скриншота с моего сайта.

Скриншот старого сайта

A screenshot of an old podcast

Скриншот нового сайта

A screenshot of a new podcast

Давайте разберем шаг за шагом, чего мы хотим достигнуть. Мы хотим извлечь следующие данные из 139 серии, которые размещены на  21 странице:

  • заголовок
  • собеседник
  • подзаголовок со списком тем
  • номер трека каждого эпизода на SoundCloud
  • дата
  • номер эпизода
  • текст записок эпизода
  • ссылки из записок эпизода

Мы будем переходить по страницам, а Mechanize будет кликать каждую ссылку эпизода. На следующей странице мы найдем всю необходимую нам информацию. Используя полученные данные, мы добавим их во frontmatter (переменные конкретной страницы) и body файлов разметки каждого эпизода.

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

def compose_markdown

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

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

Попутно, я настоятельно рекомендую вам начать думать о том, как улучшить данный код. Это будет вашим финальным заданием по окончании этой статьи. Небольшая подсказка: всегда проще разбить большие методы на более маленькие. Как только вы поймете, как работает код, вы сможете улучшать мои доработки в вводить свои.

Я уже начал добавлять некоторые методы в небольшие вспомогательные методы. Если вы читали мои предыдущие статьи о коде с "запашком" и его рефакторинге, вам легко будет применить, излагаемые там принципы, на практике. Но, не переживайте, если у вас до сих пор голова идет кругом, - мы все там были. Продолжайте изучать, и в какой-то момент все станет намного яснее.

Полный код

Зачем нам следующий код: require "Nokogiri"? Ведь Mechanize обеспечивает нас всеми необходимым для скрэпинга. Как мы рассказывали в предыдущей статье, Mechanize основывается на Nokogiri, что позволяет нам выбирать содержимое страниц. Но нам необходимо было описать этот пакет в первой статье, так вся наша последующая работа строиться на нем.

Pry

Первым делом о главном. Перед тем как перейти к коду, мне хотелось бы показать вам, как можно эффективно проверить, что ваш код работает как нужно на протяжении всего исполнения. Как вы могли заметить, я добавил еще один инструмент. Среди прочего, Pry очень полезен при отладке.

Если вы добавите код Pry.start(binding) в любой части кода, вы можете проинспектировать ваш код в том, месте где вы добавили данную строку. Вы можете добавлять pry в определенные части приложения. Pry помогает проводить отладку приложения шаг за шагом. Для примера, давайте добавим pry сразу после функции write_page, и посмотри действительно ли мы получаем link.

Pry

Если вы выполните этот скрипт, то получите что-то вроде этого.

Результат

Запрашивая link, мы можем проверить, что мы на правильном пути, перед тем, как идти дальше.

Терминал

Похоже то, что нам нужно. Отлично, можно продолжать дальше. Очень важно пройти шаг за шагом по всему приложению, это гарантирует, что вы не потеряетесь в коде и всегда будете знать как что работает. Я не буду здесь погружаться в детали Pry, так как это может занять целую статью. Могу только порекомендовать вам использовать Pry как альтернативу IRB shell (командной оболочке Ruby). Вернемся к нашему основному заданию.

Скрэпер

Теперь когда вы ознакомились со всеми кусочками головоломки, предлагаю пройтись по каждому из них, и разобрать некоторые интересные моменты. Давайте начнем с ключевых частей.

podcast_scraper.rb

Что происходит в методе scrape? Во-первых, я пройду по индексной странице старого подкаста. Я использую старую ссылку на приложение на Heroku, так как новый сайт уже расположен на betweenscreens.fm. Мне надо было пройти по 20 эпизодам.

Я разбил цикл по переменной link_range, и обновлял его на каждом повторе. А переход по страницам был очень прост, я всего лишь добавлял эту переменную в URL каждой страницы. Просто и эффективно

Метод def scrape

Затем, на каждой новой странице с 8 эпизодами, я использовал page.links для обозначения ссылок, на которые нужно кликать, чтобы перейти на страницу отдельного эпизода. Я решил использовать диапазон links[2..8], так как он оставался неизменным. Это также самый простой способ определить необходимые ссылки с каждой страницы. Нет необходимости перебирать CSS-селекторы.

Затем мы передаем эту ссылку методу write_page для извлечения всей страницы. Большая часть работы делается здесь. Берем ссылку, кликаем на нее, и переходим на страницу с полной информацией, где мы можем начать экстракцию содержимого. На этой странице мы найдем всю информацию, которая нам понадобится для создания структуры эпизодов на моем новом сайте.

метод def write_page

метод def extract_data

Как вы можете видеть выше, мы берем параметр detail_page, и применяем к нему кучу методов извлечения. Мы извлекаем следующие параметры, interviewee, title, sc_id, text, episode_title, и episode_number. Я переделал много целевых вспомогательные методов, которые отвечают за функции извлечения. Давайте их рассмотрим:

Вспомогательные методы

Методы экстракции

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

Мы ищем определенный селектор на странице и получаем текст без лишних пробелов.

Удаляем из заголовка ? и #, так как они не особо сочетаются с переменными front matter эпизодов. Больше о переменных front matter далее. 

Здесь нам придется немного потрудиться, чтобы извлечь идентификаторы SoundCloud для всех треков. Сначала нам нужно получить iframe с атрибутом href для soundcloud.com и затем конвертировать его в строку для сканирования.

Затем сопоставить регулярные выражения с идентификаторами трека - soundcloud_id и "221003494".

Экстракция заметок шоу проходит довольно таки просто. Нам нужно только выбрать параграфы заметок из страниц с отдельными эпизодами. Здесь никаких сюрпризов.

То же самое касается и субтитров, за исключением того, что это только подготовка к аккуратному извлечению нумерации эпизодов.

Здесь нам понадобиться еще несколько регулярных выражений. Давайте посмотрим, что у нас есть до и после применения регулярных выражений.

episode_subtitle

number (номер эпизода)

Еще один шаг до получения номера.

Просто удалим знак нумерации (#). Вуаля, у нас есть первый номер эпизода - 139. Предлагаю взглянуть на другие утилитные методы, прежде чем мы соберем все это вместе.

Утилитные методы

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

метод def clean_date

Запустим еще одно регулярное выражение, чтобы выделить даты, которые выглядят вот так " Aug 26, 2015". Как видите, пока не особенно практично. Из атрибута string_date субититров нам нужно создать объект Date. Иначе он будет бесполезен для создания записей Middleman.

string_date

Таким образом берем строку и пропускаем ее через метод Date.parse. Результат выглядит намного более перспективным.

Date (дата)

метод def build_tags

Здесь метод принимает две переменные: title и interviewee, которые мы создали в методе extract_data, и удаляет все символы пайп (вертикальная черта). Мы заменим все символы пайп запятыми, а символы @, ?, #  и & пробелами; и, наконец, позаботимся о сокращениях для атрибута with.

Метод def strip_pipes

В конце мы добавим имя собеседника в список тэгов, и разделим тэги запятыми.

До

После

Каждый из этих тегов, в конечном итоге, будет ссылкой на посты на определенную тему. Все это происходит в методе extract_data. Давайте еще раз посмотрим, где мы находимся:

Метод def extract_data

Все, что нам осталось сделать - это вернуть хэш параметров с данными, которые мы выбрали. Мы можем послать хеш в метод compose_markdown, который получает наши, уже готовые к записи, данные в виде файла, который нужен для моего нового сайта.

Пишем посты

Метод def compose_markdown

Для публикации подкастов на сайте Middleman, я решил перепрофилировать систему блога. Вместо того, чтобы создавать исключительно блог посты, я создал заметки шоу для эпизодов, которые отображают эпизоды с SoundCloud через iframe. На индексных сайтах, я отображаю только iframe плюс заголовок, и прочее.

Формат, необходимый для этой работы состоит из переменных, названных front matter. По сути это параметры ключ/значение для моего статического сайта. Это замена базы данных моего старого сайта на Sinatra.

Такие данные, как имя собеседника, дата, код трека на SoundCloud, номер эпизода и т. п. находятся между тремя знаками тире (---) над файлом эпизода. Ниже идет контент каждого эпизода - вопросы, ссылки, спонсоры и т. д.

Front Matter

В методе compose_markdown, я использую HERODOC для создания файла с атрибутами (frontmatter: в Middleman переменные конкретной страницы - прим. переводчика) для каждого эпизода. Мы передаем параметры в этот метод из хэша параметров - извлекаем все данные, которые мы получили через вспомогательный метод extract_data.

Метод def compose_markdown

Вот шаблон нового эпизода подкаста. Как раз за этим мы и пришли. Возможно, вы задаетесь вопросом о данном конкретном синтаксисе: #{options[:interviewee]}. Я, как обычно, интерполировал строку, но так как мы уже находимся внутри <<-heredoc, можно не использовать двойные кавычки.

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

Для этого последнего шага, нам нужно подготовить следующие ингредиенты: дату, имя собеседника, и число эпизодов. Ну и, конечно, markdown_text, который мы получили из метода compose_markdown.

Метод def write_page

Затем нам только нужно взять параметры file_name и markdown_text и создать файл.

Метод def write_page

Давайте разобьем его на части. Для каждого поста мне нужен определенный формат: что-то вроде 2016-10-25-Avdi-Grimm-120. Я хочу, чтобы имя файлов начиналось с даты, и также включало имя собеседника и номер эпизода.

Чтобы отвечать требованиям для новых постов Middleman, мне нужно пропустить имя собеседника через вспомогательный метод dasherize, чтобы Avdi Grimm из получилось Avdi-Grimm. Ничего волшебного, но взглянуть стоит:

Метод def dasherize

Этот метод удаляет пробелы в имени собеседника, и заменяет пробел между Avdi и Grimm тире. Остальное имя файла также разделено тире: "date-interviewee-name-episodenumber".

Метод def write_page

Так как мы получаем содержимое экстракции прямо из HTML сайта, я не могу сразу использовать .md или .markdown в качестве расширения файла. Я решил использовать .html.erb.md. Для будущих эпизодов, которые я буду создавать без экстракции, можно будет убрать .html.erb, и использовать только .md.

После этих шагов, цикл в функции scrape остановится, и у нас будет один эпизод, который выглядит примерно так:

2014-12-01-Avdi-Grimm-1.html.erb.md

Наш скрэпер начнет свою работу с последнего эпизода, и будет переходит по эпизодам до первого. А для демонстрационных целей нам подойдет и первый эпизод. Вверху вы можете видеть front matter вместе извлеченными данными.

Все эти данные раньше содержались в базе данных моего приложения на Sinatra - номер эпизода, дата, имя собеседника, и т.п. А теперь они готовы для моего статического сайта на Middleman. Все что ниже двойных трех тире (---) - это текст из заметок шоу: по большей части вопросы и ссылки.

Заключительные мысли

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

Как я говорил раньше, теперь настало время вам поэкспериментировать с кодом. Переделайте код немного. Попробуйте упростить его. У вас много возможностей для рефакторинга кода.

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

Ну и, как обычно, ступайте шаг за шагом, и не слишком разочаровывайтесь, если что-то не получается сразу. Это не просто нормально для большинства, но и само собой разумеется. Это часть пути. Хорошего скрэпинга!

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.