알고리즘 스터디로 하루에 1문제를 풀고 있는데, 매일 어떤 문제를 풀지 결정하고 Readme에 업데이트 하는게 번거로웠다. 깃허브 액션을 사용해서 문제 선정과 업데이트를 자동화 해보자.
1. 문제 은행 만들기
문제는 프로그래머스의 레벨 2를 목표로 했다. 문제 목록을 크롤링하기 위해서 프로그래머스에서 문제 목록을 어떻게 불러오는지 확인했다. API를 호출해서 문제 목록을 불러오는 것을 확인할 수 있었다. 요청은 아래와 같은 형식으로, perPage와 page 파라미터로 페이지네이션을 한다고 생각했다.
https://school.programmers.co.kr/api/v1/school/challenges/?perPage=20&levels[]=2&languages[]=python3&order=recent&search=&page=1
그런데 perPage 파라미터의 값을 바꿔도 항상 20개만 반환하는 것을 보니 서버에서 막아둔 것 같다.
응답 사진을 보면 totalPages를 반환한다. 그래서 문제의 totalPages에 도달할 때 까지 요청을 나눠서 보내는 방식으로 문제를 수집했다.
# problem_crawler.py
import requests
import json
def get_total_pages(url):
response = requests.get(url + "1")
if response.status_code == 200:
data = json.loads(response.content)
return data['totalPages']
return 0
def get_programmers_level2_python3_problems(url, page):
response = requests.get(url + str(page))
if response.status_code == 200:
data = json.loads(response.content)
return data['result']
return []
def crawling():
url = "https://school.programmers.co.kr/api/v1/school/challenges/?perPage=50&levels[]=2&languages[]=python3&order=recent&search=&page="
total_pages = get_total_pages(url)
problems = []
for i in range(1, total_pages + 1):
problems.extend(get_programmers_level2_python3_problems(url, i))
with open("problems.json", "w") as f:
json.dump(problems, f, indent=4)
if __name__ == '__main__':
crawling()
2. ReadMe 업데이트 스크립트
랜덤으로 안 푼 문제 하나를 뽑아서 ReadMe를 수정한다.
# update_readme.py
import datetime
import json
import re
import random
from problem_crawler import crawling
# 최초 1회차는 미리 정해져 있어야함
def update(p):
with open("README.md", "r") as f:
pattern = r"\d+"
match = re.search(pattern, f.readlines()[-1])
n = match.group()
with open("README.md", "a+") as f:
today = datetime.date.today()
template = f"\n| {int(n) + 1}회차({today.year}.{today.month}.{today.day}) | [{p['title']}](https://school.programmers.co.kr/learn/courses/30/lessons/{p['id']})|"
f.write(template)
# 랜덤으로 안 푼 문제 하나 뽑음
def choose_one():
with open("problems.json", "r") as f:
problems = json.load(f)
problem = problems[random.randrange(len(problems))]
while problem.get('solved', False):
problem = problems[random.randrange(len(problems))]
problem['solved'] = True
with open("problems.json", "w") as f:
json.dump(problems, f, indent=4)
return problem
if __name__ == '__main__':
try:
with open("problems.json", "r") as f:
pass
except:
crawling()
update(choose_one())
3. Workflow
업데이트 스크립트를 실행하고, 업데이트한 ReadMe를 커밋, 푸시하도록 한다. 어떻게 ReadMe를 파일 입출력으로 수정할 수 있을까? 이게 가능한 이유는 actions/checkout@v2가 저장소를 Github Action이 실행되는 클라우드 가상 환경의 로컬 파일 시스템으로 복제하기 때문에 파일 입출력으로 ReadMe를 수정할 수 있다.
참고로 적어두자면 cron의 최소 실행 주기는 5분이라고 Github docs에서 설명하고 있다.
name: Update README
on:
schedule:
- cron: '0 0 * * 1-5' # 분 시 일 월 요일 -> (UTC+0 기준) 평일 9시 0분에 실행
jobs:
update-readme:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run update script
run: python update_readme.py # 예시 스크립트
- name: Commit and push if changed
run: |
git config --global user.email "begreat@kakao.com"
git config --global user.name "OuOHoon"
git add README.md
git commit -m "Update README" -a || echo "No changes to commit"
git push
잘 실행되는 것을 볼 수 있다.
Github Action Workflow 문법 메모
예제 워크플로 코드와 함께 메모해둔다.
name: Example Workflow
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run a script
run: echo "Hello, world!"
name: 워크플로의 이름이다. Github UI에서 워크플로를 식별하는데 사용한다.
on: 워크플로가 어떤 이벤트에 의해 트리거될지 정의한다. push, pull_request, schedule 등의 이벤트가 있다.
jobs: 워크플로는 하나 이상의 작업(job)으로 구성된다. 각 작업은 독립적으로 실행되며, 여러 단계(step)를 포함할 수 있다.
runs-on: 실행할 환경(ubuntu-latest, windows-lastest)을 지정한다.
steps: 각 작업은 여러 단계로 구성된다. 각 단계는 uses(다른 액션을 사용할 때) 또는 run(커맨드 라인 명령을 실행할 때) 키워드를 사용해서 정의한다.
env: 환경 변수를 설정한다. 이 변수들은 워크플로의 모든 단계에서 사용할 수 있다.