공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
4.1 KiB
Python
110 lines
4.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""공주시: 인페이지(#nav) 탭을 가진 페이지의 각 탭 패널 안에 '게시판'이 있는지 점검.
|
|
|
|
대상: _gongju_navcount 에서 잡힌 14개 페이지(동일도메인 자동 재탐지).
|
|
각 탭 <a href="#navN"> → 패널 element(id=navN) 내부에서 게시판 신호 탐지:
|
|
· 목록 table(td 안 링크 다수) · 페이징 · '총 N건' · 상세링크(view.do/mode=V/nttId/BBSMSTR) · iframe
|
|
판정: 신호 1+ → 그 탭은 '게시판 포함' 가능.
|
|
|
|
사용: python -X utf8 _gongju_navtab_board.py
|
|
"""
|
|
import re, warnings
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
import openpyxl, requests
|
|
from bs4 import BeautifulSoup
|
|
warnings.filterwarnings('ignore')
|
|
|
|
XLSX = r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\2.공주시\충청남도_공주시.xlsx'
|
|
DOMAIN = 'gongju.go.kr'
|
|
H = {'User-Agent': 'Mozilla/5.0 Chrome/120 Safari/537.36'}
|
|
DETAIL = re.compile(r'(view\.do|mode=V|nttId|BBSMSTR|selectBoard|selectBbs)', re.I)
|
|
TOTAL = re.compile(r'총\s*[\d,]+\s*(건|개)')
|
|
|
|
|
|
def get_navtabs(soup):
|
|
"""type3(#nav) 탭그룹의 [(label, panel_id)] 반환 (가장 큰 그룹)."""
|
|
best = []
|
|
for ul in soup.find_all('ul'):
|
|
cls = ' '.join(ul.get('class') or []).lower()
|
|
if 'tab-ul' not in cls:
|
|
continue
|
|
tabs = []
|
|
for a in ul.find_all('a'):
|
|
h = (a.get('href') or '').strip()
|
|
t = a.get_text(strip=True)
|
|
if h.startswith('#') and len(h) > 1 and t:
|
|
tabs.append((t, h[1:]))
|
|
if len(tabs) >= 2 and len(tabs) > len(best):
|
|
best = tabs
|
|
return best
|
|
|
|
|
|
def board_signals(panel):
|
|
if panel is None:
|
|
return []
|
|
sig = []
|
|
# 목록 테이블: 행 3+ 이고 링크 포함
|
|
for tb in panel.find_all('table'):
|
|
rows = tb.find_all('tr')
|
|
links = tb.find_all('a', href=True)
|
|
if len(rows) >= 3 and len(links) >= 3:
|
|
sig.append('목록table')
|
|
break
|
|
if panel.select('.paging,.pagination,.board_paging,.bbs_paging'):
|
|
sig.append('페이징')
|
|
if TOTAL.search(panel.get_text(' ', strip=True)):
|
|
sig.append('총건수')
|
|
if any(DETAIL.search(a['href']) for a in panel.find_all('a', href=True)):
|
|
sig.append('상세링크')
|
|
if panel.find('iframe'):
|
|
sig.append('iframe')
|
|
return sig
|
|
|
|
|
|
def fetch(r, u):
|
|
try:
|
|
return r, u, requests.get(u, headers=H, timeout=15, verify=False).content
|
|
except Exception:
|
|
return r, u, None
|
|
|
|
|
|
def main():
|
|
ws = openpyxl.load_workbook(XLSX).active
|
|
targets = [(r, ws.cell(r, 11).value) for r in range(3, ws.max_row + 1)
|
|
if isinstance(ws.cell(r, 11).value, str) and DOMAIN in ws.cell(r, 11).value]
|
|
htmls = {}
|
|
with ThreadPoolExecutor(max_workers=8) as ex:
|
|
for f in as_completed([ex.submit(fetch, r, u) for r, u in targets]):
|
|
r, u, h = f.result()
|
|
if h:
|
|
htmls[r] = (u, h)
|
|
|
|
found_pages = 0
|
|
board_hits = 0
|
|
for r in sorted(htmls):
|
|
u, h = htmls[r]
|
|
soup = BeautifulSoup(h, 'html.parser')
|
|
tabs = get_navtabs(soup)
|
|
if len(tabs) < 2:
|
|
continue
|
|
found_pages += 1
|
|
cat = ws.cell(r, 6).value or ws.cell(r, 5).value or ws.cell(r, 4).value or ''
|
|
per = []
|
|
for label, pid in tabs:
|
|
sig = board_signals(soup.find(id=pid))
|
|
per.append((label, sig))
|
|
any_board = any(s for _, s in per)
|
|
flag = '🟢게시판 발견' if any_board else '— 게시판 없음(정적 콘텐츠)'
|
|
print(f'\n[행{r}] {cat} 탭{len(tabs)}개 {flag}')
|
|
print(f' {u}')
|
|
for label, sig in per:
|
|
mk = ('게시판? ' + ','.join(sig)) if sig else '정적'
|
|
print(f' · {label[:30]:<30} {mk}')
|
|
if sig:
|
|
board_hits += 1
|
|
print(f'\n===== 요약: {found_pages}개 페이지 / 게시판 신호 탭 {board_hits}개 =====')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|