프로젝트

일반

사용자정보

Actions

새기능 #5806

완료됨

새기능 #5801: 신유형개방 권리확인 사이트 수집

[신유형개방 권리확인] KRIHS국토연구원 7개 게시판 수집 (2,247건)

이호영이(가) 약 한달 전에 추가함. 약 한달 전에 수정됨.

상태:
완료
우선순위:
보통
담당자:
시작시간:
2026/07/21
완료기한:
진척도:

0%

추정시간:
난이도:

설명

개요

KRIHS국토연구원(krihs.re.kr) 7개 게시판 / 2,247건을 수집해 게시판별 개별 파일(7개)로 생성했다.

게시판 경로 건수 시트
보도자료 연구원소식 > 보도자료 1,034 권리확인
국토용어사전 국토교육 > 국토교육자료 807 권리처리
우수보고서 소개 연구원소식 > 우수보고서 소개 276 권리처리
지도로 보는 우리국토 국토교육 > 국토교육자료 77 권리처리
국토교육영상 국토교육 > 국토교육자료 40 권리처리
국토교육 워크북 국토교육 > 국토교육자료 8 권리처리
국토TED 국토교육 > 국토교육자료 5 권리처리

분석

KRIHS는 eZ 계열 CMS로, 같은 사이트 안에서 게시판 타입이 5가지로 갈렸다. 하나의 파서로 처리할 수 없어 타입별 파서를 분리했다.

타입 게시판 목록 구조 페이징 상세 URL
A. 테이블형 board.es 보도자료 <table> nPage act=view&list_no=
B. 리포트형 gallery.es 우수보고서 소개 ul.report_list nPage act=view&list_no=
C. 갤러리형 gallery.es 워크북 / 지도로 보는 우리국토 ul.blog_list / ul.gallery_list nPage act=view&list_no=
D. 외부링크형 gallery.es 국토교육영상 / 국토TED ul.gallery_list nPage 유튜브 외부 URL
E. 사전형 bbsList.es 국토용어사전 <table> pageIndex bbsView.es?num=

걸렸던 지점 4가지

① 페이징 파라미터가 게시판마다 다름
.es 계열은 nPage, 사전형만 pageIndex. 총 페이지 수도 전자는 <strong>1</strong>/<b>104</b> 표기, 후자는 페이징 링크 최대값으로 받아야 함.

② 목록 제목이 서버에서 잘림(..)
보도자료·우수보고서·지도로보는우리국토는 목록 제목이 잘려 나온다. 잘린 건만 골라 상세페이지에서 전체 제목을 재취득(병렬 8쓰레드). 전건 상세 조회 대비 요청을 크게 줄임.

③ 상세페이지 제목 셀렉터가 게시판마다 다름
대부분 article.board_view > h2.title 이지만 우수보고서는 article.blog_view > .report_info > strong.tit. 첫 시도 때 이걸 놓쳐 13건이 잘린 제목 그대로 남았고, 셀렉터를 폴백 리스트로 바꿔 해결.

④ 중첩 <li> 때문에 항목 분할 실패
우수보고서 목록은 항목 <li> 안에 저자·게시일이 다시 <li>로 들어있어 <li>...</li> 정규식 분할이 깨졌다(등록일 276건 전수 누락). list_no 등장 위치를 기준으로 본문을 청크로 자르는 방식으로 변경해 해결.

핵심 코드

페이지 수 판정

def krihs_pages(url_tpl):
    h = get(url_tpl.format(1))
    m = re.search(r'<strong>1</strong>/<b>(\d+)</b>', h)   # .es 계열 표기
    if m:
        return int(m.group(1)), h
    idx = re.findall(r'(?:pageIndex|nPage)=(\d+)', h)      # 사전형: 링크 최대값
    return (max(int(x) for x in idx) if idx else 1), h

중첩 <li> 회피 — list_no 기준 청크 분할

# 항목별로 list_no 를 기준으로 본문을 잘라낸다 (중첩 <li> 때문에 <li> 분할 불가)
marks, seen = [], set()
for m in re.finditer(r'act=view&(?:amp;)?list_no=(\d+)', body):
    if m.group(1) not in seen:
        seen.add(m.group(1))
        marks.append((m.group(1), m.start()))

chunks = [(ln, body[st:(marks[i + 1][1] if i + 1 < len(marks) else len(body))])
          for i, (ln, st) in enumerate(marks)]

for list_no, ch in chunks:
    tm = (re.search(r'class="tit">(.*?)</strong>', ch, re.S)          # report_list
          or re.search(r'class="title[^"]*">(.*?)</strong>', ch, re.S))  # gallery_list
    title = clean(tm.group(1))
    dm = re.search(r'(\d{4}-\d{2}-\d{2})', ch)
    items.append({'title': title,
                  'url'  : f'{BASE}/gallery.es?mid={mid}&bid={bid}&act=view&list_no={list_no}',
                  'date' : dm.group(1) if dm else '',
                  'need_detail': title.endswith('..')})   # 잘린 제목 표시

상세 제목 재취득 (셀렉터 폴백 + 병렬)

from concurrent.futures import ThreadPoolExecutor

DETAIL_TITLE_RES = [
    re.compile(r'class="board_view">\s*<h2 class="title">(.*?)</h2>', re.S),        # board.es / gallery.es 일반
    re.compile(r'class="report_info">.*?<strong class="tit">(.*?)</strong>', re.S),  # 우수보고서(0029)
    re.compile(r'<article class="blog_view">.*?<strong class="tit">(.*?)</strong>', re.S),
]

def krihs_detail_title(url):
    h = get(url)
    for rx in DETAIL_TITLE_RES:
        m = rx.search(h)
        if m:
            return clean(m.group(1))
    return ''

def fill_titles(items, workers=8):
    todo = [it for it in items if it.get('need_detail')]   # 잘린 건만
    with ThreadPoolExecutor(max_workers=workers) as ex:
        list(ex.map(lambda it: it.update(title=krihs_detail_title(it['url']) or it['title']), todo))
    return items

사전형 (pageIndex) 파싱

tpl = f'{BASE}/krihsLibraryDictionary/bbsList.es?mid={mid}&pageIndex={{}}'
for row in re.findall(r'<tr>(.*?)</tr>', h[h.find('board_list'):], re.S):
    m = re.search(r'href="(/krihsLibraryDictionary/bbsView\.es\?[^"]+)"[^>]*>(.*?)</a>', row, re.S)
    if not m:
        continue
    num = re.search(r'num=(\d+)', m.group(1)).group(1)
    items.append({'title': clean(m.group(2)),
                  'url'  : f'{BASE}/krihsLibraryDictionary/bbsView.es?mid={mid}&num={num}',  # 파라미터 정규화
                  'date' : re.findall(r'(\d{4}-\d{2}-\d{2})', row)[0]})

사용 패키지

패키지 버전 용도
Python 3.13.0 실행 환경
openpyxl 3.1.5 템플릿 복사·기재·서식·하이퍼링크

표준 라이브러리: urllib.request / ssl / re / html / json / time / os / concurrent.futures

검수

  • 7개 게시판 전체 잘린 제목(..) 0건 / 등록일 누락 0건 / URL 중복 0건
  • 무작위 표본을 실제 요청해 응답 HTML에 제목 포함 여부 대조 → 불일치 0건
  • 하이퍼링크 생성 수 ↔ 데이터 행 수 일치 확인

특이사항

  • 국토교육영상·국토TED는 내부 상세페이지가 없다. 목록 썸네일을 누르면 유튜브로 바로 나가는 구조라 F열(게시물 내용 URL)에 유튜브 주소가 들어간다. 기관 확인 필요.
  • 게시판별 공공누리 부착 현황을 페이지 하단 div.kogl 에서 확인해둔 상태 (G열은 현재 공란)
    • 1유형: 국토교육 워크북
    • 4유형: 국토교육영상, 국토TED, 국토용어사전, 지도로 보는 우리국토
    • 미부착: 보도자료, 우수보고서 소개
  • 제출 양식의 1-1.작성안내에 따라 등록일은 8자리 숫자(20260714)로 기재. 템플릿 샘플 데이터는 2026-07-14 문자열로 되어 있어 서로 다름 → 작성안내 기준을 따름.
  • 사이트 표기 게시판명으로 파일명을 지정(예: …(KRIHS국토연구원)_국토용어사전.xlsx). 분류명 기준(영상자료·간행물)으로 하면 게시판명이 중복되어 파일명이 충돌하기 때문.
Actions #1

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

Actions

내보내기 Atom PDF