# -*- coding: utf-8 -*- """공주시: 인페이지(#nav) 탭을 가진 페이지의 각 탭 패널 안에 '게시판'이 있는지 점검. 대상: _gongju_navcount 에서 잡힌 14개 페이지(동일도메인 자동 재탐지). 각 탭 → 패널 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()