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

114 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
# 트리(JSON: [[d1,d2,d3,d4,url],...]) → classify+본문탭전개 → nodes.json
# 사용: python _cb_nodes.py <host> <tree.json> <out_nodes.json>
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
try: ctx.set_ciphers('DEFAULT@SECLEVEL=1')
except Exception: pass
HOST=sys.argv[1]; TREE=sys.argv[2]; OUT=sys.argv[3]
BASE='https://'+HOST
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|selectBoardList|/cop/bbs/|BBSMSTR|BbsNtt',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 board_signal(html):
# 내용기반 게시판 판별: 총게시물 카운트 + 등록일/작성일/조회수 동시존재
a=bool(re.search(r'\s*게시물|게시물\s*[:]?\s*\d|총\s*\d+\s*건',html))
b=bool(re.search(r'등록일|작성일|조회수',html))
return a and b
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
selfk=re.search(r'key=(\d+)',self_url); selfk=selfk.group(1) if selfk else None
if not selfk: return []
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
keys=[re.search(r'key=(\d+)',a.get('href') or '') for a in anchors]
keys=[k.group(1) for k in keys if k]
if selfk not in keys: continue
if len(keys)<len(anchors)-1: continue
out=[]
for a in anchors:
lab=a.get_text(strip=True); href=a.get('href'); k=re.search(r'key=(\d+)',href or '')
if not lab or not k or k.group(1)==selfk: continue
out.append((lab,href))
if out: return out
return []
def classify(u):
u=absu(u); k=kind(u); M=None; O=[]; det=None
if k=='게시판':
hh=get(u); M=board_M(hh); O=has_opentype(hh)
ss=BeautifulSoup(hh,'html.parser')
for a in ss.find_all('a'):
href=a.get('href') or ''
if re.search(r'NttView|nttNo=|addBbsNttView|selectBoardArticle|nttId=',href): det=absu(href); break
elif k=='페이지':
hh=get(u); O=has_opentype(hh)
if board_signal(hh): # 내용기반 게시판 승격
k='게시판'; M=board_M(hh)
ss=BeautifulSoup(hh,'html.parser')
for a in ss.find_all('a'):
href=a.get('href') or ''
if re.search(r'NttView|nttNo=|addBbsNttView|selectBoardArticle|nttId=|mode=view|articleNo=|idx=|seq=',href): det=absu(href); break
return k,M,O,det
tree=json.load(open(TREE,encoding='utf-8'))
print('트리노드',len(tree),flush=True)
nodes=[]; menu_urls=set(absu(t[4]) for t in tree if t[4]); seen_tab=set()
for i,row in enumerate(tree):
d1,d2,d3,d4,url = row[0],row[1],row[2],row[3],row[4]
khint = row[5] if len(row)>5 else None
u=absu(url); k,M,O,det=classify(url)
if khint and khint!='페이지' and k=='페이지': # URL로 페이지 판정됐지만 트리힌트가 게시판/사이트면 힌트 우선
k=khint
if k=='게시판':
hh=get(u); M=board_M(hh); O=has_opentype(hh)
ss=BeautifulSoup(hh,'html.parser')
for a in ss.find_all('a'):
href=a.get('href') or ''
if re.search(r'NttView|nttNo=|addBbsNttView|selectBoardArticle|nttId=|view',href): det=absu(href); break
if k=='none' and not u: continue
nodes.append({'d':[d1,d2,d3,d4,''],'url':u,'kind':k,'M':M,'O':O,'det':det})
if k=='페이지' and re.search(r'(contents|sub)\.do',u):
for lab,href in page_tabs(get(u),u):
tu=absu(href)
if tu in menu_urls or tu in seen_tab: continue
seen_tab.add(tu); tk,tM,tO,tdet=classify(href)
dd=[d1,d2,d3,d4,'']; depth=max((idx for idx in range(4) if dd[idx]),default=0)
child=dd[:]
child[4 if depth>=3 else depth+1]=lab
nodes.append({'d':child,'url':tu,'kind':tk,'M':tM,'O':tO,'det':tdet,'tab':True})
if i%25==0: print('%d/%d'%(i+1,len(tree)),flush=True)
json.dump(nodes,open(OUT,'w',encoding='utf-8'),ensure_ascii=False)
from collections import Counter
print('총행',len(nodes),'| L',dict(Counter(n['kind'] for n in nodes)),'| 탭',sum(1 for n in nodes if n.get('tab')),'| O',sum(1 for n in nodes if n['O']))