fix: 글 주제와 무관한 이미지(애니/게임/엔터) 필터링 추가
This commit is contained in:
+47
-4
@@ -207,7 +207,7 @@ def build_json_ld(article: dict, blog_url: str = '') -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _is_platform_logo(image_url: str) -> bool:
|
def _is_platform_logo(image_url: str) -> bool:
|
||||||
"""플랫폼 로고/아이콘 이미지인지 판별 — 대표 이미지로 부적합"""
|
"""플랫폼 로고/아이콘/광고 이미지인지 판별 — 대표 이미지로 부적합"""
|
||||||
skip_patterns = [
|
skip_patterns = [
|
||||||
'logo', 'icon', 'avatar', 'banner', '/ad/',
|
'logo', 'icon', 'avatar', 'banner', '/ad/',
|
||||||
'google.com/images/branding', 'googlenews', 'google-news',
|
'google.com/images/branding', 'googlenews', 'google-news',
|
||||||
@@ -215,11 +215,53 @@ def _is_platform_logo(image_url: str) -> bool:
|
|||||||
'facebook.com', 'twitter.com', 'naver.com/favicon',
|
'facebook.com', 'twitter.com', 'naver.com/favicon',
|
||||||
'default_image', 'placeholder', 'noimage', 'no-image',
|
'default_image', 'placeholder', 'noimage', 'no-image',
|
||||||
'og-default', 'share-default', 'sns_', 'common/',
|
'og-default', 'share-default', 'sns_', 'common/',
|
||||||
|
# 광고/게임/이벤트 관련 패턴
|
||||||
|
'ad.', 'ads.', '/adv/', '/promo/', '/event/', '/game/',
|
||||||
|
'adimg', 'adserver', 'doubleclick', 'googlesyndication',
|
||||||
|
'akamaihd.net', 'cdn.ad', 'click.', 'tracking.',
|
||||||
]
|
]
|
||||||
url_lower = image_url.lower()
|
url_lower = image_url.lower()
|
||||||
return any(p in url_lower for p in skip_patterns)
|
return any(p in url_lower for p in skip_patterns)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_relevant_image(image_url: str, article: dict) -> bool:
|
||||||
|
"""이미지가 글 주제와 관련 있는지 판별"""
|
||||||
|
if not image_url:
|
||||||
|
return False
|
||||||
|
url_lower = image_url.lower()
|
||||||
|
|
||||||
|
# 엔터테인먼트/애니메이션/게임 관련 URL 패턴 — 글 주제와 무관할 가능성 높음
|
||||||
|
entertainment_patterns = [
|
||||||
|
'game', 'gaming', 'casino', 'slot', 'poker', 'lottery',
|
||||||
|
'anime', 'animation', 'cartoon', 'drama', 'movie', 'film',
|
||||||
|
'entertainment', 'kpop', 'idol', 'singer', 'actor',
|
||||||
|
'breadbarbershop', 'bread', 'character', 'webtoon',
|
||||||
|
'advert', 'sponsor', 'promo', 'event_banner', 'event/',
|
||||||
|
'/show/', '/program/', '/tv/', '/ott/',
|
||||||
|
]
|
||||||
|
|
||||||
|
# 글 코너/태그 추출
|
||||||
|
corner = article.get('corner', '').lower()
|
||||||
|
tags = article.get('tags', [])
|
||||||
|
if isinstance(tags, str):
|
||||||
|
tags = [t.strip().lower() for t in tags.split(',')]
|
||||||
|
else:
|
||||||
|
tags = [t.lower() for t in tags]
|
||||||
|
topic = article.get('topic', '').lower() + ' ' + article.get('title', '').lower()
|
||||||
|
|
||||||
|
# 경제/IT/사회 관련 글인데 엔터테인먼트 이미지면 거부
|
||||||
|
serious_corners = ['ai인사이트', '스타트업', '재테크', '경제', '사회', '정치', '국제']
|
||||||
|
is_serious = any(c in corner for c in serious_corners) or any(
|
||||||
|
kw in topic for kw in ['경제', '투자', '금융', '정책', '기술', 'ai', '스타트업']
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_serious and any(p in url_lower for p in entertainment_patterns):
|
||||||
|
logger.info(f"이미지 관련성 불일치로 제외: {image_url[:80]}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _fetch_og_image(url: str) -> str:
|
def _fetch_og_image(url: str) -> str:
|
||||||
"""원본 기사 URL에서 og:image 메타태그 크롤링"""
|
"""원본 기사 URL에서 og:image 메타태그 크롤링"""
|
||||||
if not url or not url.startswith('http'):
|
if not url or not url.startswith('http'):
|
||||||
@@ -307,9 +349,10 @@ def _search_real_article_image(sources: list, topic: str = '') -> str:
|
|||||||
|
|
||||||
def fetch_featured_image(article: dict) -> str:
|
def fetch_featured_image(article: dict) -> str:
|
||||||
"""대표 이미지: RSS 이미지 → 원문 기사 → og:image → Wikipedia 순으로 시도"""
|
"""대표 이미지: RSS 이미지 → 원문 기사 → og:image → Wikipedia 순으로 시도"""
|
||||||
# 1) RSS 수집 시 가져온 소스 이미지 (플랫폼 로고 제외)
|
# 1) RSS 수집 시 가져온 소스 이미지 (플랫폼 로고 + 관련성 검사)
|
||||||
source_image = article.get('source_image', '')
|
source_image = article.get('source_image', '')
|
||||||
if source_image and source_image.startswith('http') and not _is_platform_logo(source_image):
|
if source_image and source_image.startswith('http') and not _is_platform_logo(source_image):
|
||||||
|
if _is_relevant_image(source_image, article):
|
||||||
try:
|
try:
|
||||||
resp = requests.head(source_image, timeout=5, allow_redirects=True)
|
resp = requests.head(source_image, timeout=5, allow_redirects=True)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
@@ -320,14 +363,14 @@ def fetch_featured_image(article: dict) -> str:
|
|||||||
# 2) 원본 기사 URL에서 og:image 크롤링
|
# 2) 원본 기사 URL에서 og:image 크롤링
|
||||||
source_url = article.get('source_url', '')
|
source_url = article.get('source_url', '')
|
||||||
og_image = _fetch_og_image(source_url)
|
og_image = _fetch_og_image(source_url)
|
||||||
if og_image:
|
if og_image and _is_relevant_image(og_image, article):
|
||||||
return og_image
|
return og_image
|
||||||
|
|
||||||
# 3) Google News 소스 → DuckDuckGo로 실제 기사 검색 → og:image
|
# 3) Google News 소스 → DuckDuckGo로 실제 기사 검색 → og:image
|
||||||
sources = article.get('sources', [])
|
sources = article.get('sources', [])
|
||||||
if sources:
|
if sources:
|
||||||
real_image = _search_real_article_image(sources, article.get('title', ''))
|
real_image = _search_real_article_image(sources, article.get('title', ''))
|
||||||
if real_image:
|
if real_image and _is_relevant_image(real_image, article):
|
||||||
return real_image
|
return real_image
|
||||||
|
|
||||||
# 4) Pexels API (키가 있을 때)
|
# 4) Pexels API (키가 있을 때)
|
||||||
|
|||||||
Reference in New Issue
Block a user