
오하아사 디스코드 봇은 지난 화요일 퇴근 후 충동적으로 만들기 시작해, 수요일과 목요일 양일 짧은 테스트를 마치고 디스코드 서버에 투입한 프로젝트였다.
하지만 다시 생각해도, 제대로 검증하기에는 부족한 기간이었던 것 같다.
나는 이전 포스트에서 올렸던 아사히 방송 아침별점 사이트가 매일 업로드되는 줄 알았다.
하지만 다시 사이트를 살펴보니... 월요일~금요일 아침 5시부터였다.


결국 첫 주 토요일, 금요일과 똑같은 오하아사 순위가 올라가 이상함을 눈치챘다.
원인을 파악하기 위해 아침별점 X 계정은 주말에 어떤 사이트에서 순위 정보를 가져오나 찾아봤는데, 다른 사이트의 링크가 있다는 것을 알게 된다.
심지어 두 사이트의 구조와 리스트 아이템의 이름이 달랐다. 평일 사이트는 영어 학명인 데에 비해, 주말 사이트는 일본어 영어 표기로 작성되어 있었다.
비슷하지만 내용의 코드를 작성하는 과정에서, 코드의 재활용성을 높이기 위해 SOLID 원칙에 의거하여 리팩토링을 진행했다.
요일을 확인해야 하는 라이브러리가 필요해 datetime을 추가한다.
from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
import requests
import os
from datetime import datetime
평일과 주말 사이트에 모두 대응하기 위해 요일별로 매핑 데이터를 새로 구성했다.
지난 코드에서는 별자리 별로 맵핑한 게 다였지만, 이번엔 url과 selector 정보도 함께 매핑했다.
SIGN_CONFIG = {
"weekday": {
"url": "https://www.asahi.co.jp/ohaasa/week/horoscope/",
"selector": "ul.oa_horoscope_list li",
"map": {
"aries": "양자리", "taurus": "황소자리", "gemini": "쌍둥이자리",
"cancer": "게자리", "leo": "사자자리", "virgo": "처녀자리",
"libra": "천칭자리", "scorpio": "전갈자리", "sagittarius": "사수자리",
"capricorn": "염소자리", "aquarius": "물병자리", "pisces": "물고기자리"
}
},
"weekend": {
"url": "https://www.tv-asahi.co.jp/goodmorning/uranai/",
"selector": ".rank-box li",
"map": {
"ohitsuji": "양자리", "ousi": "황소자리", "futago": "쌍둥이자리",
"kani": "게자리", "sisi": "사자자리", "otome": "처녀자리",
"tenbin": "천칭자리", "sasori": "전갈자리", "ite": "사수자리",
"yagi": "염소자리", "mizugame": "물병자리", "uo": "물고기자리"
}
}
}
Playwright를 사용하여 HTML 소스를 가져오는 함수도 단일화 했다.
여기에 주말 사이트는 networkidle 이슈가 있어 domcontentloaded가 확인될 때까지 대기하는 코드를 추가했다.
def fetch_html(url, selector, timeout=20000):
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(user_agent="Mozilla/5.0")
page = context.new_page()
try:
page.goto(url, wait_until="domcontentloaded", timeout=40000)
page.wait_for_selector(selector, timeout=timeout)
return page.content()
finally:
browser.close()
브라우저를 실행하거나(Playwright), 네트워크 요청을 보내거나(Requests), 파일에서 읽어오는 로직과 같은 외부 환경 관련 기능을 다른 함수로 분리한 후, 오직 '추출 규칙'의 변화에만 반응하는 함수를 별도로 만들었다.

주중과 주말의 추출 로직이 달라져야 하므로, 입력되는 mode에 따라 추출하는 규칙을 다르게 작동하도록 만들었다.
def parse_horoscope_data(html, mode):
soup = BeautifulSoup(html, 'html.parser')
config = SIGN_CONFIG[mode]
results = []
if mode == "weekday":
items = soup.select(config["selector"])
for item in items:
classes = item.get('class', [])
sign_key = next((c for c in classes if c in config["map"]), "unknown")
results.append(sign_key)
else:
# 주말 사이트는 a 태그의 data-label 사용
items = soup.select(f"{config['selector']} a")
for item in items:
sign_key = item.get('data-label', '').strip().lower()
results.append(sign_key)
return results
주중과 주말 모두 같은 출력 포맷을 유지하도록 메세지 구성 로직을 통일시켰다.
def format_message(sign_keys, mode):
if not sign_keys:
return "❌ 데이터를 찾지 못했습니다. 사이트 구조가 변경되었을 수 있습니다."
config = SIGN_CONFIG[mode]
msg_lines = ["✨ **오늘의 오하아사 별자리 순위** ✨\n"]
for rank, key in enumerate(sign_keys, start=1):
korean_sign = config["map"].get(key, f"알 수 없음({key})")
if rank == 1: emoji = "🥇"
elif rank == 2: emoji = "🥈"
elif rank == 3: emoji = "🥉"
else: emoji = "🔹"
msg_lines.append(f"{emoji} **{rank}위**: {korean_sign}")
return "\n".join(msg_lines)
이제 위에 작성한 함수들을 하나로 모아 실제로 실행되는 메인 함수를 만든다.
각 기능들의 추상화가 이루어졌기에 변화가 안정적이게 되었다.
상위 개념인 정책이 하위 개념인 도구에 의존하지 않게 설계되었기 때문에, 이 함수는 프로그램의 중심 잡대 역할을 하게 된다.
def get_horoscope_ranking(mode):
try:
# 1. 모드에 맞는 설정을 확인
config = SIGN_CONFIG[mode]
# 2. 데이터 가져오기
html = fetch_html(config["url"], config["selector"])
# 3. 원본에서 의미 있는 정보 추출
sign_keys = parse_horoscope_data(html, mode)
# 4. 추출된 정보의 변환
return format_message(sign_keys, mode)
except Exception as e:
return f"❌ 크롤링 중 에러 발생 ({mode}): {e}"
datetime의 현재 요일을 가져오는 함수는 weekday()다.
0번부터 월요일, 1번은 화요일,...6번은 일요일이다.
UTC 기준 평일과 주말은 다음과 같다.
가져온 출력으로 다음과 같이 mode를 달리하여 결과값을 가져오도록 메인 함수를 구성한다.
if __name__ == "__main__":
weekday = datetime.now().weekday()
mode = "weekend" if weekday in [4, 5] else "weekday"
# 한국 시간이 적용된 로컬 환경이라면 아래 코드를 사용한다
# mode = "weekday" if weekday < 5 else "weekend"
result_message = get_horoscope_ranking(mode)
send_discord(result_message)

오전 6시에 발견한 문제는 정확히 12시간 뒤, 오후 6시에 수정되어 적용됐다. 아침부터 오후까지 약속이 있었다
혹시 추가적인 문제가 발생하지 않기를 바라겠지만... 다음 일주일 동안은 추가로 확인되는 이슈를 종합해 한 번에 적용할 예정이다.