프로젝트

일반

사용자정보

새기능 #5803

이호영이(가) 약 한달 전에 변경

## 개요 

 중앙경찰학교 `알림/참여 > 공지사항` 게시판 **505건**(2006-06-05 ~ 2026-07-06)을 전건 수집해 `2. 권리처리` 시트에 기재했다. 

 - 목록: `https://www.cpa.go.kr/user/bbs/BD_selectBbsList.do?q_bbsCode=1001&q_rowPerPage=1000&q_currPage=1` 
 - 상세: `https://www.cpa.go.kr/user/bbs/BD_selectBbs.do?q_bbsCode=1001&q_bbscttSn=NNNNN` 

 ## 분석 

 **페이지 수만큼 반복 요청하지 않고 1회 요청으로 전건을 받았다.** 

 목록 URL에 `q_rowPerPage`(페이지당 행 수) 파라미터가 있고, 서버가 이 값을 상한 검증 없이 그대로 받는 것을 확인했다. 기본 10 → **1000으로 지정해 51페이지를 1페이지로 압축**. 서버 부하도 줄고 페이징 경계에서 생기는 중복·누락 리스크도 없어졌다. 

 확인한 점 

 1. 공지 게시물 번호 컬럼은 `class="admin-notice show-col"` — 상단 고정공지와 일반글을 구분해야 함. 
 2. 게시물 고유키는 **`q_bbscttSn`** — 초기건은 5자리 숫자, 최근건은 `20260706165259718` 형태의 **타임스탬프형**으로 혼재 → 숫자로 캡스팅하면 안 되고 문자열 그대로 다뤄야 함. 
 3. HTML이 `&`로 이스케이프되어 있어 정규식에 반영 필요. 
 4. 사이드메뉴(LNB)의 `q_bbsCode` 매핑을 먼저 덤프해 **1001 = 공지사항** 임을 확인하고 진행. 

 ## 핵심 코드 

 **게시판 코드 매핑 확인** 

 ```python 
 import re 
 html = open('page.html', encoding='utf-8', errors='replace').read() 
 for code, name in re.findall(r'selectBbsList\.do\?q_bbsCode=(\d+)[^>]*>([^<]+)</a>', html): 
     if name.strip(): 
         print(f'    bbsCode {code}: {name.strip()}') 
 # -> bbsCode 1001: 공지사항 
 ``` 

 **1회 요청으로 전건 수집** 

 ```bash 
 curl -s -k "https://www.cpa.go.kr/user/bbs/BD_selectBbsList.do?q_bbsCode=1001&q_rowPerPage=1000&q_currPage=1" \ 
      -A "Mozilla/5.0" -o big.html 
 ``` 

 **목록 파싱** 

 ```python 
 import re 

 doc = open('big.html', encoding='utf-8', errors='replace').read() 

 # q_bbscttSn 은 숫자/타임스탬프 혼재 -> 문자열로 취급 
 rows = re.findall( 
     r'<a href="BD_selectBbs\.do\?q_bbsCode=1001&amp;q_bbscttSn=(\d+)"[^>]*>(.*?)</a>', 
     doc, re.S) 

 print('total post links:', len(rows), '| distinct sn:', len(set(sn for sn, _ in rows))) 
 ``` 

 **엑셀 반영 (`2. 권리처리` 시트)** 

 ```python 
 import openpyxl, json, datetime 
 from copy import copy 

 posts = json.load(open('posts.json', encoding='utf-8')) 
 posts.sort(key=lambda x: int(x[0]))            # 게시물 번호 오름차순 

 wb = openpyxl.load_workbook('중앙경찰학교.xlsx') 
 ws = wb['2. 권리처리'] 

 SITE    = '중앙경찰학교' 
 PATH    = '알림/참여 > 공지사항' 
 BOARD = '공지사항' 
 BASE    = 'https://www.cpa.go.kr/user/bbs/BD_selectBbs.do?q_bbsCode=1001&q_bbscttSn=' 

 tmpl_styles = {c: copy(ws.cell(4, c)._style) for c in range(1, 13)}     # 4행 서식을 템플릿으로 

 start, old_last = 4, 406 
 for r in range(start, max(start + len(posts) - 1, old_last) + 1):       # 기존 예시데이터 정리 
     for c in range(1, 13): 
         ws.cell(r, c).value = None 

 for i, (num, sn, title, date, author) in enumerate(posts): 
     r = start + i 
     y, m, d = map(int, date.split('-')) 
     vals = {1: i + 1, 2: SITE, 3: PATH, 4: BOARD, 
             5: title, 6: BASE + sn, 8: datetime.datetime(y, m, d)} 
     for c in range(1, 13): 
         cell = ws.cell(r, c) 
         cell._style = copy(tmpl_styles[c]) 
         if c in vals: 
             cell.value = vals[c] 

 wb.save('중앙경찰학교.xlsx') 
 ``` 

 ## 사용 패키지 

 | 패키지 | 버전 | 용도 | 
 |---|---|---| 
 | Python | 3.13.0 | 실행 환경 | 
 | openpyxl | 3.1.5 | 엑셀 기재·서식 복사 | 
 | curl | - | 목록 HTML 다운로드 (`-k` SSL 검증 우회) | 

 표준 라이브러리: `re / json / datetime / copy / io / sys` 

 ## 특이사항 

 - **`q_rowPerPage=1000` 트릭**이 핵심. 51회 요청 → 1회로 줄였다. 다만 서버가 항상 허용하는 것은 아니므로, 다른 사이트에선 반드시 응답 건수를 먼저 확인하고 써야 한다. 
 - 인증서 체인 이슈로 curl `-k`, Python 쪽은 `ssl.CERT_NONE` 컨텍스트로 요청. 
 - 시트 하단에 안내 문구(병합셀)가 있어 기존 행을 지우기 전에 **병합 해제 → 값/서식 백업 → 재기재** 순서로 처리했다. 
 - 게시물 고유키가 숫자형과 타임스탬프형으로 섞여 섮여 있어 **int 변환 없이 문자열로** 다뤄야 URL이 깨지지 않는다.

뒤로