11#!/usr/bin/env python3
22# .github/scripts/update_readme.py
3- # Debug/verbose version — 폴더/파일/매칭/entries 정보 전부 출력 & 로그파일 작성
3+ # Debug + Robust + Conflict-resolution version
4+ # - verbose logging (update_readme_debug.log)
5+ # - certifi verify for requests
6+ # - normalize unicode/spaces
7+ # - fetch from solved.ac & programmers
8+ # - parse existing README table
9+ # - detect files bj_/pg_ and prefer file-based platform (remove conflicting entry)
10+ # - preserve pre/post content and write README
411
512import os , re , time , unicodedata , requests , sys
613from bs4 import BeautifulSoup
@@ -81,7 +88,7 @@ def fetch_programmers_info(pid):
8188 log ("[fetch_programmers_info] exception:" , e )
8289 return {"title" :"UNKNOWN" ,"level" :"Lv.?" ,"link" :url }
8390
84- # find table block (same robust logic )
91+ # find table block (robust)
8592def find_table_block (lines ):
8693 header_idx = None
8794 for i , line in enumerate (lines ):
@@ -107,13 +114,24 @@ def find_table_block(lines):
107114 post = lines [rows_end :]
108115 return pre , header , rows , post
109116
117+ # parse a table row -> normalized platform, number, title, level, status
110118def parse_row_line (line ):
111119 cols = [c .strip () for c in line .strip ().split ("|" )[1 :- 1 ]]
112120 status = cols [1 ] if len (cols ) > 1 else "⬜"
113- platform = cols [2 ] if len (cols ) > 2 else ""
121+ raw_platform = cols [2 ] if len (cols ) > 2 else ""
122+ raw_platform_l = raw_platform .lower ()
123+ # normalize platform to exact tokens used internally
124+ if "백준" in raw_platform or "acmicpc" in raw_platform_l or "baek" in raw_platform_l :
125+ platform = "백준"
126+ elif "프로그래머스" in raw_platform or "programmers" in raw_platform_l or "pg" in raw_platform_l :
127+ platform = "프로그래머스"
128+ else :
129+ platform = raw_platform .strip () or "UNKNOWN"
130+
114131 number_cell = cols [3 ] if len (cols ) > 3 else ""
115132 title_cell = cols [4 ] if len (cols ) > 4 else ""
116133 level = cols [5 ] if len (cols ) > 5 else ""
134+
117135 num = None ; num_link = ""
118136 m = re .search (r"\[(\d+)\]\(([^)]+)\)" , number_cell )
119137 if m :
@@ -122,15 +140,18 @@ def parse_row_line(line):
122140 m2 = re .search (r"(\d+)" , number_cell )
123141 if m2 :
124142 num = m2 .group (1 )
143+
125144 title = ""
126145 m = re .search (r"\[(.*?)\]\(([^)]+)\)" , title_cell )
127146 if m :
128147 title = m .group (1 )
129148 else :
130149 title = title_cell
150+
131151 return {"platform" : platform , "number" : num , "number_link" : num_link , "title" : title , "level" : level , "status" : status , "raw" : line }
132152
133153def platform_rank (p ): return 0 if p .strip ()== "백준" else 1
154+
134155def level_rank (p , lvl ):
135156 lvl = normalize_spaces (lvl or "" )
136157 if p .strip ()== "백준" :
@@ -145,7 +166,7 @@ def level_rank(p, lvl):
145166 except : pass
146167 return 999
147168
148- # core: process_directory with verbose logging
169+ # core: process_directory with verbose logging + conflict resolution
149170def process_directory (dir_path ):
150171 readme_path = os .path .join (dir_path , "README.md" )
151172 log (f"[start] folder={ dir_path } (readme_exists={ os .path .isfile (readme_path )} )" )
@@ -199,28 +220,38 @@ def process_directory(dir_path):
199220 }
200221 log (f"[existing_entries] count={ len (entries )} " )
201222
202- # incorporate matched files
223+ # incorporate matched files (file-based additions) and collect found sets
224+ found_bj = set ()
225+ found_pg = set ()
203226 for item in matched :
204227 fn = item ["file" ]; name ,_ = os .path .splitext (fn )
205228 if item ["bj" ]:
206229 pid = re .search (r"bj_(\d+)" , name ).group (1 )
207230 status = "❌" if "_FAIL" in name else "✅"
208231 key = ("백준" , pid )
209- if key in entries :
210- entries [key ]["status" ] = status
211- else :
212- entries [key ] = {"platform" :"백준" ,"number" :pid ,"number_link" :"" ,"title" :"" ,"level" :"" ,"status" :status }
232+ entries [key ] = {"platform" :"백준" ,"number" :pid ,"number_link" :"" ,"title" :"" ,"level" :"" ,"status" :status }
233+ found_bj .add (pid )
213234 if item ["pg" ]:
214235 pid = re .search (r"pg_(\d+)" , name ).group (1 )
215236 status = "❌" if "_FAIL" in name else "✅"
216237 key = ("프로그래머스" , pid )
217- if key in entries :
218- entries [key ]["status" ] = status
219- else :
220- entries [key ] = {"platform" :"프로그래머스" ,"number" :pid ,"number_link" :"" ,"title" :"" ,"level" :"" ,"status" :status }
238+ entries [key ] = {"platform" :"프로그래머스" ,"number" :pid ,"number_link" :"" ,"title" :"" ,"level" :"" ,"status" :status }
239+ found_pg .add (pid )
221240
222241 log (f"[after_scan_entries] count={ len (entries )} keys={ list (entries .keys ())} " )
223242
243+ # --- CONFLICT RESOLUTION: prefer file-based platform
244+ # If both platforms exist for same number, remove the one without a corresponding file.
245+ # i.e., if pg file exists -> remove ('백준', num) entry; if bj file exists -> remove ('프로그래머스', num) entry
246+ for num in list (found_pg ):
247+ if ("백준" , num ) in entries and ("프로그래머스" , num ) in entries and num in found_pg :
248+ log (f"[conflict] removing 백준 entry for { num } because pg file exists" )
249+ entries .pop (("백준" , num ), None )
250+ for num in list (found_bj ):
251+ if ("프로그래머스" , num ) in entries and ("백준" , num ) in entries and num in found_bj :
252+ log (f"[conflict] removing 프로그래머스 entry for { num } because bj file exists" )
253+ entries .pop (("프로그래머스" , num ), None )
254+
224255 # if no entries and no existing readme -> nothing to do
225256 if not entries and not os .path .isfile (readme_path ):
226257 log (f"[skip] no entries and no readme -> nothing to do for { dir_path } " )
@@ -292,22 +323,6 @@ def process_directory(dir_path):
292323 log (f"[ERROR] writing README { readme_path } : { e } " )
293324 return False
294325
295- def platform_rank (p ): return 0 if p .strip ()== "백준" else 1
296-
297- def level_rank (p , lvl ):
298- lvl = normalize_spaces (lvl or "" )
299- if p .strip ()== "백준" :
300- return BOJ_LEVEL_RANK .get (lvl , 999 )
301- m = re .search (r"(?:Lv\.?|Level)?\s*\.?\s*(\d+)" , lvl , re .IGNORECASE )
302- if m :
303- try : return int (m .group (1 ))
304- except : pass
305- m2 = re .search (r"(\d+)" , lvl )
306- if m2 :
307- try : return int (m2 .group (1 ))
308- except : pass
309- return 999
310-
311326def main ():
312327 # clear log
313328 try :
0 commit comments