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

117 lines
3.9 KiB
Python

"""사이트 본문 탭(서브내비) 탐지 스캐너.
각 사이트 페이지를 받아 본문 탭 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' <ul class={cls}> 탭 {len(links)}개:')
for t, h in links:
print(f' - {t} -> {h}')
if errs:
print(f'\n=== 에러 {len(errs)}개 ===')
for r, url, err in errs[:20]:
print(f'[행{r}] {url} : {err}')
if __name__ == '__main__':
main()