DB_JOB/작업파일/완료/광역_사이트맵/충청북도/_gs_crawl.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

108 lines
4.2 KiB
Python

# -*- coding: utf-8 -*-
# 괴산군 통합 수집: 메가메뉴 트리 + 본문탭 전개 + L/M/O 분류 (정적 크롤)
import sys,io,json,re,time,urllib.request,ssl
from bs4 import BeautifulSoup
sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='utf-8')
ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
BASE='https://www.goesan.go.kr'
HOST='goesan.go.kr'
def absu(u):
if not u: return u
if u.startswith('http'): return u
if u.startswith('/'): return BASE+u
return BASE+'/'+u
_cache={}
def get(u):
if u in _cache: return _cache[u]
h=''
for _ in range(2):
try:
req=urllib.request.Request(u,headers={'User-Agent':'Mozilla/5.0'})
h=urllib.request.urlopen(req,timeout=25,context=ctx).read().decode('utf-8','ignore'); break
except Exception: time.sleep(1)
_cache[u]=h; return h
def kind(u):
if not u: return 'none'
if u.startswith('http') and HOST not in u: return '사이트'
if 'eminwon' in u: return '사이트'
if re.search(r'selectBbsNttList|selectOthbcInfoList|BbsNtt',u): return '게시판'
if 'contents.do' in u: return '페이지'
if 'addBbsNttView' in u: return '페이지'
return '페이지'
def board_M(html):
m=re.search(r'게시물[^\d]{0,15}([\d,]+)', html)
if m:
try: return int(m.group(1).replace(',',''))
except: return 0
return 0
def has_opentype(html):
mm=re.findall(r'img_opentype0?(\d)', html)
return sorted(set(int(x) for x in mm)) if mm else []
def page_tabs(html, self_url):
s=BeautifulSoup(html,'html.parser')
cont=s.select_one('#contents,.contents,.sub_content,.sub_contents,#content,.content_in') or s
selfkey=re.search(r'key=(\d+)',self_url)
selfkey=selfkey.group(1) if selfkey else None
for ul in cont.find_all('ul'):
cls=' '.join(ul.get('class') or [])
if not re.search(r'tab',cls,re.I): continue
anchors=ul.find_all('a')
if len(anchors)<2: continue
tabs=[]
for a in anchors:
lab=a.get_text(strip=True); href=a.get('href')
if not lab: continue
tabs.append((lab,href))
# exclude self/전체
out=[]
for lab,href in tabs:
k=re.search(r'key=(\d+)',href or '')
if lab in ('전체','전체보기') and (not k or (selfkey and k.group(1)==selfkey)): continue
if href and href.rstrip('/')==self_url.rstrip('/'): continue
out.append((lab,href))
if out: return out
return []
tree=json.load(open('_gs_tree.json',encoding='utf-8'))
nodes=[] # each: dict
def classify_url(u):
u=absu(u); k=kind(u); M=None; O=[]; det=None
if k=='게시판':
h=get(u); M=board_M(h); O=has_opentype(h)
s=BeautifulSoup(h,'html.parser')
for a in s.find_all('a'):
href=a.get('href') or ''
if re.search(r'NttView|nttNo=|addBbsNttView',href): det=absu(href); break
elif k=='페이지':
h=get(u); O=has_opentype(h)
return k,M,O,det
print('트리노드',len(tree),flush=True)
for i,(d1,d2,d3,d4,url) in enumerate(tree):
u=absu(url); k,M,O,det=classify_url(url)
node={'d':[d1,d2,d3,d4,''],'url':u,'kind':k,'M':M,'O':O,'det':det}
nodes.append(node)
# 본문탭 전개 (페이지만)
if k=='페이지' and 'contents.do' in u:
tabs=page_tabs(get(u), u)
for lab,href in tabs:
tu=absu(href); tk,tM,tO,tdet=classify_url(href)
# 탭은 부모 leaf 다음 레벨
dd=[d1,d2,d3,d4,'']
# 부모 leaf 깊이 찾기
depth=max(idx for idx in range(4) if dd[idx]) if any(dd[:4]) else 0
child=dd[:]
if depth>=3: child[4]=lab # d4 있으면 탭=H(5번째)
else: child[depth+1]=lab # 아니면 한단계 아래
nodes.append({'d':child,'url':tu,'kind':tk,'M':tM,'O':tO,'det':tdet,'tab':True})
if i%20==0: print('%d/%d %s'%(i+1,len(tree),d3 or d2 or d1),flush=True)
json.dump(nodes,open('_gs_nodes.json','w',encoding='utf-8'),ensure_ascii=False)
from collections import Counter
print('총행',len(nodes),'| L:',Counter(n['kind'] for n in nodes))
print('탭전개행',sum(1 for n in nodes if n.get('tab')))
print('O부착행',sum(1 for n in nodes if n['O']))