我一个一个踩过的坑说,很多东西不是看文档能学会的。先说结论:这套“重庆旅游推荐系统”做下来,核心就三件事——把数据搞到手、把推荐逻辑算出来、把结果好看地展示出去。技术栈很常规:Python 3.8 + Flask 2.x + 爬虫 + ECharts 可视化。适合有一定 Python 基础、想完整走一遍爬虫到 Web 展示全流程的人参考。项目不大,但五脏俱全,从数据采集到前端展示每一环都会遇到真实的问题。
1. 项目全貌:这套系统到底在解决什么问题
1.1 核心需求拆解
先说清楚要做的事——一个能推荐重庆旅游景点的 Web 系统。用户打开网页,能看到重庆各个景点的热度排名、评分分布、门票价格区间,还能根据自己的预算和偏好筛选出合适的行程。
重庆这个城市很有意思,景点类型跨度极大,从洪崖洞、磁器口这类网红打卡地,到武隆天坑、大足石刻这种需要专门安排一天的自然与人文景观,再到长江索道、南山一棵树这种城市观景点,数据特征完全不同。推荐系统如果只按评分排序,必然翻车,因为不同类型景点的游客评价维度和数量级差异很大,直接排序会让小众高质量景点永远排不上号。
所以设计目标定成了三个:
- 爬取真实可用的景点数据,包括名称、所在区域、评分、评论数、门票价格、游玩时长、开放时间、标签分类。
- 计算一个综合推荐分,不是单纯按评分排,而是把热度、口碑、性价比、适游度按权重融合。
- 用可视化图表展示数据分布,让用户一眼看懂哪些景点值得去、什么时候去、预算大概多少。
1.2 技术选型为什么是 Python + Flask + 爬虫 + 可视化
这个组合不是最炫酷的,但一定是最务实的。我想把它拆开说:
- Python:没有任何悬念,爬虫和数据分析生态最成熟的语言,没有之一。requests、BeautifulSoup、pandas 这些库把数据处理的脏活累活都包了。
- Flask:选择它而不是 Django,核心原因是项目规模小,推荐逻辑不复杂,Flask 的路由和请求处理足够轻量,写起来快。Django 的 admin 后台、ORM、中间件体系对这种单页应用来说太重了。
- 爬虫:数据没有现成 API,公开数据散落在各大旅游网站和点评平台。爬虫是唯一靠谱的方式,而且爬下来的数据结构化程度高,方便后续分析和计算。
- 可视化:ECharts,国产图表库,文档友好、示例丰富、图表类型多。重要的是它支持动态数据加载,后端返回 JSON,前端直接渲染,开发体验非常好。
这套组合最大的好处是每个环节都有成熟方案,踩坑时有大量社区经验可查。你做的时候不用发明轮子,把精力花在数据的质量和推荐逻辑本身。
1.3 系统整体工作流程
整个系统跑起来是一条完整链路,我必须先把流程展示清楚,后面所有细节都是围绕这条线展开的:
爬虫采集原始数据 -> 数据清洗与去重 -> 存入 SQLite 数据库 -> Flask 后端读取数据并计算推荐分 -> 提供 JSON 接口 -> 前端页面用 Ajax 请求接口 -> ECharts 渲染图表和推荐列表我特意选 SQLite 而不是 MySQL,因为系统是单机部署、数据量撑死几千条,SQLite 零配置、单文件、移动方便,对教学和演示场景足够。如果你打算上生产环境多用户并发访问,再考虑换 MySQL,但推荐逻辑和接口代码基本不用改,换数据库连接串就行。这个设计决策在初期就考虑到了扩展性。
2. 爬虫模块:数据源头怎么干净地搞定
2.1 爬虫目标与数据字段设计
爬虫是整套系统的地基,数据质量直接决定推荐结果靠不靠谱。我先明确要看哪些字段,而不是看到一个页面就乱抓:
| 字段名 | 类型 | 说明 | 示例 |
|---|---|---|---|
| spot_id | int | 景点唯一标识 | 10001 |
| spot_name | string | 景点名称 | 洪崖洞 |
| area | string | 所在城区 | 渝中区 |
| rating | float | 游客评分 0-5 | 4.6 |
| comment_num | int | 评论数量 | 32876 |
| ticket_price | float | 参考票价,0 表示免费 | 0 |
| play_time | float | 建议游玩时长(小时) | 2.5 |
| tags | string | 逗号分隔的标签 | 网红打卡,夜景,免费 |
| open_time | string | 开放时间 | 全天开放 |
| hot_rank | int | 全网热度排名 | 3 |
字段设计阶段就要想清楚后面推荐逻辑需要什么。比如评论数和热度排名是衡量“热度”的重要信号,光有评分不行——一个 4.9 分的冷门景点和一个 4.5 分的万人景点,对普通游客来说前者不一定更合适。这些字段在后面算推荐分时会全部用到。
2.2 爬虫实现方案:静态页面与动态页面分而治之
重庆景点数据分布在多个平台,有些是静态页面直接渲染的,有些是动态加载的。我实际测试下来,几个主流旅游平台的页面渲染方式完全不同,需要针对性地写爬虫。
静态页面处理方式
对于服务端渲染的页面,直接用 requests 拿 HTML,BeautifulSoup 解析。这里给出一个最小可用的抓取代码,我在项目里实测稳定运行:
import requests from bs4 import BeautifulSoup import time headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } def fetch_static_page(url): resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() soup = BeautifulSoup(resp.text, 'html.parser') return soup静态解析的套路就那几步:定位景点名称所在的 HTML 节点,通常在一个带特定 class 的 div 或 a 标签里;解析评分,注意有些网站是 4.6 分但样式是星星图片,需要从 img 的 alt 属性里抠数字;评论数要处理“3.2万”这种带中文单位的字符串。我封装了一个简单函数来处理:
def parse_comment_num(text): # 处理 "3.2万" -> 32000 text = text.strip() if not text: return 0 if '万' in text: return int(float(text.replace('万', '')) * 10000) return int(text)动态加载页面的处理
动态加载是爬虫最大的坑,目前平台普遍用 JavaScript 异步加载数据。直接 requests 抓 HTML 只能拿到空壳,数据是通过 XHR 接口返回 JSON。面对这种情况,第一反应不是上 Selenium,而是分析网络请求,找到那个隐藏的 JSON 数据接口。
说个踩坑经历:某平台景点列表页,肉眼看到的景点信息全部是动态渲染的,抓 HTML 什么都拿不到。打开浏览器开发者工具,切到 Network 面板,刷新页面,发现一个请求返回了 JSON 数组,里面包含景点名称、热度排名、人均消费等字段。那才是真正的数据源。而且这个接口返回的数据结构化程度很高,直接用 json.loads 解析就行,比解析 HTML 干净十倍。
使用 Selenium 不是不行,但有几个实际问题:浏览器驱动要匹配本地 Chrome 版本、启动浏览器实例内存开销大、并发爬取基本不可能、容易被网站识别为机器人。如果能找到底层 JSON 接口,性能和稳定性都吊打模拟浏览器方案。我的建议是:先花时间分析接口,实在找不到再上 Selenium。通常 Network 面板里搜关键词“spot”、“scenic”或者“poi”,就能锁定真正的数据接口。
2.3 反爬应对与请求频率控制
反爬是整个爬虫环节最头疼的事情,我只说实测有用的招数,不扯那些花哨的理论。
User-Agent 伪装只是基本功。我一开始只设置了浏览器 UA,跑了不到两百条请求就被限流,返回来一堆验证码页面。后来完善了请求头,加上 Accept、Accept-Language、Referer 这些字段,情况好了很多。缺 Referer 是最明显的爬虫特征,很多反爬策略首先看这个。
请求频率必须控制。我见过新手写爬虫,for 循环里不 sleep,三秒钟把对方网站几百个页面打爆,然后 IP 被封就开始骂网站反爬变态。将心比心,谁的服务都不愿意被打爆。我用的策略很简单:每抓取一个页面后随机 sleep 2 到 5 秒,并且随机打乱顺序,让请求时间间隔看起来不那么规律。
import random import time def polite_sleep(): # 随机停顿模拟人类浏览节奏 time.sleep(random.uniform(2, 5))IP 被限制的兜底方案。如果目标网站封了你的本地 IP,最简单的解法是拨号换 IP(适用于家庭宽带)或者用云服务器的弹性 IP。注意我不建议在这个项目上投入太多成本买代理池,因为演示数据的量级很小,换一次 IP 重新跑就能解决。
还有一点必须说:爬虫的合规红线要自觉守住。只抓公开的静态信息(景点名称、评分、地址),不碰用户隐私数据,不绕过登录鉴权,不抓取付费内容。控制请求频率不影响网站正常服务。这套系统做出来用于学习和个人研究是没问题的,不要用于商业牟利。
2.4 数据清洗与去重策略
爬完数据不能直接入库,脏数据会把推荐逻辑带偏。我遇到的情况主要有几类:
- 重复数据:同一个景点在不同页面爬了多次,或者不同别名指向同一天。去重逻辑以景点名称作为唯一键,再结合地址做二次校验。SQLite 里建唯一索引,插入前先查重。
- 缺失字段:部分景点的门票价格、开放时间在页面上显示“暂无”。处理方式是给默认值,比如门票默认 0(免费),开放时间默认“请电话咨询”,不能留空。
- 类型混乱:评论数有“3.2万”和“32876”两种格式,标签有“夜景/网红”和“网红打卡,夜景”之分。统一用 2.2 节的 parse_comment_num 函数处理,标签统一用逗号分隔。
- 坐标纠偏:如果下一步要做地图可视化,经纬度数据存在偏移问题。国内地图一般用腾讯/高德坐标系,爬下来的坐标可能是 GCJ-02,是标准 WGS-84 加偏移的结果。做地图分布图时建议直接用前端地图组件的逆地址解析,减少后端纠偏的工作量。
清洗完的数据结构要统一,格式要规范,这是站在后面推荐算法和可视化的角度去要求的。在这个阶段偷懒,后面每个环节都会加倍难受。
3. Flask 后端与推荐逻辑:让数据产生真正的价值
3.1 Flask 项目结构与路由设计
整个 Flask 项目的目录结构保持简单清晰,工程化程度不用太高,但规范要立好:
cqtourism/ ├── app.py # Flask 入口 ├── recommend.py # 推荐算法核心逻辑 ├── database.py # 数据库连接与查询操作 ├── crawler/ │ ├── spider.py # 爬虫主逻辑 │ └── cleaner.py # 数据清洗 ├── data/ │ └── tourism.db # SQLite 数据库文件 ├── static/ │ ├── css/ │ ├── js/ │ │ ├── echarts.min.js │ │ └── main.js │ └── images/ ├── templates/ │ └── index.html # 主页面 └── requirements.txtFlask 的核心路由只有三个,加一个主页:
from flask import Flask, render_template, jsonify, request import recommend app = Flask(__name__) @app.route('/') def index(): # 渲染主页面 return render_template('index.html') @app.route('/api/recommend') def api_recommend(): # 接收筛选参数,返回推荐列表 budget = request.args.get('budget', 500, type=int) days = request.args.get('days', 3, type=int) preference = request.args.get('preference', '', type=str) result = recommend.get_recommendations(budget, days, preference) return jsonify(result) @app.route('/api/stats') def api_stats(): # 返回可视化需要的统计数据 stats = recommend.get_stats() return jsonify(stats) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)路由设计的原则是让接口负责数据供应,页面负责展示渲染。前后端分离的好处是,不管后续要加小程序端还是 APP 端,接口可以直接复用。
3.2 推荐算法:加权评分模型的建模与实现
推荐系统的核心是“综合推荐分”的计算。网上很多教程直接按评分排序,这是最懒也最不负责任的做法。我把评分当成一个基础信号,然后叠加了热度、性价比和适游度三个维度。
推荐分公式,我在项目里实际使用的形式如下:
score = rating * 0.35 + hot_score * 0.30 + value_score * 0.20 + fit_score * 0.15每个子项都要归一化到 0-5 区间,不然量纲不一致,一个权重再高也白搭。
rating:原始评分,本身就是 0-5,不需要额外处理,但要注意异常值,比如没人评论的景点显示 5 分,这个数据没有参考意义。我设了评论数小于 50 的景点评分降权处理,乘以 0.8 再加一个小的惩罚项。
hot_score:热度分,基于评论数和全网热度排名。评论数分布跨度太大,直接线性映射会让头部景点霸榜。我用对数压缩转换:
import math def hot_score(comment_num, hot_rank): # 评论数取对数压缩量纲 comment_score = min(math.log10(comment_num + 1) * 1.2, 5) # 热度排名前10加奖励分 if hot_rank <= 10: comment_score = min(comment_score + 0.5, 5) return round(comment_score, 2)这条公式我调了好几版。最开始的版本直接用评论数线性映射,结果武隆天坑这种全球知名的景点被洪崖洞远远甩开。加了对数压缩后,头部之间的差距就合理了,一个 3 万评论和 8 万评论的景点,热度分差距只有 0.7 分而不是 3 倍差距,这才是用户真实感知上的热度差异。
value_score:性价比分,逻辑很朴素——门票 0 元的高分景点性价比最高。公式是基准分 5 分起步,每多收 50 元门票扣 0.5 分,扣完为止:
def value_score(ticket_price): price = max(float(ticket_price), 0) score = max(5 - price / 100, 0) return round(score, 2)注意这里门票超过 500 元的景点分值直接归零,虽然极端但符合直觉——你去一个地方玩,花 500 块门票还得给它高分就是违反基本判断了。当然这个阈值可以根据定位调整,如果做高端定制游模型,权重和扣分系数都得重调。
fit_score:适游度,这个维度是结合用户筛选条件动态算的。用户选择偏好标签(比如“夜景”、“亲子”、“徒步”),命中一个偏好标签加 1 分,封顶 5 分;用户预算低于门票价格时大幅扣分,预算覆盖则得基础分。
def fit_score(spot, budget, preference): score = 2.5 # 基础分 # 预算扣分 ticket = float(spot['ticket_price']) if budget > 0 and ticket > budget: score -= min((ticket - budget) / 100, 2.5) # 偏好加分 if preference: tags = spot['tags'].split(',') for tag in tags: if tag.strip() in preference: score += 1 return round(min(score, 5), 2)最终推荐列表按 score 降序排列,返回 Top 20。加上用户交互后,每次筛选都能得到贴合需求的结果,不再是死板的静态榜单。
3.3 数据库操作与接口联调的注意点
数据库操作我封装成独立的 database.py,避免业务逻辑里到处写 SQL。重点说一下 SQLite 并发问题,这是 Flask 开发模式下最容易踩的坑。
Flask 开发服务器默认多线程处理请求,而 SQLite 在有写操作时只能一个线程写,多个线程同时写会报database is locked。我的解决思路很粗暴但有效:只有爬虫模块会写数据库,数据入库完成后才启动 Web 服务,Web 运行期间全部是只读操作。这样读写互不干扰,基本不会触发锁问题。
接口联调时还需要注意字段序列化。SQLite 查询结果默认是 sqlite3.Row 对象,不能直接 jsonify,需要先转成字典:
def row_to_dict(row): return {col[0]: row[col[0]] for col in row.description}抖音上的人总说“细节决定成败”,这个转换细节没处理好,接口调试就能卡你一小时,报错还看得莫名其妙。
4. 数据可视化与前端展示:让用户一眼看懂推荐结果
4.1 ECharts 图表选型与数据协议
可视化的核心不是图表多炫,而是信息表达准确。我在这一版里选了四类图表,每一类都有自己的信息承载目标。
景点热度 Top 10 柱状图:横向柱状图,展示评论数最多的景点。用对数轴处理数据量级差异,不然前两名把后面全部压扁。
评分-价格散点图:横轴是门票价格,纵轴是评分,每个点是一个景点。这张图能一眼看出哪些景点“便宜又好货”,也就是左上角区域的点,以及哪些景点“贵但评分低”,右下角的坑。
景点类型分布饼图:按标签统计比例,比如“自然风光”、“历史人文”、“网红打卡”、“亲子游”、“美食体验”各自占比多少。顶多展示 Top 6 标签,其他归入“其他”,不然图上看不清。
区域景点密度地图:这个最有重庆特色,重庆是山城,各区县景点分布很不均衡,渝中区密度极高,武隆、大足这类远郊则每个区一两个大景点。地图用 ECharts 的地图组件配合散点坐标,视觉冲击力很强。
数据协议统一走 JSON,结构约定如下:
{ "code": 0, "data": { "hot_top10": [ {"name": "洪崖洞", "value": 32876}, {"name": "长江索道", "value": 24050} ], "scatter": [ {"name": "武隆天坑", "price": 135, "rating": 4.8} ], "tag_distribution": [ {"name": "自然风光", "value": 12}, {"name": "历史人文", "value": 9} ], "map_data": [ {"name": "渝中区", "value": 15}, {"name": "武隆区", "value": 3} ] } }前端收到 JSON 后,用 ECharts 加载配置项渲染。协议设计的原则是后端尽量算好汇总数据,前端只做展示,不要让前端自己去做 map reduce 之类的数据变换,浏览器算海量数据性能会很差。
4.2 推荐结果页面的交互动效实现
页面布局采用上下分区:顶部是筛选条件栏,中间是可视化图表区,底部是推荐景点卡片列表。用户调整预算滑块或点选偏好标签后,通过 Ajax 重新请求推荐接口,页面局部刷新而不是整页跳转。
这部分我直接给出核心的 JavaScript 交互骨架:
async function fetchRecommendations(params) { const query = new URLSearchParams(params).toString(); const resp = await fetch(`/api/recommend?${query}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); return resp.json(); } document.getElementById('budgetSlider').addEventListener('change', async (e) => { const budget = e.target.value; const preference = getSelectedTags(); const data = await fetchRecommendations({ budget, preference }); renderSpotCards(data.data); renderScatterChart(data.data); });这里有一个性能细节:每次筛选条件变化后,接口返回最新数据并同时更新图表和推荐卡片,这要求后端把推荐结果和统计数据统一放在一个接口里返回,避免前端发两次请求。我做的时候就发现,如果分开两个接口,前后端联调难度增加不少,而且用户体验会有明显卡顿感。
4.3 页面布局与用户体验打磨
前端用的样式库是我自己写的一整套轻量 CSS,没有引 Bootstrap,因为项目的页面结构简单,引框架反而要覆盖它一大堆默认样式。页面整体色调用了橙色和深灰搭配,暗示火锅和山城的基调。这不是什么高深设计,但比默认裸页面强很多。
布局细节上,我做了几个关键决定:
- 筛选条件吸顶:用户往下滚动看景点头像时,筛选栏固定在页面顶部,不想看某个类型的景点时能随时调整,不用滚回顶部。这个体验提升非常直观。
- 图片懒加载:景点列表页面的图片资源比较多,用
loading="lazy"属性延迟加载首屏外的图片,页面首屏打开速度快了一倍以上。 - 卡片悬浮效果:鼠标悬停在景点卡片上时,卡片轻微上浮并显示阴影,提示用户可点击查看详情。交互反馈清晰,不花哨。
移动端适配也是重点,因为很多用户实际是用手机查旅游攻略。媒体查询做断点:窄屏下图表区从并排变成上下排列,推荐卡片从四列变成单列。这些适配代码不难写,但你不做的话,手机上看页面就会很难受。
5. 部署上线与真实踩坑记录
5.1 本地开发环境配置与依赖管理
项目依赖我全部写在 requirements.txt 里,核心依赖版本如下:
flask==2.2.5 requests==2.31.0 beautifulsoup4==4.12.2 pandas==2.0.3 gunicorn==21.2.0版本锁定的原因很现实:不锁版本的后果是过半年再打开项目,依赖全都升了大版本,代码跑不起来了。最典型的是 pandas API 变动,或者 Flask 从 2.x 升到 3.x 后有些行为变了。锁定版本不是保守,是对自己未来的时间负责。
虚拟环境我建议用 venv,和 Conda 无关,Python 3.3+ 自带,轻量无污染:
python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install -r requirements.txt5.2 云服务器部署流程
部署目标我选了一台 2 核 4G 的云服务器,系统 Ubuntu 22.04。整套部署流程就三步,足够简单:
第一步:同步代码并安装依赖
# 服务器上执行 git clone <你的仓库地址> cd cqtourism python3 -m venv venv source venv/bin/activate pip install -r requirements.txt第二步:用 Gunicorn 启动 Flask 应用
Flask 自带的开发服务器不能直接用于生产环境,性能极差而且有安全风险。我使用 Gunicorn 做 WSGI 服务器,指定监听端口和 worker 数量:
gunicorn -w 2 -b 0.0.0.0:5000 app:app-w 2表示开两个 worker 进程,对于这种小型应用,2 是最优值,再多的 worker 反而因为 SQLite 的读锁机制和内存开销拖累性能。
第三步:Nginx 反向代理
Nginx 负责接收外部流量,转发给 Gunicorn。配置核心部分如下:
server { listen 80; server_name your_domain_or_ip; location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /path/to/cqtourism/static/; expires 7d; } }Nginx 做反向代理的好处是静态文件(JS、CSS、图片)直接由 Nginx 处理,不走 Flask,压力小很多。expires 7d给静态资源加了七天的浏览器缓存,重复访问加载速度提升明显。
5.3 部署后的踩坑排查表
这部分我整理成一张表,把部署和运行期间遇到过的典型问题都列出来,后面自己做项目或者给别人排查都能直接对照:
| 症状 | 原因 | 解决方案 |
|---|---|---|
| 页面访问 502 | Gunicorn 没启动或进程崩溃 | 看 gunicorn 日志,检查依赖是否全部安装 |
| 图片加载慢 | 静态文件走 Flask 未走 Nginx | 检查 Nginx 的 location /static/ 配置是否正确 |
| 推荐榜单数据缺失 | 爬虫阶段数据清洗不彻底 | 检查数据库空值,重新跑清洗脚本 |
| 中文乱码 | requests 未指定编码 | UTF-8 强制编码,res.encoding = 'utf-8' |
| 爬虫请求被拒 | 没有设置 Referer 头 | 补全请求头字段后再尝试 |
| 地图区域无法显示 | 地图数据加载方式不对 | ECharts 地图要注册 GeoJSON,或改用散点模式 |
| 接口返回 500 | 数据库连接被锁或字段序列化失败 | 检查 row_to_dict 转换逻辑,确保查询结果未被重复引用 |
排查的通用思路是:从后往前逐层定位。先看浏览器开发者工具的 Network 面板,请求是否到达后端;再看 Flask 日志,路由是否执行出错;最后看数据库,确认数据是否一致。一层层拆掉断点,问题很快就能缩小到一个很小的范围。
5.4 爬虫被封 IP 后的完整补救流程
有一次跑爬虫,跑到一半控制台刷出验证码 HTML,我就知道 IP 被盯上了。整个补救流程我走了三遍,最终稳定下来,记录一下:
- 立即停止所有爬虫任务,不能继续发请求,否则封禁时间会翻倍。
- 等待 30 到 60 分钟,让服务器的限流窗口过去。
- 更换出口 IP。我是通过路由器重启换家庭 IP 解决的,最省事。
- 程序层面调整策略:增加随机延时到 5-10 秒,降低单次请求数据量,从大列表页改为景点详情页直接采集。
- 加一个“断点续爬”机制:定期把已爬取的景点 ID 存到本地文件,重新启动爬虫时跳过已存在的 ID,不重复请求。
为什么要强调断点续爬?因为 IP 封禁往往发生在爬虫运行中,如果你没有断点续爬,解封后从头再来,又得重新发一遍已经成功的请求,被封的概率更大。这是个死循环。加了断点续爬后,每次只需把剩下的数据抓完,哪怕中途再被封,也能从上次的位置继续。
5.5 推荐系统的后续扩展空间
整套系统做完之后,我思考过它的演化方向,这里分享三个我认为最容易扩展的点:
爬虫的定时增量更新。把单次爬虫脚本改成可定时执行的模块,比如用 APScheduler 每隔一周自动抓一次评论数和热度排名,数据库里的数据就不会陈旧。毕竟景点热度是实时变化的,一个周期性的推荐榜单才有长期参考价值。
引入真正的协同过滤算法。当前系统使用的是基于规则的加权评分,每个用户看到的推荐结果高度相似。如果积累一定量用户行为数据(点击、收藏、搜索),可以做基于用户的协同过滤或者基于物品的协同过滤,给用户更个性化的结果。这块算力要求不大,你的笔记本完全跑得动。
利用大模型优化推荐理由。别的不说,现在推荐结果下方展示的推荐理由都是模板化语句,如果接入大模型 API,根据景点的标签和用户筛选条件生成一段动态推荐语,整个系统的体验感会提升一个档次。这个改动主要在后端加一个接口调用,前端加一个文本渲染区域,架构不需要大改。
我个人在实际操作中的体会是:这套系统最大的收获不是学会 Flask 路由怎么写,也不是会调几个 ECharts 配置,而是打通了“数据采集 -> 数据清洗 -> 数据建模 -> 数据展示”的完整链路。很多人单独学爬虫、单独学 Web 开发、单独学可视化,都能写出一个小 demo,但把它们串在一起时才发现真正的坑都在接口处。数据格式对不上、字段命名不一致、跨域配置缺失、请求频率失控,每一个环节的粗糙都会在下游放大。做这种全栈小项目,本质上是在练习“用工程思维搞定一个真实问题”的能力。
最后再分享一个小技巧:开发阶段把 Flask 的 debug 模式和 SQLite 的查询日志打开,每条 SQL 都打印出来,能很清楚地看到接口调用背后到底跑了哪些查询、有没有慢查询。这个小习惯帮我节省了大量排查时间,你上手做的时候一定会感谢这个选择。