|
""" |
|
Module lấy tin thể thao quốc tế từ các nguồn miễn phí |
|
Hỗ trợ RSS feeds và API miễn phí |
|
""" |
|
|
|
import requests |
|
import feedparser |
|
from bs4 import BeautifulSoup |
|
import json |
|
from datetime import datetime, timedelta |
|
import re |
|
from typing import List, Dict, Optional |
|
import logging |
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
logger = logging.getLogger(__name__) |
|
|
|
class SportsNewsAggregator: |
|
"""Class tổng hợp tin thể thao từ nhiều nguồn miễn phí""" |
|
|
|
def __init__(self): |
|
self.sources = { |
|
'bbc_sport': { |
|
'url': 'http://feeds.bbci.co.uk/sport/rss.xml', |
|
'type': 'rss', |
|
'language': 'en' |
|
}, |
|
'espn': { |
|
'url': 'https://www.espn.com/espn/rss/news', |
|
'type': 'rss', |
|
'language': 'en' |
|
}, |
|
'sky_sports': { |
|
'url': 'https://www.skysports.com/rss/12040', |
|
'type': 'rss', |
|
'language': 'en' |
|
}, |
|
'vnexpress_sport': { |
|
'url': 'https://vnexpress.net/rss/the-thao.rss', |
|
'type': 'rss', |
|
'language': 'vi' |
|
}, |
|
'thanhnien_sport': { |
|
'url': 'https://thanhnien.vn/rss/the-thao.rss', |
|
'type': 'rss', |
|
'language': 'vi' |
|
} |
|
} |
|
|
|
self.session = requests.Session() |
|
self.session.headers.update({ |
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' |
|
}) |
|
|
|
def get_rss_news(self, source_config: Dict) -> List[Dict]: |
|
"""Lấy tin từ RSS feed""" |
|
try: |
|
response = self.session.get(source_config['url'], timeout=10) |
|
response.raise_for_status() |
|
|
|
feed = feedparser.parse(response.content) |
|
news_items = [] |
|
|
|
for entry in feed.entries[:10]: |
|
|
|
pub_date = None |
|
if hasattr(entry, 'published_parsed') and entry.published_parsed: |
|
pub_date = datetime(*entry.published_parsed[:6]) |
|
elif hasattr(entry, 'updated_parsed') and entry.updated_parsed: |
|
pub_date = datetime(*entry.updated_parsed[:6]) |
|
|
|
|
|
summary = self.clean_text(getattr(entry, 'summary', '')) |
|
title = self.clean_text(getattr(entry, 'title', '')) |
|
|
|
news_item = { |
|
'title': title, |
|
'summary': summary, |
|
'link': getattr(entry, 'link', ''), |
|
'published': pub_date.isoformat() if pub_date else None, |
|
'source': source_config.get('name', 'Unknown'), |
|
'language': source_config.get('language', 'en') |
|
} |
|
|
|
news_items.append(news_item) |
|
|
|
return news_items |
|
|
|
except Exception as e: |
|
logger.error(f"Error fetching RSS from {source_config['url']}: {str(e)}") |
|
return [] |
|
|
|
def clean_text(self, text: str) -> str: |
|
"""Làm sạch văn bản HTML và ký tự đặc biệt""" |
|
if not text: |
|
return "" |
|
|
|
|
|
soup = BeautifulSoup(text, 'html.parser') |
|
text = soup.get_text() |
|
|
|
|
|
text = re.sub(r'\s+', ' ', text).strip() |
|
|
|
return text |
|
|
|
def get_all_sports_news(self, language: Optional[str] = None, limit: int = 20) -> List[Dict]: |
|
"""Lấy tin thể thao từ tất cả nguồn""" |
|
all_news = [] |
|
|
|
for source_name, source_config in self.sources.items(): |
|
|
|
if language and source_config.get('language') != language: |
|
continue |
|
|
|
logger.info(f"Fetching news from {source_name}") |
|
source_config['name'] = source_name |
|
|
|
if source_config['type'] == 'rss': |
|
news_items = self.get_rss_news(source_config) |
|
all_news.extend(news_items) |
|
|
|
|
|
all_news.sort(key=lambda x: x.get('published', ''), reverse=True) |
|
|
|
return all_news[:limit] |
|
|
|
def get_football_news(self, limit: int = 10) -> List[Dict]: |
|
"""Lấy tin bóng đá cụ thể""" |
|
all_news = self.get_all_sports_news(limit=50) |
|
|
|
|
|
football_keywords = [ |
|
'football', 'soccer', 'fifa', 'premier league', 'champions league', |
|
'bóng đá', 'world cup', 'euro', 'manchester', 'liverpool', 'arsenal', |
|
'real madrid', 'barcelona', 'psg', 'bayern', 'juventus' |
|
] |
|
|
|
football_news = [] |
|
for news in all_news: |
|
title_lower = news['title'].lower() |
|
summary_lower = news['summary'].lower() |
|
|
|
if any(keyword in title_lower or keyword in summary_lower |
|
for keyword in football_keywords): |
|
football_news.append(news) |
|
|
|
return football_news[:limit] |
|
|
|
def format_news_for_tts(self, news_items: List[Dict], language: str = 'vi') -> str: |
|
"""Format tin tức thành văn bản phù hợp cho TTS""" |
|
if not news_items: |
|
return "Không có tin thể thao mới nào được tìm thấy." |
|
|
|
if language == 'vi': |
|
intro = "Tin thể thao quốc tế mới nhất:\n\n" |
|
else: |
|
intro = "Latest international sports news:\n\n" |
|
|
|
formatted_text = intro |
|
|
|
for i, news in enumerate(news_items[:5], 1): |
|
title = news['title'] |
|
summary = news['summary'][:200] + "..." if len(news['summary']) > 200 else news['summary'] |
|
|
|
if language == 'vi': |
|
news_text = f"Tin thứ {i}: {title}. {summary}\n\n" |
|
else: |
|
news_text = f"News {i}: {title}. {summary}\n\n" |
|
|
|
formatted_text += news_text |
|
|
|
return formatted_text |
|
|
|
|
|
def get_sports_news_content(news_type: str = 'all', language: str = 'vi', limit: int = 5) -> str: |
|
""" |
|
Hàm chính để lấy nội dung tin thể thao |
|
|
|
Args: |
|
news_type: 'all', 'football', 'basketball', etc. |
|
language: 'vi' hoặc 'en' |
|
limit: Số lượng tin tức |
|
|
|
Returns: |
|
Chuỗi văn bản đã format cho TTS |
|
""" |
|
try: |
|
aggregator = SportsNewsAggregator() |
|
|
|
if news_type == 'football': |
|
news_items = aggregator.get_football_news(limit=limit) |
|
else: |
|
news_items = aggregator.get_all_sports_news(language=language, limit=limit) |
|
|
|
return aggregator.format_news_for_tts(news_items, language) |
|
|
|
except Exception as e: |
|
logger.error(f"Error getting sports news: {str(e)}") |
|
if language == 'vi': |
|
return f"Xin lỗi, có lỗi khi lấy tin thể thao: {str(e)}" |
|
else: |
|
return f"Sorry, error getting sports news: {str(e)}" |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
print("Testing sports news aggregator...") |
|
|
|
content = get_sports_news_content('all', 'vi', 3) |
|
print("Vietnamese sports news:") |
|
print(content) |
|
print("\n" + "="*50 + "\n") |
|
|
|
content_en = get_sports_news_content('all', 'en', 3) |
|
print("English sports news:") |
|
print(content_en) |
|
|