Actions
새기능 #5803
완료됨새기능 #5801: 신유형개방 권리확인 사이트 수집
[신유형개방 권리확인] 중앙경찰학교 공지사항 수집 (505건)
시작시간:
2026/07/10
완료기한:
진척도:
0%
추정시간:
난이도:
중
설명
개요¶
중앙경찰학교 알림/참여 > 공지사항 게시판 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페이지로 압축. 서버 부하도 줄고 페이징 경계에서 생기는 중복·누락 리스크도 없어졌다.
확인한 점
- 공지 게시물 번호 컬럼은
class="admin-notice show-col"— 상단 고정공지와 일반글을 구분해야 함. - 게시물 고유키는
q_bbscttSn— 초기건은 5자리 숫자, 최근건은20260706165259718형태의 타임스탬프형으로 혼재 → 숫자로 캡스팅하면 안 되고 문자열 그대로 다뤄야 함. - HTML이
&로 이스케이프되어 있어 정규식에 반영 필요. - 사이드메뉴(LNB)의
q_bbsCode매핑을 먼저 덤프해 1001 = 공지사항 임을 확인하고 진행.
핵심 코드¶
게시판 코드 매핑 확인
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회 요청으로 전건 수집
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
목록 파싱
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&q_bbscttSn=(\d+)"[^>]*>(.*?)</a>',
doc, re.S)
print('total post links:', len(rows), '| distinct sn:', len(set(sn for sn, _ in rows)))
엑셀 반영 (2. 권리처리 시트)
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이 깨지지 않는다.
Actions