o2o-site-ontology/scripts/export-xlsx.py
hbyang df8ce5f117 전국 지역별 펜션 키워드 데이터셋 + 엑셀 산출
기획 변경: 군산 단일 지역 → 전국 지역별.

- data/regions.json — 전국 54개 펜션 수요 지역 마스터
  시도/지역/지역키/성격(해변·산간·호수·강변·도심·섬·계곡)/대표 관광지 202곳
  복합 지명은 별칭으로 분리 (보령·대천 → 대천 + 별칭 보령)
- scripts/build-nationwide-dataset.mjs — 지역 성격에 맞는 시설·시즌만 전개
  평창·무주 오션뷰 0건, 태안·거제 산뷰 0건으로 검증
- scripts/export-xlsx.py — 4개 시트 엑셀 (키워드/지역마스터/지역별요약/사용가이드)
- npm run dataset:nationwide

7,138건 / 지역당 평균 110건.
군산 974건을 54개 지역에 곱하면 5만 건이 되지만, 단일 지역 검증에서
저장분의 89%가 미사용이었으므로 티어를 매겨 눌렀다.

검색량·경쟁도 열은 비워 두었다 — 키워드도구로 채운 뒤 월 10 미만을 걷어내야 한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 17:04:40 +09:00

156 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""전국 펜션 키워드 데이터셋 → 엑셀.
python3 scripts/export-xlsx.py
검색량·경쟁도 열은 비워 둔다 — 네이버 검색광고 키워드도구에서 받아 채우는 자리."""
import json, collections
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
SRC = 'data/nationwide-pension-keywords.json'
OUT = 'data/전국_펜션_SEO_AEO_키워드.xlsx'
INK = '1F2A2B'
ACC = '0D6A60'
HEAD = PatternFill('solid', fgColor='0D6A60')
BAND = PatternFill('solid', fgColor='F1F5F4')
TIER = {'주력': 'DFF0EC', '보조': 'FFFFFF', '롱테일': 'F7F7F5', '태그': 'F6EAD2'}
THIN = Side(style='thin', color='D5DCDB')
BOX = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
SLOT = {
'코어': '메인 페이지 (주력)', '광역': '광역 랜딩',
'의도': '메인 / 예약 페이지', '동반자': '객실 페이지',
'시설': '시설 페이지', '관광지': '주변 여행 페이지',
'시즌': '블로그 · 프로모션', '분위기': '블로그 · 소개',
'여행형태': '블로그 · 코스', '질문형': 'FAQ (AEO · FAQPage)',
'태그': '필터 UI (SEO 아님)',
}
data = json.load(open(SRC, encoding='utf-8'))
items = data['items']
regions = json.load(open('data/regions.json', encoding='utf-8'))['regions']
wb = Workbook()
def style_header(ws, ncols, height=22):
for c in range(1, ncols + 1):
cell = ws.cell(row=1, column=c)
cell.fill = HEAD
cell.font = Font(bold=True, color='FFFFFF', size=10)
cell.alignment = Alignment(horizontal='center', vertical='center')
cell.border = BOX
ws.row_dimensions[1].height = height
ws.freeze_panes = 'A2'
ws.auto_filter.ref = f'A1:{get_column_letter(ncols)}{ws.max_row}'
def widths(ws, ws_widths):
for i, w in enumerate(ws_widths, start=1):
ws.column_dimensions[get_column_letter(i)].width = w
# ────────────────────────────────── 1. 키워드
ws = wb.active
ws.title = '키워드'
cols = ['시도', '지역', '지역키', '지역성격', '키워드', '종류', '의도', '카테고리',
'티어', '관련도', '월간검색수', '경쟁도', '추천 배치', '비고']
ws.append(cols)
for it in items:
ws.append([
it['sido'], it['region'], it['regionKey'], it['regionType'],
it['keyword'], it['kind'], it['intent'], it['category'],
it['tier'], it['relevance'], None, None,
SLOT.get(it['category'], ''), None,
])
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, max_col=len(cols)):
fill = PatternFill('solid', fgColor=TIER.get(row[8].value, 'FFFFFF'))
for c in row:
c.font = Font(size=10, color=INK)
c.border = BOX
c.fill = fill
row[9].number_format = '0.00'
row[10].number_format = '#,##0'
row[4].font = Font(size=10, bold=True, color=INK)
style_header(ws, len(cols))
widths(ws, [8, 12, 24, 14, 30, 8, 14, 10, 9, 9, 12, 10, 22, 16])
# ────────────────────────────────── 2. 지역 마스터
ws = wb.create_sheet('지역마스터')
ws.append(['시도', '지역', '지역키', '지역성격', '대표 관광지 (앵커)', '키워드 수'])
cnt = collections.Counter(i['regionKey'] for i in items)
for r in regions:
ws.append([r['sido'], r['name'], r['key'], '·'.join(r['type']),
', '.join(r['spots']), cnt.get(r['key'], 0)])
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, max_col=6):
for c in row:
c.font = Font(size=10, color=INK); c.border = BOX
c.alignment = Alignment(vertical='center', wrap_text=(c.column == 5))
style_header(ws, 6)
widths(ws, [8, 14, 26, 14, 70, 10])
# ────────────────────────────────── 3. 지역별 요약
ws = wb.create_sheet('지역별요약')
tiers = ['주력', '보조', '롱테일', '태그']
ws.append(['시도', '지역'] + tiers + ['합계'])
per = collections.defaultdict(collections.Counter)
meta = {}
for i in items:
per[i['regionKey']][i['tier']] += 1
meta[i['regionKey']] = (i['sido'], i['region'])
for key, c in sorted(per.items(), key=lambda kv: (-sum(kv[1].values()))):
sido, name = meta[key]
ws.append([sido, name] + [c[t] for t in tiers] + [sum(c.values())])
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, max_col=7):
for c in row:
c.font = Font(size=10, color=INK); c.border = BOX
style_header(ws, 7)
widths(ws, [8, 14, 9, 9, 10, 9, 9])
# ────────────────────────────────── 4. 사용 가이드
ws = wb.create_sheet('사용가이드')
guide = [
('이 파일은 무엇인가', ''),
('', f"전국 {data['regionCount']}개 펜션 수요 지역 × 검색 패턴으로 만든 SEO/AEO 키워드 후보 {len(items):,}건."),
('', '지역 성격(해변·산간·호수·도심·섬)에 맞는 시설 키워드만 전개했다. 산간 지역에 오션뷰 키워드는 없다.'),
('', ''),
('⚠ 반드시 먼저 읽을 것', ''),
('', '이 키워드는 검색 패턴으로 생성한 것이지 실제 검색 데이터가 아니다.'),
('', '네이버 검색광고 > 도구 > 키워드도구 에서 월간검색수를 받아 [월간검색수] 열을 채운 뒤'),
('', '월 10 미만은 걷어내야 한다. 앞선 단일 지역 검증에서 저장분의 89%가 한 번도 쓰이지 않았다.'),
('', ''),
('티어의 뜻', ''),
('주력', '페이지의 대표 키워드 후보. 한 페이지에 1개만 쓴다.'),
('보조', 'h2/h3 와 본문에 배치. 페이지당 3~5개.'),
('롱테일', '블로그·상세 페이지용. 검색량 확인 후 취사선택.'),
('태그', '사이트 필터 UI 용. SEO 키워드가 아니다.'),
('', ''),
('한 페이지에 몇 개를 넣나', ''),
('', 'title 1개 · h1 1개 · meta description 2~3개 · h2/h3 3~5개 · 본문 5~10개'),
('', 'meta keywords 태그는 쓰지 않는다 (구글은 2009년부터 랭킹에 반영하지 않는다).'),
('', '한 페이지에 주력을 여러 개 넣으면 주제가 희석돼 어느 것으로도 안 잡힌다.'),
('', ''),
('AEO (답변엔진)', ''),
('', '[카테고리=질문형] 행이 AEO 용이다. FAQPage 구조화 데이터로 8~15쌍 넣는다.'),
('', '답변은 2~3문장, 업체 정보에 근거한 사실만 쓴다.'),
('', ''),
('다음 단계', ''),
('', '1. 키워드도구로 [월간검색수]·[경쟁도] 채우기'),
('', '2. 월 10 미만 행 제거'),
('', '3. 관련도 높음 + 볼륨 중간 + 경쟁 낮음 조합을 우선 채택'),
('', '4. [추천 배치] 열대로 페이지에 배분'),
]
ws.append(['항목', '내용'])
for a, b in guide:
ws.append([a, b])
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, max_col=2):
bold = bool(row[0].value) and not row[1].value
row[0].font = Font(size=10, bold=True, color=ACC if bold else INK)
row[1].font = Font(size=10, color=INK)
row[1].alignment = Alignment(wrap_text=True, vertical='center')
style_header(ws, 2)
widths(ws, [22, 100])
wb.save(OUT)
print(f'{OUT}')
print(f' 시트: ' + ', '.join(s.title for s in wb.worksheets))
print(f' 키워드 {len(items):,}행 / 지역 {len(regions)}')