"""충청북도 11개 시·군 사이트맵 URL/패턴 탐지.""" import re import warnings from urllib.parse import urljoin, urlparse from concurrent.futures import ThreadPoolExecutor, as_completed import requests 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} CITIES = [ ('괴산군', 'https://www.goesan.go.kr/www/index.do'), ('단양군', 'https://www.danyang.go.kr/dy21/1'), ('보은군', 'https://www.boeun.go.kr/www/index.do'), ('영동군', 'https://www.yd21.go.kr/'), ('옥천군', 'https://www.oc.go.kr/www/'), ('음성군', 'https://www.eumseong.go.kr/www/index.do'), ('제천시', 'https://www.jecheon.go.kr/www/index.do'), ('증평군', 'https://www.jp.go.kr/kor.do'), ('진천군', 'https://www.jincheon.go.kr/home/intro.do'), ('청주시', 'https://www.cheongju.go.kr/www/index.do'), ('충주시', 'https://www.chungju.go.kr/www/index.do'), ] def fetch(url, timeout=12): try: r = requests.get(url, headers=H, timeout=timeout, verify=False, allow_redirects=True) r.encoding = r.apparent_encoding if r.status_code == 200: return r.url, r.text except Exception as e: return None, f'ERR: {e}' return None, f'HTTP {r.status_code}' def find_sitemap_anchor(html, base): soup = BeautifulSoup(html, 'html.parser') found = [] for a in soup.find_all('a', href=True): txt = a.get_text(strip=True) href = a['href'] if not (txt and href): continue if '사이트맵' in txt or '전체메뉴' in txt or 'sitemap' in href.lower() or 'allMenu' in href: full = urljoin(base, href) found.append((txt, full)) seen = set() uniq = [] for t, u in found: if u not in seen and u != base: seen.add(u) uniq.append((t, u)) return uniq def analyze_sitemap_page(html): """Score candidate containers by anchor count and class hints.""" soup = BeautifulSoup(html, 'html.parser') candidates = [] # Common containers for sel in [ 'div.sitemap.type1', 'div.sitemap.type2', 'div.sitemap', 'div.sitemap_grep', 'div.amThum', 'ul.sitemap_list', '#sitemap', '#contents ul.sitemap', 'ul.sitemap', 'ul.depth1_ul', 'ul.depth1-ul', 'ul.depth1', 'ul.top_menu', '#gnb', 'nav#gnb', 'nav.gnb', '.allmenu', '.allMenu', '#allMenu', 'div.menu_all', 'div.totalMenu', ]: for el in soup.select(sel): ac = len(el.find_all('a')) if ac < 30: continue cls = ' '.join(el.get('class', [])) eid = el.get('id', '') candidates.append((ac, sel, f'{el.name}#{eid}.{cls}')) # Fallback: search any container by id/class containing 'sitemap' or 'allMenu' for el in soup.find_all(True, class_=True): cls = ' '.join(el.get('class', [])) if re.search(r'\b(sitemap|allmenu|amthum|depth1)\b', cls, re.I): ac = len(el.find_all('a')) if 30 <= ac <= 2000: candidates.append((ac, f'class~{cls[:30]}', f'{el.name}.{cls}')) candidates.sort(reverse=True) return candidates[:5] def probe(name, base): print(f'\n=== {name} === {base}') real, html = fetch(base) if not real: print(f' 메인 실패: {html}') return name, None print(f' 메인 OK: {real}') # 1) Find sitemap links from main page links = find_sitemap_anchor(html, real) print(f' 사이트맵 링크 후보: {len(links)}') for t, u in links[:5]: print(f' "{t}" → {u}') # 2) Common paths to try parsed = urlparse(base) origin = f'{parsed.scheme}://{parsed.netloc}' common = [ '/www/sitemap.do', '/www/sub.do?key=121', '/www/contents.do?key=121', '/kor/sitemap.do', '/kor/sitemap_11.do', '/kor/sitemap_1.do', '/sitemap.do', '/sitemap.html', '/www/sitemap/', '/main/sitemap.do', '/home/sitemap.do', '/dy21/sitemap.do', '/www/cms/sitemap.do', ] candidates = [u for _, u in links] + [origin + p for p in common] seen = set() best = None for c in candidates: if c in seen: continue seen.add(c) u2, h2 = fetch(c, timeout=10) if not u2: continue info = analyze_sitemap_page(h2) if info: top = info[0] if best is None or top[0] > best[1][0]: best = (c, top, info) if best: url, top, info = best print(f' ★ 사이트맵 URL: {url}') for ac, sel, desc in info: print(f' {sel}: a={ac} {desc[:80]}') return name, {'url': url, 'best_sel': info[0][1], 'a_count': info[0][0]} print(' 사이트맵 못 찾음') return name, None def main(): results = {} with ThreadPoolExecutor(max_workers=6) as ex: futs = {ex.submit(probe, n, b): n for n, b in CITIES} for f in as_completed(futs): n, info = f.result() results[n] = info print('\n=== 요약 ===') for n, _ in CITIES: info = results.get(n) if info: print(f' {n}: {info["url"]} [{info["best_sel"]}, a={info["a_count"]}]') else: print(f' {n}: NOT FOUND') if __name__ == '__main__': main()