# -*- coding: utf-8 -*- """부여군 N(저작물 유형) 재판정 — 장식/공통UI 이미지 false '이미지' 제거. - phase234 detect_media 는 KOGL 마크만 제외 → file_icon.gif·see_btn.gif·btn_page_*.gif 등 게시판 스킨/공통버튼 아이콘을 '이미지'로 오탐. - [[feedback_N_image_rule]] 적용: 장식/공통UI·페이징버튼·첨부아이콘 제외, 실제 사진/업로드·지도/PDF임베드만 인정. - 대상: 현재 N 에 '이미지' 포함된 같은도메인 행만(false 양성 제거 전용 → 이미지 추가 안 함). - 게시판은 상세글 상위5 추적. body=#txt|#contents|main. 사용: python -X utf8 _buyeo_rejudge_N.py [--write] """ import sys, warnings, re from urllib.parse import urljoin import requests, openpyxl from bs4 import BeautifulSoup warnings.filterwarnings('ignore') S = requests.Session(); S.headers.update({'User-Agent': 'Mozilla/5.0'}) XLSX = r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\7.부여군\충청남도_부여군.xlsx' DOMAIN = 'buyeo.go.kr' KOGL = re.compile(r'(?:new_)?img_open(?:type|code)\d', re.I) # 장식/공통UI: 기존 논산 DECO + 부여 게시판 스킨/공통버튼/첨부아이콘 패턴 보강 DECO = re.compile( r'/site/common/img/|move\.png|no[-_]?img|blank\.|spacer\.|' r'/img/(?:icon|ico|bul|bullet|arrow|btn|bg|tit|h\d)|' r'ico_|btn_|bul_|bg_|_bg\.|icon_|' r'file_icon|_icon\.|icon\.gif|_btn\.|see_btn|' # 첨부아이콘·바로보기버튼 r'/skin/|/images/(?:kr/)?common/|/common/img/|' # 게시판 스킨·공통 UI 디렉터리 r'btn_page|page_(?:next|prev|first|last)', re.I) # 페이징 버튼 MAPPDF = re.compile(r'pdf|viewer\.html|map|kakao|daum|naver.*map|google.*map|/map', re.I) VIDEO = re.compile(r'youtube\.com|youtu\.be|/embed/|\.mp4|\.webm|vimeo', re.I) DETAIL = re.compile(r'mode=V|view\.do|/view|seq=|idx=|mng_no=|bbsSeq|nttId|no=', re.I) def get(url): r = S.get(url, verify=False, timeout=20) return BeautifulSoup(r.content, 'html.parser') def body(soup): return soup.select_one('#txt') or soup.select_one('#contents') or soup.select_one('main') or soup def real_imgs(node): n = 0 for im in node.find_all('img'): src = im.get('src') or '' if not src or KOGL.search(src) or DECO.search(src): continue n += 1 return n def media_flags(node): img = real_imgs(node) > 0 vid = False for ifr in node.find_all('iframe'): s = ifr.get('src') or '' if VIDEO.search(s): vid = True elif MAPPDF.search(s): img = True for a in node.find_all('a'): if VIDEO.search(a.get('href') or ''): vid = True if node.find('video'): vid = True return img, vid def detail_links(soup, base): out = [] for a in body(soup).find_all('a'): h = a.get('href') or '' if h.startswith('#'): continue if DETAIL.search(h): out.append(urljoin(base, h)) seen = set(); res = [] for u in out: if u not in seen: seen.add(u); res.append(u) return res[:5] def judge(k, L, M, N): if not isinstance(k, str) or DOMAIN not in k: return N, 'skip(외부)' try: soup = get(k) except Exception as e: return N, f'fetch실패:{str(e)[:25]}' bd = body(soup) txtlen = len(bd.get_text(strip=True)) if L == '게시판' and (M in (0, '0', None)): return '없음', 'board0' has_img, has_vid = media_flags(bd) detail = False if L == '게시판' and not has_img: for du in detail_links(soup, k): try: di, dv = media_flags(body(get(du))) if di: has_img = True if dv: has_vid = True if di or dv: detail = True if has_img and has_vid: break except Exception: pass parts = [] if txtlen >= 1: parts.append('어문') if has_img: parts.append('이미지') if has_vid: parts.append('영상') return (','.join(parts) if parts else '없음'), ('detail' if detail else '') def main(): write = '--write' in sys.argv wb = openpyxl.load_workbook(XLSX); ws = wb.active changes = [] for r in range(3, ws.max_row + 1): N = ws.cell(r, 14).value if not (isinstance(N, str) and '이미지' in N): continue k = ws.cell(r, 11).value; L = ws.cell(r, 12).value; M = ws.cell(r, 13).value new, tag = judge(k, L, M, N) if new != N: changes.append((r, N, new, tag, ws.cell(r, 7).value or ws.cell(r, 6).value, k)) print(f'현재 N에 이미지 포함 행 재판정 → 변경 {len(changes)}건') for r, old, new, tag, label, k in changes: print(f' 행{r} [{old} → {new}] {tag} | {label} | {str(k)[-45:]}') if write and changes: for r, old, new, tag, label, k in changes: ws.cell(r, 14).value = new wb.save(XLSX) print(f'\n저장: {XLSX} ({len(changes)}건 수정)') elif not write: print('\n(계획만. --write 로 반영)') if __name__ == '__main__': main()