import sys, io, re, ssl, json, urllib.request sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') from concurrent.futures import ThreadPoolExecutor ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE BASE = 'https://www.jangsu.go.kr/index.jangsu?menuCd=' def fetch(cd): u = BASE + cd return urllib.request.urlopen(urllib.request.Request(u, headers={'User-Agent':'Mozilla/5.0'}), context=ctx, timeout=25).read().decode('utf-8','replace') COMMON = ('kogl.or.kr','elis.go.kr','jeonbuk.work.go.kr','/index.jangsu') def content_seg(h): m = re.search(r'id="contents"(.*?)(?:', '', seg, flags=re.S) def analyze(cd): try: h = fetch(cd) except Exception as e: return {'err': str(e)[:50]} seg = content_seg(h) txt = re.sub(r'\s+', ' ', re.sub(r'<[^>]+>', ' ', seg)).strip() # 탭 taps = [] tm = re.search(r'', h, re.S) if tm: for am in re.finditer(r'menuCd=(DOM_000000\d{12})"[^>]*>(?:)?\s*([^<]+)', tm.group(1)): taps.append((am.group(1), am.group(2).strip())) # 외부 사이트 exts = re.findall(r'href="(https?://[^"]+)"', seg) real_ext = [e for e in exts if not any(c in e for c in COMMON)] is_site = bool(real_ext) and bool(re.search(r'해당\s*사이트로\s*이동|홈페이지\s*바로가기|버튼을\s*클릭', txt)) # 게시판 is_board = 'bbs_list' in seg mcount = None bm = re.search(r'전체\s*([\d,]+)\s*건', txt) if bm: mcount = int(bm.group(1).replace(',', '')) # 공공누리 om = re.search(r'공공누리\s*제?\s*(\d)\s*유형', txt) otype = om.group(1) + '유형' if om else '미부착' # 이미지(본문 실이미지 — 데코 제외) imgs = re.findall(r']+src="([^"]+)"', seg) deco = re.compile(r'opentype|kogl|btn_|icon|blank|move|/template/|/images/|no_img', re.I) realimg = [s for s in imgs if not deco.search(s)] if is_site: L = '사이트'; K = real_ext[0]; M=None; N=None; O='미부착' elif is_board: L = '게시판'; K = BASE+cd; M = mcount if mcount is not None else 0 N = '어문' if M and M>0 else '없음'; O = otype else: L = '페이지'; K = BASE+cd; M = 1 N = '어문,이미지' if len(realimg)>=2 else '어문'; O = otype return {'taps':taps,'L':L,'K':K,'M':M,'N':N,'O':O,'site':is_site,'board':is_board} tree = json.load(open(r'D:\01.프로젝트\DB수집\작업파일\_스크립트\_jangsu_tree.json', encoding='utf-8')) 대, 중, 소 = tree['대'], tree['중'], [tuple(x) for x in tree['소']] # 1차: 모든 소분류 분석 print(f'소분류 {len(소)}개 크롤 시작...', file=sys.stderr) res = {} def run(cd_full): return cd_full, analyze(cd_full) with ThreadPoolExecutor(max_workers=8) as ex: for cd_full, r in ex.map(run, [x[3] for x in 소]): res[cd_full] = r # 2차: 탭 자식 분석 (소분류에 탭 있으면) tab_cds = set() for cd, r in res.items(): if r.get('taps'): for tcd, _ in r['taps']: tab_cds.add(tcd) tab_res = {} with ThreadPoolExecutor(max_workers=8) as ex: for cd_full, r in ex.map(run, sorted(tab_cds)): tab_res[cd_full] = r print(f'소분류 {len(res)} + 탭 {len(tab_res)} 분석 완료', file=sys.stderr) # 행 빌드 rows = [] # dict: 대,중,소,세부(G),K,L,M,N,O errs = [] for d, e, f, cd, lab in 소: r = res.get(cd, {}) if r.get('err'): errs.append((cd, lab, r['err'])) Dl, El = 대.get(d, ''), 중.get(d+e, '') taps = r.get('taps') or [] if taps: for tcd, tlab in taps: tr = tab_res.get(tcd, {}) rows.append({'D':Dl,'E':El,'F':lab,'G':tlab,'K':tr.get('K', BASE+tcd), 'L':tr.get('L','페이지'),'M':tr.get('M',1),'N':tr.get('N','어문'),'O':tr.get('O','미부착')}) else: rows.append({'D':Dl,'E':El,'F':lab,'G':None,'K':r.get('K', BASE+cd), 'L':r.get('L','페이지'),'M':r.get('M',1),'N':r.get('N','어문'),'O':r.get('O','미부착')}) json.dump({'rows':rows,'errs':errs}, open(r'D:\01.프로젝트\DB수집\작업파일\_스크립트\_jangsu_rows.json','w',encoding='utf-8'), ensure_ascii=False, indent=1) from collections import Counter print(f'총 행 {len(rows)} | L분포 {dict(Counter(x["L"] for x in rows))} | 탭전개 소분류 {sum(1 for r in res.values() if r.get("taps"))} | 에러 {len(errs)}') print('saved _jangsu_rows.json')