목표
국민대학교 학사일정을 스크랩해와서 내 웹사이트의 캘린더 컴포넌트에 추가하는 것이 목표이다.
학사일정은 당해 3월부터 다음해 2월까지 포함한다. 그래서 매년 3월 1일마다 자동으로 해당연도를 스크랩해왔으면 했다.
노마드코더의 파이썬 스크래퍼 만들기 강의를 수강한 후 흉내내어 스크립트를 만들었고, 챗지피티에게 마무리를 맡겨서(class를 만들어 주었다) 최종 스크래퍼를 완성했다.
scraper.py
import requests
from bs4 import BeautifulSoup
BeautifulSoup를 사용해서 스크래핑을 한다. requests로 목표 웹사이트의 url에서 get하고 결과물을 api에 post한다.
def __init__(self, year):
self.year = year
self.all_events = []
self.url = f"{...}/index.do?yyyy={self.year}"
학사일정 url을 보니 저 부분이 연도를 담당하고 있었다.
response = requests.get(self.url)
soup = BeautifulSoup(response.content, "html.parser")
events = soup.find("table", id="monthTable").find_all("tr")
date_str = title.find_previous_sibling("td") if title else None
pattern = r"(\d{1,2})\.(\d{1,2})\s\(\w\)\s~\s(\d{1,2})\.(\d{1,2})\s\(\w\)"
이런 식으로 전체 페이지 html에서 원하는 정보로 좁혀 가고
event_data = {
"title": title.text.strip(),
"start_date": start_date,
"end_date": end_date,
}
self.all_events.append(event_data)
이런걸 반복문으로 매 일정마다 해준다. 여기까지가 EventScraper라는 class에서 하는 일이다.
scraper = EventScraper(2025)
scraped_data = scraper.get_events()
response = requests.post(api_url, json=scraped_data)
이런식으로 원하는 연도를 인자로 넣어서 돌리고 결과를 api로 쏘아준다.
파이썬을 어떻게 돌리지
근데 파이썬은 자바스크립트 런타임?에서 실행을 못하는 모양이다. 그렇다고 일년에 한 번 돌아가는 코드를 위해 파이썬 서버를 만들어주는 것은 본능적으로 꺼려진다. 파이썬은 구글코랩에서밖에 안해봤는데. 웹 프로젝트의 일부로 돌려야 한다면 어떻게 돌려야 할까?
파이썬 스크래퍼가 replit에서 잘 되니, 본 프로젝트에서 뭔가 트리거를 했을때 replit이 돌아가게 하면 되지 않을까? 가 첫번째 아이디어였다. 하지만 어떻게 해야할지 모르겠어서 못했다. Flask 같은거 자꾸 하라는데 그게 파이썬 서버 아닌가? 아직 파이썬에 대해 깊게 들어가고 싶지는 않았다.
그다음으로는 스크래퍼를 자바스크립트로 번역(?)하는 것을 시도했었다. 그렇게 하면 자바스크립트 런타임으로 끌고 들어올 수 있고, replit같은거 필요 없이 내 프로젝트 저장소로 코드를 가지고 들어올 수 있다. 근데 아무리 해도 자바스크립트로 똑같이 구현이 안됐다. 하지만 인터넷에는 자바스크립트로 웹 스크래퍼 만든 사람이 많이 보이는 걸 보면 문제가 자바스크립트가 아니라 나라는 건 확실하다. 어쨌든 아무리 챗지피티를 갈궈도 자바스크립트 스크래퍼는 [] 이렇게 빈 배열만을 반환했다. 그 와중에 status는 200으로 성공적이었다. 인터넷에서 하란 대로 헤더에 user agent 같은것도 바꿔봤는데 안됐다.
디버깅을 시도하는 중에 뭔가 데이터가 찍히는 건 확인했는데 내가 예상했던 html이 아니라 개발중에 사용한 변수이름 리스트? 같은 예상치 못한 구조로 나와서 관뒀다. 그리고 그때 국민대 학사일정 사이트도 FullCalendar 라이브러리를 사용해서 달력을 만들고 있다는 것을 알게 되었다.
사용한 툴들?
랜덤하게 api 엔드포인트를 만들어주고 거기로 온 request들을 실시간으로 보여주는 webhook.site를 많이 이용했다. next.js에서 api 엔드포인트를 동작하게 만드는 데 성공하기 전까지 말이다.
그리고 내가 만든 api 엔드포인트가 제대로 만들어졌는지 확인할 때 Insomnia도 가끔 사용했다. Postman은 뭐가 다른지 나중에 한 번 써봐야겠다.
파이썬을 어떻게 돌리지 - GitHub Actions
답은 먼 곳에 있지 않았다. 깃헙에 GitHub Actions라는 게 있었다. 이게 cron job이라고 정해진 시간에 맞춰서 뭔가를 실행해주는 것도 해주고 파이썬도 돌려준다.
방법: 파이썬 스크립트를 포함한 리포지토리의 Actions 탭에 들어가서 새 workflow를 만든다. 그리고 YML에 맞춰서 workflow를 작성한 후 커밋한다. 그러면 .github/workflows에 저장된다.
name: Run Scraper Every March
on:
schedule:
- cron: '*/5 * * * *' // 테스트용
workflow_dispatch:
jobs:
scraper:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run script
run: python scraper.py
- name: Check for file changes
id: changed-files
run: |
if [[ -n $(git status --porcelain) ]]; then
echo "files_changed=true" >> $GITHUB_ENV
fi
- name: Commit and push changes
if: env.files_changed == 'true'
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "[email protected]"
git add .
git commit -m "Update scraped data" || exit 0
git push
이때 스크립트는 경로만 잘 적어주면 아무데나 위치시켜도 되는데 나는 requirements.txt와 함께 root에 넣었다.
그리고 vscode에서 수정했는데
Can't push refs to remote. Try running 'Pull' first to integrate your changes.
가 뜨면서 커밋이 안됨.
refusing to allow a Personal Access Token to create or update workflow `...` without `workflow` scope
이런 것도 뜸.
여기서 해결. 토큰을 repo, workflow 선택해서 다시 만들고 Keychain Access에서 넣어주면 됨
5분마다 한 번으로 설정했는데 한번은 2분만에 됐고 한시간이 지나도 안되기도 했다. 원래 그렇다고 한다. 그 다음에는 매시간 48분으로 설정(48 * * * * )해 놓았었는데 항상 50분이 넘어서 돌아갔다. 정확한 시간이 중요한 기능은 아니니까 내버려 두기로 했다.
스크립트 수정: api에 post하지 말고 json 저장
JSON을 api에 넣었다가 fetch하는 것보다 로컬에 json파일로 저장해놨다가 읽는게 더 쉽다고 해서 그러기로 했다. 그게 캐싱하는것과 비슷한 효과라고 한 것 같다.
import os
import json
os와 json을 import하고 (이건 requirement에 안적어도 된다 이유는 까먹음)
def save_to_json(self, filename="app/data/academic_calendar.json"):
os.makedirs(os.path.dirname(filename), exist_ok=True)
try:
with open(filename, "w", encoding="utf-8") as f:
json.dump(self.all_events, f, ensure_ascii=False, indent=4)
print(f"Data saved to {filename}")
except Exception as e:
print(f"Error saving JSON: {e}")
이렇게 기존에 있는 경로인 app/data에다가 기존에 없는 파일인 academic_calendar.json을 만들어서 넣어달라고 했다.
Run git config --global user.name "GitHub Actions"
[main dfda96e] Update scraped data
1 file changed, 222 insertions(+)
create mode 100644 app/data/academic_calendar.json
remote: Permission to {...} denied to github-actions[bot].
fatal: unable to access {...}: The requested URL returned error: 403
Error: Process completed with exit code 128.
이런! 뭔가 안됐다. 로그를 보니 파일을 만드는것까진 성공했는데 권한이 없다고 Commit을 못했다.
이렇게 하니까 해결됐다. 이제 app/data에 학사일정이 json으로 정리되어서 들어있다. 🎉
교내 정보 시스템 정기점검 작업
다음날 이상한 일이 일어났다. actions-user가 커밋한 걸 보니까 잘 있던 캘린더 데이터를 지워버린 것이었다. GitHub Actions 로그를 봤더니 Error during HTTP request: 404 Client Error: Not Found for url:
이 찍혀 있었고 url로 들어가 보니 아니나 다를까 국민대 홈페이지가 정기점검 중이었다.
이렇게 에러가 일어났을 경우 데이터를 저장하지 말고 종료해야 한다.
import sys
- 에러를 받는 코드 밑에
sys.exit(1)
을 써준다.
except requests.RequestException as e:
print(f"Error during HTTP request: {e}")
sys.exit(1)
except Exception as e:
print(f"An error occurred during parsing: {e}")
sys.exit(1)