백엔드

[BOJ-AutoSync] 3. FastAPI로 BOJ 조회 API 개발하기

곽곽 2025. 3. 16. 20:54

어느 정도 BOJ 조회 Python 코드가 작성됐으니, 이젠 이 함수들을 REST API로 바꿔봅시다!

저는 FastAPI를 이용해 기존 코드를 API로 구현했습니다. 이번에는 FastAPI 프로젝트 구조에 대해 포스팅해보겠습니다.

 

프로젝트 구조

app/
│── main.py              # FastAPI 실행 진입점
│── config.py            # 환경 변수 관리
│── services/
│   ├── selenium_driver.py   # WebDriver 세션 유지
│   ├── auth.py              # 로그인 기능 + 쿠키 저장
│   ├── scraper.py           # 제출 내역 크롤링 & 코드 가져오기
│── routers/
│   ├── auth_routes.py       # 로그인 관련 API 라우트
│   ├── submission_routes.py # 제출 내역 및 코드 조회 API 라우트
│── models/
│   ├── schemas.py           # Pydantic 데이터 모델 정의
│── requirements.txt         # 필요한 라이브러리 목록

 

services 디렉토리

services는 외부 서비스나 내부 비즈니스 로직을 처리하는 코드들을 모아둔 폴더입니다.

selenium_driver.py

Selenium WebDriver 세션을 생성하고 유지하는 모듈입니다. Selenium을 통해 BOJ 페이지에 접속하고, 필요한 데이터를 가져올 때 재사용할 수 있도록 WebDriver 인스턴스를 관리합니다.

from selenium import webdriver
import time

driver = None

def get_driver():
    global driver
    if driver is None:
        options = webdriver.ChromeOptions()
        # options.add_argument("--headless")  # GUI 없이 실행        
        # options.add_argument("--disable-gpu")  
        # options.add_argument("--window-size=1920x1080")
        # options.add_argument("--no-sandbox")
        # options.add_argument("--disable-dev-shm-usage")
        # options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36")
        driver = webdriver.Chrome(options=options)
    return driver

def fetch_url(url):
    get_driver()
    driver.get(url)
    time.sleep(3)  # 페이지 로드 대기

    page_source = driver.page_source
    return page_source

boj_auth.py

BOJ 로그인 기능과 쿠키 저장 로직을 담당합니다. BOJ에 로그인한 상태를 유지하기 위해 세션 쿠키를 잘 관리해야 하므로, 이 모듈에서는 로그인 처리와 세션 유지에 관한 메서드를 구현합니다.

from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from app.services.selenium_driver import get_driver
from app.config import BASE_URL

def login(boj_id: str, boj_pwd: str):
    driver = get_driver()
    driver.get(f"{BASE_URL}/signin")

    try:
        # 최대 10초 동안 로그인 필드가 나타날 때까지 대기
        id_input = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.NAME, "login_user_id"))
        )
        id_input.send_keys(boj_id)

        pw_input = driver.find_element(By.NAME, "login_password")
        pw_input.send_keys(boj_pwd)
        pw_input.send_keys(Keys.RETURN)

        WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.LINK_TEXT, "로그아웃"))
        )

        print(f"{boj_id} 로그인 성공")
        return True
    except Exception as e:
        print(f"로그인 실패: {e}")
        driver.quit()
        return False

html_parser.py

크롤링된 제출 내역으로부터 특정 제출 번호에 대한 코드를 가져오는 메인 로직이 담긴 파일입니다.

from bs4 import BeautifulSoup

def parse_submission_row(columns):
    submission_id = columns[0].text.strip()  # 제출 ID
    user_id = columns[1].find("a").text.strip()  # 사용자 아이디
    problem_id = columns[2].find("a").text.strip()  # 문제 번호
    problem_title = columns[2].find("a").get("title", "").strip()  # 문제 제목
    result = columns[3].find("span").text.strip()  # 채점 결과
    memory = columns[4].text.strip() + "KB"  # 메모리 사용량
    time = columns[5].text.strip() + "ms"  # 실행 시간
    language = columns[6].text.strip()  # 사용 언어
    code_length = columns[7].text.strip() + "B"  # 코드 길이

    return {
        "submission_id": submission_id,
        "user_id": user_id,
        "problem_id": problem_id,
        "problem_title": problem_title,
        "result": result,
        "memory": memory,
        "time": time,
        "language": language,
        "code_length": code_length,
    }

def parse_submission_list(html_content):
    soup = BeautifulSoup(html_content, "html.parser")
    submission_list = {}

    for row in soup.select("tr"):
        cols = row.find_all("td")
        if len(cols) == 0:
            continue
        
        info = parse_submission_row(cols)
        submission_list[info["problem_id"]] = info
    
    return submission_list

def parse_source_code(html_content):
    soup = BeautifulSoup(html_content, "html.parser")

    code_box = soup.find("textarea", {"class": "codemirror-textarea"})
    source_code = code_box.text.strip() if code_box else "코드 없음"

    return source_code

 

routers 디렉토리

routers는 실제로 API 라우트가 정의되는 폴더입니다.

boj.py

BOJ 로그인 기능과 제출 내역 조회 및 제출 코드를 조회하는 API 라우트가 이곳에 정의됩니다.

from fastapi import APIRouter, HTTPException
from app.services.auth import login
from app.services.html_parser import parse_submission_list, parse_source_code
from app.services.selenium_driver import fetch_url
from app.models.schemas import BojLoginRequest
from app.config import BASE_URL

router = APIRouter(prefix="/boj", tags=["Boj"])

@router.post("/login")
def login_api(request: BojLoginRequest):
    if login(request.boj_id, request.boj_pwd):
        return {"message": "Login successful", "status_code": 200}
    else:
        return {"message": "Login failed", "status_code": 401}

@router.get("/submissions/{boj_id}")
def get_submission_list(boj_id: str):
    status_url = f"{BASE_URL}/status?user_id={boj_id}&result_id=4"
    html_content = fetch_url(status_url)
    submission_list = parse_submission_list(html_content)

    if not submission_list:
        raise HTTPException(status_code=404, detail="No submission list")
    return submission_list

@router.get("/submissions/code/{submission_id}")
def get_source_code(submission_id: str):
    status_url = f"{BASE_URL}/source/{submission_id}"
    html_content = fetch_url(status_url)
    source_code = parse_source_code(html_content)

    if not source_code:
        raise HTTPException(status_code=404, detail="No source code")
    return source_code

 

models 디렉토리

이 디렉토리에는 주로 데이터 모델을 정의하는 파일들이 들어갑니다. FastAPI와 함께 사용되는 Pydantic 모델이나, DB 모델이 있다면 이 폴더에서 관리합니다.

schemas.py

Pydantic을 이용해 데이터 모델(스키마)을 정의한 파일입니다. FastAPI에서 요청이나 응답으로 주고받을 데이터 형태를 명확히 기술함으로써, 입력 데이터 검증과 자동 문서화가 이뤄집니다.

from pydantic import BaseModel

class BojLoginRequest(BaseModel):
    boj_id: str
    boj_pwd: str

 

main.py

FastAPI 애플리케이션의 진입점 파일입니다. FastAPI 객체를 생성하고, routers 폴더 내에 정의된 라우트를 연결(등록)합니다. 또, 서버 실행을 위한 uvicorn.run 함수를 호출할 때 필요한 설정도 이곳에서 구성합니다.

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import boj_routers

app = FastAPI()

app.include_router(boj_routers.router)

@app.get("/")
def read_root():
    return {"message": "Hello, FastAPI!"}

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

 

config.py

환경 변수를 관리하는 모듈입니다. 예를 들어 API 토큰, DB 접속 정보, Selenium 설정과 같은 민감한 정보를 .env 파일 또는 운영 환경에서 받을 수 있도록 구성할 수 있습니다.

import os

BASE_URL = "https://www.acmicpc.net"