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