공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
5.3 KiB
Python
124 lines
5.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""공공기관 N 이미지 몽타주 재판정 — 1단계: 렌더+콘텐츠이미지 크기필터 + 풀페이지 스샷 + 몽타주.
|
|
|
|
대상: N에 '이미지' 포함 & L!=사이트 & URL 있는 행.
|
|
출력: _temp\nshot_{기관}\ (스샷 png + montage_*.png) + plan_{기관}.json
|
|
plan: {row, url, L, N, n_imgs(필터통과), auto='어문'(이미지없음) | 'candidate'(몽타주판정)}
|
|
사용: python _공공기관_nshot.py <기관명>
|
|
"""
|
|
import sys, os, re, json, warnings
|
|
import openpyxl
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
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'
|
|
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)', re.I)
|
|
|
|
JS_IMGS = """() => {
|
|
const out = [];
|
|
for (const i of document.querySelectorAll('img')) {
|
|
const r = i.getBoundingClientRect();
|
|
out.push({src: i.currentSrc||i.src||'', nw: i.naturalWidth, nh: i.naturalHeight, rw: Math.round(r.width), rh: Math.round(r.height)});
|
|
}
|
|
// 배경이미지도 일부
|
|
return out;
|
|
}"""
|
|
|
|
|
|
def content_imgs(items):
|
|
cnt = 0
|
|
for it in items:
|
|
src = it.get('src', '')
|
|
if not src or NOISE.search(src):
|
|
continue
|
|
nw, nh, rw, rh = it.get('nw', 0), it.get('nh', 0), it.get('rw', 0), it.get('rh', 0)
|
|
if nw >= 170 and nh >= 110 and rw >= 100 and rh >= 80:
|
|
cnt += 1
|
|
return cnt
|
|
|
|
|
|
def run(name):
|
|
from playwright.sync_api import sync_playwright
|
|
xlsx = os.path.join(OUTDIR, f'{name}.xlsx')
|
|
wb = openpyxl.load_workbook(xlsx)
|
|
ws = wb.active
|
|
targets = []
|
|
for r in range(3, ws.max_row + 1):
|
|
if ws.cell(r, 2).value is None:
|
|
break
|
|
L = ws.cell(r, 12).value
|
|
N = ws.cell(r, 14).value or ''
|
|
url = ws.cell(r, 11).value
|
|
if L != '사이트' and '이미지' in N and url and isinstance(url, str) and url.startswith('http'):
|
|
targets.append((r, url, L, N))
|
|
outdir = os.path.join(TEMP, f'nshot_{name}')
|
|
os.makedirs(outdir, exist_ok=True)
|
|
print(f'[{name}] 이미지후보 {len(targets)}행 렌더…')
|
|
plan = []
|
|
shots = [] # (row, N, path)
|
|
with sync_playwright() as p:
|
|
b = p.chromium.launch()
|
|
pg = b.new_page(user_agent=UA, viewport={'width': 1280, 'height': 1600})
|
|
for idx, (r, url, L, N) in enumerate(targets):
|
|
try:
|
|
pg.goto(url, timeout=25000, wait_until='domcontentloaded')
|
|
pg.wait_for_timeout(1800)
|
|
items = pg.evaluate(JS_IMGS)
|
|
nc = content_imgs(items)
|
|
if nc == 0:
|
|
plan.append({'row': r, 'url': url, 'L': L, 'N': N, 'n_imgs': 0, 'auto': '어문'})
|
|
else:
|
|
sp = os.path.join(outdir, f'r{r}.png')
|
|
try:
|
|
pg.screenshot(path=sp, full_page=False)
|
|
except Exception:
|
|
pass
|
|
plan.append({'row': r, 'url': url, 'L': L, 'N': N, 'n_imgs': nc, 'auto': 'candidate'})
|
|
if os.path.exists(sp):
|
|
shots.append((r, N, sp))
|
|
except Exception as e:
|
|
plan.append({'row': r, 'url': url, 'L': L, 'N': N, 'n_imgs': -1, 'auto': '어문', 'err': str(e)[:40]})
|
|
if (idx + 1) % 20 == 0:
|
|
print(f' {idx+1}/{len(targets)}')
|
|
b.close()
|
|
# 몽타주 그리드 (후보만)
|
|
cols, cw, ch = 4, 300, 360
|
|
try:
|
|
font = ImageFont.truetype('malgun.ttf', 16)
|
|
except Exception:
|
|
font = ImageFont.load_default()
|
|
mont_paths = []
|
|
per = cols * 5 # 20개/장
|
|
for gi in range(0, len(shots), per):
|
|
chunk = shots[gi:gi + per]
|
|
rows_n = (len(chunk) + cols - 1) // cols
|
|
canvas = Image.new('RGB', (cols * cw, rows_n * ch), 'white')
|
|
d = ImageDraw.Draw(canvas)
|
|
for j, (r, N, sp) in enumerate(chunk):
|
|
try:
|
|
im = Image.open(sp).convert('RGB')
|
|
im.thumbnail((cw - 8, ch - 26))
|
|
except Exception:
|
|
continue
|
|
cx, cy = (j % cols) * cw, (j // cols) * ch
|
|
canvas.paste(im, (cx + 4, cy + 22))
|
|
d.rectangle([cx, cy, cx + cw - 1, cy + ch - 1], outline='gray')
|
|
d.text((cx + 4, cy + 3), f'r{r}', fill='red', font=font)
|
|
mp = os.path.join(outdir, f'montage_{name}_{gi//per+1}.png')
|
|
canvas.save(mp)
|
|
mont_paths.append(mp)
|
|
auto_amun = sum(1 for x in plan if x['auto'] == '어문')
|
|
cand = sum(1 for x in plan if x['auto'] == 'candidate')
|
|
with open(os.path.join(outdir, f'plan_{name}.json'), 'w', encoding='utf-8') as f:
|
|
json.dump(plan, f, ensure_ascii=False, indent=1)
|
|
print(f'[{name}] 자동강등(이미지無) {auto_amun} | 몽타주후보 {cand} | 몽타주 {len(mont_paths)}장')
|
|
for mp in mont_paths:
|
|
print(' MONT:', mp)
|
|
print(' PLAN:', os.path.join(outdir, f'plan_{name}.json'))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
run(sys.argv[1])
|