11import os
22import re
3+ import time
34import requests
45from bs4 import BeautifulSoup
56
67ROOT_DIR = "."
78VALID_EXT = (".py" , ".cpp" , ".java" , ".kt" , ".js" )
89
10+ # solved.ac level mapping (0..30 -> readable)
11+ LEVEL_MAP = {
12+ 0 : "Unrated" ,
13+ 1 : "Bronze V" , 2 : "Bronze IV" , 3 : "Bronze III" , 4 : "Bronze II" , 5 : "Bronze I" ,
14+ 6 : "Silver V" , 7 : "Silver IV" , 8 : "Silver III" , 9 : "Silver II" , 10 : "Silver I" ,
15+ 11 : "Gold V" , 12 : "Gold IV" , 13 : "Gold III" , 14 : "Gold II" , 15 : "Gold I" ,
16+ 16 : "Platinum V" , 17 : "Platinum IV" , 18 : "Platinum III" , 19 : "Platinum II" , 20 : "Platinum I" ,
17+ 21 : "Diamond V" , 22 : "Diamond IV" , 23 : "Diamond III" , 24 : "Diamond II" , 25 : "Diamond I" ,
18+ 26 : "Ruby V" , 27 : "Ruby IV" , 28 : "Ruby III" , 29 : "Ruby II" , 30 : "Ruby I" ,
19+ }
20+
921# -----------------------------
1022# 문제 파일 수집
1123# -----------------------------
@@ -38,73 +50,108 @@ def parse_readme(lines):
3850 max_index = - 1
3951
4052 for line in lines :
53+ # 표에서 "순번 | 아이콘 | 플랫폼 | [번호](링크)" 형태를 잡음
4154 m = re .search (
42- r"\|\s*(\d+)\s*\|\s*(⬜|✅|❌)\s*\|\s*(백준|프로그래머스)\s*\|\s*\[?(\d+)\]?" ,
55+ r"\|\s*(\d+)\s*\|\s*(?: ⬜|✅|❌)\s*\|\s*(백준|프로그래머스)\s*\|\s*\[?(\d+)\]?" ,
4356 line ,
4457 )
4558 if m :
46- idx , _ , _ , number = m .groups ()
59+ idx , _ , number = m .groups ()
4760 existing_numbers .add (number )
4861 max_index = max (max_index , int (idx ))
4962
5063 return existing_numbers , max_index
5164
5265
5366# -----------------------------
54- # 백준 정보 수집 (정상 동작 )
67+ # solved.ac API 시도 (가장 안정적 )
5568# -----------------------------
56- def fetch_baekjoon_info (number ):
57- url = f"https://www.acmicpc.net/ problem/{ number } "
69+ def fetch_from_solvedac (number ):
70+ url = f"https://solved.ac/api/v3/ problem/show?problemId= { number } "
5871 headers = {
59- "User-Agent" : "Mozilla/5.0"
72+ "Accept" : "application/json" ,
73+ "User-Agent" : "github-actions/auto-readme (https://github.com)"
6074 }
75+ try :
76+ resp = requests .get (url , headers = headers , timeout = 8 )
77+ if resp .status_code != 200 :
78+ return None
79+ data = resp .json ()
80+ # solved.ac returns localized titles; try titleKo first then title
81+ title = data .get ("titleKo" ) or data .get ("title" ) or data .get ("title_en" ) or data .get ("title" )
82+ level_val = data .get ("level" )
83+ level = LEVEL_MAP .get (level_val , "UNKNOWN" ) if isinstance (level_val , int ) else "UNKNOWN"
84+ return {"title" : title or "UNKNOWN" , "level" : level , "link" : f"https://www.acmicpc.net/problem/{ number } " }
85+ except Exception :
86+ return None
6187
62- res = requests .get (url , headers = headers , timeout = 10 )
63- if res .status_code != 200 :
64- return {
65- "title" : "UNKNOWN" ,
66- "level" : "UNKNOWN" ,
67- "link" : url ,
68- }
69-
70- soup = BeautifulSoup (res .text , "html.parser" )
7188
72- # 문제 제목
73- title_tag = soup .select_one ("#problem_title" )
74- title = title_tag .text .strip () if title_tag else "UNKNOWN"
75-
76- # solved.ac tier 이미지에서 난이도 추출
77- tier_img = soup .select_one ("img.solvedac-tier" )
78- level = "UNKNOWN"
79-
80- if tier_img and "src" in tier_img .attrs :
81- src = tier_img ["src" ]
82- # 예: silver5.svg
83- tier = src .split ("/" )[- 1 ].replace (".svg" , "" )
89+ # -----------------------------
90+ # acmicpc.net 페이지 폴백 스크래핑
91+ # -----------------------------
92+ def fetch_from_acmicpc (number ):
93+ url = f"https://www.acmicpc.net/problem/{ number } "
94+ headers = {
95+ "User-Agent" : "Mozilla/5.0 (compatible; github-actions; +https://github.com)"
96+ }
97+ try :
98+ resp = requests .get (url , headers = headers , timeout = 10 )
99+ if resp .status_code != 200 :
100+ return {"title" : "UNKNOWN" , "level" : "UNKNOWN" , "link" : url }
101+
102+ soup = BeautifulSoup (resp .text , "html.parser" )
103+
104+ # 제목 시도 (여러 가능성에 대비)
105+ title_tag = soup .select_one ("#problem_title" ) or soup .select_one ("h1" ) or soup .select_one ("title" )
106+ title = title_tag .text .strip () if title_tag else "UNKNOWN"
107+
108+ # solved.ac tier 이미지를 찾아서 난이도 추출
109+ level = "UNKNOWN"
110+ # 이미지(예: /tier/silver5.svg) 또는 alt 속성에 tier가 있는 경우
111+ tier_img = soup .find ("img" , {"class" : re .compile (".*tier.*" , re .I )}) or soup .find ("img" , {"src" : re .compile (".*/tier/.*" , re .I )})
112+ if tier_img and tier_img .get ("src" ):
113+ src = tier_img ["src" ]
114+ # 파일명 예: silver5.svg -> silver5
115+ base = os .path .basename (src ).replace (".svg" , "" ).replace (".png" , "" )
116+ # 'silver5' -> 'Silver V'
117+ m = re .match (r"([a-zA-Z]+)(\d+)" , base )
118+ if m :
119+ kind , num = m .groups ()
120+ kind = kind .lower ()
121+ kind_map = {
122+ "bronze" : "Bronze" ,
123+ "silver" : "Silver" ,
124+ "gold" : "Gold" ,
125+ "platinum" : "Platinum" ,
126+ "diamond" : "Diamond" ,
127+ "ruby" : "Ruby" ,
128+ }
129+ if kind in kind_map :
130+ lvl = num .upper ()
131+ level = f"{ kind_map [kind ]} { lvl } "
132+
133+ return {"title" : title or "UNKNOWN" , "level" : level or "UNKNOWN" , "link" : url }
134+ except Exception :
135+ return {"title" : "UNKNOWN" , "level" : "UNKNOWN" , "link" : url }
84136
85- tier_map = {
86- "bronze" : "Bronze" ,
87- "silver" : "Silver" ,
88- "gold" : "Gold" ,
89- "platinum" : "Platinum" ,
90- "diamond" : "Diamond" ,
91- "ruby" : "Ruby" ,
92- }
93137
94- for k , v in tier_map .items ():
95- if tier .startswith (k ):
96- level = f"{ v } { tier [len (k ):].upper ()} "
97- break
138+ # -----------------------------
139+ # 통합 fetch 함수 (solved.ac 우선, acmicpc 폴백)
140+ # -----------------------------
141+ def fetch_baekjoon_info (number ):
142+ # 1) try solved.ac API
143+ info = fetch_from_solvedac (number )
144+ if info :
145+ return info
98146
99- return {
100- "title" : title ,
101- "level" : level ,
102- "link" : url ,
103- }
147+ # 2) fallback to scrapping acmicpc
148+ # small sleep to be polite if many requests
149+ time .sleep (0.5 )
150+ return fetch_from_acmicpc (number )
104151
105152
106153# -----------------------------
107- # 프로그래머스 정보
154+ # 프로그래머스 정보 (심플)
108155# -----------------------------
109156def fetch_programmers_info (number ):
110157 return {
@@ -119,10 +166,14 @@ def fetch_programmers_info(number):
119166# -----------------------------
120167def make_row (index , status , platform , number , info ):
121168 icon = "✅" if status == "SUCCESS" else "❌"
169+ # 안전하게 이스케이프 할 필요가 있으면 여기서 처리 가능
170+ title = info .get ("title" , "UNKNOWN" )
171+ level = info .get ("level" , "UNKNOWN" )
172+ link = info .get ("link" , f"https://www.acmicpc.net/problem/{ number } " )
122173 return (
123174 f"| { index :03d} | { icon } | { platform } | "
124- f"[{ number } ]({ info [ ' link' ] } ) | "
125- f"{ info [ ' title' ] } | { info [ ' level' ] } | |\n "
175+ f"[{ number } ]({ link } ) | "
176+ f"{ title } | { level } | |\n "
126177 )
127178
128179
0 commit comments