DB_JOB/작업파일/_스크립트/_jangsu_collect.py
hehihoho3 df16c98366 백업: DB수집 전체 스냅샷 (공공기관2 정리 전)
공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:15:40 +09:00

101 lines
4.6 KiB
Python

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"(.*?)(?:<footer|id="footer|<!-- //CONTENTS)', h, re.S)
seg = m.group(1) if m else h
return re.sub(r'<script.*?</script>', '', 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'<ul class="taps[^"]*">(.*?)</ul>', h, re.S)
if tm:
for am in re.finditer(r'menuCd=(DOM_000000\d{12})"[^>]*>(?:<span>)?\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'<img[^>]+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')