Django+ECharts构建景区热度看板:数据采集到可视化全流程

发布时间:2026/10/18 12:56:55
Django+ECharts构建景区热度看板:数据采集到可视化全流程 1. 一个偏静态的景点列表怎么变成实时可读的景区热度看板——项目整体设计先说项目背景。当时我接到一个需求某旅游信息服务平台打算做一轮热门旅游景点专题内容手里其实并不缺数据——基础景点名录、用户评分、评论文本、售票渠道的预订量零零散散都有一点但这些数据分布在不同的Excel表、第三方接口和数据库里每次做专题都要让运营同学手工整理一个景点一个景点地核对效率低且口径不统一。领导层希望能有一个内部系统输入城市或景点名称就能快速看到这个景点的热度趋势、用户评价关注点以及同类景区排名。说白了就是要一个旅游景点热度数据分析与可视化后台。于是我用Django搭了整套系统前后端分离后端提供数据API前端用ECharts渲染图表数据层接了MySQL和Redis采集端用Scrapy定时抓取公开数据分析端用pandas做离线计算。整个项目从需求确认到第一版可用大概花了三周核心功能包括景点基础信息管理、热门度自动计算、多维度数据可视化趋势折线图、城市分布地图、评论关键词词云、景区类型占比饼图以及PDF报告导出。这套系统上线之后运营同学查一个景点的热度不用再翻三张表格输入景点名点一下三秒内出结果。而我个人最大的感悟是这个项目真正有价值的地方不在可视化大屏本身而在那个热门度结果是怎么算出来的——数据清洗口径、权重设计、更新策略这些才是决定系统可用性的关键。下面我把整个建设过程拆开讲从数据采集、热度模型、Django性能优化到可视化落地每一步都有可以直接参考的实现细节。2. 数据采集方案公开数据集为主、定向补采为辅的双轨策略2.1 先想清楚要哪几张表动手写爬虫之前我先把目标数据实体理清了。旅游景点数据分析最核心的表一定是景点表但它自己撑不起热门这个结论必须搭配几个外围表一起统计。最终我设计了五张核心表表名主要字段数据来源更新频率scenic_spots景点ID、名称、城市、省份、经纬度、类型自然/人文/主题乐园等、开放时间、票价区间公开名录数据集每周spot_reviews景点ID、用户评分1-5分、评论内容、评论时间抓取公开评论每天增量spot_visits景点ID、当日预约量/售票量、日期合作渠道接口每天spot_rankings景点ID、榜单类型、排名、上榜时间平台榜单页面每天city_dic城市ID、城市名、省份、行政区级别行政区划库月度这里的核心思路是把静态信息和动态数据分开。景点名称、经纬度、类型这些一年变不了几次没必要天天抓而评分、评论、预约量这些高频变化的数据才需要走增量更新。设计上分开存储也为后面的热度计算提供了清晰的数据源。2.2 采集端的实操方案采集工作我并没有全用Scrapy裸写而是分了两路一路是公开数据集导入。很多省市文旅部门、开放数据平台都会发布A级景区名录、星级景区名单字段相对规整。我按月下载这些名录清洗后导入scenic_spots表。这里有个小坑同一个景区在不同名录里写法不一致比如黄山风景区有的写成安徽黄山有的带国家级风景名胜区后缀。我的处理方法是对景点名做规范化——去空格、统一繁体简体、去掉行政前缀后做一次去重合并再人工抽检200条确保合并质量。另一路是定向爬虫补采动态数据。评分和评论从公开评论站点抓预约量从对接的渠道接口拉榜单数据解析页面结构获取。因为旅游平台的反爬策略比较敏感我没有做高并发而是用Scrapy默认的并发设置加上随机延迟中间件每两个请求之间随机延迟3到8秒每天定时任务在凌晨跑单次任务控制在数十分钟内完成跑了一个月没有出现过封禁。# 随机延迟中间件片段 import random from scrapy import signals class RandomDelayMiddleware: def process_request(self, request, spider): delay random.uniform(3, 8) time.sleep(delay) return None提示如果你也想抓公开评论数据尽量选择有明确API或开放协议的来源比如某些平台提供了开发者接口申请通道走正规接口比硬爬页面稳定得多。这里不具体指某个平台但原则上先找API再考虑爬HTML。2.3 清洗规则里最容易被忽略的三个细节数据清洗这件事我踩了不少坑说三个印象最深的第一个是评分的归一化问题。不同来源的评分口径完全不一样有的平台是5分制有的是10分制还有的显示4.5分实际上是4.48四舍五入。清洗时我统一规定所有评分先转成浮点数再映射到百分制原始分÷满分×100存储时保留两位小数。这样后面热度模型里做加权就不会出现一个5分制平台的4.8分和一个10分制平台的9.2分打架的情况。第二个是评论去重。同一段用户评论可能被不同渠道转载直接统计关键词频率会把重复内容算成高热度。我做了一层基于内容hash的去重评论正文先做jieba分词取前50个词的词序列做MD5如果hash相同就只保留最早那条。第三个是缺失值处理策略。经纬度缺失的景点我调用了一个本地离线坐标系转换库根据地名粗略补充票价缺失的不填0而是填NULL让统计层单独处理——因为票价0可能代表免费景观也可能就是没采集到混为一谈会导致分析结果失真。3. 热门度模型评分、客流、舆情三要素的加权计算3.1 为什么不能直接用评分最高的就是最热门的这是整个项目里我花时间最多也是我觉得最有价值的一部分。最开始运营同学的需求很简单把评分最高的前50个景点列出来就行。但实操几天就发现问题了一个4.9分但只有32条评论的小众景点和一个4.7分但有两万条评论、每天预约量排全省前五的知名景区哪个更热门显然是后者。评价类数据的样本量本身就是一个信号——评论多说明去的人多、讨论的人多。直接按评分排序等于把口碑好和受欢迎混为一谈。所以我构建了一个综合热度指数把三个维度的数据合成一个0到100的分值口碑维度基于用户评分的加权平均值权重40客流维度基于景区预约量/售票量的近期趋势权重40舆情维度基于评论数量级和评论情感倾向权重20这个权重分配当时是和各渠道运营讨论后定的原因是对于热门这个概念实际客流和行为数据比态度数据更有说服力——有人去了、有人在讨论比单纯说一句好更有热度含金量。3.2 各维度得分的具体算法口碑评分40分制取最近90天所有评分的加权平均时间越近权重越高。具体做法是把评论按周分组最近一周权重1.0、前一周0.9、再往前0.8……最多回溯13周。这样处理能避免一年前被刷了一波好评几个月后热度还在虚高的问题。import pandas as pd import numpy as np def compute_reputation_score(reviews_df): # reviews_df: 包含评论时间、评分百分制 reviews_df[review_time] pd.to_datetime(reviews_df[review_time]) reviews_df[week_diff] ((pd.Timestamp.now() - reviews_df[review_time]).dt.days // 7).clip(0, 12) reviews_df[weight] 1.0 - reviews_df[week_diff] * 0.1 weighted_avg np.average( reviews_df[score], weightsreviews_df[weight] ) return round(weighted_avg * 0.4, 2) # 口径百分制评分×40%客流评分40分制这个维度用的不是当天的绝对值而是近30天日均预约量与去年同期日均量如果没有去年数据就用近90天均值的比值。这样设计的原因很实际不同规模的景区基础客流差异巨大故宫和一个小型博物馆比绝对值没有意义比增长趋势更能体现当前热度。比值大于1.2说明景区近期明显走热得满分区间的高位比值在0.8到1.2之间是平稳期低于0.8则得分下降。另外为了防止节假日数据造成突刺我还会把国家法定节假日这一天的数据单独剔除用前后三天的均值替代。舆情评分20分制这个维度分两部分。第一部分是评论量的对数归一化——评论数取log10之后做一个min-max映射这个处理很重要因为热门景区的评论量级从几百到几十万都有如果不取对数直接线性归一化评论量少的景区基本全得0分。第二部分是情感倾向用SnowNLP对评论摘要做情感判断计算正面评论占比。两部分各占10分。import math from snownlp import SnowNLP def compute_sentiment_score(comment_text): s SnowNLP(comment_text) return s.sentiments # 0~1 之间的情感积极度最终热度指数就是这三个得分加总。为了验证这个模型没有跑偏我拿历史三周的数据跑了一遍把排名结果和当时平台自己的人工编辑推荐做了对比Top 20重合率大约85%说明模型和人工判断高度一致剩下的15%反而是模型发现了人工没注意的上升期景点这让我对这套算法有了信心。3.3 离线计算与实时查询的配合热度模型不是每次请求页面时现算的——那样性能上完全撑不住。我的做法是每日凌晨用pandas批量计算全量景点的热度分写入一张hotspot_scores表白天所有API都只查这张表。为了支持当天数据也能反映的场景我给客流维度预留了一个实时修正参数白天每隔两小时拉一次当天预约量用EMA平滑方式叠加到当天的预估热度上。这样既保证了计算成本可控又不至于让看板上的数据看起来是昨天的。4. Django对大数据量的承接层设计ORM优化、缓存策略与异步任务4.1 最初版本为什么连数据库都扛不住系统第一版是比较原始的写法——页面访问时直接ORM查找所有景点数据在前端模板里做循环拼接图表配置。结果数据量到了十几万条评论、几千个景点的时候页面打开就要3秒以上。我后来分析发现主要原因有三个第一查询没有走索引。spot_reviews表按spot_id查询评论时字段上没建联合索引导致全表扫描。第二N1查询广泛存在。列表页展示50个景点时每个景点又单独查一次评分、一次评论数、一次预约量总共150多次数据库往返。第三图表配置在模板里拼JSON每次请求都要重新序列化一遍几千个点的大数组。这三个问题加起来数据量一大系统就变成了能跑但不能用。4.2 三板斧索引优化、聚合缓存、异步预热先建索引。这一步最简单也最有效果。给常用的查询维度建了联合索引评论表建spot_id, review_time复合索引预约量表建spot_id, visit_date复合索引热度分表建spot_id, calc_date复合索引。建完之后单点景区的评论查询从几百毫秒降到了十几毫秒。再上缓存。热门度API是读多写少的典型场景我直接用Django自带的cache框架接Redis做后端。按景点详情维度缓存热度分和聚合摘要过期时间设为30分钟。这样最热的接口景点详情页的体验可以稳定在100毫秒以内。from django.core.cache import cache def get_spot_hotscore(spot_id): cache_key fhotspot_score:{spot_id} score cache.get(cache_key) if score is not None: return score score HotspotScore.objects.filter( spot_idspot_id, calc_datedate.today() ).values(score, rank, trend).first() cache.set(cache_key, score, timeout1800) return score最后把重计算全部挪到异步任务。热度模型计算、评论情感分析、PDF报告生成这类耗时的操作统一放到Celery异步队列里。Web请求只负责查已算好的结果绝不触发重计算。4.3 大数据量下的分页策略游标分页而不是页码分页景点列表页最开始用传统页码分页offset在数据量大时会越来越慢。我改成了游标分页前端把最后一次返回的景点热度分和景点ID传给后端后端用where条件直接定位下一页起点。这样即使有几万个景点翻到最后一页的速度也和第一页差不多。def cursor_paginated_spots(request): last_score float(request.GET.get(last_score, 1000)) last_id int(request.GET.get(last_id, 0)) qs HotspotScore.objects.filter( calc_datedate.today(), score__ltlast_score ).exclude(spot_idlast_id).order_by(-score, spot_id)[:20] return qs注意游标分页适合滚动加载的后台看板但不适合我想直接跳到第50页的场景。如果你需要在页面上显示页码跳转那还是老老实实用页码分页但一定要在order_by字段上建好索引。4.4 本地测试与生产环境数据量差异导致的坑开发环境我用的SQLite当时查询都很快换到生产MySQL后发现接口慢了一倍不止。排查之后才发现是开发环境SQLite不区分大小写排序而MySQL按utf8mb4_bin时大小写敏感order_by的排序结果和开发环境不一致导致缓存命中率下降。后来我把开发环境也切到MySQL的Docker容器并在最开始的配置里就统一了字符集排序规则utf8mb4_general_ci这类问题就再没出现过。这个教训让我养成了一个习惯新项目一开始就把开发、测试、生产三套环境的数据库类型保持一致省后续很多排查精力。5. 可视化落地的关键细节图表选型、数据口径与交互设计5.1 为什么选了ECharts而不是其他可视化库对比了ECharts、Plotly、AntV G2、D3.js之后我最终选了ECharts。核心原因有三点一是它对后端工程师最友好。ECharts的配置项是纯JSON对象后端把统计结果序列化给前端前端直接setOption渲染不需要像D3那样自己操作SVG元素开发效率高得多。二是地图能力开箱即用。城市分布地图用ECharts内置的geo组件配一份GeoJSON就能展示省份热力图不用额外引地图库。三是大屏适配经验成熟。ECharts配合CSS的rem/rem适配方案能比较轻松地把图表缩放到不同分辨率的屏幕上。5.2 四个图表的落地过程城市分布地图数据层先按省份聚合景点热度分输出形如[{name: 浙江省, value: 86.5}, ...]的数组前端用geo组件渲染。这里有一个关键点景点经纬度不能直接用来在地图上打散点因为热门景区会相对集中比如一个城市有多个5A景区直接打点会大面积重叠。我的做法是先用DBSCAN聚类把同一城市的景点热度合并再按城市中心点打点点的大小映射热度值。热度趋势折线图按天聚合热度指数输出最近30天的序列。这个图本身很简单但我额外加了一条7日移动平均线因为单日热度数据受周末和天气影响抖动很大移动平均线能让运营同学一眼看出趋势是上升还是下降。评论关键词词云每天定时任务把当天新增评论汇总后做jieba分词过滤掉停用词统计词频Top 100写入一张keyword_freq表。前端用词云图组件渲染。这个功能运营反馈很好能快速看到用户对一个景区的核心评价维度——排队夜景亲子性价比这些词一目了然。景区类型占比饼图这个图最简单统计每种景点类型的数量占比但加了一个联动逻辑——点击饼图的某个类型扇区下方的景点列表会自动筛选出该类型的热门景区。这个交互让原本只展示数据的饼图变成了一个筛选器后面我越用越觉得可视化图表如果只是被动展示数字价值就打折了带交互的图表才能真的帮用户做信息筛选。5.3 数据口径统一前端绝不自己算聚合项目初期有个前端的同学为了方便直接在前端对后端返回的明细数据做reduce求和结果出现了一个Bug饼图占比总和是102%。排查后发现是浮点数累计求和导致的精度误差再加上部分数据被重复统计。后来我定了一个规矩所有汇总、占比、聚合逻辑一律在后端和离线计算中完成前端只负责渲染后端给的结果。后端保证所有百分比之和精确等于100%比例值不做四舍五入最后一项用100减去前面的值倒推。这条规矩执行之后数据一致性问题基本绝迹。5.4 大屏适配与性能的取舍做了几个大屏页面之后我深刻体会到——旅游热度看板这类系统实际使用场景不是市中心的大屏而是运营工位的普通显示器。所以我在设计时没有刻意做1920x1080的硬适配而是采用了响应式方案图表容器宽度用百分比字体用rem最小适配宽度1200px。刷新策略也做过调整地图和词云这类重渲染组件手动刷新按钮趋势图和排行榜则允许30秒自动刷新。这样避免所有图表同时轮询接口减轻后端压力。6. 部署上线后的性能排查与日常运维6.1 上线第一天就暴露的数据库连接泄漏问题系统上线第一天中午就收到告警数据库连接数飙到80%以上。我这边部署用的是gunicorn多进程每进程都维持着自己的连接池服务一启动几十个进程加上Celery worker的预创建连接一下就把MySQL连接数吃满了。这个问题的标准解法是限制连接池大小并为每个进程设置合理上限。我用的是django-db-connection-pool库每进程连接池设为5最大溢出3配合gunicorn的--workers 4参数把总连接数控制在了40个以内告警解除。6.2 Redis缓存穿透与热点集中在一次热搜事件中的处理上线第二周遇到一个特殊情况某个景点因为综艺节目取景突然爆火运营同学在后台反复查询这个景点的热度趋势接口日志显示Redis缓存一直未命中。查了之后发现缓存过期30分钟的设计在热点数据上失效了——每一次缓存过期后如果紧接着有多个并发请求同时到达Django中简单的get_or_create写法会导致大量请求一起打到数据库。处理办法是给热点查询加互斥锁上锁后只允许一个请求去查库回填缓存其他请求短暂等待并且把这种综艺爆火型景点的热点缓存设置为永不过期每天凌晨定时重建一次。这样处理之后即使同一个景点被反复高频查询也不会压垮数据库。import redis import time r redis.Redis() def get_spot_detail_cached(spot_id): cache_key fspot_detail:{spot_id} data r.get(cache_key) if data is not None: return data lock_key fspot_detail_lock:{spot_id} if r.set(lock_key, 1, nxTrue, ex5): try: data query_detail_from_db(spot_id) r.set(cache_key, data, ex1800) finally: r.delete(lock_key) return data time.sleep(0.1) return r.get(cache_key)6.3 每日定时任务的依赖链管理数据采集、清洗、热度计算、图表数据预生成这几个任务是典型的串行依赖关系。我一开始用Celery的定时任务各自独立调度结果经常出现热度计算跑完了但评论统计还没更新这种数据不同步问题。后来我把整个流程改成了一个工作流方式采集任务完成后发一个信号触发数据校验任务校验通过后启动清洗入库清洗完成后再触发热度计算依次执行。核心实现上其实就是一套基于数据库状态的任务编排表每步任务往表里写状态下一步定时检查状态位。这个编排方式比较轻量比直接引入Airflow要简单不少实际执行下来稳定性很高。我个人的体会是中小规模的数据项目没必要一上来就上重型调度框架先把依赖关系用状态表管理清楚反而运维成本更低。7. 踩坑记录从评分被刷到节假日数据突刺五个真实问题整个项目从开发到上线积累的踩坑记录差不多有二十多条我把其中最有借鉴价值的五个拿出来复盘。第一个坑评分数据被好评返现污染。某渠道的评论区有段时间大量出现固定文案的好评。清洗时我没做文本去重导致该景点口碑分虚高。后来我在评分清洗流程中加了一条规则文本相似度超过95%的评论自动降权为原权重的20%避免垃圾行为干扰统计。第二个坑节假日数据突刺。五一假期那几天不少景区的客流数据直接翻了几倍热度模型把节假日临时高峰算成了长期热度上涨导致假期结束后排名还在高位停留了好几天。后来加上了节假日识别规则所有法定节假日以及前后各一天的数据都单独做平滑处理不参与趋势计算。第三个坑时区问题导致预约量提前消失。渠道接口返回的日期字段是UTC时区而MySQL存储用的是东八区导致每天晚上8点以后的数据会被算到第二天每天的当日客流统计少一块。修复很简单入库前统一做时间转换接口返回的UTC时间先转换到Asia/Shanghai再存库。第四个坑词云图里出现了景区名以外的乱码词。评论分词后有些网络用语、生僻地名被当成了关键词高频出现词云变得不可读。后来维护了一个自定义停用词表同时把景区名称表中的词加入jieba的用户词典让景区名在分词时被完整识别不会被切开或者被过滤掉。第五个坑排行榜排名跳变。因为热度分计算涉及评价、客流、舆情三个维度某个维度更新很晚时排名会连续跳变用户容易困惑——这个景点怎么突然从第三变成了第一后来我规定热度分在当天计算完成后锁定当天内不因数据增量更新而重算只允许第二天的全量任务刷新。看板上的排名稳定了用户反馈也正常了。最后说几句实话这套系统上线运行了几个月目前每日调度稳定运营团队日常使用正常。我自己的体会是这个项目真正难的从来不是用Django写接口或者配置ECharts这种单点技术而是把数据采集、清洗口径、热度模型、性能优化、前端交互这些环节串成一个闭环。如果你也想做一个类似的项目我的建议是先不要急着写代码花两天时间把你想要分析的热门定义清楚——到底是按评分、按客流、还是按讨论度权重怎么分配数据从哪来口径怎么统一这些问题想清楚之后技术实现反而是最顺手的一环。另外开发环境尽量和生产环境保持一致的数据库类型这种不起眼的细节往往决定了后续排障的天花板。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询