공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
117 lines
3.9 KiB
Python
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()
|