공공기관2 작업 중. _temp 몽타주(재생성가능)는 제외. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
222 lines
9.7 KiB
Python
222 lines
9.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""계룡시 방식 일괄 적용 (수정판) — 충청남도(2~15) + 충청북도(1~11).
|
|
|
|
★ 원본 phase234 검출 로직을 그대로 재사용한다(import):
|
|
- get_body(body_sel)로 '본문 영역'에 한정해 검출 → 헤더/푸터 외부링크 노이즈 제거
|
|
- extract_detail_urls(JS fn_detail 폴백 포함)로 게시판 상세를 원본과 동일하게 추적
|
|
- KOGL_IMG_PAT = img_opentype(\\d{2}).png (2자리 png), KOGL_LINK_PAT = licenseType(\\d)
|
|
|
|
각 부착 행을 재크롤링하여
|
|
1) O열(15) = 이미지(img_opentype) 유형만으로 재판정 (링크 숫자는 합치지 않음)
|
|
2) 링크가 '존재'하면서 이미지≠링크면 S열(19) 비고에 '링크주소 오기' (링크 없음은 비움)
|
|
|
|
파일별 *_backup_kogl재판정전.xlsx 백업 후 저장(기존 백업 있으면 보존).
|
|
|
|
사용: python -X utf8 _recheck_kogl_all.py [기관명 ...]
|
|
"""
|
|
import sys, os, re, shutil, warnings, openpyxl
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from urllib.parse import urlparse
|
|
|
|
sys.path.insert(0, r'D:\01.프로젝트\DB수집\_스크립트')
|
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
warnings.filterwarnings('ignore')
|
|
|
|
import _chungnam_phase234_all as CN
|
|
import _chungbuk_phase234_all as CB
|
|
|
|
O_COL, K_COL, S_COL = 15, 11, 19
|
|
WORKERS = 6
|
|
|
|
# CN/CB 모듈 SITES에 없는 기관(개별 스크립트만 존재) 보완 — body_sel은 계룡시와 동일
|
|
EXTRA_CFG = {
|
|
'공주시': {'xlsx': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\2.공주시\충청남도_공주시.xlsx',
|
|
'body_sel': ['#txt', '#contents', 'main']},
|
|
'금산군': {'xlsx': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\3.금산군\충청남도_금산군.xlsx',
|
|
'body_sel': ['#txt', '#contents', 'main']},
|
|
}
|
|
|
|
|
|
def get_cfg(name, region):
|
|
mod = CN if region == 'cn' else CB
|
|
return mod.SITES.get(name) or EXTRA_CFG[name]
|
|
|
|
# (기관명, region, weak_ssl) — xlsx/body_sel 은 각 모듈 SITES 에서 가져옴
|
|
TARGETS = [
|
|
('공주시', 'cn', False), ('금산군', 'cn', False), ('논산시', 'cn', False),
|
|
('당진시', 'cn', False), ('보령시', 'cn', False), ('부여군', 'cn', False),
|
|
('서산시', 'cn', False), ('서천군', 'cn', False), ('아산시', 'cn', False),
|
|
('예산군', 'cn', False), ('천안시', 'cn', False), ('청양군', 'cn', False),
|
|
('태안군', 'cn', False), ('홍성군', 'cn', False),
|
|
('괴산군', 'cb', False), ('단양군', 'cb', False), ('영동군', 'cb', True),
|
|
('옥천군', 'cb', False), ('음성군', 'cb', False), ('제천시', 'cb', False),
|
|
('증평군', 'cb', False), ('진천군', 'cb', False), ('청주시', 'cb', False),
|
|
('충주시', 'cb', False),
|
|
]
|
|
|
|
|
|
# 확장 이미지 패턴: img_opentype / img_opencode, 1~2자리, png/jpg/jpeg/gif
|
|
BROAD_IMG_PAT = re.compile(r'(?:new_)?img_open(?:type|code)(\d{1,2})\.(?:png|jpe?g|gif)', re.I)
|
|
|
|
|
|
def _valid(n):
|
|
return 1 <= n <= 4 # 공공누리 유형은 1~4만 유효
|
|
|
|
|
|
def detect_split(body, LINK_PAT):
|
|
"""이미지명(확장패턴) 유형과 링크(licenseType) 유형을 분리 검출. 유형 1~4만."""
|
|
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)))
|
|
# 이미지: src 속성 + style 배경이미지 + raw HTML(누락 방지)
|
|
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):
|
|
"""O열 값과 '링크주소 오기' 비고 여부 결정.
|
|
- 이미지 있으면 이미지 우선(권위). 링크 존재 & 이미지≠링크면 비고.
|
|
- 이미지 없고 링크만: 1·2·3·4 전부면 범례(설명)페이지 → 미부착. 아니면 링크 유형 인정.
|
|
- 둘 다 없으면 미부착."""
|
|
if img_t:
|
|
newO = ','.join(f'{n}유형' for n in sorted(img_t))
|
|
return newO, 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 make_fetch(region, weak_ssl):
|
|
if region == 'cn':
|
|
return CN.fetch # fetch(url, timeout)
|
|
sess = CB.make_session(weak_ssl)
|
|
return lambda url, timeout=12: CB.fetch(sess, url, timeout)
|
|
|
|
|
|
def _domain(host):
|
|
"""go.kr/or.kr 등 2단계 TLD 고려해 등록 도메인(끝 3라벨) 반환."""
|
|
labels = (host or '').split('.')
|
|
return '.'.join(labels[-3:]) if len(labels) >= 3 else host
|
|
|
|
|
|
def same_site(base_url, target_url):
|
|
"""상세 링크가 같은 기관 도메인일 때만 True (외부 사이트 KOGL 오탐 방지)."""
|
|
return _domain(urlparse(base_url).hostname) == _domain(urlparse(target_url).hostname)
|
|
|
|
|
|
def detect_row(url, body_sel, mod, fetch_fn):
|
|
soup = fetch_fn(url)
|
|
if soup is None:
|
|
return None, None, 'fetch_fail'
|
|
body = mod.get_body(soup, body_sel)
|
|
form, _ = mod.detect_form(body)
|
|
img_t, link_t = detect_split(body, mod.KOGL_LINK_PAT)
|
|
if form == '게시판':
|
|
for du in mod.extract_detail_urls(body, url, limit=5):
|
|
if not same_site(url, du): # 외부 도메인 상세링크 제외
|
|
continue
|
|
ds = fetch_fn(du, 10)
|
|
if ds is None:
|
|
continue
|
|
db = mod.get_body(ds, body_sel)
|
|
di, dl = detect_split(db, mod.KOGL_LINK_PAT)
|
|
img_t |= di
|
|
link_t |= dl
|
|
return img_t, link_t, 'ok'
|
|
|
|
|
|
def process_site(name, region, weak_ssl):
|
|
mod = CN if region == 'cn' else CB
|
|
cfg = get_cfg(name, region)
|
|
xlsx, body_sel = cfg['xlsx'], cfg['body_sel']
|
|
print(f'\n{"="*70}\n■ {name} [{region}] ({os.path.relpath(xlsx)})')
|
|
wb = openpyxl.load_workbook(xlsx)
|
|
ws = wb.active
|
|
targets = []
|
|
for r in range(3, ws.max_row + 1):
|
|
o = ws.cell(r, O_COL).value
|
|
if not o or str(o).strip() == '미부착':
|
|
continue
|
|
url = ws.cell(r, K_COL).value
|
|
if url:
|
|
targets.append((r, str(url).strip(), o, ws.cell(r, S_COL).value))
|
|
if not targets:
|
|
print(' 부착 행 없음 → 변경 없음')
|
|
return dict(name=name, total=0, o_changed=0, to_none=0, mismatch=0, fetch_fail=0)
|
|
|
|
print(f' 부착 행 {len(targets)}건 재크롤링 (본문영역+상세추적, workers={WORKERS}, weak_ssl={weak_ssl})')
|
|
fetch_fn = make_fetch(region, weak_ssl)
|
|
|
|
def work(t):
|
|
r, url, oldO, oldS = t
|
|
img_t, link_t, status = detect_row(url, body_sel, mod, fetch_fn)
|
|
return (r, url, oldO, oldS, img_t, link_t, status)
|
|
|
|
results = []
|
|
with ThreadPoolExecutor(max_workers=WORKERS) as ex:
|
|
for res in ex.map(work, targets):
|
|
results.append(res)
|
|
|
|
o_changed = to_none = mismatch = fetch_fail = 0
|
|
for r, url, oldO, oldS, img_t, link_t, status in sorted(results):
|
|
if status != 'ok':
|
|
fetch_fail += 1
|
|
print(f' row{r:>3} | FETCH_FAIL | {url[:70]}')
|
|
continue
|
|
newO, do_flag = decide_O(img_t, link_t)
|
|
if newO != str(oldO).strip():
|
|
o_changed += 1
|
|
if newO == '미부착':
|
|
to_none += 1
|
|
ws.cell(r, O_COL).value = newO
|
|
print(f' row{r:>3} | O: {str(oldO):>16} -> {newO:<16} <== 변경 | img={sorted(img_t)} link={sorted(link_t)}')
|
|
if do_flag:
|
|
mismatch += 1
|
|
cur = (oldS or '').strip()
|
|
if '링크주소 오기' not in cur:
|
|
ws.cell(r, S_COL).value = '링크주소 오기' if not cur else cur + ' / 링크주소 오기'
|
|
print(f' row{r:>3} | 비고+= 링크주소 오기 | img={sorted(img_t)} != link={sorted(link_t)}')
|
|
|
|
backup = os.path.join(os.path.dirname(xlsx), f'{name}_backup_kogl재판정전.xlsx')
|
|
if not os.path.exists(backup):
|
|
shutil.copy2(xlsx, backup)
|
|
wb.save(xlsx)
|
|
print(f' ▶ {name} 완료: O변경 {o_changed} (미부착化 {to_none}) | 불일치비고 {mismatch} | fetch실패 {fetch_fail} | 저장')
|
|
return dict(name=name, total=len(targets), o_changed=o_changed, to_none=to_none,
|
|
mismatch=mismatch, fetch_fail=fetch_fail)
|
|
|
|
|
|
def main():
|
|
sel = [a for a in sys.argv[1:] if not a.startswith('-')]
|
|
sites = [t for t in TARGETS if (not sel or t[0] in sel)]
|
|
print(f'대상 {len(sites)}개: {[s[0] for s in sites]}')
|
|
summary = []
|
|
for name, region, weak in sites:
|
|
try:
|
|
summary.append(process_site(name, region, weak))
|
|
except Exception as e:
|
|
import traceback; traceback.print_exc()
|
|
print(f' !! {name} 오류: {e}')
|
|
summary.append(dict(name=name, total=-1, o_changed=0, to_none=0, mismatch=0, fetch_fail=0))
|
|
|
|
print('\n' + '=' * 70 + '\n[전체 요약]')
|
|
print(f'{"기관":<8}{"부착":>6}{"O변경":>7}{"미부착化":>8}{"불일치":>7}{"실패":>6}')
|
|
for s in summary:
|
|
print(f'{s["name"]:<8}{s["total"]:>6}{s["o_changed"]:>7}{s["to_none"]:>8}{s["mismatch"]:>7}{s["fetch_fail"]:>6}')
|
|
tot = lambda k: sum(x[k] for x in summary if x[k] >= 0)
|
|
print(f'\n합계: 부착 {tot("total")} | O변경 {tot("o_changed")} | 미부착化 {tot("to_none")} | 불일치비고 {tot("mismatch")} | fetch실패 {tot("fetch_fail")}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|