# -*- coding: utf-8 -*- """익산 게시판 행 M/N/O/P/Q 수집(행범위 지정). 글수(전체N)·N(이미지규칙+상세top10)·KOGL(open0N+표준+링크, 상세top10). 사용: python -X utf8 _익산_mnopq.py [--write] """ import sys, os, re, shutil, importlib.util from urllib.parse import urljoin from concurrent.futures import ThreadPoolExecutor, as_completed import openpyxl HERE=os.path.dirname(os.path.abspath(__file__)) s=importlib.util.spec_from_file_location('m',os.path.join(HERE,'_jeonbuk_phase234_all.py')) M=importlib.util.module_from_spec(s); s.loader.exec_module(m if False else M) sk=importlib.util.spec_from_file_location('k',os.path.join(HERE,'_임실_kogl.py')) KG=importlib.util.module_from_spec(sk); sk.loader.exec_module(KG) XLSX=r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\전북특별자치도\9.익산시\전북특별자치도_익산시.xlsx' SESS=M.make_session() TOT=re.compile(r'전체\s*([\d,]+)|총\s*([\d,]+)\s*건|총\s*게시물\s*([\d,]+)') DECO=re.compile(r'/common/|move\.png|no[-_]?img|blank|spacer|/ico|/btn|bullet|arrow|/bg|icon|see_btn|/sample|mimetype|/file_|filedown|btn_dir|/open\d',re.I) EXCL=re.compile(r'한눈에|흐름도|절차도|로고(?!송)|logo(?!song)|아이콘|배너|banner|신문고|relation_item|tracer|headline|copyright|copy_logo|popup|wa_mk|웹접근성|품질인증',re.I) AUDIO=re.compile(r'\.(?:mp3|wav|m4a|ogg|flac)\b',re.I) MAPPDF=re.compile(r'pdf|viewer\.html|/map|kakao|daum.*map',re.I) def real_imgs(body): out=[] for img in body.find_all('img'): src=img.get('src') or '' if not src or M.KOGL_IMG_PAT.search(src) or DECO.search(src): continue if EXCL.search(src+' '+(img.get('alt') or '')): continue out.append(img) return out def media(body): txt=len(body.get_text(strip=True))>30 img=len(real_imgs(body))>0; vid=False for ifr in body.find_all('iframe'): ss=ifr.get('src') or '' if M.YOUTUBE_PAT.search(ss): vid=True elif MAPPDF.search(ss): img=True if not vid and (body.find('video') or body.find('a',href=M.YOUTUBE_PAT) or M.VIDEO_EXT.search(str(body))): vid=True aud=bool(body.find('audio')) or bool(AUDIO.search(str(body))) return img,vid,aud,txt def njoin(t,i,v,a): p=[] if t:p.append('어문') if i:p.append('이미지') if v:p.append('영상') if a:p.append('오디오') return ','.join(p) if p else '없음' def collect(K): soup=None for _ in range(3): soup=M.fetch(SESS,K) if soup is not None: break if soup is None: return None body=M.get_body(soup,M.BODY_SEL); txt=body.get_text(' ',strip=True) mm=TOT.search(txt); Mval=None if mm: g=[x for x in mm.groups() if x]; Mval=int(g[0].replace(',','')) else: el=body.select_one('.total strong,.num,.board_total') if el and el.get_text(strip=True).replace(',','').isdigit(): Mval=int(el.get_text(strip=True).replace(',','')) img,vid,aud,t=media(body) types,qy,qn=KG.kogl(body); P='게시판' if types else '' dus=KG.detail_urls(body,K,10) empty=bool(re.search(r'게시물이?\s*없|등록된?\s*(?:게시물|자료)\s*가?\s*없',txt)) if (Mval==0) or (not dus and empty): return {'M':0,'N':'없음','O':'미부착','P':'','Q':''} for du in dus: ds=M.fetch(SESS,du) if not ds: continue db=M.get_body(ds,M.BODY_SEL) i2,v2,a2,t2=media(db); img=img or i2;vid=vid or v2;aud=aud or a2;t=t or t2 dt,dqy,dqn=KG.kogl(db) if dt: if not types: P='게시물' types|=dt; qy=qy or dqy; qn=qn or dqn N=njoin(t,img,vid,aud) if types: O=','.join('%d유형'%n for n in sorted(types)); Q='Y' if qy else 'N'; P=P or '게시판' else: O='미부착'; P='';Q='' return {'M':Mval if Mval is not None else 0,'N':N,'O':O,'P':P,'Q':Q} def main(): st,en=int(sys.argv[1]),int(sys.argv[2]); write='--write' in sys.argv wb=openpyxl.load_workbook(XLSX); ws=wb.active jobs=[r for r in range(st,en+1) if isinstance(ws.cell(r,11).value,str) and ws.cell(r,11).value.startswith('http')] res={} with ThreadPoolExecutor(max_workers=6) as ex: futs={ex.submit(collect,ws.cell(r,11).value):r for r in jobs} for f in as_completed(futs): r=futs[f] try: res[r]=f.result() except: res[r]=None for r in sorted(res): o=res[r] print('r%d %s'%(r,o)) if write: shutil.copy(XLSX, XLSX.replace('.xlsx','_backup_mnopq전.xlsx')) for r,o in res.items(): if not o: continue ws.cell(r,13).value=o['M']; ws.cell(r,14).value=o['N'] ws.cell(r,15).value=o['O']; ws.cell(r,16).value=o['P']; ws.cell(r,17).value=o['Q'] wb.save(XLSX); print('적용 %d행'%len([1 for o in res.values() if o])) if __name__=='__main__': main()