去年接了个很有意思的需求:某旅游运营团队想弄一套重庆旅游景点数据分析系统,用来做目的地推荐和游客画像分析。当时市面上现成的BI工具要么太贵,要么定制性不够,于是我直接用Python搭了一套基于Flask的轻量级方案,把爬虫采集、数据清洗、分析计算和可视化展示全部打通。这套系统的核心链路很简单:爬虫抓取景点基础数据,存入数据库,通过Flask提供API接口,前端用ECharts做可视化大屏展示。整个过程踩了不少坑,也沉淀了一些实战经验,今天拿出来完整复盘一遍。
如果你是正准备做旅游数据分析、爬虫可视化方向毕业设计的同学,或是想给公司搭一个轻量级数据看板的后端开发者,这篇文章应该能帮你少走很多弯路。全文没有炫技的成分,全部是能直接抄作业的代码和配置思路。
1. 项目思路:重庆旅游景点数据分析系统要解决什么问题
1.1 需求拆解:从数据采集到可视化展示的完整链路
最初的需求其实很朴素:想看重庆各个景点的热度排名、评分分布、门票价格区间、游客平均游玩时长,以及这些指标之间的关联关系。但光有数据不够,还要让非技术背景的运营同事能一眼看懂,所以必须有可视化界面。于是整条链路由四个环节组成:
- 数据采集:爬取主流旅游平台上的景点公开信息,包括景点名称、所在区域、评分、门票价格、热度指数、评论数量等。
- 数据清洗与入库:把爬下来的脏数据做统一清洗,解决缺失值、重复值、单位不统一的问题,然后写入MySQL。
- 数据分析计算:在数据库层面或Python层面做聚合计算,产出排名榜、占比分布、相关性分析等结果。
- 可视化呈现:通过Flask搭建Web服务,前端使用ECharts绘制柱状图、饼图、地图等,形成可交互的数据大屏。
这四个环节里,最容易出问题的不是爬虫,也不是可视化,而是数据清洗和分析指标的定义。比如"热度指数",不同平台的定义完全不同,有的是浏览量,有的是收藏数,如果直接用原始数值做排行,结论就会失真。所以我在采集时约定了一个统一的口径:热度指数 = 评论数权重 × 0.4 + 收藏数权重 × 0.3 + 浏览量权重 × 0.3,虽然这个公式不算严谨,但至少保证了所有景点在同一个标准下可比。
1.2 技术选型:为什么用Flask而不是Django或FastAPI
很多人在选框架时会纠结,我直接说结论:这个项目用Flask是性价比最高的选择。原因有三点:第一,项目规模不大,核心功能就几个页面和一套JSON接口,Django自带的后台管理、ORM模型等重量级功能根本用不上,反而显得笨重。第二,Flask的灵活度非常高,整个应用可以写成单文件,也可以拆成蓝图模式,后续想加功能十分方便。第三,国内学习Flask的人群基数大,遇到问题随便一搜就有解决方案,生态非常成熟。
有人可能会问,为什么不选FastAPI?FastAPI确实性能更好,还有自动生成接口文档的功能,但它更适合纯API服务的场景。我们这个项目需要渲染HTML页面,虽然Flask也可以像FastAPI一样只返回JSON,但前后端一体的模式在部署和运维上更简单,给运营同事演示的时候也更直观。我的原则是:能少引入一个依赖就少引入一个依赖,能用同步模型解决就不用异步。
1.3 整体架构:爬虫、存储、后端、前端怎么串起来
整个系统的架构可以用一句话描述:调度器触发爬虫脚本,数据落到MySQL,Flask从MySQL读数据并计算指标,前端通过AJAX请求Flask接口拿到JSON渲染图表。
爬虫脚本(独立进程) --> MySQL数据库 <-- Flask应用(ORM查询+聚合计算) --> ECharts大屏数据更新策略是定时任务每隔三天跑一次爬虫,增量更新景点数据。爬虫和Web应用完全解耦,即使爬虫挂了也不影响可视化页面的正常访问,这个设计在后期维护中帮了大忙。有次爬虫因为对方网站改版而报错,可视化页面一点没受影响,运营甚至都没发现数据是旧的。
整个系统的核心价值在于:把数据采集、存储、分析、展示四个环节标准化,形成了一个可以复用的模板。以后想换个城市做同样的系统,只需要改爬虫的URL和数据库里的城市字段,可视化层和后端逻辑完全不用动。
2. 爬虫模块:景点数据采集的完整实现
2.1 数据源分析与爬虫策略设计
众所周知,爬虫第一步不是写代码,而是分析数据源。我选了两个数据源:某综合性旅游平台的景点POI列表页和某点评网站的景点详情页。选这两个平台的原因很简单:一是它们的景点覆盖面全,重庆主城加区县加起来有400多个景点信息;二是页面结构相对规整,方便解析。
在动手写爬虫之前,我先做了一件容易被忽略的事:手动访问目标网站,查看robots协议和页面的渲染方式。这里明确一下,我的爬虫只采集公开的、不需要登录的基础信息,且严格遵守目标网站的访问频率限制。这一步既是合规要求,也是保证爬虫能长期稳定运行的前提。
页面分析结果如下:列表页是服务端渲染HTML,直接可以用requests库获取;详情页部分字段是异步加载的,需要找到XHR接口来获取。景点列表页的翻页参数是page,每页15条数据,总共约30页。详情页则通过景点ID拼接URL访问。反爬方面,两个网站都做了基础的UA校验和频率限制,但都处于"只要不过分就不会封IP"的状态。
2.2 爬虫代码实现:Request + BeautifulSoup 抓取实战
我习惯用requests加BeautifulSoup的组合,原因很直接:Scrapy的学习成本和框架复杂度对于500个数据量级的项目来说属于杀鸡用牛刀。以下是核心采集代码的关键片段,我做了简化处理:
import requests from bs4 import BeautifulSoup import time import random import pymysql HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", "Referer": "https://place.xxx.com/" } def fetch_poi_list(page): url = f"https://place.xxx.com/poi/list?city=chongqing&page={page}" resp = requests.get(url, headers=HEADERS, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "html.parser") poi_items = soup.select(".poi-item") data_list = [] for item in poi_items: name = item.select_one(".poi-name").text.strip() district = item.select_one(".poi-district").text.strip() rating = item.select_one(".poi-rating").text.strip() comments = item.select_one(".poi-comments").text.strip() data_list.append({ "name": name, "district": district, "rating": float(rating), "comments": int(comments.replace("条评论", "")) }) return data_list这里有一个关键细节:解析时如果某个字段为空,select_one返回的是None,直接调用text会报错。所以我在实际代码里写了一个安全取值函数,把所有字段的解析都包了一层异常判断。这个细节在数据量小的时候看不出区别,当页面结构调整时,它能让你快速定位是哪个字段出了问题。
请求策略方面,我设置每两个请求之间随机休眠1到3秒,并且每次请求前随机更换User-Agent。实测下来,300多个景点的全量采集大概耗时15分钟,没有触发任何反爬机制。这里想提醒一句:爬虫一定要克制,宁可慢一点也不要疯狂请求,被封IP的代价远大于省下来的那几分钟。
2.3 反爬应对与请求策略
说到反爬,我去重强调一遍:这里的"应对"是指合理规避访问频率限制,绝不涉及绕过登录鉴权或破解加密参数。
实际开发中遇到的反爬主要有三层:第一层是UA校验,这个最简单,伪装一个正常的浏览器UA就行。第二层是访问频率限制,比如同一IP在短时间内请求超过阈值就弹出验证码,解法是控制请求间隔并随机化。第三层是数据加密,比如某些平台的评分信息是用CSS偏移或JS动态渲染的,这种处理起来复杂度较高,我的建议是直接换数据源,没必要死磕一个平台。
还有一个容易被忽略的点:爬虫程序要有异常重试机制。我用的方案是装饰器方式,遇到网络超时或HTTP 5xx错误时,自动重试3次,每次重试间隔指数递增。
def retry(max_retries=3): def decorator(func): def wrapper(*args, **kwargs): for i in range(max_retries): try: return func(*args, **kwargs) except Exception as e: if i == max_retries - 1: raise time.sleep(2 ** i) return None return wrapper return decorator2.4 数据清洗:从爬取结果到可用数据
爬下来的数据不能直接用,因为脏数据太多了。我在清洗阶段遇到过的情况给你们列一下:评分字段爬下来是字符串"4.5",需要转float;评论数是"1.2万条评论",需要换成int;部分景点没有门票信息,缺失值需要填充;还有同一个景点在两个平台都有数据,需要根据名称加区域做去重合并。
清洗工具用的pandas,处理这些表格型数据非常顺手。核心洗逻辑如下:
import pandas as pd df = pd.DataFrame(raw_data) df["rating"] = pd.to_numeric(df["rating"], errors="coerce") df["comments"] = df["comments"].str.replace("条评论", "").str.replace("万", "e4") df["comments"] = pd.to_numeric(df["comments"], errors="coerce") df["ticket_price"].fillna("免费", inplace=True) df.drop_duplicates(subset=["name", "district"], keep="first", inplace=True) df.dropna(subset=["name"], inplace=True)清洗完成的数据统一写入MySQL。表结构设计我在下一章讲,这里先展示写入部分的思路。因为数据量不大,我直接用pymysql单条循环插入,没有用批量提交,一是逻辑简单易排查,二是几百条数据单条插入也很快。
3. 数据库设计与数据分析:让数据产生价值
3.1 数据库表结构设计
数据库设计遵循"够用且不冗余"的原则,一共建了三张表:景点基础信息表、景点类型表、数据更新日志表。景点基础信息表是核心,字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT PRIMARY KEY AUTO_INCREMENT | 景点ID |
| name | VARCHAR(100) | 景点名称 |
| district | VARCHAR(50) | 所属区县 |
| category | VARCHAR(50) | 景点类型(自然风光/人文古迹/主题乐园等) |
| rating | DECIMAL(2,1) | 评分 |
| ticket_price | VARCHAR(50) | 门票价格描述(如"免费"、"85元") |
| play_time | VARCHAR(50) | 建议游玩时长 |
| heat_index | INT | 热度指数 |
| comment_count | INT | 评论数 |
| recommend_rate | DECIMAL(5,2) | 推荐率百分比 |
| update_time | DATETIME | 数据更新时间 |
设计这张表的时候有一个考量:ticket_price字段为什么用VARCHAR而不是DECIMAL?因为"免费"和"85元"混在一起,还有"待定"这种特殊情况,强行转数值会丢失信息。我的处理方式是保留原始字符串,另外加一个ticket_price_num字段存放纯数字,免费填0,待定填NULL,这样数据分析时就可以对数值字段做运算。
3.2 数据分析维度与指标计算
数据入库只是第一步,分析才是系统的灵魂。结合旅游行业关注的核心指标,我设定了五个分析维度:景点热度排行、区县景点数量分布、评分与热度关联分析、门票价格区间分布、游玩时长分布。每个维度对应大屏上的一个图表。
这里重点说说评分为热度关联分析的计算逻辑。先按评分区间把景点分为三档:高分段(4.5以上)、中分段(4.0到4.5)、低分段(4.0以下),然后统计每个档位的平均热度指数和平均评论数,最后用皮尔逊相关系数量化评分与热度之间的相关性。
def calc_correlation(df): rating_series = df["rating"].astype(float) heat_series = df["heat_index"].astype(float) corr = rating_series.corr(heat_series) return round(corr, 4)实测算出来评分和热度指数的相关系数大约是0.6左右,说明两者存在中等程度正相关。这个结论对运营的价值在于:不能只看单一指标排序,高评分景点可能热度不高,存在营销推广的机会。
3.3 分析结果存储与缓存
对于数据量不大的系统,每次请求都实时计算完全没问题,但为了应对后续数据量增长,我还是做了两层优化:第一层是MySQL加索引,针对district和rating字段建了联合索引,查询速度能提升一个数量级;第二层是Python进程内缓存,用字典存储热门查询结果,设置10分钟过期时间,过期后再从数据库拉取。
缓存实现不需要引入Redis这种重量级组件,直接写一个带过期时间的字典即可。Flask应用是单进程部署的,进程内缓存完全够用。
import time cache = {} def get_cached_data(key, expire=600): now = time.time() if key in cache and now - cache[key]["time"] < expire: return cache[key]["data"] data = query_from_mysql(key) cache[key] = {"time": now, "data": data} return data4. Flask后端与API接口:数据如何高效地传给前端
4.1 Flask项目骨架与蓝图组织
项目结构调整成了蓝图模式,虽然Flask支持单文件启动,但业务逻辑增多后单文件会变得难以维护。我的目录结构如下:
chongqing_tourism/ ├── app.py ├── config.py ├── models.py ├── services/ │ ├── crawl_service.py │ └── analysis_service.py ├── api/ │ ├── __init__.py │ ├── overview.py │ └── ranking.py ├── templates/ │ ├── base.html │ └── dashboard.html ├── static/ │ ├── css/ │ ├── js/ │ └── echarts/ └── requirements.txtapp.py只负责创建Flask实例、注册蓝图、启动服务,核心逻辑全部拆到对应的模块。这样做的直接好处是:爬虫相关代码在crawl_service.py里,分析相关代码在analysis_service.py里,API接口在api/目录下各自独立,某一个模块出问题不会影响其他模块。
4.2 可视化数据接口实现:从数据库到JSON
后端接口的核心逻辑是把分析结果转成前端ECharts需要的JSON格式。比如热度排行接口,返回的数据结构是:
{ "code": 200, "data": { "categories": ["洪崖洞", "磁器口", "解放碑", ...], "values": [98, 95, 92, ...] } }接口实现直接查数据库聚合后返回,逻辑不复杂但有一个要点:尽量在SQL层完成排序和聚合,不要查出全表数据后在Python里排序。MySQL的GROUP BY和ORDER BY性能相当好,在数据库里做完运算再传输,接口响应时间基本能控制在50毫秒以内。
from flask import Blueprint, jsonify from db import query_db ranking_bp = Blueprint("ranking", __name__) @ranking_bp.route("/api/ranking/hot") def hot_ranking(): sql = """ SELECT name, heat_index FROM scenic_spot ORDER BY heat_index DESC LIMIT 10 """ rows = query_db(sql) data = { "categories": [row["name"] for row in rows], "values": [row["heat_index"] for row in rows] } return jsonify({"code": 200, "data": data})4.3 接口性能优化技巧
接口性能优化有一个容易被忽视的地方:JSON序列化时中文编码问题。Flask默认的jsonify在返回中文时,如果不设置app.config["JSON_AS_ASCII"] = False,前端拿到的就是\uXXXX转义字符,虽然不影响解析,但调试时非常痛苦。
另一个优化点是数据库连接复用。不要在每次请求时新建连接,那样开销太大。我用的方案是定义一个全局的数据库连接对象,在Flask的teardown_appcontext钩子里自动关闭连接。当然并发量再高的场景就该上连接池了,不过这个系统并发需求不高,全局连接就够用。
import sqlite3 # 实际项目用MySQL from flask import g def get_db(): if "db" not in g: g.db = sqlite3.connect("tourism.db") return g.db @app.teardown_appcontext def close_db(exception): db = g.pop("db", None) if db is not None: db.close()接口设计还有一个实用经验:分页参数和图表类型参数通过URL的query string传递,这样前端可以根据用户交互动态调整图表展示范围。比如/api/ranking/hot?top=20可以控制展示Top20还是Top10,后端只需要把limit设为参数即可。
5. 可视化大屏:ECharts让数据一眼看懂
5.1 ECharts可视化方案:图表选型与配置
可视化部分选了ECharts,原因有三:上手简单、图表类型全、社区案例丰富,而且它支持地图组件,重庆的地图数据很容易获取。整个大屏规划了5个核心图表模块:
- 热度排行Top10横向柱状图,一眼看出最火的景点。
- 区县景点数量分布地图,用颜色深浅表示景点密度。
- 评分区间占比饼图,直观展示整体口碑水平。
- 门票价格散点图,X轴是门票价格,Y轴是热度指数,颜色表示评分,直接看出三个维度的关系。
- 游玩时长分布玫瑰图,展示游客在不同景点停留的时长分布。
每种图表在ECharts里都有对应的配置项,但配置的时候有几个关键细节需要特别注意。地图图表的JSON数据需要引入重庆的GeoJSON,这个在网上有标准资源可以下载,导入后设置map: "重庆",然后根据district字段匹配名称即可高亮区域。
5.2 大屏页面搭建与数据渲染
大屏页面的布局采用Grid布局,分成三列:左侧三个图表竖排,中间一个大地图,右侧两个图表。背景色用了深蓝色渐变色,图表标题白色,整体风格偏科技感,适合投到会议室的大屏上。
页面渲染逻辑很简单:页面加载时通过fetch并行请求所有接口,拿到数据后分别实例化对应的图表,最后在window.onresize事件里调用chart.resize()保证自适应。这里有一个很实用的技巧:因为多个接口是并行的,可以同时请求,比串行请求节省大约一半的等待时间。
async function initDashboard() { const [hotData, distData, ratingData, scatterData, timeData] = await Promise.all([ fetch('/api/ranking/hot').then(res => res.json()), fetch('/api/distribution/district').then(res => res.json()), fetch('/api/distribution/rating').then(res => res.json()), fetch('/api/analysis/scatter').then(res => res.json()), fetch('/api/distribution/playtime').then(res => res.json()) ]); initHotChart(hotData.data); initDistrictMap(distData.data); initRatingChart(ratingData.data); initScatterChart(scatterData.data); initPlaytimeChart(timeData.data); }ECharts的异步数据加载是前端开发中常见的坑点。初始化图表时如果数据还没返回,通常会先渲染一个空白图,数据到了再setOption。这里要注意第二次setOption时notMerge参数要不要设成true,如果设为false,第一次的配置会保留,不利于完全刷新图表的场景。
5.3 交互优化与细节处理
大屏不光是静态展示,还要支持一些交互操作。我做的最实用的交互是点击地图上的区县,触发联动筛选。比如点击"渝中区",右侧的热度排行图就会只展示渝中区的景点排名,其他图表也会跟着切换。这个联动通过给地图图表绑定click事件来实现。
myMap.on('click', function(params) { const district = params.name; updateAllChartsByDistrict(district); });在联动更新时,为了避免等待数据延迟导致的视觉闪烁,我在更新前给每个图表加了一个加载动画遮罩。ECharts自带showLoading和hideLoading方法,调用起来非常简单,但这个细节对用户体验的提升非常明显。
还有一个小细节:图表切换时的动画效果。ECharts默认的animationDuration是1000ms,在大屏场景下稍微有点拖沓,我改成了500ms,切换更利落。如果有观众录制视频,动画太快也会显得突兀,这个按实际情况调整。
6. 部署上线与运行维护:如何让系统稳定跑起来
6.1 本地运行与依赖管理
开发环境是Windows,运行时依赖全部写在requirements.txt里。这里有一个实战经验:Python项目一定要用虚拟环境隔离依赖,否则过一段时间后你根本分不清哪些包是项目需要的哪些是全局环境残留的。
pip install -r requirements.txt python app.py依赖清单里有几个版本坑需要提前说明。Flask 2.x版本和Werkzeug存在兼容性问题,如果安装最新版Werkzeug,启动时会报ImportError,解决方法是锁定Werkzeug的版本在2.1以下。这是我在项目配置中临时固定版本的原因。
flask==2.2.5 werkzeug==2.1.2 requests==2.31.0 beautifulsoup4==4.12.2 pymysql==1.1.0 pandas==2.0.36.2 服务器部署与持续运行
部署到服务器时,我是用Gunicorn加Nginx的组合。Flask自带的开发服务器性能弱,不能直接用于生产环境。Gunicorn使用syncworker模式,4个进程,启动命令如下:
gunicorn -w 4 -b 127.0.0.1:5000 app:appNginx做反向代理,把80端口的请求转发到5000端口,同时负责静态文件的直接返回。这样设计的好处是:Nginx处理静态资源的速度比Flask快得多,加载页面时JS和CSS文件不会拖慢应用进程。
服务器部署还有一个容易被忽略的问题:防火墙和安全组。如果是在云服务器上部署,记得在控制台的安全组规则里开放80端口,不然外部用户无法访问。这个坑我踩过一次,排查了很久才发现不是代码问题,而是安全组没放行。
定时爬虫在服务器上用的是crontab,每天凌晨3点执行一次爬虫脚本,避免爬虫抢占白天的带宽资源。
0 3 * * * cd /opt/chongqing_tourism && /usr/bin/python3 scripts/crawl_daily.py >> logs/crawl.log 2>&16.3 定时更新与日志监控
日志监控是应用稳定运行的重要保障。我在爬虫脚本和Flask应用里都加上了日志记录,日志保存到logs/目录,按天分割。日志格式固定为时间 - 等级 - 模块 - 内容,方便后续用grep排查问题。
爬虫日志记录的内容包括:本次抓到多少条数据、新增多少条、更新多少条、耗时多少秒、是否存在异常。接口日志记录的内容包括:请求路径、请求参数、响应状态、耗时毫秒数。这些日志是后期排查问题时的第一手材料。
如果对外提供正式服务,我还会加上一个简单的心跳接口,监控系统每隔5分钟请求一次,连续3次失败就发告警邮件。不过这个系统是内部使用的,这个机制暂时没有启用。
7. 常见问题与避坑记录:一次说清楚
7.1 爬虫与数据处理高频问题速查
| 问题现象 | 原因分析 | 解决方案 |
|---|---|---|
| 爬取返回403 | User-Agent被识别或缺少Referer | 伪造成浏览器请求头,必要时增加Cookie |
| 中文乱码 | 页面编码判断错误 | 指定resp.encoding = "utf-8"或从headers获取charset |
| 部分景点字段为空 | 页面结构不含该字段 | 不做强校验,用pandas填充默认值 |
| 爬几次后IP被禁 | 访问频率过高 | 随机休眠1到3秒,降低单位时间请求数 |
| MySQL插入报编码错误 | 连接未指定utf8mb4 | 连接URL加charset=utf8mb4参数 |
数据清洗阶段最容易踩的坑是单位换算。评论数爬下来是"1.2万",直接用int()会报错,必须先做字符串替换把"万"换成"10000"。这个我在之前的代码示例里已经展示过,但这里想强调一下:类似"万"、"亿"这种中文数字单位,实际项目中还会遇到多种变体,处理时一定要全面考虑。
7.2 Flask与可视化常见问题排查
后端接口返回正常但前端图表不显示,这类问题基本都出在JSON格式与ECharts配置不匹配。我调试时会在浏览器控制台先console.log打印接口返回的数据,用肉眼看一遍数据结构,再检查配置项里data的引用路径。
还有一个很经典的坑:ECharts图表容器没有设置高度。图表容器如果用百分比高度,而父元素没有明确高度时,图表渲染出来高度为0,表现为"什么都没有"。解决方法是给图表容器设置height: 500px或让父容器也撑满高度。
Flask端也有一个常见的坑:调试模式开启后,代码修改会自动重载,但有时代码存在语法错误导致应用直接崩溃,这时候浏览器显示连接被拒绝。解决方案是启动前用python -m py_compile app.py做语法检查。
7.3 独家的三个确定性避坑经验
最后分享几条常规文档里看不到的实操经验。第一,爬虫解析时不要完全相信CSS选择器的class名称,很多平台的页面结构会随着版本迭代改动,建议在解析前先打印一段HTML确认结构,否则很容易在页面更新后莫名报错。
第二,数据库表设计阶段一定要加update_time字段。有了它,排查数据时效性问题时就能分清是数据没更新还是展示逻辑错误。我接触过好几个项目都没有这个字段,出问题后很难定位。
第三,前端图表与后端接口的数据格式约定要提前敲死。我的做法是写一个简单的接口文档,规定所有接口统一返回{code: 200, data: {...}}结构,这样前后端并行开发时互相不会阻塞。强烈建议在项目一开始就和合作方约定好数据格式,不要等到联调阶段再来扯皮。
写在最后
这个系统从零到一跑通大概花了两周,后续维护基本上只需要关注爬虫数据源是否稳定。整个项目的难点不在某个具体技术点上,而在于如何把爬虫、数据库、后端、前端四个环节衔接顺畅。我个人比较满意的设计是爬虫与Web应用完全解耦,以及分析指标的统一口径设计,这两点让系统的扩展性和可维护性都提升了不少。
如果你准备照着这个思路做类似的数据分析可视化系统,我可以给你三点建议。第一,先把数据源确认清楚,花足够多的时间做页面分析,写代码只是最后一小步。第二,强烈建议画出数据流向图再动手,明确每个模块的输入输出,这会让你少写很多无用代码。第三,不要过度设计,你现在需要的技术方案和你想象的技术方案往往差距很大,能在一天内上线的方案就是好方案。
最后再分享一个小技巧:把爬虫得到的数据导出一份CSV到本地,每次页面展示时先用CSV数据做功能验证,等一切正常再接数据库。这样能避免在数据库连接出问题时一直怀疑前端逻辑有bug。下次有人问起我这个系统,我肯定会建议他们先从CSV版本开始,那是最快看到成果的方式。