네이버 AI 브리핑은 공식 API가 없어 지금까지 답변 노출을 재지 못했다. SerpApi naver_ai_overview(제3자 SERP API)로 조회하는 엔진을 붙였다(haewon 결정 2026-09-15). search.naver.com robots.txt 가 수집을 금지하므로 고객 리포트·랜딩에는 넣지 않고 내부 측정까지만 반영했다. 고객 노출은 법무 확인 뒤에 따로 한다. - run_question_bank_openai.py: --engine naver_briefing, --device, --no-cache(반복 측정 시 1시간 캐시 무시). 브리핑이 없는 문항은 answer 를 비우고 briefing=false 로 기록. 인용 출처(sources) 보관 - monthly_ai_measure.py: 기본 엔진 3개(키 없으면 네이버 건너뜀), supporter_builds.report.naverBriefing(노출률·언급·출처 상위, legalReviewed=false) - sentiment_qb.py: 빈 답변은 판정 API를 부르지 않고 미언급 처리 - merge_qb_results.py: NAVER 열·브리핑 없음 표시, 측정일을 생성일이 아닌 엔진별 실제 날짜로 - export_qb_excel.py: 네이버 열·NAVER상세 시트, --out 으로 기존 파일 보존. TOP50 점수는 기존 정의(ChatGPT+Perplexity, 만점 840) 유지 실측(뷰성형외과, 모바일, 2026-09-15): 브리핑 노출 119/120, 브랜드 질문 언급 36/36, 논브랜드 1/84. 인용 출처는 네이버 블로그 87·카페 51·굿닥 23·뷰 공식 20 순. SerpApi 123회, 감성 판정 $0.37. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
145 lines
6.8 KiB
Python
145 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""OpenAI·Perplexity·네이버 AI 브리핑 실측 JSONL을 합쳐 질문별 통합 비교표 MD를 생성한다.
|
|
네이버 결과 파일이 없으면 두 엔진만으로 만든다."""
|
|
import json
|
|
import os
|
|
import time
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
QUESTIONS = os.path.join(ROOT, "scripts", "question_bank_viewclinic.json")
|
|
OUT_MD = os.path.join(ROOT, "docs", "reports", "viewclinic", "03_question_bank", "Viewclinic_QB_Results_Combined.md")
|
|
|
|
CAT_LABEL = {
|
|
"A": "A. 브랜드 직접", "B": "B. 위치·운영", "C": "C. 지역 추천·비교",
|
|
"D1": "D1. 가슴성형", "D2": "D2. 눈성형", "D3": "D3. 코성형",
|
|
"D4": "D4. 안면윤곽·양악", "D5": "D5. 지방성형", "D6": "D6. 피부·리프팅",
|
|
"D7": "D7. 재수술", "E": "E. 신뢰·안전", "F": "F. 가격·이벤트", "G": "G. 후기·평판",
|
|
}
|
|
# 질문 문장에 병원명이 들어간 브랜드 질문 (뷰 언급이 당연한 그룹)
|
|
def is_brand(q):
|
|
return "뷰성형외과" in q["question"]
|
|
|
|
|
|
def canon(name):
|
|
"""빈도 집계용 병원명 정규화: 표기 변형을 하나로 합친다."""
|
|
n = name.strip().lower().replace(" ", "")
|
|
if "view" in n and ("plastic" in n or "성형" in n) or n.startswith("뷰성형"):
|
|
return "뷰성형외과"
|
|
for suf in ("성형외과의원", "성형외과", "의원", "클리닉", "clinic", "plasticsurgery"):
|
|
if n.endswith(suf) and len(n) > len(suf):
|
|
n = n[: -len(suf)]
|
|
break
|
|
return n
|
|
|
|
|
|
def load(engine):
|
|
p = os.path.join(ROOT, "scripts", "out", f"qb_{engine}_results.jsonl")
|
|
rows = {}
|
|
if os.path.exists(p):
|
|
for line in open(p):
|
|
if line.strip():
|
|
r = json.loads(line)
|
|
rows[r["id"]] = r
|
|
return rows
|
|
|
|
|
|
def measured_on(data):
|
|
"""엔진 결과의 실제 측정일(ts 최소~최대). 생성일로 찍으면 과거 측정이 오늘 측정처럼 보인다."""
|
|
days = sorted({(r.get("ts") or r.get("measured_at") or "")[:10] for r in data.values()} - {""})
|
|
if not days:
|
|
return "미상"
|
|
return days[0] if days[0] == days[-1] else f"{days[0]}~{days[-1]}"
|
|
|
|
|
|
def cell(r):
|
|
if not r:
|
|
return "미실측", "-"
|
|
if r.get("engine") == "naver_briefing" and not r.get("briefing"):
|
|
return "브리핑 없음", "-"
|
|
clinics = [c for c in r.get("clinics", []) if isinstance(c, str)][:5]
|
|
rank = r.get("view_rank")
|
|
mark = f"O ({rank}위)" if r.get("view_mentioned") else "X"
|
|
return mark, ", ".join(clinics) if clinics else "(병원 언급 없음)"
|
|
|
|
|
|
def main():
|
|
qs = json.load(open(QUESTIONS))
|
|
oa, px, nv = load("openai"), load("perplexity"), load("naver_briefing")
|
|
engines = [("ChatGPT(OpenAI)", oa), ("Perplexity", px)] + ([("네이버 AI 브리핑", nv)] if nv else [])
|
|
|
|
lines = ["# 뷰성형외과 Question Bank 실측 결과: " + " x ".join(e for e, _ in engines) + " 통합", "",
|
|
"측정일: " + " · ".join(f"{e} {measured_on(d)}" for e, d in engines),
|
|
"측정 방식: OpenAI Responses API + web_search(gpt-4o, 서울 고정) / Perplexity API(sonar)"
|
|
+ (" / 네이버 AI 브리핑(SerpApi naver_ai_overview, 제3자 SERP API, 모바일)" if nv else "") + ". "
|
|
"답변엔진 소비자 UI 자동 조회는 약관 위반이므로 정식 API로 측정했다."
|
|
+ (" 네이버는 공식 API가 없어 제3자 API를 썼으며, 고객 리포트 사용 전 법무 확인이 필요하다." if nv else ""), ""]
|
|
|
|
# 집계
|
|
stats = {}
|
|
for eng, data in engines:
|
|
b = [q for q in qs if is_brand(q) and q["id"] in data]
|
|
nb = [q for q in qs if not is_brand(q) and q["id"] in data]
|
|
bm = sum(1 for q in b if data[q["id"]].get("view_mentioned"))
|
|
nbm = sum(1 for q in nb if data[q["id"]].get("view_mentioned"))
|
|
stats[eng] = (len(b), bm, len(nb), nbm)
|
|
|
|
lines += ["## 요약", "",
|
|
"| 엔진 | 브랜드 질문 뷰 언급 | 논브랜드 질문 뷰 언급 |", "|---|---|---|"]
|
|
for eng, (b, bm, nb, nbm) in stats.items():
|
|
lines.append(f"| {eng} | {bm}/{b} ({bm*100//max(b,1)}%) | {nbm}/{nb} ({nbm*100//max(nb,1)}%) |")
|
|
if nv:
|
|
shown = [r for r in nv.values() if r.get("briefing")]
|
|
lines += ["", f"네이버 AI 브리핑 노출: {len(shown)}/{len(nv)}문항. 브리핑이 뜨지 않은 문항은 언급 없음으로 셌다."]
|
|
lines += ["",
|
|
"브랜드 질문은 질문 문장에 '뷰성형외과'가 들어간 문항이다. "
|
|
"승부처인 논브랜드 질문(추천·비교·시술 정보)에서의 언급률이 AI Discovery의 실제 성적이다.", ""]
|
|
|
|
# 병원별 빈도 (논브랜드 질문 한정)
|
|
for eng, data in engines:
|
|
freq, label = {}, {}
|
|
for q in qs:
|
|
if is_brand(q) or q["id"] not in data:
|
|
continue
|
|
seen = set()
|
|
for i, c in enumerate([c for c in data[q["id"]].get("clinics", []) if isinstance(c, str)][:5]):
|
|
key = canon(c)
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
label.setdefault(key, c)
|
|
freq.setdefault(key, [0, 0])
|
|
freq[key][0] += 1
|
|
if i == 0:
|
|
freq[key][1] += 1
|
|
freq = {label[k]: v for k, v in freq.items()}
|
|
lines += [f"### {eng}: 논브랜드 질문에서 언급된 병원 상위 15", "",
|
|
"| 병원 | 언급 질문 수 | 1순위 등장 수 |", "|---|---|---|"]
|
|
for name, (cnt, first) in sorted(freq.items(), key=lambda x: -x[1][0])[:15]:
|
|
lines.append(f"| {name} | {cnt} | {first} |")
|
|
lines.append("")
|
|
|
|
# 문항별 표 (카테고리 섹션별)
|
|
lines += ["## 문항별 결과", "",
|
|
"GPT/PPLX" + ("/NAVER" if nv else "") + " 컬럼: O(순위) = 뷰성형외과 언급과 답변 내 등장 순번, X = 미언급"
|
|
+ (", 브리핑 없음 = 네이버가 AI 브리핑을 띄우지 않음" if nv else "") + ".", ""]
|
|
cur = None
|
|
for q in qs:
|
|
cat = q["id"].split("-")[0]
|
|
if cat != cur:
|
|
cur = cat
|
|
head = "| # | 질문 | 사전판정 | GPT | GPT 상위 병원 | PPLX | PPLX 상위 병원 |" + (" NAVER | NAVER 상위 병원 |" if nv else "")
|
|
lines += [f"### {CAT_LABEL.get(cat, cat)}", "", head, "|" + "---|" * (head.count("|") - 1)]
|
|
om, oc = cell(oa.get(q["id"]))
|
|
pm, pc = cell(px.get(q["id"]))
|
|
row = f"| {q['id']} | {q['question']} | {q['prior']} | {om} | {oc} | {pm} | {pc} |"
|
|
if nv:
|
|
nm, nc = cell(nv.get(q["id"]))
|
|
row += f" {nm} | {nc} |"
|
|
lines.append(row)
|
|
open(OUT_MD, "w").write("\n".join(lines) + "\n")
|
|
print(f"저장: {OUT_MD} (openai {len(oa)} / perplexity {len(px)} / naver_briefing {len(nv)}문항)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|