# -*- coding: utf-8 -*- """김제시 인페이지 탭(#anchor) 수량 → M 기입. 페이지 본문의 div[class*=basic_tab] 중 탭 링크가 전부 '#anchor'인 그룹(예 basic_tab2>ul.col4, #tab1~#tabN)을 인페이지 스크롤탭으로 보고 M=탭수 기입(공주/금산 전례, 매뉴얼 1-5b: #anchor는 행 분리 안함). menuCd 형제 nav(basic_tab depth4 등, href=실제 .gimje)는 제외. L은 페이지 유지. 대상: 17행~끝(요청). 3~16행은 참고 보고만. 사용: python -X utf8 _김제_인페이지탭수.py dry | run """ import os, sys, re, io, time, shutil from concurrent.futures import ThreadPoolExecutor, as_completed import warnings; warnings.filterwarnings('ignore') sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', line_buffering=True) import requests from bs4 import BeautifulSoup import openpyxl HERE = os.path.dirname(os.path.abspath(__file__)) XP = os.path.join(HERE, '작업파일', '광역_사이트맵', '전북특별자치도', '3.김제시', '전북특별자치도_김제시.xlsx') UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120 Safari/537.36'} def fetch(session, url): r = session.get(url, headers=UA, verify=False, timeout=10, allow_redirects=True) meta = re.search(rb'charset=["\']?\s*([\w-]+)', r.content[:3000], re.I) r.encoding = meta.group(1).decode('ascii', 'ignore') if meta else r.apparent_encoding return BeautifulSoup(r.text, 'html.parser') BASIC_TAB = re.compile('basic_tab') def inpage_tabs(soup): """가장 큰 '전부 #anchor' basic_tab 그룹의 (탭수, 라벨).""" best, labels = 0, None for d in soup.find_all('div', class_=BASIC_TAB): ul = d.find('ul') if not ul: continue links = ul.select('li > a') if len(links) < 2: continue hrefs = [(a.get('href') or '').strip() for a in links] if all(h.startswith('#') for h in hrefs): if len(links) > best: best = len(links) labels = [a.get_text(strip=True) for a in links] return best, labels def main(): mode = sys.argv[1] if len(sys.argv) > 1 else 'dry' wb = openpyxl.load_workbook(XP); ws = wb.active last = max(r for r in range(3, ws.max_row + 1) if ws.cell(r, 2).value not in (None, '')) def lab(r): return ' > '.join(str(ws.cell(r, c).value) for c in range(4, 11) if ws.cell(r, c).value not in (None, '')) # 대상 수집: L=페이지 & http URL def targets(lo, hi): out = [] for r in range(lo, hi + 1): L = ws.cell(r, 12).value url = ws.cell(r, 11).value if L == '페이지' and isinstance(url, str) and url.startswith('http'): out.append((r, url)) return out main_t = targets(17, last) pre_t = targets(3, 16) session = requests.Session() def scan(rows_urls): res = {} def w(ru): r, u = ru try: n, ls = inpage_tabs(fetch(session, u)) return r, n, ls except Exception: return r, 0, None with ThreadPoolExecutor(max_workers=10) as ex: for f in as_completed([ex.submit(w, ru) for ru in rows_urls]): r, n, ls = f.result() if n >= 2: res[r] = (n, ls) return res print(f'스캔: 17~{last} ({len(main_t)}개 페이지), 참고 3~16 ({len(pre_t)}개)') t0 = time.time() main_hits = scan(main_t) pre_hits = scan(pre_t) print(f'스캔완료 {time.time()-t0:.0f}s') print(f'\n[대상 17~끝] 인페이지탭 발견 {len(main_hits)}행:') for r in sorted(main_hits): n, ls = main_hits[r] old = ws.cell(r, 13).value print(f' r{r}: M {old}→{n} [{lab(r)}] 탭={ls}') if pre_hits: print(f'\n[참고 3~16] 인페이지탭 {len(pre_hits)}행 (요청범위 밖, 미적용):') for r in sorted(pre_hits): n, ls = pre_hits[r] print(f' r{r}: M {ws.cell(r,13).value}→{n}? [{lab(r)}] 탭={ls}') if mode != 'run': return bak = XP.replace('.xlsx', '_backup_인페이지탭전.xlsx') shutil.copy(XP, bak) for r, (n, ls) in main_hits.items(): ws.cell(r, 13).value = n try: wb.save(XP); print(f'\n저장완료. {len(main_hits)}행 M 갱신. 백업 {os.path.basename(bak)}') except PermissionError: wb.save(XP.replace('.xlsx', '_LP.xlsx')); print('\n!! 잠김 → _LP') if __name__ == '__main__': main()