Как создать API гороскопа с помощью Beautiful Soup и Flask

Вы когда-нибудь читали свой гороскоп в газете или видели его по телевизору? Что ж, многие люди все еще читают свои гороскопы: кто-то ради забавы, а кто-то искренне верит.

В этой статье мы покажем, как собрать данные с сайта Horoscope.com с помощью Beautiful Soup, а также — создать собственный API гороскопа с помощью Flask. Если этот API развернуть на общедоступном сервере, другие разработчики смогут им воспользоваться, чтобы вывести такой же гороскоп на своих сайтах или в приложениях.

Как настроить проект

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

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

$ python -m venv env

Чтобы активировать виртуальную среду с именем env, используйте следующие команды:

  • в Windows — env\Scripts\activate.bat
  • в Linux и MacOS — source env/bin/activate

Чтобы деактивировать среду (это может понадобиться позже) введите deactivate.

Установка необходимых библиотек

Теперь мы готовы устанавливать зависимости. В этом проекте мы собираемся использовать следующие модули и библиотеки:

  • requests. Библиотека requests позволяет очень легко отправлять запросы HTTP/1.1. Она не предустановлена в Python; установить её можно с помощью команды:

$ pip install requests

  • bs4. Beautiful Soup (bs4) – это библиотека Python для извлечения данных из файлов HTML и XML. Она также не предустановлена в Python, поэтому нам нужно установить её с помощью команды:

$ pip install bs4

  • Flask. Это простой и легкий в использовании микрофреймворк Python. Он помогает создавать масштабируемые и безопасные веб-приложения. Этот модуль нам тоже нужно установить:

$ pip install flask

  • Flask-RESTX. Позволяет создавать API с помощью документации Swagger. Этот модуль, как и все предыдущие, не является предустановленным, поэтому используем команду:

$ pip install flask-restx

Мы также будем использовать переменные среды в этом проекте. Чтобы справиться с этой задачей, мы установим еще один модуль под названием python-decouple:

$ pip install python-decouple

Что ж, теперь мы готовы, давайте начинать!

[python_ad_block]

Рабочий процесс проекта

Базовый рабочий процесс проекта будет таким:

  1. Данные гороскопа будут взяты с сайта Horoscope.com.
  2. Затем данные будут использоваться нашим сервером Flask для отправки пользователю ответа в формате JSON.

Как настроить проект Flask

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

Наше приложение будет находиться в папке под названием core. Чтобы преобразовать обычный каталог в пакет Python, нам просто нужно включить в него файл __init__.py. Итак, создаем нашу основную паку:

$ mkdir core

После этого давайте создадим файл __init__.py внутри основного каталога:

$ cd core
$ touch __init__.py
$ cd ..

В корневом каталоге проекта создайте файл с именем config.py. В нем мы будем хранить конфигурации для проекта. Внутри файла добавьте следующее содержимое:

from decouple import config


class Config(object):
    SECRET_KEY = config('SECRET_KEY', default='guess-me')
    DEBUG = False
    TESTING = False
    CSRF_ENABLED = True


class ProductionConfig(Config):
    DEBUG = False
    MAIL_DEBUG = False


class StagingConfig(Config):
    DEVELOPMENT = True
    DEBUG = True


class DevelopmentConfig(Config):
    DEVELOPMENT = True
    DEBUG = True


class TestingConfig(Config):
    TESTING = True

Здесь мы создали класс Config и определили внутри него различные атрибуты. Кроме того, мы создали разные дочерние классы (для разных этапов разработки), наследующие класс Config.

Обратите внимание, что у нас в SECRET_KEY установлена ​​переменная среды с именем SECRET_KEY. Создайте файл с именем .env в корневом каталоге и добавьте туда следующее содержимое:

APP_SETTINGS=config.DevelopmentConfig
SECRET_KEY=gufldksfjsdf

Помимо SECRET_KEY, у нас есть APP_SETTINGS, относящийся к одному из классов, которые мы создали в файле config.py. Мы устанавливаем его для текущей стадии проекта.

Теперь мы можем добавить следующий код в файл __init__.py:

from flask import Flask
from decouple import config
from flask_restx import Api

app = Flask(__name__)
app.config.from_object(config("APP_SETTINGS"))
api = Api(
    app,
    version='1.0',
    title='Horoscope API',
    description='Get horoscope data easily using the below APIs',
    license='MIT',
    contact='Ashutosh Krishna',
    contact_url='https://ashutoshkrris.tk',
    contact_email='contact@ashutoshkrris.tk',
    doc='/',
    prefix='/api/v1'
)

Здесь мы сначала импортируем класс Flask из установленного модуля Flask. Затем создаем объектное приложение класса Flask. Мы используем аргумент __name__ для обозначения модуля или пакета приложения, чтобы Flask знал, где найти другие файлы, такие как template (шаблоны).

Затем мы устанавливаем конфигурации приложения в APP_SETTINGS в соответствии с переменной в файле .env.

Кроме того, мы создали объект класса Api. Нам нужно передать ему различные аргументы. Мы можем найти документацию Swagger по адресу /route. Префикс /Api/v1 будет на каждом маршруте API.

А пока давайте создадим файл routes.py в основной папке core и просто добавим следующий код:

from core import api
from flask import jsonify

ns = api.namespace('/', description='Horoscope APIs')

Нам нужно импортировать routes в файл __init__.py:

from flask import Flask
from decouple import config
from flask_restx import Api

app = Flask(__name__)
app.config.from_object(config("APP_SETTINGS"))
api = Api(
    app,
    version='1.0',
    title='Horoscope API',
    description='Get horoscope data easily using the below APIs',
    license='MIT',
    contact='Ashutosh Krishna',
    contact_url='https://ashutoshkrris.tk',
    contact_email='contact@ashutoshkrris.tk',
    doc='/',
    prefix='/api/v1'
)

from core import routes			# Add this line

Теперь у нас остался только один файл, который поможет нам запустить сервер Flask:

from core import app

if __name__ == '__main__':
    app.run()

Запустив этот файл с помощью команды python main.py, вы увидите следующий результат:

Теперь мы готовы скрапить данные с сайта Horoscope.

Как скрапить данные с сайта Horoscope.com

Если вы зайдете на сайт Horoscope.com и выберете свой знак зодиака, отобразятся данные вашего гороскопа на сегодня.

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

Но сначала, если вы видите URL-адрес текущей страницы, он выглядит примерно так: https://www.horoscope.com/us/horoscopes/general/horoscope-general-daily-today.aspx?sign=10.

URL-адрес имеет две переменные: sign и today. Значение переменной sign будет присвоено в соответствии со знаком зодиака. Переменную today можно заменить на yesterday и tomorrow.

Этот словарь поможет нам со знаками зодиака:

ZODIAC_SIGNS = {
    "Aries": 1,
    "Taurus": 2,
    "Gemini": 3,
    "Cancer": 4,
    "Leo": 5,
    "Virgo": 6,
    "Libra": 7,
    "Scorpio": 8,
    "Sagittarius": 9,
    "Capricorn": 10,
    "Aquarius": 11,
    "Pisces": 12
}

К примеру, если ваш знак зодиака Козерог, значение знака в URL-адресе будет 10.

Если мы хотим получить данные гороскопа на определенную дату, нам поможет URL-адрес https://www.horoscope.com/us/horoscopes/general/horoscope-archive.aspx?sign=10&laDate=20211213.

В этом адресе есть та же переменная sign, но есть и другая — laDate, которая принимает дату в формате ГГГГММДД.

Теперь мы готовы создать различные функции для получения данных гороскопа. Создайте файл utils.py и следуйте инструкциям ниже.

Как получить гороскоп на день

import requests
from bs4 import BeautifulSoup


def get_horoscope_by_day(zodiac_sign: int, day: str):
    if not "-" in day:
        res = requests.get(f"https://www.horoscope.com/us/horoscopes/general/horoscope-general-daily-{day}.aspx?sign={zodiac_sign}")
    else:
        day = day.replace("-", "")
        res = requests.get(f"https://www.horoscope.com/us/horoscopes/general/horoscope-archive.aspx?sign={zodiac_sign}&laDate={day}")
    soup = BeautifulSoup(res.content, 'html.parser')
    data = soup.find('div', attrs={'class': 'main-horoscope'})
    return data.p.text

Мы создали нашу первую функцию, которая принимает два аргумента – целое число zodiac_sign и строку day. Переменная day может иметь значения today, tomorrow, yesterday или любой даты до сегодняшнего дня в формате ГГГГ-ММ-ДД.

Если день не является произвольной датой, в нем не будет символа дефиса (-). Если дефиса нет, мы делаем запрос GET на https://www.horoscope.com/us/horoscopes/general/horoscope-general-daily-{day}.aspx?sign={zodiac_sign}. В противном случае мы меняем дату с ГГГГ-ММ-ДД на формат ГГГГММДД.

Затем мы делаем запрос GET на https://www.horoscope.com/us/horoscopes/general/horoscope-archive.aspx?sign={zodiac_sign}&laDate={day}.

После этого мы извлекаем данные HTML из содержимого ответа страницы с помощью BeautifulSoup. Нам нужно получить текст гороскопа из этого HTML-кода. Если вы проверите код любой веб-страницы, вы обнаружите следующее:

Текст гороскопа содержится в div с классом main-horoscope. С помощью функции soup.find() мы можем извлечь текстовую строку абзаца и вернуть ее.

Как получить гороскоп на неделю

def get_horoscope_by_week(zodiac_sign: int):
    res = requests.get(f"https://www.horoscope.com/us/horoscopes/general/horoscope-general-weekly.aspx?sign={zodiac_sign}")
    soup = BeautifulSoup(res.content, 'html.parser')
    data = soup.find('div', attrs={'class': 'main-horoscope'})
    return data.p.text

Эта функция очень похожа на предыдущую. Мы только изменили URL-адрес на https://www.horoscope.com/us/horoscopes/general/horoscope-general-weekly.aspx?sign={zodiac_sign}.

Как получить гороскоп на месяц

def get_horoscope_by_month(zodiac_sign: int):
    res = requests.get(f"https://www.horoscope.com/us/horoscopes/general/horoscope-general-monthly.aspx?sign={zodiac_sign}")
    soup = BeautifulSoup(res.content, 'html.parser')
    data = soup.find('div', attrs={'class': 'main-horoscope'})
    return data.p.text

Эта функция также похожа на две другие, за исключением URL-адреса, который теперь изменен на https://www.horoscope.com/us/horoscopes/general/horoscope-general-monthly.aspx?sign={zodiac_sign}.

Как создавать маршруты API

Для создания наших маршрутов API мы будем использовать Flask-RESTX. Маршруты API будут выглядеть так:

  • для ежедневных или произвольных дат — /api/v1/get-horoscope/daily?Day=today&sign=capricorn или api/v1/get-horoscope/daily?Day=2022-12-14&sign=capricorn
  • на неделю — api/v1/get-horoscope/weekly?Sign=capricorn
  • на месяц — api/v1/get-horoscope/month?Sign=capricorn

У нас есть два параметра запроса в URL: day и sign. Параметр дня day может принимать такие значения, как today, yesterday или произвольные даты, например, 2022-12-14. Параметр sign примет название знака зодиака, которое может быть в верхнем или нижнем регистре, это не имеет значения.

Для синтаксического анализа параметров запроса из URL-адреса Flask-RESTX имеет встроенную поддержку проверки данных запроса с использованием библиотеки, похожей на argparse, под названием reqparse. Чтобы добавить аргументы в URL-адрес, мы будем использовать метод add_argument класса RequestParser.

parser = reqparse.RequestParser()
parser.add_argument('sign', type=str, required=True)

Параметр type будет принимать тип параметра. Выражение required=True делает параметр запроса обязательным для передачи.

Нам нужен еще один параметр запроса — day. Но этот параметр будет использоваться только в URL-адресе ежедневного гороскопа.

Вместо того, чтобы переписывать аргументы, мы можем написать родительский анализатор, содержащий все общие аргументы, а затем расширить его с помощью copy().

parser_copy = parser.copy()
parser_copy.add_argument('day', type=str, required=True)

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

Последние штрихи в построении API гороскопа

Основные строительные блоки, предоставляемые Flask-RESTX, – это resources. Resources построены на основе подключаемых представлений Flask. Это дает вам легкий доступ к нескольким HTTP-методам: нужно лишь определить определяя методы в вашем resource.

Давайте создадим класс DailyHoroscopeAPI, который наследует класс Resource от flask_restx.

@ns.route('/get-horoscope/daily')
class DailyHoroscopeAPI(Resource):
    '''Shows daily horoscope of zodiac signs'''
    @ns.doc(parser=parser_copy)
    def get(self):
        args = parser_copy.parse_args()
        day = args.get('day')
        zodiac_sign = args.get('sign')
        try:
            zodiac_num = ZODIAC_SIGNS[zodiac_sign.capitalize()]
            if "-" in day:
                datetime.strptime(day, '%Y-%m-%d')
            horoscope_data = get_horoscope_by_day(zodiac_num, day)
            return jsonify(success=True, data=horoscope_data, status=200)
        except KeyError:
            raise NotFound('No such zodiac sign exists')
        except AttributeError:
            raise BadRequest(
                'Something went wrong, please check the URL and the arguments.')
        except ValueError:
            raise BadRequest('Please enter day in correct format: YYYY-MM-DD')

Декоратор @ns.route() устанавливает маршрут API. Внутри класса DailyHoroscopeAPI у нас есть метод get, который будет обрабатывать запросы GET. Декоратор @ns.doc() поможет нам добавить параметры запроса в URL.

Чтобы получить значения параметров запроса, мы воспользуемся методом parse_args(), который вернет нам такой словарь:

{'sign': 'capricorn', 'day': '2022-12-14'}

Затем мы можем получить значения, используя day и sign.

Как было определено в начале, у нас будет словарь ZODIAC_SIGNS. Мы используем блок try-except для обработки запроса. Если знака зодиака нет в словаре, возникает исключение KeyError. В этом случае мы отвечаем ошибкой NotFound (ошибка 404).

Кроме того, если в параметре дня есть дефис, мы пытаемся сопоставить формат даты с ГГГГ-ММ-ДД. Если дата не в этом формате, мы выдаем ошибку BadRequest (Ошибка 400). В случае, если день не содержит дефиса, мы напрямую вызываем метод get_horoscope_by_day() с аргументами sign и day.

Если в качестве значения параметра передается какая-то тарабарщина это исключение AttributeError. В этом случае возникает ошибка BadRequest.

Два других маршрута также очень похожи на предыдущий. Разница в том, что здесь нам не нужен параметр day. Поэтому, вместо использования parser_copy(), мы будем использовать parser.

@ns.route('/get-horoscope/weekly')
class WeeklyHoroscopeAPI(Resource):
    '''Shows weekly horoscope of zodiac signs'''
    @ns.doc(parser=parser)
    def get(self):
        args = parser.parse_args()
        zodiac_sign = args.get('sign')
        try:
            zodiac_num = ZODIAC_SIGNS[zodiac_sign.capitalize()]
            horoscope_data = get_horoscope_by_week(zodiac_num)
            return jsonify(success=True, data=horoscope_data, status=200)
        except KeyError:
            raise NotFound('No such zodiac sign exists')
        except AttributeError:
            raise BadRequest('Something went wrong, please check the URL and the arguments.')


@ns.route('/get-horoscope/monthly')
class MonthlyHoroscopeAPI(Resource):
    '''Shows monthly horoscope of zodiac signs'''
    @ns.doc(parser=parser)
    def get(self):
        args = parser.parse_args()
        zodiac_sign = args.get('sign')
        try:
            zodiac_num = ZODIAC_SIGNS[zodiac_sign.capitalize()]
            horoscope_data = get_horoscope_by_month(zodiac_num)
            return jsonify(success=True, data=horoscope_data, status=200)
        except KeyError:
            raise NotFound('No such zodiac sign exists')
        except AttributeError:
            raise BadRequest('Something went wrong, please check the URL and the arguments.')

Теперь наши маршруты готовы. Чтобы протестировать API, вы можете использовать документацию Swagger, доступную в /route, или использовать Postman. Запустим сервер и протестируем.

Вы можете развернуть проект на общедоступном сервере, чтобы другие разработчики также могли получить доступ к API и использовать его.

Заключение

Итак, мы разобрались, как создать API гороскопа с помощью Beautiful Soup и Flask. Мы рассмотрели как скрапить данные с сайта с помощью библиотеки requests и Beautiful Soup. Затем мы создали API, используя Flask и Flask-RESTX.

Код, используемый в данной статье, вы можете найти по адресу https://github.com/ashutoshkrris/Horoscope-API

Перевод статьи «Python Project – How to Create a Horoscope API with Beautiful Soup and Flask».