Рубрики
Без рубрики

Превращение сайта в API с BeautifulSoup и Fastapi

Heya Fellows, в этом посте я хочу показать, как создать свой первый проект с Fastapi. Интересные данные … Tagged с ShowDev, Python, WebDev, Docker.

Эйа, ребята,

В этом посте я хочу показать, как я создаю свой первый проект с Fastapi.

Интересные данные по всему сети. Что если они скрыты в HTML динамического веб -сайта, и вы хотите повторно использовать эти данные в своем собственном приложении? Превратите сайт в API!

Здесь я хочу показать вам, как построить API скрещивания в Интернете, используя Fastapi и BeautifulSoups!

  1. Соскабливание данных репозитория
  2. FASTAPI
  3. Развертывание в Героку
  4. Заключение и будущие направления

Сначала я сохранил образец трендовых репозиториев HTML, чтобы избежать отправки десятков запросов на GitHub. Я использую Httpie как клиент HTTP для выполнения запросов через терминал.

$ http -b https://github.com/trending > repositories.html

Каждый репозиторий заключен в состав статьи. Я открываю файл и пытаюсь соскрести HTML -документ.

import bs4

with open('repositories.html', 'r') as f:
    articles_html = f.read()

soup = bs4.BeautifulSoup(articles_html, "lxml")
articles =  soup.find_all("article", class_="Box-row")

print(f'number of articles: {len(articles)}')

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

def filter_articles(raw_html: str) -> str:

    raw_html = raw_html.split("\n")

    # count num of article tags (varies from 0 to 50):
    article_tags_count = 0
    tag = "article"
    for line in raw_html:
        if tag in line:
            article_tags_count += 1

    # copy HTML enclosed by first and last article-tag:
    articles_arrays, is_article = [], False
    for line in raw_html:
        if tag in line:
            article_tags_count -= 1
            is_article = True
        if is_article:
            articles_arrays.append(line)
        if not article_tags_count:
            is_article = False
    return "".join(articles_arrays)

Теперь созданный ‘BS4.Element. Примеры результатов всегда имеют ожидаемую длину. Затем мы должны получить доступ к данным в супе и хранить их в словаре. Теги, содержащие желаемые данные, можно получить с помощью супов, наводящих метод или, пройдя вдоль дерева DOM через точечную ноцию. Последнее предпочтительнее производительности! Каждый репозиторий описан 12 свойствами. Функция стала довольно длинной, поэтому я покажу только часть функции (соскребая 4 свойства).

def scraping_repositories(
    matches: bs4.element.ResultSet, 
    since: str
) -> typing.List[typing.Dict]:

    trending_repositories = []
    for rank, match in enumerate(matches):

        # relative url
        rel_url = match.h1.a["href"]

        # name of repo
        repository_name = rel_url.split("/")[-1]

        # author (username):
        username = rel_url.split("/")[-2]

        # language and color
        progr_language = match.find("span", itemprop="programmingLanguage")
            language = progr_language.get_text(strip=True)
            lang_color_tag = match.find("span", class_="repo-language-color")
            lang_color = lang_color_tag["style"].split()[-1]
        else:
            lang_color, language = None, None

        repositories = {
            "rank": rank + 1,
            "username": username,
            "repositoryName": repository_name,
            "language": language,
            "languageColor": lang_color,
        }
        trending_repositories.append(repositories)
    return trending_repositories

Для данных о разработчиках трендов я написал еще одну функцию соскоба. Хорошо, теперь, когда мы можем соскрести HTML, пользователи должны иметь возможность получить данные с помощью запроса GET.

Fastapi делает строительство API ветеркой. Вот пример:

import fastapi
import uvicorn

app = fastapi.FastAPI()

@app.get("/")
def index(myArg: str = None):
    return {"data": myArg}

if __name__ == "__main__":
    uvicorn.run(app, port=8000, host="0.0.0.0")

Декоратор операции пути @app.get ("/") обрабатывает запросы, которые перейдут в "/" маршрут с использованием операции получить. Функция работы пути index () Давайте обратимся с параметрами запроса. Фрагмент кода содержит дополнительный параметр запроса.

$ http -b http://0.0.0.0:8000/?myArg=hello

{
    "data": "hello"
}

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

/c ++? Поскольку = еженедельно и разговор

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

class AllowedDateRanges(str, Enum):
    daily = "daily"
    weekly = "weekly"
    monthly = "monthly"

При открытии документации FASTAPIS мы увидим, что будет доступно только 3 варианта для диапазона дат:

Код для маршрутизации будет записан в рамках main.py файл. Функция операции пути принимает только разрешенные параметры пути (языки программирования) и необязательные параметры запроса (диапазоны даты и разговорные языки).

@app.get("/repositories/{prog_lang}")
async def trending_repositories_by_progr_language(
    since: AllowedDateRanges = None,
):
    return {"dateRange": since}

Хорошо, теперь я знаю, как должны выглядеть конечные точки, но прежде чем пользователь вообще сможет выбирать между различными параметрами, я должен сделать динамику в Интернете, запрашивая желаемый HTML из GitHub вместо того, чтобы просто открыть локальную копию HTML. Питоны хорошо известны Запросы Модуль выполняет работу. Цель состоит в том, чтобы позволить пользователю выбирать между различными параметрами. Параметры запроса перенаправляются в качестве полезной нагрузки на GitHub для получения желаемого HTML.

import requests

payload = {
    'since': 'daily', 
    'spoken_language_code': 'en',
    }

prog_lang = 'c++'

resp = requests.get(f"https://github.com/trending/{prog_lang}", params=payload)
raw_html = resp.text

Затем я соберу 3 части: пользователь может запросить данные о трендовых репозиториях. Показанная функция операции пути дает нам возможность указать поиск репозиториев трендов (по языку программирования, периоду времени и разговорного языка. Эти аргументы перенаправляются в качестве полезной нагрузки, чтобы запросить желаемый HTML, который в последнее время скрежется и возвращается как JSON.

@app.get("/repositories/{prog_lang}")
def trending_repositories_by_progr_language(
    prog_lang: AllowedProgrammingLanguages,
    since: AllowedDateRanges = None,
    spoken_lang: AllowedSpokenLanguages = None,
):

    payload = {"since": "daily"}
    if since:
        payload["since"] = since._value_
    if spoken_lang:
        payload["spoken_lang"] = spoken_lang._value_

    resp = requests.get(f"https://github.com/trending/{prog_lang}", params=payload)
    raw_html = resp.text

    articles_html = filter_articles(raw_html)
    soup = make_soup(articles_html)
    return scraping_repositories(soup, since=payload["since"])

Но как работает приложение? Профессиональные инструменты, такие как Apachebench или K6 обычно используются для выполнения нагрузочного тестирования, но в этом случае я написал небольшой асинхронный скрипт, чтобы бомбить приложение с помощью запросов. Сравнение производительности синхронизационных или асинхронных веб -приложений без использования асинхронных запросов было бы бессмысленным. Я назову это requests_benchmark.py и поместите это в тесты/ папка. Имейте в виду, что это грубое сравнение, я просто хочу проиллюстрировать разницу между синхронным и асинхронным кодом.

import asyncio
import time
import aiohttp

URL = "http://127.0.0.1:8000/repositories/c++?since=weekly"
url_list = list([URL] * 50)

async def fetch(session, url):
    """requesting a url asynchronously"""
    async with session.get(url) as response:
        return await response.json()

async def fetch_all(urls, loop):
    """performaning multiple requests asynchronously"""
    async with aiohttp.ClientSession(loop=loop) as session:
        results = await asyncio.gather(
            *[fetch(session, url) for url in urls],
            return_exceptions=True,
        )
        return results

if __name__ == "__main__":
    t1_start = time.perf_counter()
    event_loop = asyncio.get_event_loop()
    urls_duplicates = url_list
    htmls = event_loop.run_until_complete(
        fetch_all(urls_duplicates, event_loop),
    )
    t1_stop = time.perf_counter()
    print("elapsed:", t1_stop - t1_start)

Я выполнил сценарий 3 раза, делая 20 запросов при каждом выполнении. ОК, теперь давайте заменим синхронное Запросы Библиотека от асинхронного aiohttp библиотека. Кроме того, мы добавляем Асинхронизация / ждет Ключевые слова на правильных позициях. Наш окончательный код будет выглядеть так:

@app.get("/repositories/{prog_lang}")
async def trending_repositories_by_progr_language(
    prog_lang: AllowedProgrammingLanguages,
    since: AllowedDateRanges = None,
    spoken_language_code: AllowedSpokenLanguages = None,
):

    payload = {"since": "daily"}
    if since:
        payload["since"] = since.value
    if spoken_language_code:
        payload["spoken_language_code"] = spoken_language_code.value

    url = f"https://github.com/trending/{prog_lang}"
    sem = asyncio.Semaphore()
    async with sem:
        raw_html = await get_request(url, compress=True, params=payload)
    if not isinstance(raw_html, str):
        return "Unable to connect to Github"

    articles_html = filter_articles(raw_html)
    soup = make_soup(articles_html)
    return scraping_repositories(soup, since=payload["since"])

Снова три измерения были сделаны с использованием requests_benchmark.py сценарий Среднее значение измерений было рассчитано, и запросы в секунду синхронного и асинхронного кода сравниваются как бархарт. Асинхронный код работает примерно вдвое лучше.

Еще три маршрута будут написаны, чтобы охватить все трендовые репозитории и разработчиков. Наша последняя задача – развернуть наше приложение.

Мы будем использовать Heroku, которая является отличной платформой в качестве облачного поставщика услуг (PAAS). Чтобы развернуть наш API в Heroku, нам нужен Heroku.yml файл…

build:
  docker:
    web: Dockerfile

… и Dockerfile. Для изображения Docker мы используем легкий Alpine Linux-распределение. Это приводит к изображению размером 80 МБ, которое построено при выполнении Docker Build -t GH-Trending-API. командование LXML Пакет, который мы используем для пакета WebStraing, требует libxml , C-библиотека. Поэтому нам нужно компилировать C-код, и, таким образом, создание контейнера Docker может занять до нескольких минут.

FROM python:3.9.2-alpine3.13

LABEL maintainer="Niklas Tiede "

WORKDIR /github-trending-api

COPY ./requirements-prod.txt .

RUN apk add --update --no-cache --virtual .build-deps \
    g++ \
    libxml2 \
    libxml2-dev && \
    apk add libxslt-dev && \
    pip install --no-cache-dir -r requirements-prod.txt && \
    apk del .build-deps

COPY ./app ./app

CMD uvicorn app.main:app --host 0.0.0.0 --port=${PORT:-5000}

Затем мы должны опубликовать порт, который мы определили в CMD Инструкция Dockerfile (порт 5000) к внешнему миру. Мы должны составить карту порта контейнеров на порт на хосте Docker при запуске контейнера:

$ docker run -p 5000:5000 gh-trending-api:latest

Затем мы автоматизируем процесс развертывания с помощью действий GitHub. Мы создаем release_and_deploy.yaml Файл в .github/Workflow/ Папка и поместите следующий код. Он содержит действие GitHub ” Развернуть в Heroku “Который сделает для нас развертывание.

name: GH Release, Publishing to Docker and Deployment to Heroku

on:
  push:
    tags:
      - 'v*.*.*'
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
  heroku-deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: Deploy on Heroku
      uses: akhileshns/heroku-deploy@v3.12.12
      with:
        heroku_api_key: ${{secrets.HEROKU_API_KEY}}
        heroku_app_name: "gh-trending-api"
        heroku_email: "niklastiede2@gmail.com"

Мы копируем Heroku_api_key Из настройки учетной записи Heroku и сохранить это как секрет в нашем репозитории GitHub, чтобы наше действие GitHub мог получить к нему доступ. Теперь каждый раз, когда мы вкладываем тег нашего проекта в удаленный репозиторий, этот рабочий процесс начинается. Он подталкивает проект к Heroku, который будет построить и запустить контейнер Docker в нашем приложении. URL нашего приложения может быть достигнут в https://gh-drending-api.herokuapp.com/

Ааа и вот и все! Мы развернули красиво выглядящий API 😙

Вот полный исходный код проекта: GitHub Trending API

Мне потребовалось 3 дня, чтобы построить этот API. Потребовались еще 2 дня, чтобы узнать, как использовать питоны асинхро / ждет синтаксис. Но использование асинхронного кода увеличил производительность не так сильно, как я ожидал. Похоже, что соскоба, кажется, является узким местом API, это своего рода интенсивный процессор. Я также узнал, что производительность BeautifulSoups не так хороша. Используя .найти Метод медленнее, чем спускаться по дереву DOM вручную.

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

Я также хочу упомянуть, что идея соскребания репозиториев GitHub не является моей. Он основан на этом GitHub Trending API написано в JavaScript. Их API в настоящее время не в сети, и поэтому я хотел сделать его снова доступным, используя Python и Fastapi!

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

Мой блог: Кодинг-lab.com GitHub Repo: github.com/niklastiede/github-drending-api

Оригинал: “https://dev.to/niklastiede/building-an-api-with-fastapi-1iji”