1. 项目概述:基于Django+Vue.js的小说推荐系统全栈开发
这个毕业设计项目融合了Python爬虫技术、Django后端框架和Vue.js前端框架,构建了一个具备数据采集、存储分析和可视化推荐功能的小说阅读平台。系统核心包含三大模块:分布式小说爬虫引擎负责从多个书源实时抓取更新数据,Django构建的RESTful API处理业务逻辑和推荐算法,Vue.js实现的前端界面提供沉浸式阅读体验和个性化推荐展示。
我在实际开发中发现,这类系统最难的不是基础功能实现,而是如何处理不同书源的结构差异、保证推荐算法的实时性,以及前端复杂交互状态的管理。下面分享的解决方案都是经过线上真实流量验证的可靠方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计解析
2.1 整体技术栈选型
后端采用Python+Django的组合主要考虑三点:一是Django自带的ORM能极大简化数据库操作,二是其Admin后台非常适合内容管理系统,三是丰富的第三方库支持。具体版本选择:
- Python 3.8(兼顾稳定性和新特性)
- Django 3.2 LTS(长期支持版本)
- Django REST framework 3.12(构建API)
前端选用Vue.js 2.x而非3.x,主要因为:一是毕业设计不需要Composition API等新特性,二是相关UI库生态更成熟。配套使用了:
- Vue Router管理路由
- Vuex做状态管理
- Element UI组件库
2.2 数据库设计要点
小说类数据的特点是读多写少、文本内容大,因此采用MySQL+Redis混合存储:
sql复制CREATE TABLE `novel` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL,
`author` varchar(50) NOT NULL,
`cover_url` varchar(255),
`description` text,
`category_id` int,
`word_count` int DEFAULT 0,
`heat_value` int DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`),
KEY `idx_heat` (`heat_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
章节表采用分库分表设计,按小说ID哈希分片存储。大文本内容单独用TEXT类型字段存储,避免影响主表查询性能。
3. 核心模块实现细节
3.1 分布式爬虫系统实现
小说爬虫面临三个主要挑战:反爬机制、数据一致性、更新频率控制。我们的解决方案是:
- 请求策略:
python复制def get_with_retry(url, retry=3):
proxies = ['http://proxy1:port', 'http://proxy2:port']
headers = {
'User-Agent': random.choice(UA_POOL),
'Referer': 'https://www.qidian.com'
}
for i in range(retry):
try:
resp = requests.get(url,
headers=headers,
proxies={'http': random.choice(proxies)},
timeout=10)
if resp.status_code == 200:
return resp
except Exception as e:
logging.warning(f"Retry {i+1} for {url}: {str(e)}")
time.sleep(random.uniform(1, 3))
return None
- 数据去重:
采用布隆过滤器+BASE64编码URL的方式实现高效去重:
python复制class URLManager:
def __init__(self):
self.visited = BloomFilter(capacity=1000000, error_rate=0.001)
def is_duplicate(self, url):
url_md5 = hashlib.md5(url.encode()).digest()
return base64.b64encode(url_md5) in self.visited
- 增量更新:
通过对比章节数和最后更新时间判断是否需要更新:
python复制def check_update(novel_id):
local_last = Chapter.objects.filter(novel_id=novel_id)
.order_by('-create_time').first()
remote_info = get_remote_info(novel_id)
return remote_info['chapter_count'] > local_last.chapter_num
3.2 推荐算法实现
采用混合推荐策略解决冷启动和长尾问题:
- 基于内容的推荐:
python复制def content_based_recommend(user_id, top_k=5):
# 获取用户最近阅读记录
history = ReadHistory.objects.filter(user_id=user_id)
.order_by('-create_time')[:10]
if not history:
return popular_recommend(top_k)
# 提取关键词TF-IDF向量
novels = [h.novel for h in history]
tfidf = TfidfVectorizer(stop_words=STOP_WORDS)
tfidf_matrix = tfidf.fit_transform([n.description for n in novels])
# 计算余弦相似度
all_novels = Novel.objects.exclude(id__in=[n.id for n in novels])
sim_scores = []
for novel in all_novels:
vec = tfidf.transform([novel.description])
score = cosine_similarity(tfidf_matrix, vec).mean()
sim_scores.append((novel.id, score))
# 返回TopK推荐
return sorted(sim_scores, key=lambda x: x[1], reverse=True)[:top_k]
- 协同过滤改进:
使用Surprise库实现带时间衰减的矩阵分解:
python复制from surprise import SVD, Dataset, accuracy
from surprise.model_selection import train_test_split
def train_cf_model():
# 加载评分数据
ratings = Rating.objects.all().values('user_id', 'novel_id', 'score')
df = pd.DataFrame(list(ratings))
# 定义时间衰减函数
df['weight'] = 1 / (1 + np.log1p((datetime.now() - df['rate_time']).dt.days))
df['adjusted_score'] = df['score'] * df['weight']
# 训练模型
data = Dataset.load_from_df(df[['user_id', 'novel_id', 'adjusted_score']],
reader=Reader(rating_scale=(1, 5)))
trainset, testset = train_test_split(data, test_size=0.2)
algo = SVD(n_factors=50, n_epochs=20, lr_all=0.005, reg_all=0.02)
algo.fit(trainset)
# 评估并保存模型
predictions = algo.test(testset)
rmse = accuracy.rmse(predictions)
joblib.dump(algo, 'cf_model.pkl')
return algo
3.3 前后端交互设计
3.3.1 API设计规范
采用JSON API规范设计接口,关键接口示例:
python复制class NovelListView(APIView):
def get(self, request):
page = request.query_params.get('page', 1)
page_size = 20
novels = Novel.objects.filter(status=1).order_by('-heat_value')
paginator = Paginator(novels, page_size)
try:
current_page = paginator.page(page)
serializer = NovelSerializer(current_page, many=True)
return Response({
'code': 200,
'data': {
'list': serializer.data,
'pagination': {
'total': paginator.count,
'current': page,
'page_size': page_size
}
}
})
except EmptyPage:
return Response({'code': 404, 'message': 'Page not found'}, status=404)
3.3.2 Vuex状态管理
阅读进度全局状态管理方案:
javascript复制const store = new Vuex.Store({
state: {
readingProgress: {
novelId: null,
chapterId: null,
progress: 0,
lastReadTime: null
}
},
mutations: {
UPDATE_PROGRESS(state, payload) {
state.readingProgress = {
...state.readingProgress,
...payload
}
// 本地存储
localStorage.setItem('readingProgress', JSON.stringify(state.readingProgress))
}
},
actions: {
syncProgress({ commit }, progress) {
return new Promise((resolve) => {
commit('UPDATE_PROGRESS', progress)
// 异步上报服务器
api.updateReadingProgress(progress).then(() => {
resolve()
})
})
}
}
})
4. 性能优化实践
4.1 数据库查询优化
- 批量操作替代循环查询:
python复制# 反例 - N+1查询问题
novels = Novel.objects.all()[:100]
for novel in novels:
chapters = novel.chapter_set.all() # 每次循环都查询数据库
# 正例 - 使用select_related/prefetch_related
novels = Novel.objects.select_related('author')\
.prefetch_related('chapters')[:100]
- 延迟加载大文本字段:
python复制class Chapter(models.Model):
content = models.TextField()
def get_content(self):
if not hasattr(self, '_content_cache'):
self._content_cache = self.content
return self._content_cache
4.2 缓存策略设计
采用多级缓存架构:
- 热点数据使用Redis缓存:
python复制def get_novel_detail(novel_id):
cache_key = f'novel:{novel_id}'
data = cache.get(cache_key)
if not data:
data = Novel.objects.get(id=novel_id)
# 复杂对象序列化
serialized = pickle.dumps(data)
cache.set(cache_key, serialized, timeout=3600)
return data
return pickle.loads(data)
- 使用Django的cache_page装饰器缓存整个视图:
python复制@cache_page(60 * 15)
def novel_ranking(request):
novels = Novel.objects.order_by('-heat_value')[:100]
return render(request, 'ranking.html', {'novels': novels})
5. 部署方案与监控
5.1 生产环境部署
使用Docker Compose编排服务:
yaml复制version: '3'
services:
web:
build: .
command: gunicorn novel.wsgi:application --bind 0.0.0.0:8000
volumes:
- .:/code
ports:
- "8000:8000"
depends_on:
- redis
- mysql
redis:
image: redis:6
ports:
- "6379:6379"
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: yourpassword
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:
5.2 日志与监控配置
使用Sentry捕获异常:
python复制LOGGING = {
'version': 1,
'handlers': {
'sentry': {
'level': 'ERROR',
'class': 'raven.handlers.logging.SentryHandler',
'dsn': 'your_sentry_dsn',
},
},
'loggers': {
'django': {
'handlers': ['sentry'],
'level': 'ERROR',
'propagate': False,
},
},
}
6. 开发经验与避坑指南
- 跨域问题解决方案:
python复制CORS_ALLOWED_ORIGINS = [
"http://localhost:8080",
"http://your-domain.com"
]
CORS_ALLOW_METHODS = [
'GET',
'OPTIONS',
'POST',
'PUT',
'PATCH',
'DELETE'
]
- Vue.js开发常见问题:
- 组件样式污染:使用scoped属性
html复制<style scoped>
.novel-card {
/* 样式仅作用于当前组件 */
}
</style>
- Django性能陷阱:
- 避免在循环中执行查询(N+1问题)
- 分页时不要使用count()获取总数(大数据量表会慢)
python复制# 优化分页查询
paginator = Paginator(queryset, per_page=20)
page_obj = paginator.page(1)
# 不计算总数
page_obj.object_list = list(page_obj.object_list)
- 爬虫伦理注意事项:
- 设置合理的爬取间隔(至少3秒/请求)
- 遵守robots.txt规则
- 不爬取VIP章节等付费内容
- 添加明显的版权声明和原文链接
