DB_JOB/작업파일/공공기관2/6.한국중부발전/_komipo_phase234.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

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)