Skip to content

Commit 04ceebe

Browse files
committed
[feat] solved 서버 터지는거 대비 추가
1 parent fba46f1 commit 04ceebe

4 files changed

Lines changed: 322 additions & 42 deletions

File tree

common/boj_utils.py

Lines changed: 104 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ async def get_user_solved_problems_from_solved_ac(baekjoon_id: str, target_probl
186186
# 전체 목록이 필요하거나 문제가 많으면 페이지 크롤링 사용
187187
return await _get_all_solved_problems_via_pages(baekjoon_id, target_problems, headers)
188188

189+
except (aiohttp.ClientConnectorError, aiohttp.ServerTimeoutError, asyncio.TimeoutError) as e:
190+
logger.error(f"[solved.ac 크롤링] solved.ac 서버 연결 실패: {e} (서버 다운 가능성)")
191+
return []
189192
except Exception as e:
190193
logger.error(f"[solved.ac 크롤링] 오류: {e}", exc_info=True)
191194
return []
@@ -206,7 +209,8 @@ async def check_problems_individual_queries(baekjoon_id: str, target_problems: L
206209

207210
logger.info(f"[개별 문제 확인] {baekjoon_id} - {len(target_problems)}개 문제 개별 확인 시작")
208211

209-
async with aiohttp.ClientSession(headers=headers) as session:
212+
timeout = aiohttp.ClientTimeout(total=10)
213+
async with aiohttp.ClientSession(headers=headers, timeout=timeout) as session:
210214
for problem_id in target_problems:
211215
# 각 문제마다 query: s@{handle}+{problem_id}
212216
query = f"s@{baekjoon_id}+{problem_id}"
@@ -216,7 +220,7 @@ async def check_problems_individual_queries(baekjoon_id: str, target_problems: L
216220
try:
217221
async with session.get(url) as response:
218222
if response.status != 200:
219-
logger.debug(f"[개별 문제 확인] {baekjoon_id} - 문제 {problem_id}: HTTP {response.status}")
223+
logger.debug(f"[개별 문제 확인] {baekjoon_id} - 문제 {problem_id}: HTTP {response.status} (서버 문제 가능성)")
220224
await asyncio.sleep(0.2)
221225
continue
222226

@@ -249,6 +253,10 @@ async def check_problems_individual_queries(baekjoon_id: str, target_problems: L
249253

250254
await asyncio.sleep(0.2) # Rate limiting 방지
251255

256+
except (aiohttp.ClientConnectorError, aiohttp.ServerTimeoutError, asyncio.TimeoutError) as e:
257+
logger.error(f"[개별 문제 확인] {baekjoon_id} - 문제 {problem_id}: solved.ac 서버 연결 실패: {e} (서버 다운 가능성)")
258+
await asyncio.sleep(0.2)
259+
continue
252260
except Exception as e:
253261
logger.error(f"[개별 문제 확인] {baekjoon_id} - 문제 {problem_id} 확인 중 오류: {e}")
254262
await asyncio.sleep(0.2)
@@ -258,6 +266,9 @@ async def check_problems_individual_queries(baekjoon_id: str, target_problems: L
258266
logger.info(f"[개별 문제 확인] {baekjoon_id} - {len(solved_problems)}/{len(target_problems)}개 해결")
259267
return solved_problems
260268

269+
except (aiohttp.ClientConnectorError, aiohttp.ServerTimeoutError, asyncio.TimeoutError) as e:
270+
logger.error(f"[개별 문제 확인] solved.ac 서버 연결 실패: {e} (서버 다운 가능성)")
271+
return []
261272
except Exception as e:
262273
logger.error(f"[개별 문제 확인] 오류: {e}", exc_info=True)
263274
return []
@@ -286,18 +297,21 @@ async def _check_problems_via_search_api(baekjoon_id: str, target_problems: List
286297

287298
logger.debug(f"[solved.ac 검색 API] {baekjoon_id} - 쿼리: {query} -> 인코딩: {encoded_query}")
288299

289-
async with aiohttp.ClientSession(headers=headers) as session:
300+
# 타임아웃 설정 (10초)
301+
timeout = aiohttp.ClientTimeout(total=10)
302+
async with aiohttp.ClientSession(headers=headers, timeout=timeout) as session:
290303
while page <= max_pages:
291304
url = f"https://solved.ac/problems?query={encoded_query}&page={page}"
292305
logger.debug(f"[solved.ac 검색 API] {baekjoon_id} - 페이지 {page} 크롤링: {url}")
293306

294-
async with session.get(url) as response:
295-
if response.status != 200:
296-
if page == 1:
297-
logger.warning(f"[solved.ac 검색 API] HTTP {response.status} 에러: {url}")
298-
return []
299-
# 첫 페이지가 아니면 더 이상 페이지가 없는 것으로 간주
300-
break
307+
try:
308+
async with session.get(url) as response:
309+
if response.status != 200:
310+
if page == 1:
311+
logger.warning(f"[solved.ac 검색 API] HTTP {response.status} 에러: {url} (solved.ac 서버 문제 가능성)")
312+
return []
313+
# 첫 페이지가 아니면 더 이상 페이지가 없는 것으로 간주
314+
break
301315

302316
html = await response.text()
303317
soup = BeautifulSoup(html, 'html.parser')
@@ -370,13 +384,27 @@ async def _check_problems_via_search_api(baekjoon_id: str, target_problems: List
370384

371385
page += 1
372386
await asyncio.sleep(0.3) # Rate limiting 방지
387+
except (aiohttp.ClientConnectorError, aiohttp.ServerTimeoutError, asyncio.TimeoutError) as e:
388+
if page == 1:
389+
logger.error(f"[solved.ac 검색 API] solved.ac 서버 연결 실패: {e} (서버 다운 가능성)")
390+
return []
391+
# 첫 페이지가 아니면 더 이상 페이지가 없는 것으로 간주
392+
break
393+
except Exception as e:
394+
if page == 1:
395+
logger.error(f"[solved.ac 검색 API] 예상치 못한 오류: {e}")
396+
return []
397+
break
373398

374399
# 중복 제거 및 정렬
375400
solved_problems = sorted(list(set(solved_problems)))
376401

377402
logger.info(f"[solved.ac 검색 API] {baekjoon_id} - 목표 문제 중 {len(solved_problems)}/{len(target_problems)}개 해결 (페이지 {page-1}개 크롤링)")
378403
return solved_problems
379404

405+
except (aiohttp.ClientConnectorError, aiohttp.ServerTimeoutError, asyncio.TimeoutError) as e:
406+
logger.error(f"[solved.ac 검색 API] solved.ac 서버 연결 실패: {e} (서버 다운 가능성)")
407+
return []
380408
except Exception as e:
381409
logger.error(f"[solved.ac 검색 API] 오류: {e}", exc_info=True)
382410
return []
@@ -399,19 +427,21 @@ async def _get_all_solved_problems_via_pages(baekjoon_id: str, target_problems:
399427
target_set = set(target_problems) if target_problems else None
400428
last_page = None # 마지막 페이지 번호
401429

402-
async with aiohttp.ClientSession(headers=headers) as session:
430+
timeout = aiohttp.ClientTimeout(total=10)
431+
async with aiohttp.ClientSession(headers=headers, timeout=timeout) as session:
403432
while page <= max_pages:
404433
url = f"https://solved.ac/profile/{baekjoon_id}/solved"
405434
if page > 1:
406435
url += f"?page={page}"
407436

408-
async with session.get(url) as response:
409-
if response.status != 200:
410-
if page == 1:
411-
logger.warning(f"[solved.ac 크롤링] HTTP {response.status} 에러: {url}")
412-
return []
413-
# 첫 페이지가 아니면 더 이상 페이지가 없는 것으로 간주
414-
break
437+
try:
438+
async with session.get(url) as response:
439+
if response.status != 200:
440+
if page == 1:
441+
logger.warning(f"[solved.ac 크롤링] HTTP {response.status} 에러: {url} (서버 문제 가능성)")
442+
return []
443+
# 첫 페이지가 아니면 더 이상 페이지가 없는 것으로 간주
444+
break
415445

416446
html = await response.text()
417447
soup = BeautifulSoup(html, 'html.parser')
@@ -466,6 +496,17 @@ async def _get_all_solved_problems_via_pages(baekjoon_id: str, target_problems:
466496

467497
page += 1
468498
await asyncio.sleep(0.3) # Rate limiting 방지
499+
except (aiohttp.ClientConnectorError, aiohttp.ServerTimeoutError, asyncio.TimeoutError) as e:
500+
if page == 1:
501+
logger.error(f"[solved.ac 크롤링] solved.ac 서버 연결 실패: {e} (서버 다운 가능성)")
502+
return []
503+
# 첫 페이지가 아니면 더 이상 페이지가 없는 것으로 간주
504+
break
505+
except Exception as e:
506+
if page == 1:
507+
logger.error(f"[solved.ac 크롤링] 예상치 못한 오류: {e}")
508+
return []
509+
break
469510

470511
# 중복 제거 및 정렬
471512
solved_problems = sorted(list(set(solved_problems)))
@@ -479,6 +520,37 @@ async def _get_all_solved_problems_via_pages(baekjoon_id: str, target_problems:
479520

480521
return solved_problems
481522

523+
async def check_solved_ac_server_available() -> bool:
524+
"""
525+
solved.ac 서버가 응답 가능한지 확인
526+
527+
Returns:
528+
True: 서버가 정상 응답
529+
False: 서버가 다운되었거나 응답 없음
530+
"""
531+
try:
532+
# 간단한 API 호출로 서버 상태 확인 (존재하는 사용자로 테스트)
533+
url = "https://solved.ac/api/v3/user/show?handle=wookje"
534+
headers = {
535+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
536+
}
537+
538+
timeout = aiohttp.ClientTimeout(total=5) # 빠른 확인을 위해 5초
539+
async with aiohttp.ClientSession(headers=headers, timeout=timeout) as session:
540+
async with session.get(url) as response:
541+
# 200 또는 404 모두 서버가 응답하는 것이므로 정상
542+
if response.status in [200, 404]:
543+
return True
544+
# 기타 상태코드는 서버 문제 가능성
545+
logger.warning(f"[solved.ac 서버 확인] HTTP {response.status} 응답 (서버 문제 가능성)")
546+
return False
547+
except (aiohttp.ClientConnectorError, aiohttp.ServerTimeoutError, asyncio.TimeoutError) as e:
548+
logger.error(f"[solved.ac 서버 확인] 서버 연결 실패: {e} (서버 다운 가능성)")
549+
return False
550+
except Exception as e:
551+
logger.error(f"[solved.ac 서버 확인] 오류: {e}", exc_info=True)
552+
return False
553+
482554
async def verify_user_exists(baekjoon_id: str) -> bool:
483555
"""
484556
백준(BOJ) 사용자 존재 여부 확인
@@ -497,16 +569,21 @@ async def verify_user_exists(baekjoon_id: str) -> bool:
497569
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
498570
}
499571

500-
async with aiohttp.ClientSession(headers=headers) as session:
572+
timeout = aiohttp.ClientTimeout(total=10)
573+
async with aiohttp.ClientSession(headers=headers, timeout=timeout) as session:
501574
async with session.get(url) as response:
502575
if response.status == 200:
503576
return True
504577
if response.status == 404:
505578
return False
506579
# 기타 상태코드는 보수적으로 False 처리
580+
logger.warning(f"[solved.ac API] HTTP {response.status} 에러: {url} (서버 문제 가능성)")
507581
return False
582+
except (aiohttp.ClientConnectorError, aiohttp.ServerTimeoutError, asyncio.TimeoutError) as e:
583+
logger.error(f"[solved.ac API] solved.ac 서버 연결 실패: {e} (서버 다운 가능성)")
584+
return False
508585
except Exception as e:
509-
print(f"solved.ac 사용자 확인 오류: {e}")
586+
logger.error(f"[solved.ac API] 사용자 확인 오류: {e}", exc_info=True)
510587
return False
511588

512589
async def check_problem_solved(baekjoon_id: str, problem_id: int) -> bool:
@@ -536,9 +613,11 @@ async def get_weekly_solved_count(baekjoon_id: str, start_date: datetime, end_da
536613
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
537614
}
538615

539-
async with aiohttp.ClientSession(headers=headers) as session:
616+
timeout = aiohttp.ClientTimeout(total=10)
617+
async with aiohttp.ClientSession(headers=headers, timeout=timeout) as session:
540618
async with session.get(url) as response:
541619
if response.status != 200:
620+
logger.warning(f"[solved.ac API] HTTP {response.status} 에러: {url} (서버 문제 가능성)")
542621
return {'count': 0, 'problems': []}
543622

544623
data = await response.json()
@@ -607,8 +686,11 @@ def cumulative_before(target_dt: datetime, inclusive: bool) -> int:
607686
'count': count,
608687
'problems': []
609688
}
689+
except (aiohttp.ClientConnectorError, aiohttp.ServerTimeoutError, asyncio.TimeoutError) as e:
690+
logger.error(f"[solved.ac API] solved.ac 서버 연결 실패: {e} (서버 다운 가능성)")
691+
return {'count': 0, 'problems': []}
610692
except Exception as e:
611-
print(f"주간 해결한 문제 수 조회 오류(solved.ac): {e}")
693+
logger.error(f"[solved.ac API] 주간 해결한 문제 수 조회 오류: {e}", exc_info=True)
612694
return {'count': 0, 'problems': []}
613695

614696
async def get_weekly_solved_from_boj_status(baekjoon_id: str, start_date: datetime, end_date: datetime, status_callback=None) -> Dict:

0 commit comments

Comments
 (0)