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

221 lines
8.2 KiB
Python

# -*- coding: utf-8 -*-
"""탭 확장으로 새로 추가된 행만 골라 Phase 2~4(L/M/N/O/P) 수집.
- 대상: L(게시판형태)이 비어있고 K가 같은 도메인 http URL 인 행 = 신규 탭 행.
- L/M/N: 해당 기관 _phase234.py 의 get_body/detect_form/detect_media/extract_detail_urls 재사용.
- O/P : 확정 KOGL 규칙(_recheck_kogl_all 의 BROAD_IMG_PAT + detect_split + decide_O).
이미지명 우선, 게시판은 같은도메인 상세 5건 추적, 이미지≠링크면 S열 '링크주소 오기'.
- 인코딩: 바이트로 받아 BeautifulSoup 자동판별(읍면동 등 오판 방지).
- 기존 행(L 이미 채워짐)은 절대 건드리지 않음.
사용: python -X utf8 _tab_phase234.py <엑셀경로> <Phase234 모듈경로> <도메인키워드> [body_sel(콤마)]
- 개별형(공주시 _phase234.py: get_body(soup)) → body_sel 생략
- 일괄형(_chungnam_phase234_all.py: get_body(soup, sel)) → body_sel 지정(미지정 시 #txt,#contents,main)
예) python -X utf8 _tab_phase234.py 충청남도/2.공주시/공주시_탭확장.xlsx 충청남도/2.공주시/_phase234.py gongju.go.kr
python -X utf8 _tab_phase234.py 충청남도/4.논산시/충청남도_논산시.xlsx _chungnam_phase234_all.py nonsan.go.kr "#txt,#contents,main"
"""
import re
import sys
import time
import inspect
import importlib.util
import warnings
from urllib.parse import urlparse
from concurrent.futures import ThreadPoolExecutor, as_completed
import ssl
import openpyxl
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
from bs4 import BeautifulSoup
warnings.filterwarnings('ignore')
UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
H = {'User-Agent': UA}
class WeakSSLAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
ctx = create_urllib3_context()
ctx.set_ciphers('DEFAULT@SECLEVEL=0')
ctx.options |= 0x4
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
kwargs['ssl_context'] = ctx
return super().init_poolmanager(*args, **kwargs)
SESSION = requests.Session()
SESSION.headers.update(H)
BROAD_IMG_PAT = re.compile(r'(?:new_)?img_open(?:type|code)(\d{1,2})\.(?:png|jpe?g|gif)', re.I)
def load_module(path):
spec = importlib.util.spec_from_file_location('city_p234', path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def fetch_soup(url, timeout=14):
try:
r = SESSION.get(url, timeout=timeout, verify=False)
if r.status_code == 200:
return BeautifulSoup(r.content, 'html.parser') # 바이트 → 자동 인코딩
except Exception:
pass
return None
def valid(n):
return 1 <= n <= 4
def detect_split(body, LINK_PAT):
img_t, link_t = set(), set()
for a in body.find_all('a', href=True):
m = LINK_PAT.search(a['href'])
if m and valid(int(m.group(1))):
link_t.add(int(m.group(1)))
blob = ' '.join(filter(None, (img.get('src', '') for img in body.find_all('img'))))
blob += ' ' + ' '.join(el.get('style', '') for el in body.find_all(style=True))
blob += ' ' + str(body)
for m in BROAD_IMG_PAT.finditer(blob):
n = int(m.group(1))
if valid(n):
img_t.add(n)
return img_t, link_t
def decide_O(img_t, link_t):
if img_t:
return ','.join(f'{n}유형' for n in sorted(img_t)), (bool(link_t) and link_t != img_t)
if link_t:
if {1, 2, 3, 4}.issubset(link_t):
return '미부착', False
return ','.join(f'{n}유형' for n in sorted(link_t)), False
return '미부착', False
def domain3(host):
labels = (host or '').split('.')
return '.'.join(labels[-3:]) if len(labels) >= 3 else host
def same_site(a, b):
return domain3(urlparse(a).hostname) == domain3(urlparse(b).hostname)
def main():
xlsx, p234_path, domain = sys.argv[1], sys.argv[2], sys.argv[3]
if '--weak-ssl' in sys.argv:
SESSION.mount('https://', WeakSSLAdapter())
body_sel = None
if len(sys.argv) > 4 and sys.argv[4].strip() and not sys.argv[4].startswith('--'):
body_sel = [s.strip() for s in sys.argv[4].split(',') if s.strip()]
mod = load_module(p234_path)
LINK_PAT = mod.KOGL_LINK_PAT
# get_body 시그니처 자동 대응: 일괄형은 (soup, selectors), 개별형은 (soup)
needs_sel = len(inspect.signature(mod.get_body).parameters) >= 2
sel = body_sel or ['#txt', '#contents', 'main']
get_body = (lambda s: mod.get_body(s, sel)) if needs_sel else mod.get_body
print(f'get_body 인자 {2 if needs_sel else 1}개 | body_sel={sel if needs_sel else "(미사용)"}')
wb = openpyxl.load_workbook(xlsx)
ws = wb.active
targets = []
for r in range(3, ws.max_row + 1):
L = ws.cell(r, 12).value
url = ws.cell(r, 11).value
if L not in (None, '') :
continue # 기존 행 보존
if not (isinstance(url, str) and domain in url):
continue
targets.append((r, url))
print(f'대상 신규행: {len(targets)}')
def work(t):
r, url = t
soup = fetch_soup(url)
if soup is None:
return r, {'note': '접근 실패'}
body = get_body(soup)
form, count = mod.detect_form(body)
has_img, has_vid, has_txt = mod.detect_media(body)
img_t, link_t = detect_split(body, LINK_PAT)
P_loc = '게시판' if img_t or link_t else ''
if form == '게시판':
for du in mod.extract_detail_urls(body, url, limit=5):
if not same_site(url, du):
continue
ds = fetch_soup(du, 10)
if ds is None:
continue
db = get_body(ds)
di, dv, dt = mod.detect_media(db)
has_img = has_img or di; has_vid = has_vid or dv; has_txt = has_txt or dt
dimg, dlink = detect_split(db, LINK_PAT)
if (dimg or dlink) and not P_loc:
P_loc = '게시물'
img_t |= dimg; link_t |= dlink
N = mod.n_string(has_txt, has_img, has_vid)
O, mismatch = decide_O(img_t, link_t)
P = '' if O == '미부착' else (P_loc or '게시판')
return r, {'L': form, 'M': count if form == '게시판' else 1,
'N': N, 'O': O, 'P': P, 'mismatch': mismatch}
t0 = time.time()
results = {}
done = 0
with ThreadPoolExecutor(max_workers=8) as ex:
futs = [ex.submit(work, t) for t in targets]
for fut in as_completed(futs):
r, res = fut.result()
results[r] = res
done += 1
if done % 40 == 0:
print(f' 진행 {done}/{len(targets)} ({time.time()-t0:.0f}s)')
print(f'크롤링 완료 ({time.time()-t0:.0f}s)')
fail = 0
for r, res in results.items():
if res.get('note'):
fail += 1
if not ws.cell(r, 19).value:
ws.cell(r, 19).value = res['note']
continue
ws.cell(r, 12).value = res['L']
ws.cell(r, 13).value = res['M']
ws.cell(r, 14).value = res['N']
ws.cell(r, 15).value = res['O']
if res['P']:
ws.cell(r, 16).value = res['P']
if res['mismatch']:
cur = (ws.cell(r, 19).value or '').strip()
ws.cell(r, 19).value = '링크주소 오기' if not cur else cur + ' / 링크주소 오기'
try:
wb.save(xlsx)
saved = xlsx
except PermissionError:
saved = xlsx.replace('.xlsx', '_LP.xlsx')
wb.save(saved)
print(f'!! 원본 잠김(Excel 열림). 대체 저장: {saved}')
from collections import Counter
Lc = Counter(res.get('L') for res in results.values())
Oc = Counter(res.get('O') for res in results.values())
print(f'저장: {xlsx}')
print('L 분포:', dict(Lc))
print('O 분포:', dict(Oc))
print(f'접근실패: {fail}')
if __name__ == '__main__':
main()