공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
3.7 KiB
Python
90 lines
3.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""검출된 본문이미지가 '진짜 사진'인지 크기로 검증. 아이콘·작은배너·서명 제외.
|
|
기준: 가로>=250 AND 세로>=250 (또는 한 변>=400) 그리고 용량>=12KB → 사진으로 인정.
|
|
JPEG/PNG/GIF 헤더만 읽어 치수 파악(전체 다운로드 최소화는 생략, 작은파일이라 통째 읽음).
|
|
사용: python _imgcheck.py "<json: [[행,menuCd,라벨], ...]>" (해당 보드 본문이미지 표본 검사)
|
|
"""
|
|
import urllib.request, ssl, re, html as ht, sys, json, struct, io
|
|
|
|
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
|
BASE='https://www.gimje.go.kr'
|
|
|
|
def get_bytes(url):
|
|
req=urllib.request.Request(url, headers={'User-Agent':'Mozilla/5.0'})
|
|
return urllib.request.urlopen(req,context=ctx,timeout=25).read()
|
|
|
|
def get(url):
|
|
return get_bytes(url).decode('utf-8','replace')
|
|
|
|
def img_size(data):
|
|
"""(w,h) from JPEG/PNG/GIF bytes, else (None,None)."""
|
|
if data[:8]==b'\x89PNG\r\n\x1a\n':
|
|
w,h=struct.unpack('>II', data[16:24]); return w,h
|
|
if data[:3]==b'GIF':
|
|
w,h=struct.unpack('<HH', data[6:10]); return w,h
|
|
if data[:2]==b'\xff\xd8': # JPEG
|
|
i=2
|
|
while i<len(data)-9:
|
|
if data[i]!=0xFF: i+=1; continue
|
|
m=data[i+1]
|
|
if m in (0xC0,0xC1,0xC2,0xC3,0xC5,0xC6,0xC7,0xC9,0xCA,0xCB):
|
|
h,w=struct.unpack('>HH', data[i+5:i+9]); return w,h
|
|
seg=struct.unpack('>H', data[i+2:i+4])[0]; i+=2+seg
|
|
return None,None
|
|
|
|
def is_real_photo(url):
|
|
if url.startswith('/'): url=BASE+url
|
|
try:
|
|
data=get_bytes(url)
|
|
except Exception as ex:
|
|
return None, f'ERR {ex}'
|
|
w,h=img_size(data); kb=len(data)//1024
|
|
if w is None: return None, f'치수불명 {kb}KB'
|
|
real = (kb>=12) and ((w>=250 and h>=250) or max(w,h)>=400)
|
|
return real, f'{w}x{h} {kb}KB'
|
|
|
|
# 본문이미지 추출(스캐너와 동일 규칙)
|
|
def view_links(menucd):
|
|
d=get(f'{BASE}/index.gimje?menuCd={menucd}')
|
|
out=[]
|
|
for l in re.findall(r'href="(/board/view\.gimje\?[^"]+)"', d):
|
|
l=ht.unescape(l)
|
|
if l not in out: out.append(l)
|
|
return out
|
|
|
|
def body_content_imgs(det):
|
|
s=det.find('bbs_view')
|
|
if s<0: s=det.find('bbs_con')
|
|
if s<0: return []
|
|
ends=[p for p in (det.find('bbs_file',s),det.find('bbs_btn',s),det.find('bbs_navi',s),det.find('bbs_paging',s)) if p>0]
|
|
seg=det[s:(min(ends) if ends else s+12000)]
|
|
out=[]
|
|
for i in re.findall(r'<img[^>]+src="([^"]+)"', seg):
|
|
i=i.strip(); low=i.lower()
|
|
if '/images/common/' in low or 'opentype' in low: continue
|
|
if any(k in low for k in ('icon','emoticon','blank.','spacer','btn_','/common/','logo')): continue
|
|
if '/upload_data/' in low or '/editor' in low or 'board_data' in low or '/se2/' in low:
|
|
out.append(i)
|
|
return out
|
|
|
|
if __name__=='__main__':
|
|
targets=json.loads(sys.argv[1])
|
|
for row,menucd,label in targets:
|
|
links=view_links(menucd)[:10]
|
|
photo_posts=0; checked_imgs=[]
|
|
for l in links:
|
|
try: det=get(BASE+l)
|
|
except: continue
|
|
imgs=body_content_imgs(det)
|
|
postreal=False
|
|
for im in imgs[:2]:
|
|
real,info=is_real_photo(im)
|
|
checked_imgs.append((real,info,im.split('/')[-1][:24]))
|
|
if real: postreal=True
|
|
if postreal: photo_posts+=1
|
|
verdict='어문,이미지' if photo_posts>0 else '어문'
|
|
print(f'행{row} | {label[:28]} | 진짜사진게시물 {photo_posts} → {verdict}')
|
|
for real,info,nm in checked_imgs[:5]:
|
|
mark='📷' if real else ('?' if real is None else '🔸아이콘')
|
|
print(f' {mark} {info} {nm}')
|