from flask import Flask, render_template, request, jsonify, session, redirect, url_for, flash
import re
import random
import json
from datetime import datetime, timedelta
import os
import logging
from functools import wraps
from typing import List, Dict, Any, Optional
import sqlite3
from contextlib import contextmanager
import bleach # 用于HTML清理
from werkzeug.security import check_password_hash, generate_password_hash
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, TextAreaField, SelectField
from wtforms.validators import DataRequired, Length, Optional as WTFOptional, Regexp
import hashlib
import secrets
from functools import lru_cache
app = Flask(__name__)
app.secret_key = secrets.token_hex(32) # 使用安全的随机密钥
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class SecureForm(FlaskForm):
"""安全表单基类"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def validate(self):
# 验证CSRF令牌
if not super().validate():
return False
return True
class DatabaseManager:
"""数据库管理器,使用SQLite优化性能"""
def __init__(self, db_path='poems.db'):
self.db_path = db_path
self.init_db()
def init_db(self):
"""初始化数据库"""
with self.get_db_connection() as conn:
# 创建诗词表
conn.execute('''
CREATE TABLE IF NOT EXISTS poems (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT NOT NULL,
content TEXT NOT NULL,
type TEXT,
tags TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建用户表
conn.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
email TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建收藏表
conn.execute('''
CREATE TABLE IF NOT EXISTS favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
poem_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id),
FOREIGN KEY (poem_id) REFERENCES poems (id),
UNIQUE(user_id, poem_id)
)
''')
# 创建评论表
conn.execute('''
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
poem_id INTEGER NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id),
FOREIGN KEY (poem_id) REFERENCES poems (id)
)
''')
# 创建索引以优化查询性能
conn.execute('CREATE INDEX IF NOT EXISTS idx_poems_author ON poems(author)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_poems_type ON poems(type)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_poems_tags ON poems(tags)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_favorites_user_poem ON favorites(user_id, poem_id)')
conn.commit()
# 检查是否有数据
cursor = conn.execute('SELECT COUNT(*) FROM poems')
count = cursor.fetchone()[0]
if count == 0:
# 如果数据库为空,从文件加载数据
self.load_poems_from_file()
@contextmanager
def get_db_connection(self):
"""获取数据库连接的上下文管理器"""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row # 使结果可以像字典一样访问
conn.execute('PRAGMA foreign_keys = ON') # 启用外键约束
try:
yield conn
finally:
conn.close()
def load_poems_from_file(self):
"""从文件加载诗词到数据库"""
poems = []
try:
with open('唐诗三百首(cn_en).txt', 'r', encoding='utf-8') as f:
content = f.read()
# 使用正则表达式分割每首诗
poem_blocks = re.split(r'\n\n+', content)
for block in poem_blocks:
if block.strip():
lines = block.strip().split('\n')
if len(lines) >= 4:
# 提取诗名和作者
title_line = lines[0]
author_line = lines[1]
# 提取中文诗句
chinese_poem = []
for i in range(2, len(lines)):
line = lines[i].strip()
if line and not line.islower() and not any(c in line for c in ['five', 'seven', 'character', 'ancient', 'verse', 'folk-song', 'regular', 'quatrain']):
chinese_poem.append(line)
if len(chinese_poem) >= 2:
poem_text = '\n'.join(chinese_poem)
poem_type = self.extract_poem_type(title_line)
tags = self.extract_tags(title_line)
poems.append({
'title': title_line,
'author': author_line,
'content': poem_text,
'type': poem_type,
'tags': ','.join(tags)
})
except FileNotFoundError:
logger.warning("诗词文件未找到,使用示例数据")
poems = self.create_sample_data()
except Exception as e:
logger.error(f"加载诗词文件时出错: {str(e)}")
poems = self.create_sample_data()
# 插入数据库
with self.get_db_connection() as conn:
conn.executemany('''
INSERT INTO poems (title, author, content, type, tags)
VALUES (?, ?, ?, ?, ?)
''', [(p['title'], p['author'], p['content'], p['type'], p['tags']) for p in poems])
conn.commit()
def extract_poem_type(self, title: str) -> str:
"""根据标题判断诗歌类型"""
if '五言' in title:
if '绝句' in title:
return '五言绝句'
elif '律诗' in title:
return '五言律诗'
else:
return '五言古诗'
elif '七言' in title:
if '绝句' in title:
return '七言绝句'
elif '律诗' in title:
return '七言律诗'
else:
return '七言古诗'
elif '乐府' in title:
return '乐府'
else:
return '古诗'
def extract_tags(self, title: str) -> List[str]:
"""从标题中提取关键词标签"""
tags = []
if '月' in title:
tags.append('月亮')
if '春' in title or '夏' in title or '秋' in title or '冬' in title:
tags.append('季节')
if '山' in title or '水' in title or '江' in title or '河' in title:
tags.append('山水')
if '思' in title or '念' in title or '怀' in title:
tags.append('思念')
if '别' in title or '送' in title:
tags.append('离别')
return tags if tags else ['其他']
def create_sample_data(self) -> List[Dict[str, Any]]:
"""创建示例数据"""
return [
{
'title': '静夜思',
'author': '李白',
'content': '床前明月光,疑是地上霜。\n举头望明月,低头思故乡。',
'type': '五言绝句',
'tags': '月亮,思乡,夜晚'
},
{
'title': '春晓',
'author': '孟浩然',
'content': '春眠不觉晓,处处闻啼鸟。\n夜来风雨声,花落知多少。',
'type': '五言绝句',
'tags': '春天,自然,早晨'
},
{
'title': '登鹳雀楼',
'author': '王之涣',
'content': '白日依山尽,黄河入海流。\n欲穷千里目,更上一层楼。',
'type': '五言绝句',
'tags': '励志,哲理,景色'
},
{
'title': '望庐山瀑布',
'author': '李白',
'content': '日照香炉生紫烟,遥看瀑布挂前川。\n飞流直下三千尺,疑是银河落九天。',
'type': '七言绝句',
'tags': '瀑布,景色,夸张'
},
{
'title': '黄鹤楼送孟浩然之广陵',
'author': '李白',
'content': '故人西辞黄鹤楼,烟花三月下扬州。\n孤帆远影碧空尽,唯见长江天际流。',
'type': '七言绝句',
'tags': '送别,友情,景色'
}
]
def get_all_poems(self, page: int = 1, per_page: int = 12) -> tuple:
"""获取所有诗词(分页)"""
# 验证参数
if page < 1 or per_page < 1:
raise ValueError("分页参数必须大于0")
offset = (page - 1) * per_page
with self.get_db_connection() as conn:
cursor = conn.execute('''
SELECT * FROM poems
ORDER BY created_at DESC
LIMIT ? OFFSET ?
''', (per_page, offset))
poems = [dict(row) for row in cursor.fetchall()]
# 获取总数
cursor = conn.execute('SELECT COUNT(*) FROM poems')
total_count = cursor.fetchone()[0]
total_pages = (total_count + per_page - 1) // per_page
return poems, total_pages
def search_poems(self, query: str, page: int = 1, per_page: int = 12) -> tuple:
"""搜索诗词(分页)"""
if not query:
return self.get_all_poems(page, per_page)
# 验证参数
if page < 1 or per_page < 1:
raise ValueError("分页参数必须大于0")
# 清理查询字符串以防止SQL注入
clean_query = self.sanitize_input(query)
offset = (page - 1) * per_page
search_term = f'%{clean_query}%'
with self.get_db_connection() as conn:
cursor = conn.execute('''
SELECT * FROM poems
WHERE title LIKE ? OR author LIKE ? OR content LIKE ? OR tags LIKE ?
ORDER BY CASE
WHEN title LIKE ? THEN 1
WHEN author LIKE ? THEN 2
WHEN content LIKE ? THEN 3
ELSE 4
END
LIMIT ? OFFSET ?
''', (search_term, search_term, search_term, search_term,
search_term, search_term, search_term, per_page, offset))
poems = [dict(row) for row in cursor.fetchall()]
# 获取总数
cursor = conn.execute('''
SELECT COUNT(*) FROM poems
WHERE title LIKE ? OR author LIKE ? OR content LIKE ? OR tags LIKE ?
''', (search_term, search_term, search_term, search_term))
total_count = cursor.fetchone()[0]
total_pages = (total_count + per_page - 1) // per_page
return poems, total_pages
def get_poem_by_id(self, poem_id: int) -> Optional[Dict[str, Any]]:
"""根据ID获取诗词"""
# 验证ID类型和范围
if not isinstance(poem_id, int) or poem_id < 1:
raise ValueError("无效的诗词ID")
with self.get_db_connection() as conn:
cursor = conn.execute('SELECT * FROM poems WHERE id = ?', (poem_id,))
row = cursor.fetchone()
return dict(row) if row else None
def get_poems_by_author(self, author: str, page: int = 1, per_page: int = 12) -> tuple:
"""根据作者获取诗词(分页)"""
# 验证参数
if page < 1 or per_page < 1:
raise ValueError("分页参数必须大于0")
# 清理输入以防止SQL注入
clean_author = self.sanitize_input(author)
offset = (page - 1) * per_page
with self.get_db_connection() as conn:
cursor = conn.execute('''
SELECT * FROM poems
WHERE author LIKE ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
''', (f'%{clean_author}%', per_page, offset))
poems = [dict(row) for row in cursor.fetchall()]
# 获取总数
cursor = conn.execute('SELECT COUNT(*) FROM poems WHERE author LIKE ?', (f'%{clean_author}%',))
total_count = cursor.fetchone()[0]
total_pages = (total_count + per_page - 1) // per_page
return poems, total_pages
def get_poems_by_type(self, poem_type: str, page: int = 1, per_page: int = 12) -> tuple:
"""根据类型获取诗词(分页)"""
# 验证参数
if page < 1 or per_page < 1:
raise ValueError("分页参数必须大于0")
# 清理输入以防止SQL注入
clean_type = self.sanitize_input(poem_type)
offset = (page - 1) * per_page
with self.get_db_connection() as conn:
cursor = conn.execute('''
SELECT * FROM poems
WHERE type = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
''', (clean_type, per_page, offset))
poems = [dict(row) for row in cursor.fetchall()]
# 获取总数
cursor = conn.execute('SELECT COUNT(*) FROM poems WHERE type = ?', (clean_type,))
total_count = cursor.fetchone()[0]
total_pages = (total_count + per_page - 1) // per_page
return poems, total_pages
def get_poems_by_tag(self, tag: str, page: int = 1, per_page: int = 12) -> tuple:
"""根据标签获取诗词(分页)"""
# 验证参数
if page < 1 or per_page < 1:
raise ValueError("分页参数必须大于0")
# 清理输入以防止SQL注入
clean_tag = self.sanitize_input(tag)
offset = (page - 1) * per_page
with self.get_db_connection() as conn:
cursor = conn.execute('''
SELECT * FROM poems
WHERE tags LIKE ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
''', (f'%{clean_tag}%', per_page, offset))
poems = [dict(row) for row in cursor.fetchall()]
# 获取总数
cursor = conn.execute('SELECT COUNT(*) FROM poems WHERE tags LIKE ?', (f'%{clean_tag}%',))
total_count = cursor.fetchone()[0]
total_pages = (total_count + per_page - 1) // per_page
return poems, total_pages
def get_all_authors(self) -> List[str]:
"""获取所有作者"""
with self.get_db_connection() as conn:
cursor = conn.execute('SELECT DISTINCT author FROM poems ORDER BY author')
return [row['author'] for row in cursor.fetchall()]
def get_all_types(self) -> List[str]:
"""获取所有类型"""
with self.get_db_connection() as conn:
cursor = conn.execute('SELECT DISTINCT type FROM poems ORDER BY type')
return [row['type'] for row in cursor.fetchall()]
def get_all_tags(self) -> List[str]:
"""获取所有标签"""
with self.get_db_connection() as conn:
cursor = conn.execute('SELECT tags FROM poems')
all_tags = set()
for row in cursor.fetchall():
if row['tags']:
all_tags.update(tag.strip() for tag in row['tags'].split(','))
return sorted(list(all_tags))
def sanitize_input(self, input_str: str) -> str:
"""清理输入,防止SQL注入"""
# 使用正则表达式移除可能的SQL注入字符
if input_str is None:
return ""
# 移除危险字符
sanitized = re.sub(r'[;\'"\\]', '', input_str)
# 限制长度以防止过长输入
return sanitized[:1000].strip()
class SessionManager:
"""会话管理器,负责用户设置的管理"""
@staticmethod
def init_session():
"""初始化会话"""
defaults = {
'font_family': '楷体',
'font_size': '16px',
'background_color': '#f8f9fa',
'favorites': [],
'recent_views': [],
'view_history': [],
'last_activity': datetime.now()
}
for key, default_value in defaults.items():
if key not in session:
session[key] = default_value
@staticmethod
def update_settings(data: Dict[str, Any]) -> bool:
"""更新用户设置"""
try:
if 'font_family' in data:
# 验证字体名称
allowed_fonts = ['楷体', '宋体', '黑体', '仿宋', '微软雅黑', 'Arial', 'Times New Roman']
if data['font_family'] in allowed_fonts:
session['font_family'] = data['font_family']
if 'font_size' in data:
# 验证字体大小格式
size_pattern = r'^\d+px$'
if re.match(size_pattern, data['font_size']):
session['font_size'] = data['font_size']
if 'background_color' in data:
# 验证颜色格式
color_pattern = r'^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$'
if re.match(color_pattern, data['background_color']):
session['background_color'] = data['background_color']
session['last_activity'] = datetime.now()
return True
except Exception as e:
logger.error(f"更新设置时出错: {str(e)}")
return False
@staticmethod
def toggle_favorite(user_id: int, poem_id: int) -> bool:
"""切换收藏状态"""
if not isinstance(user_id, int) or not isinstance(poem_id, int) or user_id < 1 or poem_id < 1:
raise ValueError("无效的用户ID或诗词ID")
db_manager = DatabaseManager()
with db_manager.get_db_connection() as conn:
try:
cursor = conn.execute('SELECT id FROM favorites WHERE user_id = ? AND poem_id = ?', (user_id, poem_id))
existing = cursor.fetchone()
if existing:
# 删除收藏
conn.execute('DELETE FROM favorites WHERE user_id = ? AND poem_id = ?', (user_id, poem_id))
else:
# 添加收藏
conn.execute('INSERT INTO favorites (user_id, poem_id) VALUES (?, ?)', (user_id, poem_id))
conn.commit()
return True
except sqlite3.IntegrityError:
return False # 如果违反唯一约束则返回False
@staticmethod
def add_to_recent_views(poem_id: int):
"""添加到最近浏览"""
if not isinstance(poem_id, int) or poem_id < 1:
raise ValueError("无效的诗词ID")
recent = session.get('recent_views', [])
if poem_id not in recent:
recent.insert(0, poem_id)
if len(recent) > 10: # 只保留最近10个
recent = recent[:10]
session['recent_views'] = recent
def error_handler(f):
"""错误处理装饰器"""
@wraps(f)
def decorated_function(*args, **kwargs):
try:
# 检查会话是否过期
last_activity = session.get('last_activity')
if last_activity:
if datetime.now() - last_activity > timedelta(hours=24): # 24小时后会话过期
session.clear()
return f(*args, **kwargs)
except ValueError as e:
logger.error(f"参数错误在函数 {f.__name__}: {str(e)}")
return render_template('error.html', error="请求参数错误"), 400
except Exception as e:
logger.error(f"函数 {f.__name__} 发生错误: {str(e)}")
return render_template('error.html', error="页面加载失败"), 500
return decorated_function
def validate_poem_id(f):
"""验证诗词ID装饰器"""
@wraps(f)
def decorated_function(poem_id, *args, **kwargs):
if not isinstance(poem_id, int) or poem_id < 1:
return render_template('error.html', error="无效的诗词ID"), 400
db_manager = DatabaseManager()
try:
poem = db_manager.get_poem_by_id(poem_id)
if not poem:
return render_template('error.html', error="诗词不存在"), 404
except ValueError:
return render_template('error.html', error="无效的诗词ID"), 400
return f(poem_id, *args, **kwargs)
return decorated_function
def sanitize_input(input_str: str) -> str:
"""清理用户输入,防止XSS攻击"""
if input_str is None:
return ""
# 使用bleach库清理HTML标签
clean_input = bleach.clean(input_str, strip=True)
return clean_input.strip()[:1000].strip() # 限制长度
@lru_cache(maxsize=128)
def get_cached_poems(page: int, per_page: int) -> tuple:
"""缓存诗词数据以提高性能"""
db_manager = DatabaseManager()
return db_manager.get_all_poems(page, per_page)
# 初始化数据库管理器
db_manager = DatabaseManager()
@app.route('/')
@error_handler
def index():
"""首页"""
SessionManager.init_session()
page = request.args.get('page', 1, type=int)
per_page = 12
# 使用缓存提高性能
poems, total_pages = get_cached_poems(page, per_page)
return render_template('index.html',
poems=poems,
page=page,
total_pages=total_pages,
all_authors=db_manager.get_all_authors(),
all_types=db_manager.get_all_types(),
all_tags=db_manager.get_all_tags())
@app.route('/search')
@error_handler
def search():
"""搜索页面"""
query = request.args.get('q', '')
page = request.args.get('page', 1, type=int)
per_page = 12
if query:
# 清理搜索查询
clean_query = sanitize_input(query)
poems, total_pages = db_manager.search_poems(clean_query, page, per_page)
else:
poems, total_pages = db_manager.get_all_poems(page, per_page)
return render_template('search.html',
poems=poems,
query=query,
page=page,
total_pages=total_pages)
@app.route('/author/<author>')
@error_handler
def by_author(author):
"""按作者查看"""
# 清理作者名
clean_author = sanitize_input(author)
page = request.args.get('page', 1, type=int)
per_page = 12
poems, total_pages = db_manager.get_poems_by_author(clean_author, page, per_page)
return render_template('author.html',
poems=poems,
author=author,
page=page,
total_pages=total_pages)
@app.route('/type/<poem_type>')
@error_handler
def by_type(poem_type):
"""按类型查看"""
# 清理诗歌类型
clean_type = sanitize_input(poem_type)
page = request.args.get('page', 1, type=int)
per_page = 12
poems, total_pages = db_manager.get_poems_by_type(clean_type, page, per_page)
return render_template('type.html',
poems=poems,
poem_type=poem_type,
page=page,
total_pages=total_pages)
@app.route('/tag/<tag>')
@error_handler
def by_tag(tag):
"""按标签查看"""
# 清理标签
clean_tag = sanitize_input(tag)
page = request.args.get('page', 1, type=int)
per_page = 12
poems, total_pages = db_manager.get_poems_by_tag(clean_tag, page, per_page)
return render_template('tag.html',
poems=poems,
tag=tag,
page=page,
total_pages=total_pages)
@app.route('/poem/<int:poem_id>')
@validate_poem_id
@error_handler
def poem_detail(poem_id):
"""诗词详情页面"""
poem = db_manager.get_poem_by_id(poem_id)
if not poem:
return render_template('error.html', error="诗词不存在"), 404
# 获取上一首和下一首
with db_manager.get_db_connection() as conn:
cursor = conn.execute('SELECT id FROM poems ORDER BY id')
all_ids = [row['id'] for row in cursor.fetchall()]
try:
current_index = all_ids.index(poem_id)
prev_id = all_ids[current_index - 1] if current_index > 0 else None
next_id = all_ids[current_index + 1] if current_index < len(all_ids) - 1 else None
except ValueError:
prev_id = next_id = None
# 添加到最近浏览
SessionManager.add_to_recent_views(poem_id)
return render_template('poem.html',
poem=poem,
poem_id=poem_id,
prev_id=prev_id,
next_id=next_id)
@app.route('/random')
@error_handler
def random_poem():
"""随机诗词"""
with db_manager.get_db_connection() as conn:
cursor = conn.execute('SELECT id FROM poems ORDER BY RANDOM() LIMIT 1')
row = cursor.fetchone()
if not row:
return render_template('error.html', error="暂无诗词数据"), 500
poem_id = row['id']
poem = db_manager.get_poem_by_id(poem_id)
SessionManager.add_to_recent_views(poem_id)
# 获取上一首和下一首
with db_manager.get_db_connection() as conn2:
cursor = conn2.execute('SELECT id FROM poems ORDER BY id')
all_ids = [row['id'] for row in cursor.fetchall()]
try:
current_index = all_ids.index(poem_id)
prev_id = all_ids[current_index - 1] if current_index > 0 else None
next_id = all_ids[current_index + 1] if current_index < len(all_ids) - 1 else None
except ValueError:
prev_id = next_id = None
return render_template('poem.html',
poem=poem,
poem_id=poem_id,
prev_id=prev_id,
next_id=next_id)
@app.route('/api/settings', methods=['POST'])
@error_handler
def update_settings():
"""更新用户设置"""
data = request.json
if not data:
return jsonify({'success': False, 'error': '无效的数据'}), 400
# 清理输入数据
if 'font_family' in data:
data['font_family'] = sanitize_input(data['font_family'])
if 'font_size' in data:
data['font_size'] = sanitize_input(data['font_size'])
if 'background_color' in data:
data['background_color'] = sanitize_input(data['background_color'])
success = SessionManager.update_settings(data)
if success:
return jsonify({'success': True})
else:
return jsonify({'success': False, 'error': '设置更新失败'}), 500
@app.route('/api/favorites', methods=['POST'])
@error_handler
def toggle_favorite():
"""切换收藏状态"""
data = request.json
poem_id = data.get('poem_id') if data else None
if poem_id is None:
return jsonify({'success': False, 'error': '无效的诗词ID'}), 400
# 验证ID是否存在
poem = db_manager.get_poem_by_id(poem_id)
if not poem:
return jsonify({'success': False, 'error': '诗词ID不存在'}), 400
# 检查用户是否已登录
user_id = session.get('user_id')
if not user_id:
return jsonify({'success': False, 'error': '请先登录'}), 401
try:
success = SessionManager.toggle_favorite(user_id, poem_id)
if success:
return jsonify({'success': True})
else:
return jsonify({'success': False, 'error': '操作失败'}), 500
except ValueError as e:
return jsonify({'success': False, 'error': str(e)}), 400
@app.errorhandler(404)
def not_found(error):
return render_template('error.html', error="页面未找到"), 404
@app.errorhandler(500)
def internal_error(error):
return render_template('error.html', error="服务器内部错误"), 500
@app.errorhandler(400)
def bad_request(error):
return render_template('error.html', error="请求参数错误"), 400
@app.errorhandler(401)
def unauthorized(error):
return render_template('error.html', error="未授权访问"), 401
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))
打分
最新发布