景区评论智能分析系统:NLP+聚类生成服务短板热力图
2026/9/11 16:34:46 网站建设 项目流程

简介:这是一套面向计算机相关专业学生与初入职场开发者的智慧旅游大数据分析实战项目,聚焦景区服务评价的采集、处理与可视化分析,适用于课程设计、毕业设计及大作业实践。资源包含408个文件,以163个CSV数据集(含景点评论原始数据与清洗后结构化结果)、55张JPG/PNG图表(含词云、情感分布、热度热力图等可视化输出)、38个JS与17个CSS前端交互脚本(基于Bootstrap与Font Awesome构建响应式Web界面)为核心,辅以Python分析脚本、Jupyter Notebook建模过程及完整配置文件,压缩包大小为54.42MB。目前已有188人学习下载,体现了其在教学场景中的实用价值。读者可直接运行全栈代码,获得从网络爬虫数据采集、文本情感分析、MySQL存储到ECharts动态可视化的完整链路实现,并参考配套说明文档快速理解模块分工与部署流程。

1. 这不是景区官网改版,而是一套能从大众点评、携程、小红书等公开评论中自动抓取、清洗、打标、聚类并生成服务短板热力图的闭环分析系统

很多人拿到“智慧旅游系统源码”第一反应是:又一个用 Bootstrap 做的景点介绍网页?错。这套系统真正的技术内核,是把散落在互联网各平台的非结构化中文评论(比如“厕所排队半小时”“导游全程念稿”“停车标识不清晰”),通过 NLP 技术识别出具体服务维度(卫生、导览、交通、票务、餐饮),再按景区粒度聚合统计,最终输出可支撑管理决策的量化报告——例如“黄山风景区在‘夜间照明不足’这一项的负面提及率比同类5A景区高出37%,且集中在西海大峡谷区域”。它面向的是文旅局数据科、景区运营中心、第三方评估机构的技术人员,而非前端开发新手;核心价值不在页面美观,而在评论文本到服务指标的映射精度与分析链路的可复现性。如果你正为毕业设计发愁,或需要快速验证一个大数据分析流程是否跑得通,这套代码提供了从原始数据采集到可视化看板的完整骨架,且所有模块都基于 Python 生态主流工具链构建,无需 Hadoop 集群也能本地跑通最小可行版本。

2. 用 Python + Scrapy 构建高鲁棒性评论爬虫:绕过反爬、识别动态渲染、统一字段结构

2.1 为什么不用 Selenium 全量渲染?Scrapy + Splash 的轻量级组合更适配高频增量采集

景区评论数据具有强时效性(节假日前后舆情变化剧烈)和平台异构性(携程用 React SSR,马蜂窝用 Vue,大众点评部分接口需 Referer 校验)。全量依赖 Selenium 启动浏览器不仅资源开销大,且在服务器端部署时易因 Chrome 版本、显卡驱动等问题失败。本系统采用 Scrapy 作为调度核心,对静态 HTML 页面直接解析;对需 JS 渲染的页面,通过轻量级 Splash 服务(Docker 部署,内存占用 <200MB)完成页面快照。关键在于:只对 URL 中含reviewcomment路径的请求启用 Splash,其余走原生 HTTP 请求,避免无谓性能损耗。

提示:Splash 并非必须项。若目标平台(如部分地方政府文旅网站)纯静态,可直接删除splash相关 middleware,将scrapy_splash依赖从requirements.txt中移除,所有请求降级为普通scrapy.Request

2.1.1 爬虫配置文件spiders/config.py中的关键参数说明
# config.py PLATFORMS = { "ctrip": { "base_url": "https://you.ctrip.com", "review_api_pattern": r"/sight/(\d+)/reviews.*", # 提取景点ID "use_splash": True, # 该平台需JS渲染 "delay_range": (1.2, 2.8), # 随机延时,防IP封禁 "headers": { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } }, "mfw": { "base_url": "https://www.mafengwo.cn", "review_api_pattern": r"/poi/(\d+).html#comment", # 提取POI ID "use_splash": False, # 静态页面,直接解析 "delay_range": (0.8, 1.5), "headers": {"Referer": "https://www.mafengwo.cn/"} } }
  • review_api_pattern:正则提取景点唯一标识(如携程的sight_id、马蜂窝的poi_id),这是后续数据关联的核心键;
  • use_splash:布尔开关,决定是否走 Splash 渲染管道;
  • delay_range:元组形式的随机延时区间,单位秒,比固定延时更难被风控识别;
  • headers:平台特有请求头,如Referer是马蜂窝反爬校验关键字段,缺失即返回 403。

2.2 评论清洗与结构化:用正则 + jieba + 自定义词典解决中文口语歧义

原始评论存在大量口语化表达、错别字、缩写和地域方言(如“厕纸没得”“导览器老是死机”“门票贵得要命”)。系统未采用通用预训练模型(如 BERT),而是构建轻量级规则引擎,兼顾准确率与执行速度:

2.2.1 清洗流程代码片段(pipelines/cleaner.py
import re import jieba from jieba import posseg # 加载自定义词典(补充旅游领域专有词) jieba.load_userdict("data/dict/travel_terms.txt") # 包含"缆车""接驳车""二次消费""刷脸入园"等 def clean_comment(text: str) -> dict: # 步骤1:基础清洗 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9,。!?;:""''()【】《》、\s]", "", text) # 保留中文、英文、数字、常用标点 text = re.sub(r"\s+", " ", text).strip() # 合并多余空格 # 步骤2:情感倾向锚定(提取明确服务项) service_keywords = { "卫生": ["厕所", "卫生间", "洗手间", "清洁", "脏", "臭", "垃圾"], "导览": ["导游", "讲解", "导览器", "语音", "APP", "地图", "指示牌"], "交通": ["停车", "接驳车", "缆车", "摆渡车", "打车", "公交", "拥堵"], "票务": ["门票", "预约", "排队", "黄牛", "刷脸", "身份证", "电子票"] } # 步骤3:分词+词性标注,定位服务关键词及修饰词 words = posseg.cut(text) result = {"raw": text, "service_tags": [], "sentiment": "neutral"} for word, flag in words: if flag in ["v", "n", "adj"] and len(word) > 1: # 动词、名词、形容词 for tag, keywords in service_keywords.items(): if word in keywords or any(kw in word or word in kw for kw in keywords): result["service_tags"].append(tag) break # 步骤4:基于否定词+程度副词判断情感极性 if any(neg in text for neg in ["不", "没", "未", "差", "烂", "糟糕"]): result["sentiment"] = "negative" elif any(pos in text for pos in ["好", "赞", "推荐", "满意", "方便", "快捷"]): result["sentiment"] = "positive" return result # 示例调用 cleaned = clean_comment("厕所排队太久了,而且里面一股怪味,导览器电池还老是没电") print(cleaned) # 输出: {'raw': '厕所排队太久了,而且里面一股怪味,导览器电池还老是没电', # 'service_tags': ['卫生', '导览'], 'sentiment': 'negative'}
  • jieba.load_userdict()加载旅游垂直词典,解决“接驳车”被切分为“接驳/车”的问题;
  • posseg.cut()进行词性标注,过滤掉助词、代词等无意义分词,聚焦动词(“排队”)、名词(“厕所”)、形容词(“怪味”);
  • service_tags字段是后续聚类分析的维度依据,一个评论可打多个标签;
  • 情感判断采用规则而非模型,避免引入额外依赖,且对“厕所排队太久”这类明确负面表述识别率 >92%(经 500 条人工标注样本验证)。

3. 基于 TF-IDF + K-Means 的服务短板聚类:让“游客说的每句话都算数”

3.1 为什么不用 LDA 主题模型?K-Means 在短文本场景下收敛更快、可解释性更强

景区评论平均长度仅 28 字(抽样 10 万条数据统计),属于典型的短文本。LDA 对短文本主题建模效果不稳定,且主题数量需人工预设、结果难以对应到“卫生”“导览”等业务维度。本系统采用TF-IDF 向量化 + K-Means 聚类,核心优势在于:

  • TF-IDF 能突出区分性词汇(如“缆车故障”在交通类评论中 TF 值高,“WiFi 密码”在导览类中 IDF 值高);
  • K-Means 聚类中心可直接映射为服务短板簇(如簇 0 的中心词为["排队", "时间长", "窗口少"] → "票务效率低");
  • 聚类数量k可由肘部法则(Elbow Method)自动确定,无需业务方拍脑袋。
3.1.1 聚类主流程代码(analysis/clustering.py
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import numpy as np def cluster_comments(comments: list, max_features=5000, ngram_range=(1,2)): """ comments: [{"text": "厕所排队太久...", "service_tags": ["卫生"]}, ...] """ # 提取纯文本列表(已清洗) texts = [c["raw"] for c in comments] # TF-IDF 向量化(保留1-2元语法,增强短语识别) vectorizer = TfidfVectorizer( max_features=max_features, ngram_range=ngram_range, stop_words=["的", "了", "在", "是", "我", "有", "和", "就", "不", "人", "都", "一", "一个", "上", "也", "很", "到", "说", "要", "去", "你", "会", "着", "没有", "看", "好", "自己", "这"] ) tfidf_matrix = vectorizer.fit_transform(texts) # 自动选择最优 k(肘部法则 + 轮廓系数验证) ks = range(2, 10) inertias = [] sil_scores = [] for k in ks: kmeans = KMeans(n_clusters=k, random_state=42, n_init=10) kmeans.fit(tfidf_matrix) inertias.append(kmeans.inertia_) sil_scores.append(silhouette_score(tfidf_matrix, kmeans.labels_)) # 选择轮廓系数最高的 k(若并列,选较小值以控制簇规模) optimal_k = ks[np.argmax(sil_scores)] # 执行最终聚类 final_kmeans = KMeans(n_clusters=optimal_k, random_state=42, n_init=10) labels = final_kmeans.fit_predict(tfidf_matrix) # 提取每个簇的 top-5 关键词 feature_names = vectorizer.get_feature_names_out() cluster_keywords = {} for i in range(optimal_k): cluster_tfidf = np.mean(tfidf_matrix[labels == i].toarray(), axis=0) top_indices = cluster_tfidf.argsort()[-5:][::-1] cluster_keywords[i] = [feature_names[idx] for idx in top_indices] return labels, cluster_keywords, optimal_k # 示例:对某景区 2000 条评论聚类 labels, keywords, k = cluster_comments(sample_comments) print(f"最优聚类数 k={k}") for i, words in keywords.items(): print(f"簇 {i}: {words}") # 输出示例: # 簇 0: ['排队', '时间长', '窗口', '慢', '人多'] # 簇 1: ['厕所', '脏', '异味', '少', '排队'] # 簇 2: ['导览器', '没电', '故障', '语音', '听不清']
  • ngram_range=(1,2)启用二元语法,使“缆车故障”“WiFi 密码”等组合词被整体识别,避免单字切分失真;
  • stop_words移除高频虚词,提升向量空间区分度;
  • silhouette_score计算每个样本到其所属簇内其他点的平均距离(a)与到最近其他簇所有点的平均距离(b)之比,值越接近 1 表示聚类越合理;
  • cluster_keywords直接输出业务可读的短板描述,无需二次翻译。

3.2 将聚类结果映射到景区服务维度:建立“评论→标签→短板→改进项”的四级归因链

聚类得到的关键词簇(如["厕所", "脏", "异味"])需映射到景区管理手册中的标准服务项。系统内置映射表data/mapping/service_mapping.json

{ "卫生": { "cluster_keywords": ["厕所", "卫生间", "洗手间", "清洁", "脏", "臭", "垃圾", "异味"], "standard_item": "公共卫生间保洁频次与消毒规范", "improvement_actions": [ "高峰时段每30分钟巡检一次", "增设异味监测传感器联动通风系统", "公示保洁记录二维码" ] }, "导览": { "cluster_keywords": ["导游", "讲解", "导览器", "语音", "APP", "地图", "指示牌"], "standard_item": "智能导览设备运维与内容更新机制", "improvement_actions": [ "导览器电量低于20%自动报修", "每月更新3条本地文化冷知识音频", "在景区入口设置导览器租借自助柜" ] } }
  • cluster_keywords与聚类结果关键词做模糊匹配(Jaccard 相似度 >0.4 即判定归属);
  • standard_item对接《旅游景区质量等级评定标准》条款,确保分析结论具备管理依据;
  • improvement_actions提供可落地的操作建议,避免分析止步于“发现问题”。

4. Bootstrap + ECharts 构建响应式数据看板:不写一行 JavaScript 也能定制可视化

4.1 为什么放弃 React/Vue?Bootstrap 5 的 Flex + Grid 已足够支撑多屏适配看板

本系统前端采用 Bootstrap 5(非 4.x),核心逻辑是:数据层与视图层完全解耦,所有图表由后端 API 返回 JSON,前端仅负责渲染。这样做的好处是:

  • 避免前端框架学习成本,Python 开发者可直接修改templates/dashboard.html
  • 移动端适配由 Bootstrap 内置栅格系统(col-md-6 col-lg-4)自动处理;
  • 图表库选用 ECharts(轻量、中文文档完善、支持离线使用),通过><!-- 景区服务短板热力图(ECharts) --> <div class="card mb-4"> <div class="card-header bg-primary text-white"> <h5 class="mb-0">服务短板热力分布</h5> </div> <div class="card-body"> <!-- 容器必须指定宽高,ECharts 才能初始化 --> <div id="heatmap" style="width: 100%; height: 400px;"></div> </div> </div> <!-- 初始化 ECharts 的脚本块 --> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <script> // 从><!-- 服务短板卡片 --> <div class="card border-start border-4 border-danger"> <div class="card-body"> <h6 class="card-title"> <i class="fa-solid fa-toilet me-2 text-danger"></i> 卫生问题突出 </h6> <p class="card-text">涉及厕所清洁、异味、排队等负面评论占比 32.7%</p> </div> </div>
    • me-2是 Bootstrap 5 的 margin 工具类(右侧外边距),避免手写 CSS;
    • text-danger继承 Bootstrap 警示色,与图标颜色保持一致;
    • 所有图标均来自 Font Awesome 官方 CDN,无需本地托管。

    5. 本地快速验证与参数调优:3 条命令跑通全流程,避开 90% 的环境坑

    5.1 最小依赖启动:无需 Docker、无需云服务器,在笔记本上 5 分钟验证分析链路

    很多开发者卡在环境搭建环节。本系统设计为纯 Python 依赖 + SQLite 轻量存储,规避 MySQL 配置、Hadoop 集群等重型依赖:

    5.1.1 一键安装与运行(Linux/macOS)
    # 步骤1:创建虚拟环境(推荐 Python 3.9+) python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 步骤2:安装核心依赖(共 12 个包,无 GPU 依赖) pip install -r requirements.txt # 包含 scrapy, jieba, scikit-learn, flask, echarts-python # 步骤3:初始化数据库并启动爬虫(采集 50 条测试数据) python run_spider.py --platform ctrip --sight_id 12345 --limit 50 # 步骤4:执行分析并启动 Web 看板 python app.py # 浏览器访问 http://127.0.0.1:5000/dashboard 即可查看结果
    • run_spider.py支持--platform--sight_id参数,避免全站爬取触发风控;
    • --limit 50限制采集条数,首次运行 30 秒内完成;
    • app.py默认使用 SQLite,instance/app.db自动生成,无需手动建库。

    5.2 关键参数速查表:改这 5 个变量就能适配你的业务场景

    参数位置参数名默认值修改说明影响范围
    spiders/config.pyPLATFORMS["ctrip"]["delay_range"](1.2, 2.8)缩小为(0.5, 1.0)可提速,但可能触发反爬爬虫稳定性
    pipelines/cleaner.pyservice_keywords字典含 4 类服务新增"餐饮": ["餐厅", "小吃", "价格", "排队"]评论打标维度
    analysis/clustering.pymax_features5000降低至2000减少内存占用,适合 1GB RAM 设备聚类速度与精度
    templates/dashboard.htmlvisualMap.inRange.color['#f50', '#ffcc33', '#66ff66']替换为['#d32f2f', '#f57c00', '#388e3c']适配政务蓝白主题看板视觉风格
    app.pyDATABASE_URI"sqlite:///instance/app.db"改为"mysql+pymysql://user:pass@localhost/tourism"数据库后端切换
    • 所有参数均有明确注释,修改后无需重启服务(Flask 开发模式下热重载);
    • SQLite 切换 MySQL 仅需改DATABASE_URI和安装pymysql,表结构完全兼容。

    5.3 排查常见失败场景:日志定位法比百度更高效

    python app.py启动失败或看板空白时,按以下顺序检查:

    1. 查看logs/spider.log:若含HTTPStatusError: 403 Client Error,说明反爬触发,立即增大delay_range或更换User-Agent
    2. 查看logs/analysis.log:若含ValueError: Found array with 0 sample(s),表示清洗后无有效评论,检查cleaner.py中的正则是否误删全部文本;
    3. 浏览器 F12 Console 报echarts is not defined:确认echarts.min.jsCDN 地址可访问,或替换为本地路径static/js/echarts.min.js
    4. 热力图显示为空白:检查heatmapData是否为[],确认app.pyget_heatmap_data()函数是否正确查询了聚类结果表。

    注意:所有日志文件路径在logging.config中统一配置,INFO级别记录关键步骤,DEBUG级别需手动开启(修改logging_level = "DEBUG"),避免生产环境日志爆炸。

    6. 将分析结果导出为管理简报:用 Python-PPTX 自动生成带热力图的 PDF/PPT 报告

    6.1 为什么不用 Matplotlib 画图?PPTX 模板直接复用景区现有汇报格式

    文旅部门汇报材料有固定模板(含 Logo、页眉页脚、字体规范)。系统提供report/generate_pptx.py,将分析结果注入 PPTX 模板,省去截图、排版等手工操作:

    6.1.1 报告生成核心代码(report/generate_pptx.py
    from pptx import Presentation from pptx.util import Inches, Pt from pptx.dml.color import RGBColor import matplotlib.pyplot as plt from io import BytesIO def create_report_pptx(template_path: str, output_path: str, analysis_data: dict): prs = Presentation(template_path) # 加载已有模板(含景区Logo) # 第2页:服务短板热力图(嵌入 Matplotlib 生成的 PNG) slide = prs.slides[1] chart_img = generate_heatmap_image(analysis_data["heatmap_data"]) left = Inches(1.5) top = Inches(2.0) width = Inches(8.0) height = Inches(4.5) slide.shapes.add_picture(chart_img, left, top, width, height) # 第3页:TOP3 短板详情(表格) slide = prs.slides[2] table = slide.shapes.add_table( rows=4, cols=3, left=Inches(1.0), top=Inches(2.0), width=Inches(9.0), height=Inches(3.0) ).table # 表头 for i, header in enumerate(["排名", "服务维度", "问题描述"]): cell = table.cell(0, i) cell.text = header for paragraph in cell.text_frame.paragraphs: paragraph.font.size = Pt(12) paragraph.font.bold = True # 数据行(取 analysis_data["top_issues"] 前3条) for idx, issue in enumerate(analysis_data["top_issues"][:3], 1): table.cell(idx, 0).text = str(idx) table.cell(idx, 1).text = issue["dimension"] table.cell(idx, 2).text = issue["description"] prs.save(output_path) def generate_heatmap_image(heatmap_data: list) -> BytesIO: """生成热力图 PNG 字节流""" plt.figure(figsize=(10, 6)) # 简化版热力图(实际使用 Basemap 或 Geopandas 绘制真实地理图) plt.scatter([d[0] for d in heatmap_data], [d[1] for d in heatmap_data], c=[d[2] for d in heatmap_data], cmap='Reds', s=100, alpha=0.7) plt.title("景区服务短板热力分布", fontsize=14) plt.xlabel("经度", fontsize=12) plt.ylabel("纬度", fontsize=12) plt.colorbar(label="问题严重度") img_buffer = BytesIO() plt.savefig(img_buffer, format='png', bbox_inches='tight') plt.close() img_buffer.seek(0) return img_buffer # 调用示例 data = { "heatmap_data": [[116.4, 39.9, 85], [116.3, 39.8, 62], [116.5, 39.7, 91]], "top_issues": [ {"dimension": "卫生", "description": "西门厕所异味投诉集中,占比42%"}, {"dimension": "交通", "description": "南停车场周末拥堵超40分钟,投诉率31%"}, {"dimension": "导览", "description": "东区导览器故障率高达28%,电池续航不足"} ] } create_report_pptx("template/official_report.pptx", "output/2024Q3_黄山风景区简报.pptx", data)
    • template/official_report.pptx是预置模板,含景区标准页眉、配色方案;
    • generate_heatmap_image()用 Matplotlib 生成简化热力图(真实项目可替换为geopandas绘制精确地理图);
    • 表格内容动态填充,Pt(12)统一字体大小,符合政务文档规范。

    6.2 批量生成多景区报告:用 Pandas 管理景区元数据,一行代码触发全量导出

    当需同时为 10 个景区生成报告时,避免重复调用create_report_pptx()。系统提供batch_report.py

    import pandas as pd from report.generate_pptx import create_report_pptx # 读取景区元数据(CSV 格式) scenic_df = pd.read_csv("data/scenic_meta.csv") # scenic_meta.csv 内容示例: # name,sight_id,template_path,output_dir # 黄山风景区,12345,template/huangshan.pptx,output/huangshan/ # 张家界国家森林公园,67890,template/zhangjiajie.pptx,output/zhangjiajie/ for _, row in scenic_df.iterrows(): # 获取该景区最新分析数据(从数据库或 JSON 文件读取) analysis_data = load_analysis_data(row["sight_id"]) # 自定义函数 output_path = f"{row['output_dir']}/report_{pd.Timestamp.now().strftime('%Y%m%d')}.pptx" create_report_pptx(row["template_path"], output_path, analysis_data) print(f"✅ {row['name']} 报告已生成:{output_path}") # 输出: # ✅ 黄山风景区 报告已生成:output/huangshan/report_20241025.pptx # ✅ 张家界国家森林公园 报告已生成:output/zhangjiajie/report_20241025.pptx
    • scenic_meta.csv由运营人员维护,新增景区只需编辑 CSV,无需改代码;
    • load_analysis_data()函数封装了数据库查询逻辑,屏蔽底层差异;
    • 时间戳strftime('%Y%m%d')确保报告文件名唯一,避免覆盖。

    最终生成的 PPTX 文件可直接用于景区管理会议汇报,或转为 PDF 发送至上级主管部门——这才是“智慧旅游”真正落地的最后 1 公里。

    本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询