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

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()