프로젝트

일반

사용자정보

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페이지로 압축. 서버 부하도 줄고 페이징 경계에서 생기는 중복·누락 리스크도 없어졌다.

확인한 점

  1. 공지 게시물 번호 컬럼은 class="admin-notice show-col" — 상단 고정공지와 일반글을 구분해야 함.
  2. 게시물 고유키는 q_bbscttSn — 초기건은 5자리 숫자, 최근건은 20260706165259718 형태의 타임스탬프형으로 혼재 → 숫자로 캡스팅하면 안 되고 문자열 그대로 다뤄야 함.
  3. HTML이 &로 이스케이프되어 있어 정규식에 반영 필요.
  4. 사이드메뉴(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&amp;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 #1

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

Actions

내보내기 Atom PDF