ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python与Django的旅游城市关键词分析系统实现指南

基于Python与Django的旅游城市关键词分析系统实现指南 简介基于PythonDjango的旅游城市关键词分析系统毕业设计文档面向计算机专业学生、Web开发者及需要搭建旅游信息检索系统的项目人员针对海量信息中高效获取城市旅游数据的问题提供一套完整设计思路与实现方案。围绕B/S架构通过Django框架实现关键词搜索、旅游景点与美食信息查询并运用Python可视化库与知识图谱技术展示不同城市的旅游资源分布提高信息检索效率与对比直观性。文档以docx格式呈现包含绪论、国内外研究现状、相关开发技术介绍、系统设计、功能实现等章节完整覆盖项目从背景分析到编码落地的关键环节。资源包共1个文件大小2.21MB适合作为毕业设计、课程论文或项目启动参考资料。已有131人学习可作为选题方向或写作结构的有效参考。1. 基于Python与Django的旅游城市关键词分析系统解决的不只是“爬虫分词”旅游城市关键词分析这类题目在毕业设计和中小型项目里出现频率很高但大多数人做完之后只得到一个“能跑”的结果爬了几千条评论用jieba分出词画个词云Django页面上展示一下然后就没有然后了。实际上这个标题的核心难点不在于某个单独环节而在于“旅游数据”这个垂直场景对分析链路有特殊要求——同一座城市游客、本地人、商家、自媒体写出来的文本用词习惯完全不同直接把通用停用词表和TF-IDF套上去出来的关键词往往是大而空的地名和“真的”“感觉”这类水词。这篇文章不预设你有现成代码按照“数据获取→分词建模→Django落地→部署排错”这条线把一套可复现的方案完整讲透。适合正在做相关选题的学生也适合想快速搭建文本分析展示系统的工程人员。2. 数据准备与Django项目初始化先把旅游文本的获取链路理清2.1 旅游数据源的选择逻辑与爬虫策略旅游城市关键词分析的第一步不是写爬虫而是确认从哪个数据源采集什么文本。常见做法是抓取OTA平台的城市攻略、景点评论和短评这三类文本的语体差异非常大攻略正文以陈述和推荐为主长句多关键词密度低但语义完整短评和点评则是典型的口语化短文本“赞”“美”“坑”这样的单字表达很多分词时容易丢掉情感信息。建议至少覆盖两类数据源否则分析结果会偏向某一种语体缺乏代表性。采集层用requests加BeautifulSoup就够了没必要在这类扁平数据源上引入Scrapy。需要注意两点一是请求频率必须控制简单的做法是每次请求后sleep一个随机间隔二是必须保存原始文本和来源标记为后续“按评论类型对比关键词”预留字段。存储上这个阶段的数据建议直接经过清洗后写入SQLite之后让Django的ORM来读。2.2 用Django的ORM设计数据表而不是先建数据库表很多人在这个阶段会先去Navicat建表再反向生成models这个习惯在单人项目里没有问题。但在这个项目里我建议先写models再迁移因为分析结果要临时存储多张统计表用ORM的数据迁移机制可以随时调整字段。# tourism/models.py from django.db import models class City(models.Model): name models.CharField(max_length50, uniqueTrue) province models.CharField(max_length50, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class RawComment(models.Model): SOURCE_CHOICES ( (guide, 攻略), (comment, 短评), ) city models.ForeignKey(City, on_deletemodels.CASCADE) source models.CharField(max_length10, choicesSOURCE_CHOICES) content models.TextField() weight models.FloatField(default1.0) # 来源权重短评和攻略的文本价值不同 created_at models.DateTimeField(auto_now_addTrue) class KeywordStat(models.Model): city models.ForeignKey(City, on_deletemodels.CASCADE) keyword models.CharField(max_length50) tfidf models.FloatField() textrank models.FloatField() freq models.IntegerField() year models.IntegerField(default2024) class Meta: unique_together (city, keyword, year)这里的关键设计是RawComment表里的weight字段。攻略正文可能是几百字的完整描述短评只有十几个字如果直接拼在一起做词频统计长文本中的词天然占优势。把类别的文本长度归一化后用weight调整每条内容对词频的贡献比单纯按条数统计更符合信息量逻辑。迁移命令在主项目目录执行python manage.py makemigrations tourism python manage.py migrate python manage.py shell迁移成功后进入shell下一步做数据入库测试。批量写入RawComment时注意不要用循环单条save应该用bulk_create()否则几百条数据就要等很久。写入完成后后续所有分析都基于RawComment.objects来聚合而不是重新扫描原始数据。3. 关键词提取核心jieba分词、TF-IDF与TextRank的组合使用3.1 分词前必须处理的两个问题自定义词典和停用词表旅游场景下通用jieba词典存在明显的盲区。具体表现是“鼓浪屿”“南锣鼓巷”“宽窄巷子”这类专有地名可以被正确切分但“网红打卡”“Citywalk”“出片”这类近年高频出现的新词、网络热词默认词典不识别会被拆成“网红”和“打卡”两个词或在“Citywalk”处被切成乱码。所以第一步是准备一个city_words.txt自定义词典每行一个词加词频和词性例如Citywalk 1000 nz 出片 500 v 网红打卡 300 l加载方式是在分析模块初始化时执行jieba.load_userdict(dictionary/city_words.txt)。注意词频的数值决定该词在HMM新词发现中的优先级写得太小加了等于没加一般给到500至1000之间比较合适。停用词表这个环节是关键词质量的分水岭。通用停用词表能过滤“的”“了”“很”这类高频虚词但旅游文本里有自己的“伪关键词”地名本身出现频次一定最高但“去厦门看海”和“在厦门吃沙茶面”里“厦门”对区分内容主题几乎没有贡献。我的做法是在通用表基础上把城市名、省份名、以及“我们”“真的”“感觉”“知道”这类语体水词追加进stopwords.txt每次分析时额外把当前城市的名称及其区县名动态过滤掉。3.2 TF-IDF与TextRank在旅游短文本上的表现差异jieba自带的两套关键词算法在这个场景下差异明显。TF-IDF的优点是速度快、对长文本稳定缺点是依赖语料库统计语料规模小的时候IDF部分被弱化退化成近似词频统计。TextRank则是通过词共现图计算权重不需要外部语料对“酒店干净服务好”这种短评更敏感但迭代较慢词窗大小的设置直接影响效果。推荐做法是两者都跑然后用combine_keywords策略合并结果取TF-IDF Top20和TextRank Top20的交集作为高置信关键词其余按两种分数的加权和补足到目标数量。这样避免单一算法受数据量或语体的牵制。3.3 输出结构化关键词完整可复用代码下面这个脚本可以从Django ORM中拉取采样文本完成从分词到导出的全流程import jieba import jieba.analyse import pandas as pd from tourism.models import RawComment STOPWORDS set() with open(dictionary/stopwords.txt, r, encodingutf-8) as f: for line in f: STOPWORDS.add(line.strip()) jieba.load_userdict(dictionary/city_words.txt) jieba.analyse.set_stop_words(dictionary/stopwords.txt) def extract_keywords(content, topK20): tfidf_words jieba.analyse.extract_tags( content, topKtopK, withWeightTrue, allowPOS(n, ns, vn, v, a)) textrank_words jieba.analyse.textrank( content, topKtopK, withWeightTrue, allowPOS(n, ns, vn, v, a)) return tfidf_words, textrank_words def analyze_city(city_name, sample_n800, topK20): qs RawComment.objects.filter(city__namecity_name).order_by(-weight) sampled list(qs[:sample_n]) full_text \n.join([item.content for item in sampled]) tfidf_words, textrank_words extract_keywords(full_text, topKtopK) tfidf_dict dict(tfidf_words) tr_dict dict(textrank_words) union_keys list(set(tfidf_dict) | set(tr_dict)) rows [] for kw in union_keys: if kw in STOPWORDS: continue rows.append({ keyword: kw, tfidf: round(tfidf_dict.get(kw, 0), 4), textrank: round(tr_dict.get(kw, 0), 4), freq: full_text.count(kw), }) df pd.DataFrame(rows) df[combined] df[tfidf] * 0.6 df[textrank] * 0.4 result_df df.sort_values(combined, ascendingFalse).head(topK) return result_df if __name__ __main__: result analyze_city(厦门) result.to_csv(result/xiamen_keywords.csv, indexFalse, encodingutf-8-sig)逻辑说明order_by(-weight)配合[:sample_n]实现了加权采样权重高的来源文本优先保留避免长攻略淹没短评。unwrap之后做完一次性合并然后用full_text.count(kw)计算频次这里有性能风险——count的时间复杂度是O(n*m)对每一条候选词扫一遍全文但如果候选词只有40个左右实际运行时间可以接受。最后用combined列做加权排序默认TF-IDF权重是0.6TextRank是0.4调参时应观察结果如果城市是热门短评主导建议调成0.5/0.5。输出到CSV时编码用utf-8-sig而非utf-8避免Excel打开时中文乱码。这个细节对后续人工验证分析质量很重要——很多人在Django页面上看结果正常导出就乱根因就在这里。4. Django把分析结果变成可查询页面视图、URL与词云图渲染4.1 视图函数里做聚合查询不要在模板里做逻辑分析结果已经入库接下来Django只负责展示。在views.py中写一个城市关键词视图职责是接收城市名参数、从KeywordStat表读取数据、把数据组装成前端所需的JSON结构、渲染模板。模板中不要做任何列表推导或字典取值Django模板语言的能力边界很有限把数据整理成列表字典再传给模板是最稳妥的。同时渲染频率是有成本的应该在视图层做缓存而不是完全依赖HTTP缓存。# tourism/views.py import json from django.shortcuts import render, get_object_or_404 from django.core.cache import cache from .models import City, KeywordStat, RawComment def city_keywords_view(request, city_id): cache_key fcity_keywords_{city_id} cached cache.get(cache_key) if cached: return render(request, tourism/city_detail.html, cached) city get_object_or_404(City, pkcity_id) stats KeywordStat.objects.filter(citycity).order_by(-tfidf)[:50] keyword_data [] for s in stats: keyword_data.append({ name: s.keyword, value: round(s.tfidf, 4), freq: s.freq, }) # 顶部统计信息供模板引用 total_comment RawComment.objects.filter(citycity).count() context { city: city, total_comment: total_comment, keyword_json: json.dumps(keyword_data, ensure_asciiFalse), top_keywords: keyword_data[:10], } cache.set(cache_key, context, 60 * 10) return render(request, tourism/city_detail.html, context)参数说明cache.set的第三个参数是过期时间10分钟内相同请求直接复用context避免词云图刷新时反复查库。注意JSON序列化时ensure_asciiFalse这样前端拿到的就是中文而不是\u开头的转义序列调试的时候省很多事。4.2 URL配置与前端词云的基础渲染urls.py中把城市详情页绑上参数from django.urls import path from . import views urlpatterns [ path(city/int:city_id/, views.city_keywords_view, namecity_keywords), ]模板方面词云图可以引入ECharts的wordCloud扩展直接把keyword_json注入JS变量。这种展示方式互动性好于静态图片tooltip能显示每个词的TF-IDF权重并且不需要后端生成图片文件。模板核心代码!-- templates/tourism/city_detail.html -- div idwordcloud stylewidth: 100%; height: 500px;/div script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script script let tagData {{ keyword_json|safe }}; let chart echarts.init(document.getElementById(wordcloud)); chart.setOption({ series: [{ type: wordCloud, shape: circle, width: 90%, height: 90%, sizeRange: [14, 60], rotationRange: [0, 0], textStyle: { color: function() { return rgb( [Math.round(Math.random()*160), Math.round(Math.random()*160), Math.round(Math.random()*160)].join(,) ); }}, data: tagData }] }); /script这里把rotationRange设为[0, 0]是有意为之旅游城市名大多是2到4个字旋转90度反而增加阅读成本。如果做通用新闻关键词场景可以允许旋转。模板中用safe过滤器直接输出JSON但前提是后端已保证数据没有脚本注入风险因为KeywordStat是内部程序写入的前端只读不写。4.3 按年份和来源做筛选的下钻查询纯展示Top关键词不够完整。游客最关心的往往不是静态排名而是“从2023到2024这个城市的关键词发生了哪些变化”。这一需求在Django中可以通过ORM的group_by和条件聚合实现不需要另外引入搜索引擎。from django.db.models import Count, Q from django.db.models.functions import TruncYear def keyword_trend_view(request, city_id): city get_object_or_404(City, pkcity_id) rows (KeywordStat.objects .filter(citycity) .values(year) .annotate(total_keywordsCount(keyword), high_freqCount(keyword, filterQ(tfidf__gt0.1))) .order_by(year)) return render(request, tourism/trend.html, {rows: rows})这段代码实现了按年聚合统计TruncYear配合values(year)提取年份分组Count的filter参数实现条件计数不用subquery就能统计TF-IDF大于0.1的高置信词数量。如果之后要细分短评和攻略的差异只需在filter中增加sourceguide这样的条件。5. 用StreamingHttpResponse做关键词结果导出以及部署时必调的4个参数5.1 用StreamingHttpResponse导出CSV替代一次性生成字符串分析页面常需要一个“导出结果”按钮。很多人直接构造一个大字符串用HttpResponse返回但Top50关键词数据加上频次权重后实际内容在几十KB量级一次性生成并没有性能问题。真正的问题在于如果后续扩展成“导出全站所有城市对比数据”数据量可能达到几百MB此时流式响应才是正确做法。Django的StreamingHttpResponse可以结合CSV模块边生成边输出也支持在响应头中显式指定content_type和Content-Disposition文件名中可以拼接城市名和导出时间避免浏览器每次都下载同名文件import csv from django.http import StreamingHttpResponse from .models import KeywordStat def export_keywords_csv(request, city_id): city get_object_or_404(City, pkcity_id) def generate(): writer csv.writer yield \ufeff yield 关键词,TF-IDF值,TextRank值,词频\n for s in (KeywordStat.objects .filter(citycity) .order_by(-tfidf) .values_list(keyword, tfidf, textrank, freq)): yield ,.join(str(x) for x in s) \n response StreamingHttpResponse(generate(), content_typetext/csv; charsetutf-8) response[Content-Disposition] fattachment; filename{city.name}_keywords.csv return response解析要点yield \ufeff是写BOM头和前面CSV文件用utf-8-sig编码是同一个目的——Excel环境下不带BOM的UTF-8会被识别成ANSI导致中文乱码。values_list直接在数据库层把ORM对象转换成元组免去每一行创建对象再取字段的内存开销。注意Content-Disposition里的filename带有中文时部分旧版浏览器可能解析失败稳妥做法是给中文名做一次URL编码或者简化为keywords_{city_id}.csv。5.2 部署时绕不开的参数调整SQLite并发、Waitress和静态文件Windows和Linux下的部署方案不同。常见做法是用Waitress或Gunicorn跑Django再用Nginx做反向代理和静态文件服务。几个必须调的参数参数位置建议值说明OPTIONS超时waitressserve()channel_timeout300词云图和CSV导出耗时较长默认120秒容易断静态文件settings.pySTATIC_ROOTcollectstatic不收集静态文件页面会丢失CSS样式数据库超时settings.pyDATABASESOPTIONS: {timeout: 20}SQLite并发读多写少时容易数据库锁Gunicorn进程数启动命令--workers 2旅游分析系统读多写少2个worker足够不用按CPU核心数盲目开注意一个大坑Django的runserver自带静态文件服务但在生产模式DEBUGFalse下不再提供。很多人本地页面正常、部署后CSS和JS全部404根因就是没跑collectstatic。解决方法是确认Nginx把/static/路径代理到STATIC_ROOT目录。5.3 用数据校验闭环验证关键词质量部署完成后不急着交付先用一个简单方法校验分析结果质量。人工随机选取30条原始评论看提取的关键词是否与其主题吻合再反向操作取关键词“沙茶面”在评论表中搜索包含它的句子确认该词确实指向预期含义。这一步能发现同义词问题和分词错词——例如“中山路”和“中山路步行街”是两个关键词实际指向同一地点此时需要在自定义词典里增加一个词性合并规则或者在入库时做一次别名归一化。把这条校验路径固化成测试用例后续换城市重新分析时不用重新验证逻辑只要跑一遍断言即可。本文还有配套的精品资源点击获取
返回列表