Beautifulsoup 및 Fast API를 사용하여 웹 사이트를 API로 변환

어이, 동료들,
이 글은FastapI를 사용하여 첫 번째 프로젝트를 구축하는 방법을 보여 주고 싶습니다.
인터넷에는 도처에 재미있는 데이터가 있다.만약 이 데이터가 동적 사이트의 HTML에 숨겨져 있고, 응용 프로그램에서 이 데이터를 다시 사용하기를 원한다면 어떻게 해야 합니까?사이트를 API로!
여기서 FastAPI와 Beautifulsoups를 사용하여 웹 캡처 RESTful API를 구축하는 방법을 보여 드리겠습니다.

컨텐트

  • Scraping Repository Data
  • FastAPI
  • Deployment to Heroku
  • Conclusion and Future Directions
  • 1. 저장소 데이터 삭제


    처음에, Github에 수십 개의 요청을 보내지 않도록 트렌드 라이브러리 HTML 예시를 저장했습니다.저는 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)}')
    
    다른 저장소 데이터를 긁어내려고 시도한 후, 나는 Beautiful Soup이 모든 글을 믿을 수 없다는 것을 깨달았다.일부 연구에 따르면 다른 사람들도 이 점을 관찰했다.그래서 나는 해결 방법으로 필터 함수를 썼다.이 함수는 글 태그에 포함된 모든 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"입니다.원소.ResultSet 인스턴스의 길이는 항상 예상됩니다.다음에, 우리는 탕의 데이터를 방문하여 사전에 저장해야 한다.필요한 데이터를 포함하는 태그는 soups find 방법을 사용하거나 점 기호를 통해 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
    
    트렌드 개발자의 데이터에 대해 나는 또 다른scraping 함수를 짰다.자, 이제 HTML을 캡처할 수 있습니다. 사용자는 GET를 통해 데이터를 검색할 수 있어야 합니다.

    2.FastAPI


    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("/")는 GET 조작을 사용하여 "/" 경로에 대한 요청을 처리합니다.경로 조작 함수 index() 에서 검색 매개 변수를 처리합니다.코드 세그먼트는 선택할 수 있는 검색 매개 변수를 포함합니다.
    $ http -b http://0.0.0.0:8000/?myArg=hello
    
    {
        "data": "hello"
    }
    
    Github의 끝점과 유사한 끝점을 만들 것입니다.프로그래밍 언어는 경로 파라미터를 통해 지정할 수 있고, 날짜 범위와 구어는 선택할 수 있는 조회 파라미터를 통해 지정할 수 있다.다음 예는 다음과 같습니다./c++?since=weekly&spoken_lang=deFastapI를 사용하면 사용자가 선택할 수 있는 허용 데이터 세트를 정의할 수 있습니다.허용된 속성을 포함하고 Enum 클래스에서 계승하는 클래스를 만들어야 합니다.
    class AllowedDateRanges(str, Enum):
        daily = "daily"
        weekly = "weekly"
        monthly = "monthly"
    
    FastapI 문서를 열면 다음 세 가지 날짜 범위 옵션만 사용할 수 있습니다.

    라우팅 코드는 main.py 파일에 기록됩니다.경로 조작 함수는 허용된 경로 매개 변수 (프로그래밍 언어) 와 선택할 수 있는 조회 매개 변수 (날짜 범위와 언어) 만 수락합니다.
    @app.get("/repositories/{prog_lang}")
    async def trending_repositories_by_progr_language(
        since: AllowedDateRanges = None,
    ):
        return {"dateRange": since}
    
    알겠습니다. 이제 단점이 어떤 모양인지 알겠지만, 사용자가 서로 다른 옵션을 선택할 수 있기 전에, 로컬 HTML 복사본만 열지 않고 Github에서 필요한 HTML을 요청해서 웹 캡처를 동적으로 만들어야 합니다.Pythons 유명requests 모듈이 이 작업을 완성했다.목표는 사용자가 서로 다른 매개 변수 사이에서 선택하도록 하는 것이다.요청한 매개 변수는 필요한 HTML을 받기 위해 Github로 유효 로드로 리디렉션됩니다.
    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
    
    다음에 나는 세 부분을 함께 놓을 것이다. 사용자는 트렌드 저장소의 데이터를 요청할 수 있다.표시된 경로 조작 함수는 트렌드 메모리 라이브러리에 대한 검색 (프로그래밍 언어, 시간, 말하기) 을 지정할 수 있습니다. 이 인자들은 유효한 부하로 리셋되어 필요한 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"])
    
    그러나 애플리케이션의 성능은 어떻습니까?ApacheBenchk6 같은 전문적인 도구는 부하 테스트를 수행하는 데 사용되지만, 이 예에서 나는 작은 비동기 스크립트를 작성하여 응용 프로그램을 폭격하도록 요청했다.비동기 요청을 사용하지 않는 상황에서 동기화나 비동기 웹 응용의 성능을 비교하는 것은 무의미하다.나는 그것을 requests_benchmark.py라고 명명하고 tests/ 폴더에 넣을 것이다.이것은 대략적인 비교이므로, 나는 단지 동기 코드와 비동기 코드 간의 차이를 설명하고 싶을 뿐이다.
    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)
    
    나는 매번 20개의 요청을 수행하기 위해 스크립트를 세 번 실행했다.이제 동기화requests 라이브러리를 비동기aiohttp 라이브러리로 바꿉니다.또한 올바른 위치에 async/await 키워드를 추가했습니다.최종 코드는 다음과 같습니다.
    @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 스크립트를 사용하여 다시 세 번 측정했습니다.측정의 평균치를 계산하고 동기화와 비동기화 코드의 초당 요청을 스트라이프로 비교한다.비동기 코드의 성능은 대략 원래의 두 배이다.

    모든 트렌드 라이브러리와 개발자를 포함한 세 가지 노선을 작성할 것이다.그리고 우리의 마지막 임무는 응용 프로그램을 배치하는 것이다.

    3. Heroku에 배치


    우리는 Heroku를 사용할 것이다. 이것은 우수한 플랫폼인 서비스(PaaS) 클라우드 공급 업체이다.Heroku에 API를 배치하려면 heroku.yml 파일이 필요합니다...
    build:
      docker:
        web: Dockerfile
    
    ...Dockerfile이 하나 더 있습니다.docker 이미지에 대해 경량급 linux 버전을 사용합니다.이렇게 하면 docker build -t gh-trending-api . 명령을 실행할 때 80Mb 크기의 이미지가 생성됩니다.웹스크립트 lxml 패키지는 C 라이브러리 libxml 가 필요합니다.따라서 C 코드를 컴파일해야 하기 때문에 docker 용기를 구축하는 데 몇 분의 시간이 걸릴 수 있습니다.
    FROM python:3.9.2-alpine3.13
    
    LABEL maintainer="Niklas Tiede <[email protected]>"
    
    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}
    
    그리고 Dockerfile (port 5000) 의 CMD 명령에 정의된 포트를 외부 세계에 발표해야 합니다.컨테이너를 실행할 때 컨테이너 포트를 docker 호스트의 포트에 매핑해야 합니다.
    $ docker run -p 5000:5000 gh-trending-api:latest
    
    다음은 Github를 사용하여 배포 프로세스를 자동화합니다.우리는 release_and_deploy.yaml 폴더에 .github/workflow/ 파일을 만들고 다음 코드를 배치합니다.여기에는 Github 작업Deploy to 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/[email protected]
          with:
            heroku_api_key: ${{secrets.HEROKU_API_KEY}}
            heroku_app_name: "gh-trending-api"
            heroku_email: "[email protected]"
    
    Heroku의 계정 설정에서 복사HEROKU_API_KEY하고 Github 작업에 접근할 수 있도록 기밀로 Github 저장소에 보관합니다.이제 프로젝트의 태그를 원격 저장소에 밀어넣을 때마다 이 작업 흐름이 시작됩니다.이것은 프로젝트를 Heroku로 전송하고, Heroku는 우리 프로그램의 docker 용기를 구축하고 실행합니다.Dell 애플리케이션의 웹 주소는 https://gh-trending-api.herokuapp.com/입니다.
    아, 이렇게!저희가 예쁜 API를 하나 배치했어요.😙

    4. 결론과 미래 방향


    다음은 이 프로젝트의 전체 소스 코드입니다: Github Trending API
    나는 3일 동안 이 API를 구축하는 데 시간을 썼다.또한 파이톤스async/await 문법을 어떻게 사용하는지 이틀 동안 배워야 한다.그러나 비동기 코드를 사용하여 성능을 향상시켰지만 내가 예상한 것만큼은 아니었다.스크레이퍼 자체가 API의 병목현상인 것 같고 CPU 집약형이 좀 있습니다.나는 또 Beautifulsoups의 활약이 좋지 않다는 것을 발견했다..find 메서드는 수동으로 DOM 트리를 이동하는 것보다 느립니다.
    만약 이 API가 장래에 더욱 높은 유량을 가질 수 있다면 캐시 메커니즘을 실현하는 것은 매우 재미있을 것이다.Github는 매일 트렌드 라이브러리의 순위만 몇 번 업데이트하기 때문에 Github가 업데이트되기 전에 가장 자주 사용하는 순위를 메모리에 캐시하는 것이 더욱 효율적이다.이런 방법은 같은 데이터를 중복 요청하고 삭제하는 것을 피했다.이 작업을 위해 Redis 데이터베이스를 실현하는 것은 매우 재미있을 것이다.
    내가 또 말하고 싶은 것은, 나는GitHub의 트렌드 라이브러리를 긁어가고 싶지 않다는 것이다.이것은 이것Github trending API을 바탕으로 자바스크립트로 작성된 것이다.그들의 API는 아직 출시되지 않았기 때문에 파이톤과Fast API를 다시 사용해서 접근하고 싶습니다.
    나는 이 문장이 너에게 약간의 가치가 있기를 바란다.개선 제안을 환영합니다.관심 가져주셔서 감사합니다. 즐거운 하루 되세요!🙂
    내 블로그: the-coding-lab.com
    Github 재구매 계약: github.com/NiklasTiede/Github-Trending-API

    좋은 웹페이지 즐겨찾기