DB_JOB/_스크립트/_probe_jeonbuk.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

182 lines
7.0 KiB
Python

"""전북·제주 16개 시·군 사이트맵 URL 탐색 + 컨테이너 구조 덤프."""
import re
import ssl
import sys
import warnings
from urllib.parse import urljoin, urlparse
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
from bs4 import BeautifulSoup
warnings.filterwarnings('ignore')
UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
H = {'User-Agent': UA}
class WeakSSLAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
ctx = create_urllib3_context()
ctx.set_ciphers('DEFAULT@SECLEVEL=0')
ctx.options |= 0x4
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
kwargs['ssl_context'] = ctx
return super().init_poolmanager(*args, **kwargs)
def make_session(weak=False):
s = requests.Session()
s.headers.update(H)
if weak:
s.mount('https://', WeakSSLAdapter())
return s
def fetch(session, url, timeout=20):
r = session.get(url, timeout=timeout, verify=False, allow_redirects=True)
meta = re.search(rb'<meta[^>]*charset=["\']?\s*([\w-]+)', r.content[:4096], re.I)
if meta:
r.encoding = meta.group(1).decode('ascii', errors='ignore')
else:
r.encoding = r.apparent_encoding
return r
SITES = [
('고창군', 'https://www.gochang.go.kr/index.gochang?contentsSid=3136'),
('군산시', 'https://www.gunsan.go.kr/main'),
('김제시', 'https://www.gimje.go.kr/index.gimje'),
('남원시', 'https://www.namwon.go.kr/index.do?menuUid=ff8080818e3beff0018e40e8f63e02d2'),
('무주군', 'https://www.muju.go.kr/index.9is'),
('부안군', 'https://www.buan.go.kr/index.buan?contentsSid=1'),
('순창군', 'https://www.sunchang.go.kr/'),
('완주군', 'https://www.wanju.go.kr/index.9is'),
('익산시', 'https://www.iksan.go.kr/index.do?menuUid=ff8080819a39930e019a4de8c1ae0afd'),
('임실군', 'https://www.imsil.go.kr/index.imsil'),
('장수군', 'https://www.jangsu.go.kr/index.jangsu'),
('전주시', 'https://www.jeonju.go.kr/index.9is'),
('정읍시', 'https://www.jeongeup.go.kr/index.jeongeup'),
('진안군', 'https://www.jinan.go.kr/index.jinan?contentsSid=1379'),
('서귀포시', 'https://www.seogwipo.go.kr/index.htm'),
('제주시', 'https://www.jejusi.go.kr/index.ac'),
]
def find_sitemap_links(soup, base):
"""페이지에서 사이트맵으로 보이는 링크 후보 수집."""
cands = []
for a in soup.find_all('a', href=True):
txt = (a.get_text() or '').strip()
href = a['href']
onclick = a.get('onclick', '')
blob = f'{txt} {href} {onclick}'.lower()
if '사이트맵' in txt or 'sitemap' in blob:
url = href
if url.startswith('#') or url.lower().startswith('javascript:'):
# onclick에서 추출 시도
m = re.search(r"""['"]([^'"]*(?:sitemap|site_map)[^'"]*)['"]""", onclick, re.I)
if m:
url = m.group(1)
else:
continue
cands.append((txt, urljoin(base, url)))
# 중복 제거
seen = set(); out = []
for t, u in cands:
if u not in seen:
seen.add(u); out.append((t, u))
return out
def dump_structure(soup):
"""사이트맵 페이지 주요 컨테이너 후보 출력."""
# 흔한 사이트맵 컨테이너 셀렉터 후보
sels = [
'div.sitemap', 'div#sitemap', 'div.site_map', 'div#site_map',
'ul#menu_sitemap', 'div.depth.depth1', 'div.depth1',
'ul.depth1_ul', 'div.amThum', 'ul.sitemap', 'div.sitemap_wrap',
'div.contents_sitemap', 'div.sitemapWrap', 'div.site-map',
'div.allmenu', 'div#allmenu', 'div.gnb_all', 'div.total_menu',
]
found = []
for sel in sels:
els = soup.select(sel)
if els:
found.append((sel, len(els)))
print(f' 매칭 셀렉터: {found}')
# 사이트맵스러운 컨테이너 한 개 잡아서 자식 구조 덤프
target = None
for sel, _ in found:
target = soup.select_one(sel)
if target:
print(f' >>> {sel} 내부 구조:')
break
if not target:
# body에서 class에 sitemap/menu/depth 포함 div 찾기
for div in soup.find_all(['div', 'ul'], class_=True):
cls = ' '.join(div.get('class', []))
if re.search(r'sitemap|site_map|allmenu|depth1|total_menu', cls, re.I):
target = div
print(f' >>> <{div.name} class="{cls}"> 내부 구조:')
break
if not target:
print(' !! 사이트맵 컨테이너 미발견')
return
# 자식 1~3레벨 태그/클래스 요약
def summarize(el, depth=0, maxdepth=4):
if depth > maxdepth:
return
for child in el.find_all(recursive=False):
cls = '.'.join(child.get('class', []))
idv = child.get('id', '')
tag = child.name
label = tag + (f'#{idv}' if idv else '') + (f'.{cls}' if cls else '')
a = child.find('a', recursive=False)
atxt = (a.get_text().strip()[:20] if a else '')
print(' ' * (depth + 1) + f'{label}' + (f' a="{atxt}"' if atxt else ''))
if depth < 2:
summarize(child, depth + 1, maxdepth)
summarize(target)
def main():
targets = sys.argv[1:]
for name, url in SITES:
if targets and name not in targets:
continue
print(f'\n{"="*70}\n[{name}] {url}')
for weak in (False, True):
try:
sess = make_session(weak=weak)
r = fetch(sess, url)
base = f'{urlparse(r.url).scheme}://{urlparse(r.url).netloc}'
soup = BeautifulSoup(r.text, 'html.parser')
print(f' status={r.status_code} final={r.url} weak_ssl={weak}')
links = find_sitemap_links(soup, base)
print(f' 사이트맵 링크 후보: {links[:6]}')
# 가장 그럴듯한 후보 따라가기
if links:
smurl = links[0][1]
try:
r2 = fetch(sess, smurl)
soup2 = BeautifulSoup(r2.text, 'html.parser')
print(f' 사이트맵 페이지: {r2.url} (status {r2.status_code})')
dump_structure(soup2)
except Exception as e:
print(f' 사이트맵 페이지 fetch 실패: {e}')
else:
print(' >>> 인덱스 자체 구조 확인:')
dump_structure(soup)
break
except Exception as e:
if weak:
print(f' !! 실패(weak 포함): {type(e).__name__}: {e}')
else:
print(f' (일반 SSL 실패 → weak 재시도): {type(e).__name__}')
if __name__ == '__main__':
main()