공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
114 lines
6.0 KiB
Python
114 lines
6.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""komipo Phase2~4 (throttled Playwright). L 이미 채워짐. 게시판 M·N·O/P/Q.
|
|
출력: L~Q + _nshots(이미지후보 몽타주). N은 후보만 표시→몽타주 후 시각판정 별도 적용."""
|
|
import re, json, io, os, time, warnings
|
|
import openpyxl, requests
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
from playwright.sync_api import sync_playwright
|
|
warnings.filterwarnings('ignore'); requests.packages.urllib3.disable_warnings()
|
|
FN='한국중부발전.xlsx'; OUT='_nshots'; os.makedirs(OUT,exist_ok=True)
|
|
UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36'
|
|
NAME_NOISE=re.compile(r'(^ico[_\-]|icon|logo|btn[_\-]|^bul|bullet|^bg[_\-]|banner|^sns|blank|spacer|loading|arrow|^dot|footer|^foot|^header|symbol|copyright|^qr|no_img|noimage|share|facebook|insta|twitter|naver|kakao|mark_|public|kogl|opentype|opencode)',re.I)
|
|
KOGL=re.compile(r'open(?:type|code)0*(\d)\.(?:png|jpe?g|gif)',re.I)
|
|
JS=r"""()=>{
|
|
const c=document.querySelector('#contents,.contents,#content,.content,.sub_content,#container,.cont_wrap,.contents_wrap')||document.body;
|
|
const imgs=[];
|
|
for(const i of c.querySelectorAll('img')){imgs.push({src:i.currentSrc||i.src||'',alt:i.alt||'',nw:i.naturalWidth,nh:i.naturalHeight});}
|
|
let vid=0;
|
|
if(c.querySelector('video,source[src*=".mp4"]'))vid++;
|
|
for(const f of c.querySelectorAll('iframe')){if(/youtube|youtu\.be|vimeo/i.test(f.src||''))vid++;}
|
|
// 게시판 총건수/리스트
|
|
let total=null;
|
|
const m=(document.body.innerText||'').match(/(?:총|전체)\s*[:\-]?\s*([\d,]+)\s*건/);
|
|
if(m) total=parseInt(m[1].replace(/,/g,''));
|
|
let rows=document.querySelectorAll('table.board_list tbody tr, .board_list li, table tbody tr, .bbs_list tbody tr').length;
|
|
// KOGL 마크
|
|
let kogl=[];
|
|
for(const i of document.querySelectorAll('img')){const mm=(i.src||'').match(/open(?:type|code)0*(\d)/i); if(mm)kogl.push(parseInt(mm[1]));}
|
|
return {imgs,vid,total,rows,kogl:[...new Set(kogl)]};
|
|
}"""
|
|
def realimg(items):
|
|
out=[]
|
|
for it in items:
|
|
s=it['src'];
|
|
if not s: continue
|
|
base=s.split('?')[0].split('/')[-1]
|
|
if NAME_NOISE.search(base): continue
|
|
if it['nw']>=200 and it['nh']>=150 and it['nw']*it['nh']>=40000: out.append(it)
|
|
return out
|
|
wb=openpyxl.load_workbook(FN); ws=wb.active
|
|
sess=requests.Session(); sess.headers['User-Agent']=UA
|
|
targets=[]
|
|
for r in range(3,ws.max_row+1):
|
|
L=ws.cell(r,12).value; u=ws.cell(r,11).value
|
|
if L=='사이트' or not(u and str(u).startswith('http')): continue
|
|
targets.append((r,str(u),L))
|
|
print('대상',len(targets),flush=True)
|
|
plan=[]; dl=[]
|
|
with sync_playwright() as p:
|
|
b=p.chromium.launch(); pg=b.new_page(user_agent=UA,viewport={'width':1280,'height':2000})
|
|
for idx,(r,u,L) in enumerate(targets):
|
|
d=None
|
|
for attempt in range(3):
|
|
try:
|
|
pg.goto(u,timeout=25000,wait_until='domcontentloaded'); pg.wait_for_timeout(900)
|
|
d=pg.evaluate(JS); break
|
|
except Exception:
|
|
time.sleep(1.5)
|
|
rec={'row':r,'L':L,'url':u}
|
|
if d:
|
|
ri=realimg(d['imgs']); rec['nimg']=len(ri); rec['vid']=d['vid']; rec['kogl']=d['kogl']
|
|
# M
|
|
if L=='게시판':
|
|
M=d['total'] if d['total'] is not None else (d['rows'] if d['rows'] else 0)
|
|
ws.cell(r,13).value=M; rec['M']=M
|
|
else:
|
|
ws.cell(r,13).value=1
|
|
# O/P/Q
|
|
if d['kogl']:
|
|
ws.cell(r,15).value=','.join(str(x) for x in sorted(set(d['kogl'])))
|
|
ws.cell(r,16).value='게시물' if L=='게시판' else '게시물'
|
|
ws.cell(r,17).value='Y'
|
|
else:
|
|
ws.cell(r,15).value='미부착'
|
|
# N 후보 다운로드
|
|
if ri:
|
|
rec['auto']='candidate'
|
|
for it in ri[:2]:
|
|
try:
|
|
rr=sess.get(it['src'],timeout=10,verify=False); im=Image.open(io.BytesIO(rr.content)).convert('RGB'); im.thumbnail((280,280))
|
|
lp=os.path.join(OUT,f'r{r}_{len(dl)}.jpg'); im.save(lp,quality=78)
|
|
lab=ws.cell(r,7).value or ws.cell(r,6).value or ws.cell(r,5).value or ''
|
|
dl.append((r,str(lab)[:14],lp))
|
|
except Exception: pass
|
|
else:
|
|
rec['auto']='어문' if (L!='게시판' or rec.get('M',1)) else '없음'
|
|
# 임시 N: 후보면 어문,이미지 / 영상 / 빈게시판 없음 — 시각판정 전 자동
|
|
if d['vid'] and not ri: ws.cell(r,14).value='어문,영상'
|
|
elif ri: ws.cell(r,14).value='어문,이미지'
|
|
elif L=='게시판' and rec.get('M',0)==0: ws.cell(r,14).value='없음'
|
|
else: ws.cell(r,14).value='어문'
|
|
else:
|
|
rec['err']=1; ws.cell(r,13).value=ws.cell(r,13).value or (1 if L=='페이지' else 0); ws.cell(r,14).value='어문'; ws.cell(r,15).value='미부착'
|
|
plan.append(rec)
|
|
if (idx+1)%20==0:
|
|
wb.save(FN); print(f' {idx+1}/{len(targets)} (저장)',flush=True)
|
|
time.sleep(0.4)
|
|
b.close()
|
|
wb.save(FN)
|
|
# 몽타주
|
|
try: font=ImageFont.truetype('malgun.ttf',15)
|
|
except: font=ImageFont.load_default()
|
|
cols,cw,ch=6,200,230; per=cols*7; mps=[]
|
|
for gi in range(0,len(dl),per):
|
|
ch_=dl[gi:gi+per]; rn=(len(ch_)+cols-1)//cols
|
|
cv=Image.new('RGB',(cols*cw,rn*ch),'white'); dr=ImageDraw.Draw(cv)
|
|
for j,(r,lab,lp) in enumerate(ch_):
|
|
try: im=Image.open(lp).convert('RGB'); im.thumbnail((cw-8,ch-28))
|
|
except: continue
|
|
cx,cy=(j%cols)*cw,(j//cols)*ch; cv.paste(im,(cx+4,cy+24)); dr.rectangle([cx,cy,cx+cw-1,cy+ch-1],outline='gray'); dr.text((cx+4,cy+4),f'r{r}',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)
|
|
print('완료. 후보',len(dl),'몽타주',len(mps),flush=True)
|
|
for mp in mps: print(' MONT:',mp,flush=True)
|