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

82 lines
3.7 KiB
Python

# -*- coding: utf-8 -*-
"""클래스명 무관 본문탭 탐지: 표본 페이지에서 '자기 URL을 포함하면서 2+실제링크 &
1+ 신규(엑셀에 없는) URL'을 가진 UL을 찾아 그 class 를 집계. 1-5b 조건2~4의 클래스 비의존 버전.
미지의 탭 클래스명을 발견하기 위함.
사용: python -X utf8 _tab_classfind.py <엑셀> <base> <도메인> [표본=50] [--weak-ssl]
"""
import sys, warnings, ssl
from collections import Counter
from urllib.parse import urlsplit, urljoin
from concurrent.futures import ThreadPoolExecutor, as_completed
import openpyxl, requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
from bs4 import BeautifulSoup
warnings.filterwarnings('ignore')
H={'User-Agent':'Mozilla/5.0 Chrome/120 Safari/537.36'}
class W(HTTPAdapter):
def init_poolmanager(self,*a,**k):
c=create_urllib3_context();c.set_ciphers('DEFAULT@SECLEVEL=0');c.options|=0x4
c.check_hostname=False;c.verify_mode=ssl.CERT_NONE;k['ssl_context']=c
return super().init_poolmanager(*a,**k)
S=requests.Session();S.headers.update(H)
if '--weak-ssl' in sys.argv: S.mount('https://',W())
def norm(u):
s=urlsplit(u);return urlsplit(urljoin('http://x/',s.path)).path.rstrip('/').lower()
def absu(h,base):
h=(h or '').strip()
if not h or h.startswith(('javascript:','#')): return ''
if h.startswith(('http://','https://')): return h
return urljoin(base.rstrip('/')+'/',h.lstrip('/'))
def main():
xlsx,base,domain=sys.argv[1],sys.argv[2],sys.argv[3]
nums=[a for a in sys.argv[4:] if a.isdigit()]
nsamp=int(nums[0]) if nums else 50
wb=openpyxl.load_workbook(xlsx,read_only=True);ws=wb.active
urls=[];existing=set()
for row in ws.iter_rows(min_row=3,min_col=11,max_col=11,values_only=True):
u=row[0]
if isinstance(u,str) and u.startswith('http'):
existing.add(norm(u))
if domain in u: urls.append(u)
wb.close()
step=max(1,len(urls)//nsamp);sample=urls[::step][:nsamp]
print(f'표본 {len(sample)}/{len(urls)} ({domain})')
cls_cnt=Counter();examples={}
def work(u):
try:
r=S.get(u,timeout=15,verify=False);return u,r.content
except Exception:return u,None
with ThreadPoolExecutor(max_workers=8) as ex:
for f in as_completed([ex.submit(work,u) for u in sample]):
u,html=f.result()
if not html:continue
cur=norm(u);soup=BeautifulSoup(html,'html.parser')
for ul in soup.find_all('ul'):
links=[]
ok=True
for a in ul.find_all('a'):
t=a.get_text(strip=True)
au=absu(a.get('href'),base)
if not t:continue
if not au: ok=False;break
links.append(au)
if not ok or len(links)<2:continue
norms=[norm(x) for x in links]
if cur not in norms:continue # 조건3: 자기 탭그룹
if sum(1 for n in norms if n not in existing)<1:continue # 조건4: 신규1+
cls=' '.join(ul.get('class') or []) or '(no-class)'
cls_cnt[cls]+=1
examples.setdefault(cls,(u,[ (a.get_text(strip=True), absu(a.get('href'),base)) for a in ul.find_all('a') if a.get_text(strip=True)][:6]))
print('\n=== 본문탭(자기URL포함+신규有) UL class 빈도 ===')
if not cls_cnt: print(' (없음 — 본문탭 진짜 없음 가능성)')
for cls,c in cls_cnt.most_common(15):
print(f' {c:3d}회 [{cls}]')
eu,el=examples[cls];print(f' 예: {eu}')
for t,h in el: print(f' - {t} -> {h}')
if __name__=='__main__':main()