CoderCastrov logo
CoderCastrov
Руби

Libri: RubyGem для библиофилов с интерфейсом командной строки

Libri: RubyGem для библиофилов с интерфейсом командной строки
просмотров
5 мин чтение
#Руби

Я поделюсь своим подходом на основе процесса, как я создал Libri и опубликовал его на RubyGems.org, а также расскажу о некоторых технических проблемах, с которыми я столкнулся во время разработки. Этот проект специально фокусируется на парсинге, что означает извлечение данных на основе HTML и CSS с веб-страницы. Вот видео, демонстрирующее, как работает Libri:

1. Поиск

После просмотра нескольких идей для парсинга, включая парсинг Noti.st, или 80,000 Hours’ Problem Profiles, или Adafruit’s Raspberry Pi projects, я решил вернуться к теме, которая может быть простой, значимой и полезной для многих: книги. При поиске веб-сайта для парсинга у меня было несколько вариантов: веб-сайт Man Booker, раздел Goodreads’ Awards, а также список Penguin’s Award Winners.

Я выбрал веб-страницу с наградами Barnes & Noble для парсинга, так как она кажется наиболее полной и актуальной.

2. Стратегия

Для создания гема с использованием Bundler я начал с выполнения команды bundle gem libri в терминале в рабочем каталоге Libri. Это создаст структуру файлов (называемую каталогом настроек) для нашего гема, чтобы мы могли сразу приступить к кодированию.

Я убедился, что на моем компьютере также установлены следующие зависимости:

  • Rake, используется для создания локальной копии нашего гема, которую мы будем использовать для загрузки и публикации на RubyGems.org
  • OpenURI, используется для открытия URL-адреса как HTML-файла
  • Nokogiri, используется для разбора HTML и XML значений с веб-страницы
  • Pry, используется как локальная песочница и инструмент отладки
  • Colorize, используется для стилизации текста в терминале с использованием разных цветов

3. Архитектура

Теперь для Libri я хотел сделать работу трех вещей в моем терминале:

Для этого я структурировал свою папку lib следующим образом, разделив классы CLI, scraper, awards, books и book.

Упрощенная структура директорий для Libri

Каждый класс отвечает за разные части гема:

  • Класс CLI отвечает за интерфейс терминала, взаимодействующий с пользователем
  • Класс scraper парсит текстовое содержимое с веб-страницы
  • Класс awards создает новые экземпляры объекта Awards из значений хэша, возвращенных методом Scraper.scrape_barnes_noble
  • Класс books создает новые экземпляры объекта Books из значений хэша, возвращенных методом Scraper.scrape_award(award)
  • Класс book создает новые экземпляры объекта Books из значений хэша, возвращенных методом Scraper.scrape_book(book)

4. Разработка

Этот этап занял больше всего времени, но в целом он был успешным, и у меня есть несколько заметок:

  • Я научился использовать многострочную строку с помощью HEREDOC, которая сама по себе имеет различные методы для достижения той же цели (например, %{...}, %Q{...}, <<-EOS...EOS).
  • Изначально, когда была вызвана команда exit, также выводилось сообщение "Пожалуйста, попробуйте еще раз.". Это было исправлено с помощью одноуровневого условного оператора if/else...end, а не цикла while input != 'exit'...end.
  • Я знал, что хочу получить доступ к нескольким уровням информации, парся разные URL-адреса, и иметь возможность передавать разные URL-адреса на основе ввода пользователя (например, если пользователь вводит награды Пулитцера, метод Scraper.scrape_award() должен возвращать информацию на основе URL-адреса наград Пулитцера. Если пользователь вводит премию Мана Букера, ожидаемый результат должен быть получен с URL-адреса Мана Букера). Я знал, что мне нужно передать URL-адрес в качестве аргумента для метода Scraper.scrape_award(). Зная это, я включил ключ :url в хэш awards верхнего уровня, значение которого будет передано в Scraper.scrape_award(). Затем хэш books второго уровня может парсить и получать доступ к переданному URL-адресу - та же концепция применяется, когда мы парсим URL-адрес третьего уровня для получения информации о каждой книге. Я не был уверен, что это будет работать, так как предыдущие лабораторные работы, над которыми я работал, не использовали многоуровневый сайт, обновляемый в реальном времени, и поэтому не требовали такого подхода. Но это сработало! Это было лучшим открытием, которое я сделал, создавая этот проект, поняв, что гибкость может быть встроена в код.
  • Я не мог получить доступ к значениям атрибутов HTML, которые не являются href. Значения рейтингов на веб-сайте B&N хранились в атрибуте aria-label, который не возвращал значение, когда я пытался получить к нему доступ. Я также не мог получить доступ к книгам, перечисленным в разделе Customers Who Bought This Item Also Bought, который также не возвращал ничего. Я все еще ищу ответы.
  • Изначально, при парсинге, я понял, что могу получить доступ к значениям хэша и отображать их из класса CLI, используя Hash[:key], даже не создавая новые объекты и присваивая им аргументы/атрибуты. Это привело к недоразумению, когда я опубликовал рабочий gem без практики методов отношений объектов Ruby, таких как "имеет много". Это было исправлено путем соответствующего редактирования классов awards, books и book. Теперь мы можем получить доступ к значениям хэша, таким как book.title и book.author, используя attr_accessor.
  • В один момент, когда в терминале отображался список книг, а затем возвращались, чтобы выбрать другую награду, список книг накапливался, и в результате получалось 20-40-60... количество книг. Это была катастрофа, и я уже почти сдался. Однако вскоре стало понятно, что ошибка вызвана вызовом метода CLI#make_book(award) каждый раз, когда вызывается CLI#menu_award, и это добавляет новый массив книг в Books.all. #make_book(award)_ _необходим для создания объекта Book и доступа к различным его атрибутам, и нам это нужно. Чтобы исправить это, был добавлен метод для очистки предыдущего созданного объекта перед вызовом #make_book(award), тем самым сбрасывая возвращаемое значение Books.all для каждого вызова меню.

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

Обсуждая свои мысли на основе этого грубого потока:

  • Что я пытаюсь сделать?
  • Делает ли Ruby то, что я ожидаю от нее? (Да/нет)
  • Если нет, что происходит вместо этого, и почему мы думаем, что это происходит?
  • Если это происходит из-за X, то если мы изменяем Y, мы ожидаем, что произойдет Z.
  • Мы проверяем нашу гипотезу, изменив Y, и видим, происходит ли Z.
  • Если происходит Z, основываясь на нашем понимании X, мы должны знать, как это исправить и достичь того, что мы пытались сделать.
  • Если Z не происходит, не сдавайтесь! Читайте и ищите помощь, и проверяйте разные понимания, чтобы найти то, которое работает с Ruby.

Это простой проект, однако, с несколькими различными компонентами, взаимодействующими друг с другом, было довольно легко потеряться в одном из них (например, как лучше получить доступ и отобразить каждую отдельную часть информации, на каком этапе объекты были созданы, а на каком нет и т.д.), и когда я потерял этот момент, я вскоре потерял представление о большой картине и мне пришлось начинать все сначала. Так что вот чтобы помнить, что нужно продолжать практиковаться и практиковаться правильно!

5. Публикация

Наконец, чтобы впервые опубликовать гем, я следовал этим простым шагам:


Надеюсь, вам понравился этот пост, и я надеюсь, что это вам понятно! Предлагайте любые предложения для гема, и я над ними поработаю. Счастливого кодирования!