DB_JOB/작업파일/공공기관2/1.한국전기안전공사/_kesco_njudge.py
hehihoho3 df16c98366 백업: DB수집 전체 스냅샷 (공공기관2 정리 전)
공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:15:40 +09:00

122 lines
5.7 KiB
Python

# -*- coding: utf-8 -*-
"""kesco N 재판정 — 67~끝 행: 본문(.contents) 콘텐츠이미지/영상 탐지 + 몽타주.
사용자 3~66 학습기준: 실사진·지도·시설/서비스 사진 있으면 어문,이미지 / 순수텍스트·조직도·텍스트목록게시판=어문.
출력: _nshots\ (rN.png + montage_*.png) + plan.json
"""
import sys, os, re, json, glob, warnings
import openpyxl
from PIL import Image, ImageDraw, ImageFont
warnings.filterwarnings('ignore')
HERE = os.path.dirname(os.path.abspath(__file__))
XLSX = os.path.join(HERE, '한국전기안전공사.xlsx')
OUT = os.path.join(HERE, '_nshots')
UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
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/|/resources/|/images/common|/img/comm)', re.I)
JS = r"""()=>{
const cont=document.querySelector('#contents,.contents,#content,.content,.sub_content,.sub_contents,#sub_content,#container')||document.body;
const imgs=[];
for(const i of cont.querySelectorAll('img')){const r=i.getBoundingClientRect();
imgs.push({src:i.currentSrc||i.src||'',alt:(i.alt||''),rw:Math.round(r.width),rh:Math.round(r.height),nw:i.naturalWidth,nh:i.naturalHeight});}
let vid=0;
if(cont.querySelector('video,source[src*=".mp4"]')) vid++;
for(const f of cont.querySelectorAll('iframe')){const s=f.src||'';if(/youtube|youtu\.be|vimeo|player\./i.test(s))vid++;}
return {imgs,vid};
}"""
DIAG = re.compile(r'(조직도|체계도|순서도|흐름도|구성도|개념도|절차도|한눈에|자세한)')
def real_imgs(items):
out = []
for it in items:
src = it.get('src', '')
if not src or NOISE.search(src):
continue
rw, rh, nw, nh = it['rw'], it['rh'], it['nw'], it['nh']
# 렌더 ≥110x80 또는 원본 ≥200x150 (장식 제외)
if (rw >= 110 and rh >= 80) or (nw >= 200 and nh >= 150 and rw >= 80):
out.append(it)
return out
def run():
wb = openpyxl.load_workbook(XLSX); ws = wb.active
os.makedirs(OUT, exist_ok=True)
targets = []
for r in range(67, ws.max_row + 1):
if ws.cell(r, 2).value is None:
continue
L = ws.cell(r, 12).value or ''
url = ws.cell(r, 11).value
if L == '사이트':
continue
if not (url and isinstance(url, str) and url.startswith('http')):
continue
targets.append((r, url, L))
print(f'대상 {len(targets)}행 렌더…')
from playwright.sync_api import sync_playwright
plan = []; shots = []
with sync_playwright() as p:
b = p.chromium.launch()
pg = b.new_page(user_agent=UA, viewport={'width': 1280, 'height': 2000})
for idx, (r, url, L) in enumerate(targets):
rec = {'row': r, 'url': url, 'L': L}
try:
pg.goto(url, timeout=25000, wait_until='domcontentloaded')
pg.wait_for_timeout(1600)
d = pg.evaluate(JS)
ri = real_imgs(d['imgs'])
rec['vid'] = d['vid']
rec['nimg'] = len(ri)
rec['samples'] = [f"{it['src'].split('/')[-1][:24]} {it['rw']}x{it['rh']} a={it['alt'][:14]}" for it in ri[:5]]
if len(ri) == 0 and d['vid'] == 0:
rec['auto'] = '어문'
else:
rec['auto'] = 'candidate'
sp = os.path.join(OUT, f'r{r}.png')
try:
cont = pg.query_selector('#contents,.contents,#content,.content,.sub_content,#container')
(cont or pg).screenshot(path=sp)
except Exception:
try: pg.screenshot(path=sp, full_page=False)
except Exception: pass
if os.path.exists(sp):
shots.append((r, L, rec.get('vid'), sp))
except Exception as e:
rec['auto'] = '어문'; rec['err'] = str(e)[:40]
plan.append(rec)
if (idx + 1) % 20 == 0:
print(f' {idx+1}/{len(targets)}')
b.close()
# 몽타주
try: font = ImageFont.truetype('malgun.ttf', 18)
except Exception: font = ImageFont.load_default()
cols, cw, ch = 4, 320, 420; per = cols * 5
mps = []
for gi in range(0, len(shots), per):
chunk = shots[gi:gi + per]
rn = (len(chunk) + cols - 1) // cols
cv = Image.new('RGB', (cols * cw, rn * ch), 'white'); dr = ImageDraw.Draw(cv)
for j, (r, L, vid, sp) in enumerate(chunk):
try:
im = Image.open(sp).convert('RGB'); im.thumbnail((cw - 8, ch - 30))
except Exception: continue
cx, cy = (j % cols) * cw, (j // cols) * ch
cv.paste(im, (cx + 4, cy + 26))
dr.rectangle([cx, cy, cx + cw - 1, cy + ch - 1], outline='gray')
tag = f'r{r} {L}' + (' VID' if vid else '')
dr.text((cx + 4, cy + 4), tag, fill='red', font=font)
mp = os.path.join(OUT, f'montage_{gi//per+1}.png'); cv.save(mp); mps.append(mp)
json.dump(plan, open(os.path.join(OUT, 'plan.json'), 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
amun = sum(1 for x in plan if x['auto'] == '어문')
cand = sum(1 for x in plan if x['auto'] == 'candidate')
vid = sum(1 for x in plan if x.get('vid'))
print(f'자동 어문 {amun} | 후보(이미지/영상) {cand} | 영상감지 {vid} | 몽타주 {len(mps)}')
for mp in mps: print(' MONT:', mp)
if __name__ == '__main__':
run()