Эйа, ребята,
В этом посте я хочу показать, как я создаю свой первый проект с Fastapi.
Интересные данные по всему сети. Что если они скрыты в HTML динамического веб -сайта, и вы хотите повторно использовать эти данные в своем собственном приложении? Превратите сайт в API!
Здесь я хочу показать вам, как построить API скрещивания в Интернете, используя Fastapi и BeautifulSoups!
- Соскабливание данных репозитория
- FASTAPI
- Развертывание в Героку
- Заключение и будущие направления
Сначала я сохранил образец трендовых репозиториев 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”