# -*- coding: utf-8 -*- """공공기관 N 이미지 — 게시판 전용 재판정(상세글 렌더 + 크기필터). 매뉴얼: 게시판 N 이미지 = 상세글 표본의 본문 실사진(첨부PDF/뷰어 제외). 대상: plan_{기관}.json 에서 L==게시판 & 원본N('이미지' 포함) 행. 각 게시판: 목록에서 상세URL 추출→상위 4개 렌더→본문 콘텐츠이미지(≥170×110, 노이즈제외) 있으면 이미지. 결과를 엑셀 N에 반영(게시판만). 사용: python _공공기관_nboard.py [기관명 ...] """ import sys, os, re, json, time, warnings, importlib.util import requests, openpyxl from urllib.parse import urljoin from bs4 import BeautifulSoup warnings.filterwarnings('ignore') OUTDIR = r'D:\01.프로젝트\DB수집\공공기관' TEMP = r'D:\01.프로젝트\DB수집\_temp' UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' spec = importlib.util.spec_from_file_location('p234', r'D:\01.프로젝트\DB수집\_스크립트\_공공기관_phase234.py') P = importlib.util.module_from_spec(spec); spec.loader.exec_module(P) NOISE = re.compile(r'(ico[_\-/]|/icon|logo|btn|bul[_\-]|bg[_\-]|banner|sns|blank|spacer|loading|arrow|/dot|line[_\.]|top_|foot|header|common|_icon|symbol|copyright|qr_|no_img|noimage|share|facebook|insta|twitter|naver|kakao|/skin/|/template/|synap|pdf|viewer|/editor/icon)', re.I) JS_IMGS = """() => { const o=[]; for(const i of document.querySelectorAll('img')){const r=i.getBoundingClientRect(); o.push({src:i.currentSrc||i.src||'',nw:i.naturalWidth,nh:i.naturalHeight,rw:Math.round(r.width),rh:Math.round(r.height)});} return o; }""" def has_content_img(items): for it in items: s = it.get('src', '') if not s or NOISE.search(s): continue if it.get('nw', 0) >= 170 and it.get('nh', 0) >= 110 and it.get('rw', 0) >= 100 and it.get('rh', 0) >= 80: return True return False def add_image(N): parts = [p for p in (N or '').split(',') if p] if '없음' in parts: parts = [p for p in parts if p != '없음'] if '어문' not in parts: parts = ['어문'] + parts if '이미지' not in parts: # 어문 다음에 이미지 out = [] for p in parts: out.append(p) if p == '어문': out.append('이미지') parts = out return ','.join(parts) def rm_image(N): parts = [p for p in (N or '').split(',') if p and p != '이미지'] return ','.join(parts) if parts else '없음' def run(name, pg): pf = os.path.join(TEMP, f'nshot_{name}', f'plan_{name}.json') if not os.path.exists(pf): print(f'[{name}] plan없음 스킵'); return plan = json.load(open(pf, encoding='utf-8')) boards = [it for it in plan if it.get('L') == '게시판' and '이미지' in (it.get('N') or '')] if not boards: print(f'[{name}] 재판정 게시판 0'); return xlsx = os.path.join(OUTDIR, f'{name}.xlsx') wb = openpyxl.load_workbook(xlsx) ws = wb.active sess = P.make_session() img_cnt = txt_cnt = 0 montshots = [] for it in boards: r, url = it['row'], it['url'] try: soup, _ = P.fetch(sess, url) if not soup: continue body = P.get_body(soup, P.BODY_SEL) details = P.extract_detail_urls(body, url, limit=4) found = False for du in details: try: pg.goto(du, timeout=20000, wait_until='domcontentloaded'); pg.wait_for_timeout(1500) if has_content_img(pg.evaluate(JS_IMGS)): found = True sp = os.path.join(TEMP, f'nshot_{name}', f'bd_r{r}.png') try: pg.screenshot(path=sp); montshots.append((r, sp)) except Exception: pass break except Exception: continue cur = ws.cell(r, 14).value or '' if found: ws.cell(r, 14).value = add_image(cur); img_cnt += 1 else: ws.cell(r, 14).value = rm_image(cur); txt_cnt += 1 except Exception: continue wb.save(xlsx) print(f'[{name}] 게시판재판정 {len(boards)}개 → 이미지 {img_cnt}·어문 {txt_cnt}') return montshots def main(): from playwright.sync_api import sync_playwright probe = {r['name']: r for r in json.load(open(r'D:\01.프로젝트\DB수집\_스크립트\_공공기관_probe.json', encoding='utf-8'))} order = sorted(probe.values(), key=lambda x: -int(x['num'])) only = sys.argv[1:] if only: order = [p for p in order if p['name'] in only or str(p['num']) in only] with sync_playwright() as pw: b = pw.chromium.launch() pg = b.new_page(user_agent=UA, viewport={'width': 1280, 'height': 1600}) for p in order: try: run(p['name'], pg) except Exception as e: print(f"[{p['name']}] 실패: {e}") b.close() print('=== 게시판 N 재판정 완료 ===') if __name__ == '__main__': main()