Actions
새기능 #5804
완료됨새기능 #5801: 신유형개방 권리확인 사이트 수집
[신유형개방 권리확인] 광주광역시경찰청 보도자료 수집 (630건)
시작시간:
2026/07/15
완료기한:
진척도:
0%
추정시간:
난이도:
중
설명
개요¶
광주광역시경찰청 소식마당 > 사이버홍보실 > 보도자료 게시판 630건(2017-01-02 ~ 2026-07-10)을 전건 수집해 기존 크롤링 파일에 시트를 추가했다.
- 진입 URL:
https://www.gjpolice.go.kr/sub.do?r=gjpolice&mid=7020034 - 실제 목록:
https://www.gjpolice.go.kr/cop/bbs/selectBoardList.do?bbsId=BBSMSTR_000000000026&pageIndex=N - 상세:
https://www.gjpolice.go.kr/cop/bbs/selectBoardArticle.do?bbsId=BBSMSTR_000000000026&nttId=NNNNN
분석¶
eGovFrame 표준 게시판 구조. 이번에 걸렸던 지점은 3가지.
- 사용자가 보는 URL(
sub.do?mid=7020034)은 레이아웃 셸이라 게시판 데이터가 없다. 페이지 소스에서 실제 목록 엔드포인트(selectBoardList.do)와bbsId를 먼저 추적해야 했다. - 목록의 제목이
<a>텍스트가 아니라 hidden 생성된value="..."속성에 들어 있었다. 일반적인<a>제목</a>파싱으로는 빈 값만 나와,value="([^"]*)"\s*></span>패턴으로 변경. - 마지막 페이지는 상수로 박지 않고 페이징 스크립트
fn_egov_select_noticeList(N)의 최대값으로 동적 판정 → 게시물이 늘어도 그대로 재사용 가능.
핵심 코드¶
요청 유틸 (gzip 수동 해제 + 재시도)
import re, json, time, urllib.request, gzip
BBS = 'BBSMSTR_000000000026'
BASE = f'https://www.gjpolice.go.kr/cop/bbs/selectBoardList.do?bbsId={BBS}&pageIndex='
ART = f'https://www.gjpolice.go.kr/cop/bbs/selectBoardArticle.do?bbsId={BBS}&nttId='
def fetch(url, tries=3):
for t in range(tries):
try:
req = urllib.request.Request(
url, headers={'User-Agent': 'Mozilla/5.0', 'Accept-Encoding': 'gzip'})
r = urllib.request.urlopen(req, timeout=40)
data = r.read()
if r.headers.get('Content-Encoding') == 'gzip':
data = gzip.decompress(data)
return data.decode('utf-8', 'replace')
except Exception as e:
print(' retry', t, e)
time.sleep(2)
raise RuntimeError('fetch failed ' + url)
목록 파싱 (제목이 value 속성에 있음)
def parse(html):
# <script> 제거 후 행 단위 분해
rows = re.findall(r'<tr[^>]*>(.*?)</tr>',
re.sub(r'<script.*?</script>', '', html, flags=re.S), re.S)
out = []
for r in rows:
num = re.search(r'listCenter" nowrap>(\d+)</td>', r)
ntt = re.search(r'name="nttId"\s+value="(\d+)"', r)
title = re.search(r'value="([^"]*)"\s*></span>', r) # ← 제목은 value 속성
date = re.search(r'listCenter" nowrap>(\d{4}-\d{2}-\d{2})</td>', r)
if ntt and title:
out.append({
'num' : num.group(1) if num else '',
'nttId': ntt.group(1),
'title': unescape(title.group(1)),
'date' : date.group(1) if date else '',
})
return out
마지막 페이지 동적 판정 + 전체 순회
h1 = fetch(BASE + '1')
pages = [int(x) for x in re.findall(r'fn_egov_select_noticeList\((\d+)\)', h1)]
last = max(pages) if pages else 1
print('last page:', last)
all_rows, seen = [], set()
for p in range(1, last + 1):
html = h1 if p == 1 else fetch(BASE + str(p))
for row in parse(html):
if row['nttId'] in seen: # 상단 고정공지 중복 제거
continue
seen.add(row['nttId'])
row['url'] = ART + row['nttId'] # 상세 URL 생성
all_rows.append(row)
사용 패키지¶
| 패키지 | 버전 | 용도 |
|---|---|---|
| Python | 3.13.0 | 실행 환경 |
| openpyxl | 3.1.5 | 시트 추가·서식 복사·하이퍼링크 |
표준 라이브러리: urllib.request / gzip / re / json / time / datetime / copy
특이사항¶
- 수집 후 요청받아 F열 하이퍼링크를 일괄 활성화했다. 보고받는 쪽에서 바로 클릭해 원문을 확인하기 위함 → 이후 작업에서는 기본 산출물에 포함시키도록 변경.
- 기존 파일(전북·중앙경찰학교)의 시트 구조를 그대로 따라 새 시트를 추가하는 방식으로 생성 → 산출물:
전북경찰청 및 중앙경찰학교 크롤링_260713_광주광역시경찰청추가_20260715.xlsx - gzip 응답을 쓰므로
Accept-Encoding: gzip헤더와gzip.decompress()수동 처리 필요 (urllib은 requests와 달리 자동 해제하지 않음).
표시할 데이터가 없습니다.
Actions