# -*- coding: utf-8 -*- """김제 게시판 N 재판정: 게시물 상세 본문(bbs_view) 안의 실제 콘텐츠 이미지 유무 검사. 콘텐츠 이미지 = /upload_data/ (에디터 본문삽입). 제외 = /images/common/(로고·opentype마크)·이모티콘·아이콘. 첨부파일(bbs_file 영역)은 본문 이미지로 치지 않음(별도). 출력: 보드별 표본 게시물수 / 본문이미지 있는 게시물수 / 샘플 경로. """ import urllib.request, ssl, re, html as ht, sys, json ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE BASE='https://www.gimje.go.kr' def get(url): req=urllib.request.Request(url, headers={'User-Agent':'Mozilla/5.0'}) return urllib.request.urlopen(req,context=ctx,timeout=25).read().decode('utf-8','replace') def view_links(menucd): d=get(f'{BASE}/index.gimje?menuCd={menucd}') raw=re.findall(r'href="(/board/view\.gimje\?[^"]+)"', d) out=[] for l in raw: l=ht.unescape(l) if l not in out: out.append(l) return out, d def list_thumbs(listhtml): """목록 페이지의 콘텐츠 썸네일(갤러리/자료형 신호). 사이트공통 팝업·common 제외.""" imgs=re.findall(r']+src="([^"]*upload_data[^"]*)"', listhtml) out=[] for i in imgs: i=i.strip() if 'BBS_0000269' in i: continue # 사이트공통 팝업 공지 if '/images/common/' in i.lower() or 'opentype' in i.lower(): continue out.append(i) return out def body_imgs(det): s=det.find('bbs_view') if s<0: s=det.find('bbs_con') if s<0: return None, [] # 본문 컨테이너 못찾음 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] e=min(ends) if ends else s+12000 seg=det[s:e] imgs=re.findall(r']+src="([^"]+)"', seg) content=[] for i in imgs: i=i.strip() low=i.lower() if '/images/common/' in low: continue if '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: content.append(i) return len(imgs), content def scan(menucd, nmax=10): links, listhtml = view_links(menucd) thumbs=list_thumbs(listhtml) links=links[:nmax] posts=[] for l in links: try: det=get(BASE+l) except Exception as ex: posts.append({'link':l,'err':str(ex)}); continue total, cont = body_imgs(det) posts.append({'link':l,'total_img':total,'content_img':cont}) nwith=sum(1 for p in posts if p.get('content_img')) return {'checked':len(posts),'with_img':nwith,'thumbs':len(thumbs), 'samples':[p['content_img'][0] for p in posts if p.get('content_img')][:3], 'thumb_samples':thumbs[:3],'posts':posts} def targets_from_xlsx(path, rfrom=162, only_image=True): import openpyxl wb=openpyxl.load_workbook(path); ws=wb.active last=max(r for r in range(3,ws.max_row+1) if ws.cell(r,2).value not in (None,'')) out=[] for r in range(rfrom,last+1): L=ws.cell(r,12).value; N=str(ws.cell(r,14).value or '') if not (L and '게시판' in str(L)): continue if only_image and '이미지' not in N: continue url=str(ws.cell(r,11).value or '') m=re.search(r'menuCd=([A-Za-z0-9_]+)', url) if not m: continue parts=[ws.cell(r,c).value for c in range(4,11)] label=' > '.join(str(p) for p in parts if p not in (None,'')) out.append([r, m.group(1), label, N]) return out if __name__=='__main__': if sys.argv[1].startswith('--'): mode=sys.argv[1] oi = (mode=='--suspects') # --suspects: 이미지표시만 / --allboards: 전체 게시판 targets=[[r,m,l] for r,m,l,n in targets_from_xlsx('전북특별자치도_김제시.xlsx', only_image=oi)] else: targets=json.loads(sys.argv[1]) # [[row,menucd,label], ...] for row, menucd, label in targets: try: r=scan(menucd) gallery = r['thumbs']>=2 # 목록 썸네일 2개 이상 = 갤러리/자료형 body = r['with_img']>0 # 본문 인라인 사진 verdict='어문,이미지' if (gallery or body) else '어문' mode = ('갤러리' if gallery else '')+('+본문' if body else '') or '없음' print(f"행{row} | {label[:30]} | 표본{r['checked']} 본문img{r['with_img']} 목록썸네일{r['thumbs']} [{mode}] → {verdict}") for s in r['thumb_samples'][:1]: print(f" 썸네일: {s[:72]}") for s in r['samples'][:1]: print(f" 본문img: {s[:72]}") except Exception as ex: print(f"행{row} | {label[:30]} | ERR {ex}")