프로젝트

일반

사용자정보

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가지.

  1. 사용자가 보는 URL(sub.do?mid=7020034)은 레이아웃 셸이라 게시판 데이터가 없다. 페이지 소스에서 실제 목록 엔드포인트(selectBoardList.do)와 bbsId를 먼저 추적해야 했다.
  2. 목록의 제목이 <a> 텍스트가 아니라 hidden 생성된 value="..." 속성에 들어 있었다. 일반적인 <a>제목</a> 파싱으로는 빈 값만 나와, value="([^"]*)"\s*></span> 패턴으로 변경.
  3. 마지막 페이지는 상수로 박지 않고 페이징 스크립트 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

내보내기 Atom PDF