fix(insta-lab): replace Google Trends with YouTube Data API (Google API 폐기 대응)

Google이 비공식 trends endpoint 두 가지(/trends/.../rss + /trends/api/dailytrends)
모두 404로 폐기 (NAS에서 직접 호출 시 확정). 대안으로 YouTube Data API v3
mostPopular(regionCode=KR, 50개)로 source 교체:

- source 이름: google_trends → youtube_trending
- 키워드: 영상 제목 정제 (대괄호·이모지 제거, 60자 limit)
- API 키: YOUTUBE_DATA_API_KEY (agent-office와 공유, .env 그대로 활용)
- 키 미설정 시 graceful skip
- docker-compose insta-lab에 환경변수 추가
- 테스트 9/9 pass (기존 6 + youtube 3 신규)
This commit is contained in:
2026-05-17 11:54:31 +09:00
parent cfbb72051f
commit 64fbbb7958
6 changed files with 99 additions and 83 deletions

View File

@@ -1,8 +1,9 @@
"""외부 트렌드 수집 — NAVER 인기 + Google Trends + LLM 카테고리 분류.
"""외부 트렌드 수집 — NAVER 인기 + YouTube 인기 영상 + LLM 카테고리 분류.
NAVER: 카테고리별 시드 키워드로 인기 검색 → 빈도 상위 추출.
Google Trends: pytrends 4.x + daily RSS endpoint 모두 폐기/404로 깨진 상태라
'/trends/api/dailytrends' JSON API를 직접 호출 (응답 앞 `)]}'` XSSI 접두사 자름).
YouTube: Google Trends 비공식 endpoint(RSS / dailytrends JSON)가 모두 404 폐기되어
대체로 YouTube Data API v3 (`videos.list?chart=mostPopular&regionCode=KR`) 사용.
무료 일일 quota 10000, 한국 region 지원, 인기 영상 50개 제목에서 트렌드 추출.
LLM 분류 결과는 24h in-memory 캐시.
"""
@@ -17,7 +18,7 @@ from anthropic import Anthropic
from .config import (
NAVER_CLIENT_ID, NAVER_CLIENT_SECRET, DEFAULT_CATEGORY_SEEDS,
ANTHROPIC_API_KEY, ANTHROPIC_MODEL_HAIKU,
ANTHROPIC_API_KEY, ANTHROPIC_MODEL_HAIKU, YOUTUBE_DATA_API_KEY,
)
from . import db
from .news_collector import _clean
@@ -31,10 +32,17 @@ _NAVER_HEADERS = {
"X-Naver-Client-Secret": NAVER_CLIENT_SECRET,
}
GOOGLE_TRENDS_DAILY_URL = (
"https://trends.google.com/trends/api/dailytrends"
"?hl=ko&tz=-540&geo=KR&ns=15"
YOUTUBE_TRENDING_URL = "https://www.googleapis.com/youtube/v3/videos"
# YouTube 제목 정제: 대괄호·이모지·과도한 길이 제거 후 카드 주제로 적합한 키워드 형태
_TITLE_BRACKET_RE = re.compile(r"[\[【「『\(][^\]】」』\)]{0,30}[\]】」』\)]")
_EMOJI_RE = re.compile(
r"["
r"\U0001F300-\U0001FAFF" # symbols & pictographs, etc.
r"\U00002600-\U000027BF" # misc symbols, dingbats
r"\U0001F1E6-\U0001F1FF" # regional indicator
r"]"
)
_TITLE_MAX_LEN = 60
_PLACEHOLDER_SEEDS = {"...", "", "tbd", "todo", "placeholder", "example"}
@@ -167,49 +175,52 @@ def classify_keyword(keyword: str) -> str:
return cat
# ── Google Trends ─────────────────────────────────────────────────────────────
# pytrends 4.x + daily RSS endpoint(`/trends/trendingsearches/daily/rss`) 모두
# 폐기/404 상태라 Google Trends 비공식 JSON API `/trends/api/dailytrends`를 직접
# 호출. 응답 앞에 `)]}'` XSSI 보호 prefix가 붙어있어 잘라낸 후 JSON 파싱.
# 응답 구조: default.trendingSearchesDays[].trendingSearches[].title.query
# ── YouTube Trending ──────────────────────────────────────────────────────────
# YouTube Data API v3 videos.list?chart=mostPopular&regionCode=KR
# 한국 인기 영상 50개 제목에서 카드 주제로 적합한 키워드 추출.
_XSSI_PREFIX_RE = re.compile(r"^[\s\)\]\}',\n]+")
def _clean_yt_title(title: str) -> str:
"""[공식]·【속보】·🔥 등 제거 후 60자 이내로 자른다."""
if not title:
return ""
cleaned = _TITLE_BRACKET_RE.sub("", title)
cleaned = _EMOJI_RE.sub("", cleaned)
cleaned = re.sub(r"\s+", " ", cleaned).strip()
return cleaned[:_TITLE_MAX_LEN]
def fetch_google_trends() -> List[Dict[str, Any]]:
"""Google Trends Daily JSON API (한국) 직접 호출. 실패 시 빈 리스트."""
def fetch_youtube_trending() -> List[Dict[str, Any]]:
"""YouTube Data API v3 mostPopular (한국, 50개). API 키 없거나 호출 실패 시 빈 리스트."""
if not YOUTUBE_DATA_API_KEY:
logger.info("YOUTUBE_DATA_API_KEY 미설정 — youtube_trending skip")
return []
try:
resp = requests.get(
GOOGLE_TRENDS_DAILY_URL,
YOUTUBE_TRENDING_URL,
params={
"part": "snippet",
"chart": "mostPopular",
"regionCode": "KR",
"maxResults": 50,
"key": YOUTUBE_DATA_API_KEY,
},
timeout=15,
headers={"User-Agent": "Mozilla/5.0 (insta-lab trend collector)"},
)
resp.raise_for_status()
body = _XSSI_PREFIX_RE.sub("", resp.text, count=1)
data = json.loads(body)
days = data.get("default", {}).get("trendingSearchesDays", []) or []
raw_titles: List[str] = []
for day in days:
for ts in day.get("trendingSearches", []) or []:
q = (ts.get("title") or {}).get("query", "")
if isinstance(q, str):
q = q.strip()
if q:
raw_titles.append(q)
# 중복 제거 (등장 순서 유지)
seen = set()
titles: List[str] = []
for t in raw_titles:
if t not in seen:
seen.add(t)
titles.append(t)
videos = resp.json().get("items", []) or []
except Exception as e:
logger.warning("Google Trends daily fetch failed: %s", e)
logger.warning("YouTube trending fetch failed: %s", e)
return []
items: List[Dict[str, Any]] = []
total = max(1, len(titles))
for idx, kw in enumerate(titles):
seen = set()
total = max(1, len(videos))
for idx, v in enumerate(videos):
title = (v.get("snippet") or {}).get("title", "")
kw = _clean_yt_title(title)
if not kw or kw in seen:
continue
seen.add(kw)
try:
cat = classify_keyword(kw)
except Exception as e:
@@ -219,15 +230,15 @@ def fetch_google_trends() -> List[Dict[str, Any]]:
items.append({
"keyword": kw,
"category": cat,
"source": "google_trends",
"source": "youtube_trending",
"score": rank_score,
"articles_count": 0,
})
return items
def collect_google_trends() -> int:
items = fetch_google_trends()
def collect_youtube_trending() -> int:
items = fetch_youtube_trending()
for it in items:
db.add_external_trend(it)
return len(items)
@@ -235,5 +246,5 @@ def collect_google_trends() -> int:
def collect_all(categories: List[str]) -> Dict[str, int]:
naver_n = collect_naver_popular_for(categories)
google_n = collect_google_trends()
return {"naver_popular": naver_n, "google_trends": google_n}
yt_n = collect_youtube_trending()
return {"naver_popular": naver_n, "youtube_trending": yt_n}