"""사이트 본문 탭(서브내비) 탐지 스캐너. 각 사이트 페이지를 받아 본문 탭 UL을 찾아, 탭이 2개 이상인 페이지를 보고한다. 공주시: ul.tab-ul. 다른 사이트도 흔한 탭 클래스 패턴을 함께 탐지. 사용법: python -X utf8 _tab_scan.py <엑셀경로> <도메인키워드> 예: python -X utf8 _tab_scan.py 충청남도/2.공주시/충청남도_공주시.xlsx gongju.go.kr """ import re import sys import warnings from concurrent.futures import ThreadPoolExecutor, as_completed from urllib.parse import urljoin, urlparse import openpyxl 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} # 흔한 본문 탭(서브내비) 컨테이너 클래스 패턴 (소문자 비교) TAB_CLASS_PATS = [ 'tab-ul', # 공주시 'tab_ul', 'tabmenu', 'tab-menu', 'tab_menu', 'tablist', 'tab-list', 'tab_list', 'subtab', 'sub-tab', 'sub_tab', 'tab_wrap', 'tab-wrap', ] def find_tab_uls(soup): """본문 탭으로 보이는 ul들을 반환. (ul, [(text,href),...]) 리스트.""" results = [] seen = set() for ul in soup.find_all('ul'): cls = ' '.join(ul.get('class') or []).lower() if not cls: # 부모 div의 클래스도 확인 (ul 자체엔 클래스 없고 div.tab > ul 구조) parent = ul.find_parent(['div']) pcls = ' '.join(parent.get('class') or []).lower() if parent else '' if not any(p in pcls for p in TAB_CLASS_PATS): continue elif not any(p in cls for p in TAB_CLASS_PATS): continue links = [] for a in ul.find_all('a'): t = a.get_text(strip=True).replace('\xa0', '').strip() h = (a.get('href') or '').strip() if t: links.append((t, h)) if len(links) >= 2: key = tuple(t for t, _ in links) if key not in seen: seen.add(key) results.append((cls, links)) return results def scan_one(r, url): try: resp = requests.get(url, headers=H, timeout=15, verify=False) resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, 'html.parser') tabs = find_tab_uls(soup) return r, url, tabs, None except Exception as e: return r, url, [], str(e)[:60] def main(): xlsx = sys.argv[1] domain = sys.argv[2] wb = openpyxl.load_workbook(xlsx) ws = wb.active targets = [] for r in range(3, ws.max_row + 1): u = ws.cell(r, 11).value if isinstance(u, str) and domain in u: targets.append((r, u)) print(f'스캔 대상: {len(targets)}개 ({domain})') found = [] errs = [] with ThreadPoolExecutor(max_workers=8) as ex: futs = [ex.submit(scan_one, r, u) for r, u in targets] for fut in as_completed(futs): r, url, tabs, err = fut.result() if err: errs.append((r, url, err)) elif tabs: found.append((r, url, tabs)) found.sort() print(f'\n=== 탭 발견: {len(found)}개 페이지 ===') for r, url, tabs in found: e = ws.cell(r, 5).value or '' f = ws.cell(r, 6).value or '' g = ws.cell(r, 7).value or '' print(f'[행{r}] E={e} F={f} G={g}') print(f' {url}') for cls, links in tabs: print(f'