- 공공기관2/3 작업본 + 오늘 제출 17곳 D~J 카테고리 셀병합 정상화 - 한국지역난방공사 옵션2(고아셀 F98 수정)+전행 높이17 - 제출_프리랜서2_2026-06-21.zip 생성(17개 xlsx, 2,468행) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
82 lines
3.8 KiB
Python
82 lines
3.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""각 candidate 행의 본문 콘텐츠이미지(필터통과)만 모아 행라벨과 함께 큰 타일 montage 생성."""
|
|
import sys,io,os,re,json,warnings
|
|
sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='utf-8')
|
|
warnings.filterwarnings('ignore')
|
|
from playwright.sync_api import sync_playwright
|
|
from PIL import Image,ImageDraw,ImageFont
|
|
import openpyxl
|
|
TEMP=r'D:\01.프로젝트\DB수집\_temp\imgtiles_kead'
|
|
os.makedirs(TEMP,exist_ok=True)
|
|
plan=json.load(open(r'D:\01.프로젝트\DB수집\_temp\nshot_한국장애인고용공단\plan_한국장애인고용공단.json',encoding='utf-8'))
|
|
cands=[p for p in plan if p['auto']=='candidate']
|
|
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="""()=>{const out=[];const scope=document.querySelector('#contents,#content,#container,.contents,.content,#sub_content,.sub_content,main')||document.body;
|
|
for(const i of scope.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),x:Math.round(r.x),y:Math.round(r.y)});}return out;}"""
|
|
def good(it):
|
|
s=it['src']
|
|
if not s or NOISE.search(s): return False
|
|
return it['nw']>=170 and it['nh']>=110 and it['rw']>=100 and it['rh']>=80
|
|
try:
|
|
fnt=ImageFont.truetype('malgun.ttf',20); fnt2=ImageFont.truetype('malgun.ttf',16)
|
|
except: fnt=ImageFont.load_default(); fnt2=fnt
|
|
results={}
|
|
with sync_playwright() as pw:
|
|
b=pw.chromium.launch()
|
|
pg=b.new_page(viewport={'width':1280,'height':2000})
|
|
for p in cands:
|
|
r=p['row']; url=p['url']
|
|
try:
|
|
pg.goto(url,wait_until='networkidle',timeout=25000)
|
|
imgs=pg.evaluate(JS)
|
|
except Exception as e:
|
|
results[r]={'err':str(e)[:30],'imgs':[]}; continue
|
|
gi=[it for it in imgs if good(it)]
|
|
# screenshot each good image region by element via clip
|
|
crops=[]
|
|
for j,it in enumerate(gi[:4]):
|
|
try:
|
|
if it['rw']<10 or it['rh']<10 or it['y']<0: continue
|
|
clip={'x':max(0,it['x']),'y':max(0,it['y']),'width':min(it['rw'],1200),'height':min(it['rh'],700)}
|
|
path=os.path.join(TEMP,f'r{r}_{j}.png')
|
|
pg.screenshot(path=path,clip=clip)
|
|
crops.append(path)
|
|
except Exception: pass
|
|
results[r]={'n':len(gi),'crops':crops,'N':p['N']}
|
|
b.close()
|
|
json.dump({str(k):v for k,v in results.items()},open(os.path.join(TEMP,'_map.json'),'w',encoding='utf-8'),ensure_ascii=False)
|
|
# build tiled montages: each row = label + up to 4 crops side by side
|
|
rows_sorted=sorted(results.items())
|
|
CW,CH=300,200; LBLW=70; GAP=6
|
|
def load_thumb(pth):
|
|
try:
|
|
im=Image.open(pth).convert('RGB'); im.thumbnail((CW,CH)); return im
|
|
except: return None
|
|
# group 10 rows per montage
|
|
import math
|
|
page=0
|
|
batch=[]
|
|
def flush(batch,page):
|
|
if not batch: return
|
|
H=sum(CH+GAP for _ in batch)+GAP
|
|
W=LBLW+(CW+GAP)*4+GAP
|
|
canvas=Image.new('RGB',(W,H),'white'); d=ImageDraw.Draw(canvas)
|
|
y=GAP
|
|
for r,info in batch:
|
|
d.text((4,y+4),f'r{r}',fill='red',font=fnt)
|
|
d.text((4,y+30),f'#{info.get("n",0)}',fill='blue',font=fnt2)
|
|
x=LBLW
|
|
for c in info.get('crops',[]):
|
|
t=load_thumb(c)
|
|
if t: canvas.paste(t,(x,y))
|
|
d.rectangle([x,y,x+CW,y+CH],outline='gray')
|
|
x+=CW+GAP
|
|
y+=CH+GAP
|
|
out=os.path.join(TEMP,f'tiles_{page}.png'); canvas.save(out); print('TILE',out)
|
|
for r,info in rows_sorted:
|
|
batch.append((r,info))
|
|
if len(batch)==10:
|
|
flush(batch,page); page+=1; batch=[]
|
|
flush(batch,page)
|
|
print('done. rows:',len(rows_sorted))
|