Libri: RubyGem для библиофилов с интерфейсом командной строки
Table Of Content
Я поделюсь своим подходом на основе процесса, как я создал 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
.
Каждый класс отвечает за разные части гема:
- Класс
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. Публикация
Наконец, чтобы впервые опубликовать гем, я следовал этим простым шагам:
Надеюсь, вам понравился этот пост, и я надеюсь, что это вам понятно! Предлагайте любые предложения для гема, и я над ними поработаю. Счастливого кодирования!