# -*- coding: utf-8 -*- # 충북 범용 N 몽타주 크롤러: python _cb_Ncrawl.py <엑셀경로> <출력json> import openpyxl, json, sys, time, io from playwright.sync_api import sync_playwright sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='utf-8') F=sys.argv[1]; OUT=sys.argv[2] wb=openpyxl.load_workbook(F); ws=wb.active DEC=r"opentype|open0|kogl|/logo|logo\.|btn_|/icon|ico_|icon_|banner|visual|symbol|sns|/bg_|bg\.|blank|spacer|bullet|arrow|move\.png|no[-_]?img|noimg|popup|/common/|/template|/skin|emblem|/sns|share|facebook|insta|blog|twitter|youtube_|/qr|qr\." BAD=r"header|footer|gnb|lnb|snb|^nav|nav$|visual|main_|top_area|quick|banner|aside|skip|popup|familySite" JS_DET=r'''()=>{const a=[...document.querySelectorAll('a')]; return a.filter(x=>/selectBbsNttView|addBbsNttView|BbsNttView|nttNo=|nttId=|articleNo=|selectBoardArticle|view\.9is|view\.jinan|view\.jeongeup|board\/view|dataUid=|contentUid=|boardUid=|idx=/i.test((x.href||'')+(x.getAttribute&&x.getAttribute('onclick')||''))).map(x=>x.href).filter(h=>h&&h.indexOf('#')!=0).slice(0,8);}''' JS_IMG=r'''()=>{ const RB=new RegExp("%s","i"), RD=new RegExp("%s","i"); const bad=el=>{for(let n=el;n;n=n.parentElement){let c='';if(n.className){c=(typeof n.className==='string')?n.className:(n.className.baseVal||'');}c+=' '+(n.id||'');if(RB.test(c))return true;}return false;}; const dec=s=>RD.test(s||''); const imgs=[...document.querySelectorAll('img')]; const real=imgs.filter(im=>{const w=Math.max(im.naturalWidth,im.clientWidth),h=Math.max(im.naturalHeight,im.clientHeight);return (w>=240||h>=240)&&!dec(im.getAttribute('src'))&&!bad(im);}); const maps=[...document.querySelectorAll('iframe,embed,object')].filter(e=>/map|daum|kakao|google|vworld|roadview/i.test(e.src||e.data||'')).length; const pdf=[...document.querySelectorAll('iframe,embed,object')].filter(e=>/\.pdf/i.test(e.src||e.data||'')).length; return {r:real.length, m:maps, p:pdf, srcs:real.slice(0,6).map(im=>im.src)}; }''' % (BAD.replace('\\','\\\\'),DEC.replace('\\','\\\\')) rows=[] for r in range(3,ws.max_row+1): L=ws.cell(r,12).value; url=ws.cell(r,11).value; M=ws.cell(r,13).value; old=ws.cell(r,14).value if L=='사이트' or not str(url or '').startswith('http'): continue rows.append((r,L,M,url,old)) print('crawl 대상',len(rows),flush=True) def nav(pg,u): for _ in range(2): try: pg.goto(u,wait_until='domcontentloaded'); pg.wait_for_timeout(1100) pg.evaluate("window.scrollTo(0,document.body.scrollHeight)"); pg.wait_for_timeout(500) return True except Exception: time.sleep(1) return False res={} with sync_playwright() as p: b=p.chromium.launch(); pg=b.new_page(viewport={'width':1280,'height':2400}); pg.set_default_timeout(18000) for i,(r,L,M,url,old) in enumerate(rows): det=old; why=''; srcs=[] try: if L=='게시판': if M in (0,None,''): det='없음'; why='M0' elif nav(pg,url): views=pg.evaluate(JS_DET); hit=False; w='' for v in views[:5]: if v and v.startswith('http') and nav(pg,v): d=pg.evaluate(JS_IMG) if d['r']>0: hit=True; w='img%d'%d['r']; srcs=d['srcs']; break det='어문,이미지' if hit else '어문'; why=('board '+w) if hit else 'board txt' else: det=old; why='nav_fail' else: if nav(pg,url): d=pg.evaluate(JS_IMG) if d['r']>0 or d['m']>0 or d['p']>0: det='어문,이미지'; why='page img%d map%d pdf%d'%(d['r'],d['m'],d['p']); srcs=d['srcs'] else: det='어문'; why='page txt' else: det=old; why='nav_fail' except Exception as e: det=old; why='ERR:'+str(e)[:30] res[str(r)]={'L':L,'old':old,'det':det,'why':why,'srcs':srcs, 'label':ws.cell(r,8).value or ws.cell(r,7).value or ws.cell(r,6).value or ws.cell(r,5).value or ws.cell(r,4).value} if i%20==0: json.dump(res,open(OUT,'w',encoding='utf-8'),ensure_ascii=False) print('%d/%d r%d %s %s->%s (%s)'%(i+1,len(rows),r,L,old,det,why),flush=True) b.close() json.dump(res,open(OUT,'w',encoding='utf-8'),ensure_ascii=False) cand=sum(1 for k in res if res[k]['det']=='어문,이미지') print('완료 %d | 이미지후보 %d | ERR %d'%(len(res),cand,sum(1 for k in res if 'ERR' in res[k]['why'] or 'nav_fail' in res[k]['why'])))