《AI大模型趣味实战 》第8集:多端适配 个人新闻头条 基于大模型和RSS聚合打造个人新闻电台(Flask WEB版) 2
摘要
本文末尾介绍了如何实现新闻智能体的方法。在信息爆炸的时代,如何高效获取和筛选感兴趣的新闻内容成为一个现实问题。本文将带领读者通过Python和Flask框架,结合大模型的强大能力,构建一个个性化的新闻聚合平台,不仅能够自动收集整理各类RSS源的新闻,还能以语音播报的形式提供"新闻电台"功能。我们将重点探讨如何利用AI大模型优化新闻内容提取、自动生成标签分类,以及如何通过语音合成技术实现新闻播报功能,打造一个真正实用的个人新闻助手。
项目代码仓库:https://github.com/wyg5208/rss_news_flask
以下内容接上一个博客:《AI大模型趣味实战 》第7集:多端适配 个人新闻头条 基于大模型和RSS聚合打造个人新闻电台(Flask WEB版) 1
9. 系统日志集成
为了更好地监控系统运行状态和调试问题,我们需要实现一个完善的日志系统,实现日志文件自动轮转和网页查看功能:
# 日志系统配置
LOG_DIR = 'logs'
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
# 内存缓冲区,用于在UI中显示最新日志
log_buffer = deque(maxlen=1000)
# 创建自定义的日志记录器
class MemoryHandler(logging.Handler):
"""将日志记录到内存缓冲区,用于Web界面显示"""
def emit(self, record):
log_entry = self.format(record)
log_buffer.append({
'time': datetime.datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S'),
'level': record.levelname,
'message': record.getMessage(),
'formatted': log_entry
})
# 配置日志记录器
logger = logging.getLogger('rss_app')
logger.setLevel(logging.INFO)
# 小时文件处理器,每小时自动创建一个新文件
hourly_handler = TimedRotatingFileHandler(
filename=os.path.join(LOG_DIR, 'rss_app.log'),
when='H',
interval=1,
backupCount=72, # 保留3天的日志
encoding='utf-8'
)
# 设置日志文件后缀格式为 年-月-日_小时
hourly_handler.suffix = "%Y-%m-%d_%H"
hourly_handler.setLevel(logging.INFO)
hourly_handler.setFormatter(console_format)
logger.addHandler(hourly_handler)
为了让用户能够在Web界面上查看系统日志,我们添加了相应的路由和API端点:
@app.route('/system_logs')
@login_required
def system_logs():
"""显示系统日志页面"""
logger.info('访问系统日志页面')
# 获取日志文件列表
log_files = []
try:
# 获取所有日志文件并按修改时间排序
log_pattern = os.path.join(LOG_DIR, 'rss_app.log*')
all_log_files = glob.glob(log_pattern)
all_log_files.sort(key=os.path.getmtime, reverse=True)
for file_path in all_log_files:
# 获取文件信息并添加到列表
file_name = os.path.basename(file_path)
file_stats = os.stat(file_path)
file_size = file_stats.st_size / 1024 # KB
file_time = datetime.datetime.fromtimestamp(file_stats.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
# 格式化显示名称
if file_name == 'rss_app.log':
display_name = f"当前日志 ({file_size:.1f} KB) - {file_time}"
else:
# 解析时间戳
timestamp = file_name.replace('rss_app.log.', '')
try:
parsed_time = datetime.datetime.strptime(timestamp, '%Y-%m-%d_%H')
display_name = f"{parsed_time.strftime('%Y-%m-%d %H:00')} ({file_size:.1f} KB)"
except:
display_name = f"{file_name} ({file_size:.1f} KB) - {file_time}"
log_files.append({
'name': display_name,
'path': file_path
})
except Exception as e:
logger.error(f"获取日志文件列表出错: {str(e)}")
# 统计信息
stats = {
'total': len(log_buffer),
'error': sum(1 for log in log_buffer if log['level'] == 'ERROR'),
'warning': sum(1 for log in log_buffer if log['level'] == 'WARNING'),
'files': len(log_files)
}
return render_template('system_logs.html', log_files=log_files, stats=stats)
创建系统日志页面的模板,实现实时日志显示、日志过滤和历史日志查看功能:
<!-- templates/system_logs.html -->
{% extends "base.html" %}
{% block title %}系统日志{% endblock %}
{% block content %}
<div class="container-fluid">
<h1 class="mb-4">系统日志</h1>
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">实时日志监控</h5>
<div>
<button id="refreshBtn" class="btn btn-sm btn-outline-primary">刷新</button>
<button id="clearBtn" class="btn btn-sm btn-outline-secondary">清除显示</button>
<div class="btn-group ms-2">
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown">
过滤级别
</button>
<div class="dropdown-menu">
<a class="dropdown-item log-filter active" href="#" data-level="all">全部</a>
<a class="dropdown-item log-filter" href="#" data-level="ERROR">错误</a>
<a class="dropdown-item log-filter" href="#" data-level="WARNING">警告</a>
<a class="dropdown-item log-filter" href="#" data-level="INFO">信息</a>
</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<div class="log-stats mb-3">
<span class="badge bg-primary">总计: <span id="total-count">{{ stats.total }}</span></span>
<span class="badge bg-danger">错误: <span id="error-count">{{ stats.error }}</span></span>
<span class="badge bg-warning text-dark">警告: <span id="warning-count">{{ stats.warning }}</span></span>
</div>
<div id="log-container" class="log-display">
<div class="text-center my-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-2">加载日志数据...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">日志文件 ({{ stats.files }})</h5>
</div>
<div class="card-body">
<div class="list-group">
{% for log_file in log_files %}
<a href="#" class="list-group-item list-group-item-action log-file-item" data-path="{{ log_file.path }}">
{{ log_file.name }}
</a>
{% else %}
<div class="text-center py-3">
<p class="text-muted mb-0">没有找到日志文件</p>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<!-- 日志文件查看模态框 -->
<div class="modal fade" id="logFileModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="logFileTitle">日志文件</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<pre id="logFileContent" class="log-file-content">加载中...</pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// 日志系统交互JavaScript代码
$(document).ready(function() {
// 当前过滤级别
let currentLevel = 'all';
// 加载日志数据
function loadLogs() {
$.getJSON('/api/logs', { level: currentLevel }, function(data) {
const logs = data.logs;
const stats = data.stats;
// 更新统计信息
$('#total-count').text(stats.total);
$('#error-count').text(stats.error);
$('#warning-count').text(stats.warning);
// 清空并填充日志容器
const container = $('#log-container');
container.empty();
if (logs.length === 0) {
container.html('<p class="text-center text-muted my-5">没有日志记录</p>');
return;
}
// 创建日志表格
const table = $('<table class="table table-sm table-hover log-table"></table>');
const tbody = $('<tbody></tbody>');
// 添加日志行
logs.forEach(log => {
const row = $('<tr></tr>');
// 根据日志级别设置行样式
if (log.level === 'ERROR') {
row.addClass('table-danger');
} else if (log.level === 'WARNING') {
row.addClass('table-warning');
}
row.append(`<td class="log-time">${log.time}</td>`);
row.append(`<td class="log-level">${log.level}</td>`);
row.append(`<td class="log-message">${log.message}</td>`);
tbody.append(row);
});
table.append(tbody);
container.append(table);
// 滚动到底部
container.scrollTop(container[0].scrollHeight);
});
}
// 初始加载日志
loadLogs();
// 刷新按钮点击事件
$('#refreshBtn').click(function() {
loadLogs();
});
// 清除按钮点击事件
$('#clearBtn').click(function() {
$('#log-container').empty();
});
// 日志级别过滤器点击事件
$('.log-filter').click(function(e) {
e.preventDefault();
// 更新选中状态
$('.log-filter').removeClass('active');
$(this).addClass('active');
// 设置当前级别并重新加载
currentLevel = $(this).data('level');
loadLogs();
});
// 日志文件项点击事件
$('.log-file-item').click(function(e) {
e.preventDefault();
const path = $(this).data('path');
const name = $(this).text();
// 设置模态框标题
$('#logFileTitle').text(name);
$('#logFileContent').text('加载中...');
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('logFileModal'));
modal.show();
// 加载日志文件内容
$.getJSON('/api/logs/file', { file_path: path }, function(data) {
if (data.status === 'success') {
$('#logFileContent').text(data.content);
} else {
$('#logFileContent').text('加载失败: ' + data.message);
}
});
});
// 自动刷新(每10秒)
setInterval(loadLogs, 10000);
});
</script>
{% endblock %}
通过实现这些功能,我们的系统可以自动记录所有关键操作和错误信息,用户可以实时查看系统状态和历史日志,便于问题诊断和监控。
10. 移动设备优化与兼容性处理
针对移动设备访问,我们需要优化用户界面和交互体验,特别是在移动浏览器上的按钮功能:
// static/modal_fix.js
document.addEventListener('DOMContentLoaded', function() {
console.log('modal_fix.js 已加载');
// 检查jQuery和Bootstrap是否加载
if (typeof jQuery === 'undefined') {
console.error('jQuery 未加载!');
return;
}
if (typeof bootstrap === 'undefined') {
console.error('Bootstrap 未加载!');
return;
}
console.log('jQuery和Bootstrap已正确加载');
// 在新闻详情页初始化
initNewsDetailPage();
function initNewsDetailPage() {
// 检查是否是新闻详情页
if (!document.getElementById('news-content')) {
return;
}
console.log('初始化新闻详情页面');
// 初始化模态框
const shareModal = new bootstrap.Modal(document.getElementById('shareModal'), {
keyboard: true
});
const helpModal = new bootstrap.Modal(document.getElementById('helpModal'), {
keyboard: true
});
// 重新绑定按钮事件
$('#btnShare').off('click').on('click', function() {
console.log('分享按钮被点击');
shareModal.show();
});
$('#btnHelp').off('click').on('click', function() {
console.log('帮助按钮被点击');
helpModal.show();
});
// 语音朗读功能
$('#btnSpeak').off('click').on('click', function() {
try {
console.log('朗读按钮被点击');
const title = document.getElementById('news-title').innerText;
const content = document.getElementById('news-content').innerText;
console.log(`准备朗读,标题长度: ${title.length}, 内容长度: ${content.length}`);
// 组合完整的朗读文本
const fullText = title + '。' + content;
// 尝试使用Web Speech API
if ('speechSynthesis' in window) {
console.log('使用Web Speech API朗读');
// 创建语音对象
const speech = new SpeechSynthesisUtterance();
speech.text = fullText;
speech.lang = 'zh-CN';
speech.rate = 1.0; // 语速
speech.pitch = 1.0; // 音调
speech.volume = 1.0; // 音量
// 开始朗读
window.speechSynthesis.speak(speech);
} else {
console.log('Web Speech API不可用,使用后端API');
// 使用后端API生成语音
$.ajax({
url: '/api/text_to_speech',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ text: fullText }),
success: function(response) {
if (response.status === 'success') {
console.log('语音生成成功,URL:', response.audio_url);
// 创建音频元素播放
const audio = new Audio(response.audio_url);
audio.play();
} else {
console.error('语音生成失败:', response.message);
alert('语音生成失败: ' + response.message);
}
},
error: function(xhr, status, error) {
console.error('API请求失败:', error);
alert('无法连接到语音服务: ' + error);
}
});
}
} catch (e) {
console.error('朗读过程出错:', e);
alert('朗读功能出错: ' + e.message);
}
});
// 复制内容功能
$('#btnCopy').off('click').on('click', function() {
try {
console.log('复制按钮被点击');
const title = document.getElementById('news-title').innerText;
const description = document.getElementById('news-description').innerText;
const content = document.getElementById('news-content').innerText;
// 组合要复制的文本
const textToCopy = `${title}\n\n${description}\n\n${content}`;
// 使用剪贴板API
navigator.clipboard.writeText(textToCopy).then(function() {
console.log('内容已复制到剪贴板');
alert('内容已复制到剪贴板');
}).catch(function(err) {
console.error('剪贴板操作失败:', err);
alert('复制失败: ' + err.message);
});
} catch (e) {
console.error('复制过程出错:', e);
alert('复制功能出错: ' + e.message);
}
});
// 复制链接按钮
$('#copyLinkBtn').off('click').on('click', function() {
try {
const currentUrl = window.location.href;
navigator.clipboard.writeText(currentUrl).then(function() {
alert('链接已复制到剪贴板');
}).catch(function(err) {
console.error('复制链接失败:', err);
alert('复制链接失败: ' + err.message);
});
} catch (e) {
console.error('复制链接过程出错:', e);
alert('复制链接功能出错: ' + e.message);
}
});
console.log('新闻详情页面按钮事件已重新绑定');
}
});
在HTML模板中引入这个修复脚本:
<!-- templates/news_detail.html -->
{% extends "base.html" %}
{% block title %}{{ news.title }}{% endblock %}
{% block content %}
<div class="container">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('dashboard') }}">首页</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('news_list') }}">新闻列表</a></li>
<li class="breadcrumb-item active" aria-current="page">新闻详情</li>
</ol>
</nav>
<div class="card">
<div class="card-header">
<h1 id="news-title" class="h3">{{ news.title }}</h1>
<div class="mt-2 text-muted">
<small>来源: {{ news.source }}</small>
{% if news.pub_date %}
<small class="ms-3">发布时间: {{ news.pub_date.strftime('%Y-%m-%d %H:%M') }}</small>
{% endif %}
<small class="ms-3">添加时间: {{ news.add_date.strftime('%Y-%m-%d %H:%M') }}</small>
</div>
</div>
<div class="card-body">
{% if news.description %}
<div id="news-description" class="lead mb-4">{{ news.description }}</div>
{% endif %}
<div class="mb-3">
<div class="btn-group" role="group">
<button id="btnSpeak" class="btn btn-outline-primary" type="button">
<i class="fas fa-volume-up"></i> 朗读
</button>
<button id="btnCopy" class="btn btn-outline-secondary" type="button">
<i class="fas fa-copy"></i> 复制内容
</button>
<button id="btnShare" class="btn btn-outline-success" type="button">
<i class="fas fa-share-alt"></i> 分享
</button>
<button id="btnHelp" class="btn btn-outline-info" type="button">
<i class="fas fa-question-circle"></i> 帮助
</button>
</div>
</div>
<div id="news-content" class="news-content">
{{ news.content|safe }}
</div>
{% if news.tags %}
<div class="mt-4">
<h5>标签</h5>
<div class="news-tags">
{% for tag in news.tags %}
<a href="{{ url_for('news_list', tag='{{ tag.name }}') }}" class="badge bg-primary text-decoration-none">{{ tag.name }}</a>
{% endfor %}
</div>
</div>
{% endif %}
{% if news.link %}
<div class="mt-4">
<a href="{{ news.link }}" target="_blank" class="btn btn-sm btn-outline-dark">
<i class="fas fa-external-link-alt"></i> 查看原文
</a>
</div>
{% endif %}
</div>
</div>
</div>
<!-- 分享模态框 -->
<div class="modal fade" id="shareModal" tabindex="-1" aria-labelledby="shareModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="shareModalLabel">分享此新闻</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>复制以下链接分享:</p>
<div class="input-group">
<input type="text" class="form-control" id="shareLink" value="{{ request.url }}" readonly>
<button class="btn btn-outline-secondary" type="button" id="copyLinkBtn">复制</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- 帮助模态框 -->
<div class="modal fade" id="helpModal" tabindex="-1" aria-labelledby="helpModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="helpModalLabel">功能帮助</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<h6><i class="fas fa-volume-up"></i> 朗读</h6>
<p>使用语音朗读当前新闻内容,支持中文朗读。</p>
</div>
<div class="mb-3">
<h6><i class="fas fa-copy"></i> 复制内容</h6>
<p>将新闻标题、描述和正文内容复制到剪贴板。</p>
</div>
<div class="mb-3">
<h6><i class="fas fa-share-alt"></i> 分享</h6>
<p>获取当前新闻的链接,以便分享给他人。</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='speech.js') }}"></script>
<script src="{{ url_for('static', filename='modal_fix.js') }}"></script>
{% endblock %}
通过这些优化,我们解决了移动设备上按钮不响应的问题,并提高了用户交互体验。
总结与扩展思考
项目梳理
在这个项目中,我们成功构建了一个功能完整的个人新闻聚合平台,它具有以下核心功能:
-
RSS源管理与内容抓取:支持添加、删除和管理多个RSS源,自动获取最新新闻内容。
-
大模型内容优化:使用大语言模型(GLM4)对抓取的内容进行智能处理,提取关键内容并去除无关元素。
-
自动标签生成:基于大模型分析新闻内容,自动生成相关标签,便于内容分类和检索。
-
语音合成与播报:支持将新闻内容转换为语音,实现类似广播的新闻播报功能。
-
定时任务系统:实现新闻自动抓取和定时播报功能,减少手动操作。
-
系统日志与监控:完善的日志系统,支持Web界面查看系统运行状态和历史日志。
-
移动设备适配:优化移动端用户体验,确保在各种设备上都能正常使用。
通过这个项目,我们展示了如何将大语言模型应用于实际应用场景,利用其强大的文本理解与生成能力提升应用的智能化水平。同时,项目也演示了从数据获取、处理、存储到展示的完整流程,涵盖了Web开发的各个方面。
扩展思考
- 个性化推荐系统
基于用户的阅读历史和标签偏好,我们可以实现个性化的新闻推荐功能。这可以通过分析用户与标签的交互行为,构建用户画像,然后使用协同过滤或内容匹配算法实现。
def get_recommended_news(user_id, limit=10):
"""获取给用户推荐的新闻"""
# 获取用户标签偏好
user_preferences = UserTagPreference.query.filter_by(user_id=user_id).all()
preferred_tags = [pref.tag.name for pref in user_preferences]
# 获取含有这些标签的近期新闻
recommended_news = []
if preferred_tags:
# 基于标签匹配的推荐
tagged_news = db.session.query(News)\
.join(Tag, News.id == Tag.news_id)\
.filter(Tag.name.in_(preferred_tags))\
.order_by(News.add_date.desc())\
.limit(limit*2)\
.all()
recommended_news.extend(tagged_news)
# 如果推荐数量不足,添加最新新闻
if len(recommended_news) < limit:
recent_news = News.query.order_by(News.add_date.desc())\
.limit(limit - len(recommended_news))\
.all()
recommended_news.extend(recent_news)
# 去重并限制数量
unique_news = []
news_ids = set()
for news in recommended_news:
if news.id not in news_ids and len(unique_news) < limit:
news_ids.add(news.id)
unique_news.append(news)
return unique_news
- 情感分析与分类
使用大模型进行新闻的情感分析与主题分类,为用户提供更多维度的内容筛选。
def analyze_news_sentiment(news):
"""分析新闻情感倾向"""
try:
prompt = f"""
分析以下新闻文章的情感倾向,返回一个值:
正面 - 积极、乐观的内容
中性 - 客观、中立的报道
负面 - 消极、悲观的内容
只返回一个词:正面、
## 扩展思考
### 1. 多模态内容处理
当前项目主要处理文本内容,但现代新闻往往包含图片、视频等多模态内容。我们可以扩展系统以支持这些内容:
```python
def extract_images_from_content(content):
"""从HTML内容中提取图片链接"""
try:
soup = BeautifulSoup(content, 'html.parser')
images = []
# 查找所有图片标签
for img in soup.find_all('img'):
src = img.get('src')
if src and not src.startswith('data:'): # 排除base64编码的图片
images.append({
'url': src,
'alt': img.get('alt', ''),
'width': img.get('width', ''),
'height': img.get('height', '')
})
return images
except Exception as e:
logger.error(f"提取图片时出错: {e}")
return []
def analyze_images_with_llm(images):
"""使用大模型分析图片内容"""
if not images:
return []
try:
# 构建prompt
image_urls = [img['url'] for img in images]
prompt = f"""
分析以下新闻图片链接,提供每张图片可能的内容描述。
不需要访问链接,仅根据URL和alt文本推测内容。
图片链接:
{json.dumps(image_urls, indent=2)}
"""
# 调用Ollama API
response = ollama.chat(model='glm4', messages=[
{
'role': 'user',
'content': prompt
}
])
return response['message']['content']
except Exception as e:
logger.error(f"分析图片时出错: {e}")
return "无法分析图片内容"
2. 个性化推荐系统
基于用户阅读历史和兴趣标签,实现智能推荐功能:
class UserReadHistory(db.Model):
"""用户阅读历史模型"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
news_id = db.Column(db.Integer, db.ForeignKey('news.id'))
read_time = db.Column(db.DateTime, default=datetime.datetime.now)
read_duration = db.Column(db.Integer, default=0) # 阅读时长(秒)
user = db.relationship('User', backref=db.backref('read_history', lazy=True))
news = db.relationship('News', backref=db.backref('read_by', lazy=True))
def get_personalized_recommendations(user_id, limit=10):
"""为用户生成个性化推荐"""
# 1. 获取用户感兴趣的标签
user_tags = db.session.query(TagLibrary.name)\
.join(UserTagPreference, UserTagPreference.tag_id == TagLibrary.id)\
.filter(UserTagPreference.user_id == user_id)\
.all()
user_tags = [t[0] for t in user_tags]
# 2. 获取用户最近阅读的新闻中的标签
recent_reads = db.session.query(News)\
.join(UserReadHistory, UserReadHistory.news_id == News.id)\
.filter(UserReadHistory.user_id == user_id)\
.order_by(UserReadHistory.read_time.desc())\
.limit(20)\
.all()
recent_news_ids = [n.id for n in recent_reads]
recent_tags = db.session.query(Tag.name)\
.filter(Tag.news_id.in_(recent_news_ids))\
.group_by(Tag.name)\
.all()
recent_tags = [t[0] for t in recent_tags]
# 3. 合并兴趣标签和最近阅读标签
all_tags = set(user_tags + recent_tags)
# 4. 查找包含这些标签的新闻,但用户尚未阅读
if all_tags:
recommended_news = db.session.query(News)\
.join(Tag, Tag.news_id == News.id)\
.filter(Tag.name.in_(all_tags))\
.filter(~News.id.in_(recent_news_ids))\
.order_by(News.add_date.desc())\
.limit(limit)\
.all()
return recommended_news
else:
# 如果没有标签信息,返回最新新闻
return News.query.order_by(News.add_date.desc()).limit(limit).all()
3. 社交分享与交互功能
增加社交功能,让用户能分享和讨论新闻内容:
class Comment(db.Model):
"""新闻评论模型"""
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
news_id = db.Column(db.Integer, db.ForeignKey('news.id'))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
created_at = db.Column(db.DateTime, default=datetime.datetime.now)
news = db.relationship('News', backref=db.backref('comments', lazy=True))
user = db.relationship('User', backref=db.backref('comments', lazy=True))
@app.route('/news/<int:news_id>/comment', methods=['POST'])
@login_required
def add_comment(news_id):
"""添加评论"""
content = request.form.get('content', '').strip()
if not content:
flash('评论内容不能为空', 'warning')
return redirect(url_for('news_detail', news_id=news_id))
# 使用大模型检测不良内容
is_appropriate = check_content_appropriate(content)
if not is_appropriate:
flash('评论内容不适当,请修改后重试', 'danger')
return redirect(url_for('news_detail', news_id=news_id))
# 创建评论
comment = Comment(
content=content,
news_id=news_id,
user_id=current_user.id
)
try:
db.session.add(comment)
db.session.commit()
flash('评论发布成功', 'success')
except Exception as e:
db.session.rollback()
logger.error(f"添加评论出错: {e}")
flash('评论发布失败,请稍后重试', 'danger')
return redirect(url_for('news_detail', news_id=news_id))
def check_content_appropriate(content):
"""使用大模型检查内容是否适当"""
try:
prompt = f"""
判断以下评论内容是否适当,不包含侮辱、歧视、暴力或政治敏感内容。
只回答"适当"或"不适当"。
评论内容: {content}
"""
# 调用Ollama API
response = ollama.chat(model='glm4', messages=[
{
'role': 'user',
'content': prompt
}
])
result = response['message']['content'].strip().lower()
return '适当' in result
except Exception as e:
logger.error(f"检查内容适当性时出错: {e}")
return True # 出错时默认允许
4. 语音交互与语音助手功能
将系统拓展为完整的语音助手,支持语音命令控制:
@app.route('/api/speech_command', methods=['POST'])
@login_required
def process_speech_command():
"""处理语音命令"""
try:
data = request.get_json()
if not data or 'command' not in data:
return jsonify({'status': 'error', 'message': '缺少命令参数'})
command = data['command']
logger.info(f"接收到语音命令: {command}")
# 使用大模型解析命令
parsed_command = parse_command_with_llm(command)
logger.info(f"解析后的命令: {parsed_command}")
# 执行相应操作
if parsed_command['type'] == 'read_news':
# 获取特定标签或最新的新闻
if parsed_command.get('tag'):
news_list = get_news_by_tag(parsed_command['tag'], parsed_command.get('count', 3))
else:
news_list = News.query.order_by(News.add_date.desc()).limit(parsed_command.get('count', 3)).all()
# 生成语音
news_text = "为您播报以下新闻:\n\n"
for i, news in enumerate(news_list):
news_text += f"第{i+1}条:{news.title}\n{news.description or ''}\n\n"
# 调用语音合成API
result = generate_speech_response(news_text)
return jsonify(result)
elif parsed_command['type'] == 'search_news':
# 搜索新闻
keyword = parsed_command.get('keyword', '')
news_list = News.query.filter(News.title.contains(keyword) | News.description.contains(keyword))\
.order_by(News.add_date.desc())\
.limit(5)\
.all()
if news_list:
news_text = f"找到{len(news_list)}条关于"{keyword}"的新闻:\n\n"
for i, news in enumerate(news_list):
news_text += f"第{i+1}条:{news.title}\n"
result = generate_speech_response(news_text)
return jsonify(result)
else:
return jsonify({
'status': 'success',
'message': f'没有找到关于"{keyword}"的新闻',
'audio_url': None
})
else:
return jsonify({
'status': 'error',
'message': '无法识别的命令类型'
})
except Exception as e:
logger.error(f"处理语音命令时出错: {e}")
return jsonify({
'status': 'error',
'message': str(e)
})
def parse_command_with_llm(command):
"""使用大模型解析语音命令"""
try:
prompt = f"""
解析以下语音命令,提取出命令类型和参数。返回JSON格式。
支持的命令类型:
1. read_news: 朗读新闻(可能包含标签和数量)
2. search_news: 搜索新闻(包含关键词)
3. system_status: 查询系统状态
例如:
- "给我读3条最新的科技新闻" -> {{"type": "read_news", "tag": "科技", "count": 3}}
- "搜索关于人工智能的新闻" -> {{"type": "search_news", "keyword": "人工智能"}}
- "查询系统状态" -> {{"type": "system_status"}}
命令: {command}
"""
# 调用Ollama API
response = ollama.chat(model='glm4', messages=[
{
'role': 'user',
'content': prompt
}
])
# 尝试解析JSON响应
content = response['message']['content']
# 提取JSON部分
json_match = re.search(r'\{.*\}', content, re.DOTALL)
if json_match:
json_str = json_match.group(0)
return json.loads(json_str)
else:
# 默认返回
return {'type': 'unknown'}
except Exception as e:
logger.error(f"解析语音命令时出错: {e}")
return {'type': 'error', 'message': str(e)}
5. 多语言支持与翻译功能
添加多语言支持,使系统能够抓取、翻译和展示不同语言的新闻:
class News(db.Model):
# ... 现有字段 ...
language = db.Column(db.String(10), default='zh-cn') # 新增字段
translated_title = db.Column(db.String(500))
translated_description = db.Column(db.Text)
translated_content = db.Column(db.Text)
def detect_language(text):
"""检测文本语言"""
try:
prompt = f"""
请识别以下文本的语言,并返回相应的语言代码,如:
- 中文:zh-cn
- 英文:en
- 日文:ja
- 俄文:ru
等等。只返回语言代码,不需要其他解释。
文本:
{text[:200]}
"""
# 调用Ollama API
response = ollama.chat(model='glm4', messages=[
{
'role': 'user',
'content': prompt
}
])
language_code = response['message']['content'].strip().lower()
return language_code
except Exception as e:
logger.error(f"检测语言时出错: {e}")
return 'zh-cn' # 默认中文
def translate_with_llm(text, from_lang, to_lang='zh-cn'):
"""使用大模型翻译文本"""
if not text or from_lang == to_lang:
return text
try:
prompt = f"""
请将以下{from_lang}文本翻译成{to_lang},保持原意,注意专业术语的准确性:
原文:
{text[:5000]}
只返回翻译结果,不需要添加解释。
"""
# 调用Ollama API
response = ollama.chat(model='glm4', messages=[
{
'role': 'user',
'content': prompt
}
])
translated_text = response['message']['content']
return translated_text
except Exception as e:
logger.error(f"翻译文本时出错: {e}")
return text
6. 智能摘要与内容浓缩
为长篇新闻生成简明摘要,方便用户快速了解内容:
def generate_summary_for_news(news, max_length=200):
"""为新闻生成摘要"""
try:
# 获取新闻正文
content_text = ""
if news.content:
soup = BeautifulSoup(news.content, 'html.parser')
content_text = soup.get_text(separator=' ', strip=True)
# 如果没有内容或内容太短,使用描述
if not content_text or len(content_text) < 100:
if news.description:
content_text = news.description
# 如果还是没有内容,返回标题
if not content_text:
return news.title
# 使用大模型生成摘要
prompt = f"""
为以下新闻生成一个简洁的摘要,不超过{max_length}个字符:
标题:{news.title}
内容:{content_text[:3000]}
只返回摘要,不要添加任何解释。
"""
# 调用Ollama API
response = ollama.chat(model='glm4', messages=[
{
'role': 'user',
'content': prompt
}
])
summary = response['message']['content'].strip()
return summary
except Exception as e:
logger.error(f"生成摘要时出错: {e}")
# 如果出错,返回原始描述或截断的内容
if news.description:
return news.description[:max_length] + ('...' if len(news.description) > max_length else '')
return content_text[:max_length] + ('...' if len(content_text) > max_length else '')
7. 数据分析与可视化
添加数据分析功能,生成新闻趋势报告和可视化图表:
@app.route('/analytics')
@login_required
def analytics_dashboard():
"""数据分析仪表板"""
# 获取时间范围
days = request.args.get('days', 30, type=int)
start_date = datetime.datetime.now() - datetime.timedelta(days=days)
# 获取每日新闻数量
daily_news_counts = db.session.query(
func.date(News.add_date).label('date'),
func.count().label('count')
).filter(News.add_date >= start_date).group_by(func.date(News.add_date)).all()
# 转换为图表数据格式
dates = [item.date for item in daily_news_counts]
counts = [item.count for item in daily_news_counts]
# 获取热门标签
popular_tags = db.session.query(
Tag.name,
func.count().label('count')
).filter(
Tag.news_id == News.id,
News.add_date >= start_date
).group_by(Tag.name).order_by(func.count().desc()).limit(20).all()
tag_names = [item.name for item in popular_tags]
tag_counts = [item.count for item in popular_tags]
# 生成热门话题分析
topics_analysis = generate_topics_analysis(start_date)
return render_template('analytics.html',
days=days,
dates=dates,
counts=counts,
tag_names=tag_names,
tag_counts=tag_counts,
topics_analysis=topics_analysis)
def generate_topics_analysis(start_date):
"""生成热门话题分析"""
# 获取期间的所有新闻
recent_news = News.query.filter(News.add_date >= start_date).all()
# 提取所有标题
titles = [news.title for news in recent_news]
# 使用大模型分析热门话题
if titles:
try:
prompt = f"""
分析以下{len(titles)}条新闻标题,识别出5个主要热门话题,并对每个话题进行简要分析。
返回JSON格式,每个话题包含名称、相关新闻数量和简短描述。
新闻标题:
{json.dumps(titles[:500], ensure_ascii=False, indent=2)}
返回格式示例:
[
{{"topic": "人工智能", "count": 15, "description": "主要集中在AI在医疗领域的应用,以及GPT-4的发布"}},
...
]
"""
# 调用Ollama API
response = ollama.chat(model='glm4', messages=[
{
'role': 'user',
'content': prompt
}
])
# 提取JSON部分
content = response['message']['content']
json_match = re.search(r'\[.*\]', content, re.DOTALL)
if json_match:
json_str = json_match.group(0)
return json.loads(json_str)
except Exception as e:
logger.error(f"生成热门话题分析时出错: {e}")
return []
总结
通过本项目,我们探索了如何将大语言模型与传统Web开发技术结合,打造一个智能化的新闻聚合平台。这种结合不仅增强了用户体验,也展示了AI在实际应用场景中的巨大潜力。
项目实现了以下核心功能:
- 基于RSS的多源新闻自动抓取与管理
- 使用大模型优化内容提取与标签生成
- 语音合成与新闻播报功能
- 完善的日志系统与系统监控
- 移动设备适配与跨平台兼容
扩展思考中,我们进一步探讨了多模态内容处理、个性化推荐、社交互动、语音助手、多语言支持、智能摘要以及数据分析等更高级功能。这些拓展方向展示了如何将这个基础项目发展成一个更全面、智能的信息服务平台。
从技术角度看,本项目不仅是对Flask、SQLAlchemy等传统Web开发框架的应用,更重要的是展示了如何将新兴的AI技术(如大语言模型)无缝集成到现有系统中,实现传统技术难以达成的智能功能。
对于开发者而言,这个项目提供了一个完整的参考案例,展示了从需求分析、系统设计、功能实现到优化升级的全流程,特别适合那些希望将AI能力融入自己Web应用的开发者学习和借鉴。
人工智能的快速发展正在重塑软件开发的方式和可能性,本项目只是展示了其中的一小部分潜力。未来,随着模型能力的提升和应用场景的拓展,AI驱动的软件将变得更加智能、自然和个性化,为用户创造更大的价值。