DB_JOB/작업파일/완료/광역_사이트맵/전북특별자치도/13.정읍시/_l283v2.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

157 lines
6.6 KiB
Python

# -*- coding: utf-8 -*-
"""정읍 r283~끝 L 재분류 + 게시판 M·O 재작성 (robust). dry-run -> _l283.json, --write 적용.
게시판 판정 = list.jeongeup가 유효 게시판페이지(p.bbs_total 존재)인 경우.
M = 총 게시물 수(bbs_total). 못구하면 기존 M 유지(절대 wipe 안함).
O = 상세글(view.jeongeup) 표본 img_opentype0N(P=게시물); 없으면 list본문(P=게시판); else 미부착.
유효 게시판 아님(bbs_total無=빈위젯/잘못된 board참조) + 현재 페이지 => 페이지 유지.
"""
import re, json, os, glob, time, sys, shutil, urllib.request
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
import openpyxl
DIR = os.path.dirname(os.path.abspath(__file__))
DOMAIN = 'https://www.jeongeup.go.kr'
HDR = {'User-Agent': 'Mozilla/5.0'}
OPT = re.compile(r'img_open(?:type|code)(\d{1,2})\.(?:png|jpe?g|gif)', re.I)
TOTAL = re.compile(r'class="bbs_total"[^>]*>[^<]*<strong>\s*([\d,]+)')
START = 283
SAMPLE = 12
WRITE = '--write' in sys.argv
pool = ThreadPoolExecutor(max_workers=6)
def get(u, minlen=0, tries=4):
for _ in range(tries):
try:
r = urllib.request.urlopen(urllib.request.Request(u.replace(' ', '%20'), headers=HDR), timeout=25).read().decode('utf-8', 'replace')
if len(r) >= minlen:
return r
except Exception:
pass
time.sleep(0.6)
return r if 'r' in dir() else ''
def types_in(h):
ts = set(int(x) for x in OPT.findall(h) if x.isdigit() and 1 <= int(x) <= 4)
return set() if ts == {1, 2, 3, 4} else ts
def total_of(h):
m = TOTAL.search(h)
return int(m.group(1).replace(',', '')) if m else None
def views_of(h, bid=''):
vs = [v for v in re.findall(r'view\.jeongeup\?([^"\']+)', h) if 'boardId=' in v]
if bid:
vs = [v for v in vs if 'boardId=' + bid in v]
out, seen = [], set()
for v in vs:
key = re.search(r'dataSid=(\d+)', v)
key = key.group(1) if key else v
if key in seen:
continue
seen.add(key)
out.append(v.replace('&amp;', '&'))
return out
def board_probe(k, menuCd):
"""returns dict(valid, M, src_h, bid)."""
h = get(k, 3000)
M = total_of(h)
if M is not None: # index page 자체가 리스트
bid = (re.findall(r'name="boardId"[^>]*value="(BBS_\d+)"', h) or
re.findall(r'boardId=(BBS_\d+)', h) or [''])[0]
return {'valid': True, 'M': M, 'src': h, 'bid': bid}
bid = (re.findall(r'name="boardId"[^>]*value="(BBS_\d+)"', h) or
re.findall(r'list\.jeongeup\?[^"\']*boardId=(BBS_\d+)', h) or
re.findall(r'view\.jeongeup\?[^"\']*boardId=(BBS_\d+)', h))
if not bid:
return {'valid': False, 'M': None, 'src': h, 'bid': ''}
bid = Counter(bid).most_common(1)[0][0]
csid = (re.findall(r'name="contentsSid"[^>]*value="(\d+)"', h) or [''])[0]
lu = '%s/board/list.jeongeup?menuCd=%s&boardId=%s%s&startPage=1' % (
DOMAIN, menuCd, bid, ('&contentsSid=' + csid if csid else ''))
lh = get(lu, 3000)
M = total_of(lh)
return {'valid': M is not None, 'M': M, 'src': lh, 'bid': bid}
def board_O(src, bid):
vs = views_of(src, bid)[:SAMPLE]
if vs:
ts = set()
for r in pool.map(lambda v: types_in(get(DOMAIN + '/board/view.jeongeup?' + v)), vs):
ts |= r
if ts:
return ','.join('%d유형' % n for n in sorted(ts)), '게시물', 'Y'
lt = types_in(src)
if lt:
return ','.join('%d유형' % n for n in sorted(lt)), '게시판', 'Y'
return '미부착', '', ''
t = [p for p in glob.glob(os.path.join(DIR, '*.xlsx'))
if 'backup' not in p and not os.path.basename(p).startswith(('~$', '_'))][0]
wb = openpyxl.load_workbook(t)
ws = wb.active
out = []
for r in range(START, ws.max_row + 1):
L = ws.cell(r, 12).value
k = ws.cell(r, 11).value
if not k or L not in ('페이지', '게시판'):
continue
k = str(k)
name = str(ws.cell(r, 5).value or ws.cell(r, 6).value or ws.cell(r, 7).value or ws.cell(r, 4).value)
if 'index.jeongeup' not in k or not k.startswith('http'):
out.append({'row': r, 'curL': L, 'name': name, 'verdict': 'skip', 'k': k})
continue
mc = re.search(r'menuCd=([A-Z0-9_]+)', k)
mc = mc.group(1) if mc else ''
pr = board_probe(k, mc)
rec = {'row': r, 'curL': L, 'name': name, 'curM': ws.cell(r, 13).value,
'curO': ws.cell(r, 15).value, 'curP': ws.cell(r, 16).value, 'curQ': ws.cell(r, 17).value,
'bid': pr['bid']}
if pr['valid']:
O, P, Q = board_O(pr['src'], pr['bid'])
rec.update({'verdict': '게시판', 'M_new': pr['M'], 'O_new': O, 'P_new': P, 'Q_new': Q})
tag = ' <<RECLASS' if L != '게시판' else ''
print('r%d [%s] 게시판 M=%s O=%s%s | %s' % (r, L[:2], pr['M'], O, tag, name[:18]), flush=True)
else:
rec['verdict'] = 'page'
if L == '게시판':
rec['verdict'] = 'board_keep(검증실패)' # 기존 게시판인데 list검증 실패 -> 보존
print('r%d [%s] page (bbs_total無 bid=%s) | %s' % (r, L[:2], pr['bid'], name[:18]), flush=True)
out.append(rec)
json.dump(out, open(os.path.join(DIR, '_l283.json'), 'w', encoding='utf-8'), ensure_ascii=False, indent=0)
json.dump(out, open(os.path.join(DIR, '_l283.json'), 'w', encoding='utf-8'), ensure_ascii=False, indent=0)
recl = [x for x in out if x.get('verdict') == '게시판' and x['curL'] != '게시판']
print('\nSCAN=%d 게시판=%d reclass(P→B)=%d %s'
% (len(out), sum(1 for x in out if x.get('verdict') == '게시판'), len(recl),
[(x['row'], x['name'][:10], x.get('M_new')) for x in recl]))
if WRITE:
bak = t.replace('.xlsx', '_backup_L재분류전.xlsx')
if not os.path.exists(bak):
shutil.copy(t, bak)
# 동시편집 가드: URL이 그대로인지 확인하며 행 매칭
nb = openpyxl.load_workbook(t)
nws = nb.active
applied = 0
for x in out:
if x.get('verdict') != '게시판':
continue
r = x['row']
if str(nws.cell(r, 11).value) != str(ws.cell(r, 11).value):
print(' SKIP r%d URL 변경됨(동시편집)' % r)
continue
nws.cell(r, 12).value = '게시판'
if x.get('M_new') is not None:
nws.cell(r, 13).value = x['M_new']
nws.cell(r, 15).value = x['O_new']
nws.cell(r, 16).value = x['P_new'] or None
nws.cell(r, 17).value = x['Q_new'] or None
applied += 1
nb.save(t)
print('APPLIED %d boards -> %s (백업 %s)' % (applied, os.path.basename(t), os.path.basename(bak)))
else:
print('DRY-RUN (--write 로 적용)')