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

708 lines
28 KiB
Python

"""전북특별자치도 14개 시·군 + 제주특별자치도 2개 시 Phase 1 일괄 처리.
매뉴얼: D:\\01.프로젝트\\DB수집\\사이트맵_수집_매뉴얼.md
출력: 각 폴더의 {기관명}.xlsx (D~K열)
파서 매핑은 _probe_jeonbuk*.py 탐색 결과 기반.
"""
import json
import os
import re
import shutil
import ssl
import sys
import warnings
from copy import copy
from urllib.parse import urljoin
import openpyxl
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
from bs4 import BeautifulSoup
from openpyxl.styles import Alignment, Font
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}
ROOT = r'D:\01.프로젝트\DB수집'
TEMPLATE = ROOT + r'\자료_취합_예시.xlsx'
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)
def make_session(weak_ssl=False):
s = requests.Session()
s.headers.update(H)
if weak_ssl:
s.mount('https://', WeakSSLAdapter())
return s
def fetch_html(url, session=None, timeout=25):
s = session or requests.Session()
if not session:
s.headers.update(H)
r = s.get(url, timeout=timeout, verify=False, allow_redirects=True)
meta = re.search(rb'<meta[^>]*charset=["\']?\s*([\w-]+)', r.content[:4096], re.I)
r.encoding = meta.group(1).decode('ascii', errors='ignore') if meta else r.apparent_encoding
return r.text
def clean_text(s):
return re.sub(r'\s+', ' ', s or '').strip().replace('\xa0', '').lstrip('-').strip()
def extract_href(a):
if a is None:
return ''
href = (a.get('href') or '').strip()
if not href or href.startswith('#') or href.lower().startswith('javascript:'):
return ''
return href
COLS = 'DEFGHIJ'
def rows_from_paths(tmp):
"""[{'path':[(text,href)...], 'href':..}] → D~J dict 행 리스트."""
rows = []
for item in tmp:
p = item['path']
row = {c: '' for c in COLS}
row['href'] = item['href']
for i, (t, _) in enumerate(p):
row[COLS[i] if i < len(COLS) else 'J'] = t
rows.append(row)
return rows
def walk_ul(ul, base_path, out, recursive_li=True):
"""ul > li > a (+ 중첩 ul) 재귀. 망가진 마크업(li 안 li)도 허용."""
for li in ul.find_all('li', recursive=False):
a = li.find('a', recursive=False)
if not a:
# div>a 형태(전주) 허용
d = li.find('div', recursive=False)
a = d.find('a', recursive=False) if d else None
if not a:
continue
text = clean_text(a.get_text())
href = extract_href(a)
path = base_path + [(text, href)]
if not text:
continue
# 자식 ul (정상) 또는 li 직접 중첩(망가진 마크업)
child_uls = li.find_all('ul', recursive=False)
child_lis = [c for c in li.find_all('li', recursive=False)]
if child_uls:
out.append({'path': list(path), 'href': href})
for cul in child_uls:
walk_ul(cul, path, out)
elif recursive_li and child_lis:
out.append({'path': list(path), 'href': href})
for cli in child_lis:
# cli를 단일 li로 감싼 가짜 ul처럼 처리
sub = a.find_parent() # not used
_walk_single_li(cli, path, out)
else:
out.append({'path': list(path), 'href': href})
def _walk_single_li(li, base_path, out):
a = li.find('a', recursive=False)
if not a:
d = li.find('div', recursive=False)
a = d.find('a', recursive=False) if d else None
if not a:
return
text = clean_text(a.get_text())
href = extract_href(a)
path = base_path + [(text, href)]
if not text:
return
child_uls = li.find_all('ul', recursive=False)
child_lis = li.find_all('li', recursive=False)
if child_uls:
out.append({'path': list(path), 'href': href})
for cul in child_uls:
walk_ul(cul, path, out)
elif child_lis:
out.append({'path': list(path), 'href': href})
for cli in child_lis:
_walk_single_li(cli, path, out)
else:
out.append({'path': list(path), 'href': href})
# ================================================================
# 파서들
# ================================================================
def parse_menu_div(soup, base):
"""고창/김제/임실: div.sitemap > div.menuN > h4>a (D) + div > ul > li>a (E) + ul (F) 재귀."""
rows = []
sm = soup.select_one('div.sitemap')
if not sm:
return rows
for block in sm.find_all('div', recursive=False):
h4 = block.find('h4')
D = clean_text(h4.get_text()) if h4 else ''
if not D:
continue
inner = block.find('div', recursive=False)
ul = inner.find('ul', recursive=False) if inner else block.find('ul', recursive=False)
if not ul:
rows.append({'D': D, 'href': '', **{c: '' for c in 'EFGHIJ'}})
continue
tmp = []
walk_ul(ul, [], tmp)
for r in rows_from_paths(tmp):
rows.append({'D': D, 'E': r.get('D', ''), 'F': r.get('E', ''),
'G': r.get('F', ''), 'H': r.get('G', ''), 'I': r.get('H', ''),
'J': r.get('I', ''), 'href': r.get('href', '')})
return rows
def parse_jeongeup(soup, base):
"""정읍: div.sitemap > div.st_mapNN > p.tit>a (D) + ul > li > b>a (E) + ul > li>a (F)."""
rows = []
sm = soup.select_one('div.sitemap')
if not sm:
return rows
for block in sm.find_all('div', recursive=False):
ptit = block.find('p', class_='tit')
D = clean_text(ptit.get_text()) if ptit else ''
if not D:
continue
ul = block.find('ul', recursive=False)
if not ul:
rows.append({'D': D, 'href': '', **{c: '' for c in 'EFGHIJ'}})
continue
for li in ul.find_all('li', recursive=False):
b = li.find('b', recursive=False)
b_a = b.find('a') if b else li.find('a', recursive=False)
E = clean_text(b_a.get_text()) if b_a else ''
E_href = extract_href(b_a) if b_a else ''
sub = li.find('ul', recursive=False)
if not sub:
rows.append({'D': D, 'E': E, 'href': E_href, **{c: '' for c in 'FGHIJ'}})
continue
rows.append({'D': D, 'E': E, 'href': E_href, **{c: '' for c in 'FGHIJ'}})
tmp = []
walk_ul(sub, [], tmp)
for r in rows_from_paths(tmp):
rows.append({'D': D, 'E': E, 'F': r.get('D', ''), 'G': r.get('E', ''),
'H': r.get('F', ''), 'I': r.get('G', ''), 'J': r.get('H', ''),
'href': r.get('href', '')})
return rows
def parse_group_sitemap(soup, base):
"""남원/익산: div.sitemap_group (여러개) > h4.title (D) + ul.sitemap_2dep > li>a (E) + ul (F) 재귀."""
rows = []
groups = soup.select('div.sitemap_group')
for g in groups:
h4 = g.find('h4', class_='title') or g.find('h4')
D = clean_text(h4.get_text()) if h4 else ''
if not D:
continue
ul = g.find('ul', class_='sitemap_2dep') or g.find('ul', recursive=False)
if not ul:
rows.append({'D': D, 'href': '', **{c: '' for c in 'EFGHIJ'}})
continue
tmp = []
walk_ul(ul, [], tmp)
for r in rows_from_paths(tmp):
rows.append({'D': D, 'E': r.get('D', ''), 'F': r.get('E', ''),
'G': r.get('F', ''), 'H': r.get('G', ''), 'I': r.get('H', ''),
'J': r.get('I', ''), 'href': r.get('href', '')})
return rows
def parse_namwon(soup, base):
"""남원: div.sitemap > (h4 (D) + ul (E/F 재귀)) 형제 반복."""
rows = []
sm = soup.select_one('div.sitemap')
if not sm:
return rows
curD = ''
for child in sm.find_all(['h4', 'ul'], recursive=False):
if child.name == 'h4':
curD = clean_text(child.get_text())
elif child.name == 'ul' and curD:
tmp = []
walk_ul(child, [], tmp)
for r in rows_from_paths(tmp):
rows.append({'D': curD, 'E': r.get('D', ''), 'F': r.get('E', ''),
'G': r.get('F', ''), 'H': r.get('G', ''), 'I': r.get('H', ''),
'J': r.get('I', ''), 'href': r.get('href', '')})
return rows
def parse_muju(soup, base):
"""무주: div#sitemap > div.sitemapN > (h4.sNN>a (D) + ul>li>a (E)) 형제 반복."""
rows = []
cont = soup.select_one('div#sitemap')
if not cont:
return rows
for box in cont.find_all('div', recursive=False):
curD = ''
for child in box.find_all(['h4', 'ul'], recursive=False):
if child.name == 'h4':
a = child.find('a')
curD = clean_text(a.get_text() if a else child.get_text())
elif child.name == 'ul' and curD:
for li in child.find_all('li', recursive=False):
a = li.find('a', recursive=False)
if not a:
continue
E = clean_text(a.get_text())
rows.append({'D': curD, 'E': E, 'href': extract_href(a),
**{c: '' for c in 'FGHIJ'}})
return rows
def parse_buan(soup, base):
"""부안: nav#onmenu > ul > li > div.depth_box > div.depth_boxcon > strong (D) + ul>li>a (E) + ul (F)."""
rows = []
nav = soup.select_one('nav#onmenu')
if not nav:
return rows
top = nav.find('ul')
if not top:
return rows
for li in top.find_all('li', recursive=False):
box = li.find('div', class_='depth_boxcon')
if not box:
continue
strong = box.find('strong')
a0 = li.find('a', recursive=False)
D = clean_text(strong.get_text()) if strong else clean_text(a0.get_text() if a0 else '')
if not D:
continue
ul = box.find('ul')
if not ul:
continue
tmp = []
walk_ul(ul, [], tmp)
for r in rows_from_paths(tmp):
rows.append({'D': D, 'E': r.get('D', ''), 'F': r.get('E', ''),
'G': r.get('F', ''), 'H': r.get('G', ''), 'I': r.get('H', ''),
'J': r.get('I', ''), 'href': r.get('href', '')})
return rows
def parse_sunchang(soup, base):
"""순창: ul.gnb > li > a (D) + div.box ul.gnb_2dep > li>a (E) + ul.gnb_3dep (F) + ul.gnb_4dep (G)."""
rows = []
gnb = soup.select_one('ul.gnb')
if not gnb:
return rows
for li in gnb.find_all('li', recursive=False):
a0 = li.find('a', recursive=False)
D = clean_text(a0.get_text()) if a0 else ''
if not D:
continue
ul2 = li.find('ul', class_='gnb_2dep')
if not ul2:
rows.append({'D': D, 'href': extract_href(a0), **{c: '' for c in 'EFGHIJ'}})
continue
for li2 in ul2.find_all('li', recursive=False):
a2 = li2.find('a', recursive=False)
if not a2:
continue
E = clean_text(a2.get_text())
E_href = extract_href(a2)
ul3 = li2.find('ul', class_='gnb_3dep')
if not ul3:
rows.append({'D': D, 'E': E, 'href': E_href, **{c: '' for c in 'FGHIJ'}})
continue
rows.append({'D': D, 'E': E, 'href': E_href, **{c: '' for c in 'FGHIJ'}})
for li3 in ul3.find_all('li', recursive=False):
a3 = li3.find('a', recursive=False)
if not a3:
continue
F = clean_text(a3.get_text())
F_href = extract_href(a3)
ul4 = li3.find('ul', class_='gnb_4dep')
if not ul4:
rows.append({'D': D, 'E': E, 'F': F, 'href': F_href, **{c: '' for c in 'GHIJ'}})
continue
rows.append({'D': D, 'E': E, 'F': F, 'href': F_href, **{c: '' for c in 'GHIJ'}})
for li4 in ul4.find_all('li', recursive=False):
a4 = li4.find('a', recursive=False)
if not a4:
continue
rows.append({'D': D, 'E': E, 'F': F, 'G': clean_text(a4.get_text()),
'href': extract_href(a4), **{c: '' for c in 'HIJ'}})
return rows
def parse_jangsu(soup, base):
"""장수: div.sitemap > ul.siteMapList > li.sml_1depth > a.sml_1depthBtn (D) + ul.sml_2depthList > li>a (E) + ul 재귀."""
rows = []
ul = soup.select_one('ul.siteMapList')
if not ul:
return rows
for li in ul.find_all('li', recursive=False):
a0 = li.find('a', recursive=False)
D = clean_text(a0.get_text()) if a0 else ''
if not D:
continue
ul2 = li.find('ul', recursive=False)
if not ul2:
rows.append({'D': D, 'href': extract_href(a0), **{c: '' for c in 'EFGHIJ'}})
continue
tmp = []
walk_ul(ul2, [], tmp)
for r in rows_from_paths(tmp):
rows.append({'D': D, 'E': r.get('D', ''), 'F': r.get('E', ''),
'G': r.get('F', ''), 'H': r.get('G', ''), 'I': r.get('H', ''),
'J': r.get('I', ''), 'href': r.get('href', '')})
return rows
def parse_jeonju(soup, base):
"""전주: div.sitemap_Warp (여러개) > h4.title_h4 (D) + ul > li > div>a (E) + ul/li (F) 망가진 마크업."""
rows = []
warps = soup.select('div.sitemap_Warp')
for w in warps:
h4 = w.find('h4', class_='title_h4') or w.find('h4')
D = clean_text(h4.get_text()) if h4 else ''
if not D:
continue
ul = w.find('ul', recursive=False)
if not ul:
continue
tmp = []
walk_ul(ul, [], tmp)
for r in rows_from_paths(tmp):
rows.append({'D': D, 'E': r.get('D', ''), 'F': r.get('E', ''),
'G': r.get('F', ''), 'H': r.get('G', ''), 'I': r.get('H', ''),
'J': r.get('I', ''), 'href': r.get('href', '')})
return rows
def parse_jinan(soup, base):
"""진안: div.sitemap > dl > dt (D) + dd > ul > li>a (E) + ul (F) 재귀."""
rows = []
sm = soup.select_one('div.sitemap')
if not sm:
return rows
for dl in sm.find_all('dl', recursive=False):
dt = dl.find('dt')
D = clean_text(dt.get_text()) if dt else ''
if not D:
continue
dd = dl.find('dd')
ul = dd.find('ul', recursive=False) if dd else None
if not ul:
rows.append({'D': D, 'href': '', **{c: '' for c in 'EFGHIJ'}})
continue
tmp = []
walk_ul(ul, [], tmp)
for r in rows_from_paths(tmp):
rows.append({'D': D, 'E': r.get('D', ''), 'F': r.get('E', ''),
'G': r.get('F', ''), 'H': r.get('G', ''), 'I': r.get('H', ''),
'J': r.get('I', ''), 'href': r.get('href', '')})
return rows
def parse_box_heading(soup, base):
"""서귀포/제주시: div.sitemap > div.sitemapBox|sitemap_menu > h3|h4 (D) + ul > li>a (E)."""
rows = []
sm = soup.select_one('div.sitemap')
if not sm:
return rows
for box in sm.find_all('div', recursive=False):
h = box.find(['h3', 'h4'])
D = clean_text(h.get_text()) if h else ''
if not D:
continue
ul = box.find('ul')
if not ul:
rows.append({'D': D, 'href': '', **{c: '' for c in 'EFGHIJ'}})
continue
tmp = []
walk_ul(ul, [], tmp)
for r in rows_from_paths(tmp):
rows.append({'D': D, 'E': r.get('D', ''), 'F': r.get('E', ''),
'G': r.get('F', ''), 'H': r.get('G', ''), 'I': r.get('H', ''),
'J': r.get('I', ''), 'href': r.get('href', '')})
return rows
def parse_wanju_json(soup, base):
"""완주: Playwright로 추출한 _wanju_menu.json 로드."""
rows = []
data = json.load(open(os.path.join(os.path.dirname(os.path.abspath(__file__)), '_wanju_menu.json'), encoding='utf-8'))
for r in data:
rows.append({'D': r.get('D', ''), 'E': r.get('E', ''), 'F': r.get('F', ''),
'href': r.get('href', ''), 'G': '', 'H': '', 'I': '', 'J': ''})
return rows
# ================================================================
# 사이트 설정
# ================================================================
def jb(i, name, folder, base, sitemap, parser, domain, weak=False, fetch_kind='html'):
return {'idx': i, 'name': name, 'base': base, 'sitemap': sitemap,
'sheet': f'{i:02d}_{name}', 'parser': parser, 'domain': domain,
'folder': fr'{ROOT}\작업파일\광역_사이트맵\전북특별자치도\{i}.{name}', 'weak_ssl': weak,
'fetch_kind': fetch_kind}
def jj(i, name, base, sitemap, parser, domain):
return {'idx': i, 'name': name, 'base': base, 'sitemap': sitemap,
'sheet': f'{i:02d}_{name}', 'parser': parser, 'domain': domain,
'folder': fr'{ROOT}\작업파일\광역_사이트맵\제주특별자치도\{i}.{name}', 'weak_ssl': False,
'fetch_kind': 'html'}
SITES = [
jb(1, '고창군', '', 'https://www.gochang.go.kr',
'https://www.gochang.go.kr/index.gochang?menuCd=DOM_000000106006000000', parse_menu_div, 'gochang.go.kr'),
jb(2, '군산시', '', 'https://www.gunsan.go.kr',
'https://www.gunsan.go.kr/main', parse_gunsan if False else None, 'gunsan.go.kr'),
jb(3, '김제시', '', 'https://www.gimje.go.kr',
'https://www.gimje.go.kr/index.gimje?menuCd=DOM_000000107002000000', parse_menu_div, 'gimje.go.kr'),
jb(4, '남원시', '', 'https://www.namwon.go.kr',
'https://www.namwon.go.kr/index.do?menuUid=ff8080818f2717db018f277767500088', parse_namwon, 'namwon.go.kr'),
jb(5, '무주군', '', 'https://www.muju.go.kr',
'https://www.muju.go.kr/index.9is?contentUid=ff8080816db80238016dc8e98fa10ef6', parse_muju, 'muju.go.kr'),
jb(6, '부안군', '', 'https://www.buan.go.kr',
'https://www.buan.go.kr/index.buan?contentsSid=1', parse_buan, 'buan.go.kr'),
jb(7, '순창군', '', 'https://www.sunchang.go.kr',
'https://www.sunchang.go.kr/', parse_sunchang, 'sunchang.go.kr'),
jb(8, '완주군', '', 'https://www.wanju.go.kr',
'JSON', parse_wanju_json, 'wanju.go.kr', fetch_kind='json'),
jb(9, '익산시', '', 'https://www.iksan.go.kr',
'https://www.iksan.go.kr/index.do?menuUid=ff80808199f0d11c019a041b8e35174a', parse_group_sitemap, 'iksan.go.kr'),
jb(10, '임실군', '', 'https://www.imsil.go.kr',
'https://www.imsil.go.kr/index.imsil?menuCd=DOM_000000107003000000', parse_menu_div, 'imsil.go.kr'),
jb(11, '장수군', '', 'https://www.jangsu.go.kr',
'https://www.jangsu.go.kr/index.jangsu?menuCd=DOM_000000107001000000', parse_jangsu, 'jangsu.go.kr'),
jb(12, '전주시', '', 'https://www.jeonju.go.kr',
'https://www.jeonju.go.kr/index.9is?contentUid=ff8080818c7c2e8e018c7fd67b9a03b6', parse_jeonju, 'jeonju.go.kr', fetch_kind='html5'),
jb(13, '정읍시', '', 'https://www.jeongeup.go.kr',
'https://www.jeongeup.go.kr/index.jeongeup?menuCd=DOM_000000106002000000', parse_jeongeup, 'jeongeup.go.kr'),
jb(14, '진안군', '', 'https://www.jinan.go.kr',
'https://www.jinan.go.kr/index.jinan?menuCd=DOM_000000110002000000', parse_jinan, 'jinan.go.kr'),
jj(1, '서귀포시', 'https://www.seogwipo.go.kr',
'https://www.seogwipo.go.kr/help/sitemap.htm', parse_box_heading, 'seogwipo.go.kr'),
jj(2, '제주시', 'https://www.jejusi.go.kr',
'https://www.jejusi.go.kr/guide/sitemap.do', parse_box_heading, 'jejusi.go.kr'),
]
def parse_gunsan(soup, base):
"""군산: div#all_pcmenu > div.allmenubox > a.Bmenu (D) + div.allmw ul.sub_pcmenu > li>a (E) + ul.dep3 (F) 재귀."""
rows = []
pc = soup.select_one('div#all_pcmenu')
if not pc:
return rows
for box in pc.select('div.allmenubox'):
bm = box.find('a', class_='Bmenu') or box.find('a')
D = clean_text(bm.get_text()) if bm else ''
if not D:
continue
ul = box.find('ul', class_='sub_pcmenu')
if not ul:
rows.append({'D': D, 'href': extract_href(bm), **{c: '' for c in 'EFGHIJ'}})
continue
# ★ ul.dep4 = 모바일 아코디언 잔재(F형제 전체를 '- '접두로 복제). 진짜 자식 아님 → 제거.
# 안 지우면 각 F의 G자식으로 재귀돼 카르테시안 폭발(2026-05-31 군산 208행 버그 수정).
for d4 in ul.select('ul.dep4'):
d4.decompose()
tmp = []
walk_ul(ul, [], tmp)
for r in rows_from_paths(tmp):
rows.append({'D': D, 'E': r.get('D', ''), 'F': r.get('E', ''),
'G': r.get('F', ''), 'H': r.get('G', ''), 'I': r.get('H', ''),
'J': r.get('I', ''), 'href': r.get('href', '')})
return rows
# 군산 파서 바인딩(전방참조 해결)
for _s in SITES:
if _s['name'] == '군산시':
_s['parser'] = parse_gunsan
# ================================================================
# 엑셀 생성 (충북 스크립트와 동일 로직)
# ================================================================
def write_excel(site, raw_rows):
name = site['name']
base = site['base']
domain = site['domain']
output = f"{site['folder']}\\{os.path.basename(os.path.dirname(site['folder']))}_{name}.xlsx"
def abs_url(href):
if not href:
return ''
href = href.strip()
if href.startswith(('javascript:', '#')):
return ''
if href.startswith(('http://', 'https://')):
return href
return urljoin(base + '/', href)
def is_external(url):
return url.startswith(('http://', 'https://')) and domain not in url
final_rows = []
i = 0
removed = 0
while i < len(raw_rows):
row = raw_rows[i]
if (i + 1 < len(raw_rows)
and row.get('G', '') == ''
and raw_rows[i + 1].get('D') == row.get('D')
and raw_rows[i + 1].get('E') == row.get('E')
and raw_rows[i + 1].get('F') == row.get('F')
and raw_rows[i + 1].get('G', '') != ''
and raw_rows[i + 1].get('href') == row.get('href')):
removed += 1
i += 1
continue
final_rows.append(row)
i += 1
print(f' [{name}] 원본 {len(raw_rows)} → 중복 {removed} → 최종 {len(final_rows)}')
if not final_rows:
print(f' [{name}] !! 행 0개 — 파서 점검 필요. 엑셀 생성 스킵.')
return False
shutil.copy(TEMPLATE, output)
wb = openpyxl.load_workbook(output)
ws = wb.active
ws.title = site['sheet']
HEADER = {'B1:R1', 'S1:W1', 'Y1:AA1'}
for rng in [str(m) for m in ws.merged_cells.ranges if str(m) not in HEADER]:
ws.unmerge_cells(rng)
for row in ws.iter_rows(min_row=3, max_row=ws.max_row, min_col=1, max_col=ws.max_column):
for cell in row:
cell.value = None
START = 3
template_r = 3
cur_max = ws.max_row
for idx, item in enumerate(final_rows, start=START):
if idx > cur_max:
for c in range(1, ws.max_column + 1):
srcc = ws.cell(template_r, c)
tgt = ws.cell(idx, c)
if srcc.has_style:
tgt.font = copy(srcc.font)
tgt.fill = copy(srcc.fill)
tgt.border = copy(srcc.border)
tgt.alignment = copy(srcc.alignment)
tgt.number_format = srcc.number_format
tgt.protection = copy(srcc.protection)
url = abs_url(item.get('href', ''))
ws.cell(idx, 2).value = idx - 2
ws.cell(idx, 3).value = name
ws.cell(idx, 4).value = item.get('D', '')
ws.cell(idx, 5).value = item.get('E', '')
ws.cell(idx, 6).value = item.get('F', '')
ws.cell(idx, 7).value = item.get('G', '')
ws.cell(idx, 8).value = item.get('H', '')
ws.cell(idx, 9).value = item.get('I', '')
ws.cell(idx, 10).value = item.get('J', '')
ws.cell(idx, 11).value = url
if is_external(url):
ws.cell(idx, 19).value = '외부링크'
END = START + len(final_rows) - 1
center = Alignment(horizontal='center', vertical='center', wrap_text=True)
left = Alignment(horizontal='left', vertical='center', wrap_text=False)
def merge_runs(col_letter, col_idx, group_cols=()):
runs = []
cur_val = ws.cell(START, col_idx).value
cur_grp = tuple(ws.cell(START, g).value for g in group_cols)
run_start = START
for r in range(START + 1, END + 1):
v = ws.cell(r, col_idx).value
g = tuple(ws.cell(r, gg).value for gg in group_cols)
if v == cur_val and g == cur_grp:
continue
if cur_val not in (None, '') and r - 1 > run_start:
runs.append((run_start, r - 1))
cur_val, cur_grp, run_start = v, g, r
if cur_val not in (None, '') and END > run_start:
runs.append((run_start, END))
for s, e in runs:
ws.merge_cells(f'{col_letter}{s}:{col_letter}{e}')
ws.cell(s, col_idx).alignment = center
return len(runs)
n_f = merge_runs('F', 6, group_cols=(4, 5))
n_e = merge_runs('E', 5, group_cols=(4,))
n_d = merge_runs('D', 4)
for r in range(START, END + 1):
for c in (4, 5, 6, 7):
if ws.cell(r, c).value is not None:
ws.cell(r, c).alignment = center
for r in range(1, END + 1):
ws.row_dimensions[r].height = 15
link_n = 0
for r in range(START, END + 1):
cell = ws.cell(r, 11)
u = cell.value
if u and isinstance(u, str) and u.startswith(('http://', 'https://')):
cell.hyperlink = u
old = cell.font
cell.font = Font(name=old.name or '맑은 고딕', size=old.size or 11,
bold=old.bold, italic=old.italic,
color='0000FF', underline='single')
cell.alignment = left
link_n += 1
wb.save(output)
ext_n = sum(1 for r in range(START, END + 1) if ws.cell(r, 19).value == '외부링크')
print(f' [{name}] 병합 D:{n_d} E:{n_e} F:{n_f} | 외부링크 {ext_n} | K링크 {link_n}{output}')
return True
def main():
targets = sys.argv[1:] if len(sys.argv) > 1 else [s['name'] for s in SITES]
for site in SITES:
if site['name'] not in targets:
continue
print(f"\n{'='*60}\n[{site['idx']}.{site['name']}] {site['sitemap']}\n{'='*60}")
try:
if site['fetch_kind'] == 'json':
raw_rows = site['parser'](None, site['base'])
else:
sess = make_session(weak_ssl=site.get('weak_ssl', False))
html = fetch_html(site['sitemap'], session=sess)
engine = 'html5lib' if site.get('fetch_kind') == 'html5' else 'html.parser'
soup = BeautifulSoup(html, engine)
raw_rows = site['parser'](soup, site['base'])
write_excel(site, raw_rows)
except Exception as e:
print(f' [{site["name"]}] !! 실패: {e}')
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()