# -*- coding: utf-8 -*- """공주시 227~414행 재검수용 증거 수집(쓰기 없음). 각 URL을 크롤링해 게시판 글수/관광지 썸네일수/외부링크/공공누리마크/#nav탭수/본문이미지·영상 신호를 수집. 검증용으로 226행까지 검수에서 바뀐 행 일부도 같이 수집. 출력: _스크립트/_evidence.json + 콘솔 요약 """ import sys, json, re, warnings from urllib.parse import urlparse from concurrent.futures import ThreadPoolExecutor, as_completed import requests from bs4 import BeautifulSoup import openpyxl warnings.filterwarnings('ignore') XLSX = r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\2.공주시\충청남도_공주시.xlsx' H = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120 Safari/537.36'} COMMON_IMG = re.compile(r'(flag\.jpg|slogan|/common/|/template/|btn_|/btn|icon|blank|_mark\.png|sns|share|loading)', re.I) def fetch(row, url): try: r = requests.get(url, headers=H, timeout=25, verify=False) r.encoding = r.apparent_encoding or 'utf-8' return row, url, r.status_code, r.text except Exception as e: return row, url, None, ('ERR:%s' % e) def analyze(url, html): ev = {} host = urlparse(url).netloc ev['host'] = host ev['external'] = ('gongju.go.kr' not in host) if not html or html.startswith('ERR:'): ev['error'] = html return ev s = BeautifulSoup(html, 'html.parser') # board total count cnt_el = s.select_one('.program--count strong') if cnt_el: m = re.sub(r'[^0-9]', '', cnt_el.get_text()) ev['board_count'] = int(m) if m else None ev['is_board'] = True else: ev['is_board'] = False # tursmCn thumbnails (관광지) tt = len(re.findall(r'/thumbnail/tursmCn/', html)) if tt: ev['tursm_count'] = tt # content images (static html, excludes template/common) — JS maps missed cimgs = [] for im in s.find_all('img'): src = im.get('src') or im.get('data-src') or '' if src and not COMMON_IMG.search(src): cimgs.append(src) ev['content_img'] = len(cimgs) ev['content_img_sample'] = cimgs[:4] # video signals low = html.lower() ev['video'] = bool(re.search(r'youtube\.com/embed|youtu\.be/|player\.vimeo|= 2: best = max(best, len(anchors)) if best: ev['nav_tabs'] = best return ev def main(): lo, hi = 227, 414 if len(sys.argv) > 2: lo, hi = int(sys.argv[1]), int(sys.argv[2]) wb = openpyxl.load_workbook(XLSX) ws = wb.active targets = [] rowinfo = {} for r in range(lo, hi + 1): u = ws.cell(r, 11).value if isinstance(u, str) and u.strip().startswith('http'): targets.append((r, u.strip())) rowinfo[r] = { 'D': ws.cell(r, 4).value, 'E': ws.cell(r, 5).value, 'F': ws.cell(r, 6).value, 'G': ws.cell(r, 7).value, 'K': u.strip(), 'L': ws.cell(r, 12).value, 'M': ws.cell(r, 13).value, 'N': ws.cell(r, 14).value, 'O': ws.cell(r, 15).value, } print('크롤 대상:', len(targets), '행', lo, '~', hi) out = {} done = 0 with ThreadPoolExecutor(max_workers=10) as ex: futs = [ex.submit(fetch, r, u) for r, u in targets] for f in as_completed(futs): row, url, st, html = f.result() ev = analyze(url, html) ev['status'] = st ev['row'] = row out[row] = {**rowinfo[row], **ev} done += 1 if done % 25 == 0: print(' ...', done, '/', len(targets)) json.dump(out, open(r'D:\01.프로젝트\DB수집\_스크립트\_evidence.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=1) print('저장: _evidence.json (', len(out), '행 )') if __name__ == '__main__': main()