旅游城市关键词分析系统的设计与实现——基于Python与Django
2026/9/19 0:20:42 网站建设 项目流程

简介:旅游城市关键词分析系统的完整设计与实现方案,采用Python与Django框架构建,以B/S架构承载用户输入城市名即可检索景点、美食等旅游信息,并通过Python可视化库生成各城市旅游热度与资源分布图谱,方便横向比较。docx文档面向计算机类专业完成课程设计、毕业设计的学生,也适合Web开发、信息检索、数据分析方向的学习者作为项目参考。文档从旅游信息过载和大数据检索难的实际问题切入,梳理了国内外研究现状,并围绕Django框架、自然语言处理库、知识图谱、Matplotlib/Seaborn可视化等关键技术,依次阐述系统选型、功能设计、关键词匹配搜索与旅游资源图谱展示的实现路径,结构完整、目录章节清晰,便于直接用于论文写作参照或系统开发的思路复盘。资源包共1个docx文件,大小约2.21MB,已有131人学习下载,适合用于毕业设计选题前的技术可行性调研,也可作为旅游类信息检索项目落地时的设计蓝本。

1. 旅游城市关键词分析的设计与实现:先分清算法、工程和展示三条线

一套基于python+Django的旅游城市关键词分析,拆开看其实是三条独立的线:用python把OTA评论、游记文本采集下来并做分词统计,用Django建模型、写查询接口,再用词云和多城市对比把结果展示出来。最容易踩的误判是觉得算法难,实际上把任意一批评论交给jieba跑TF-IDF,排最前面的永远是“酒店”“景区”“方便”这类和城市无关的通用词,交付时根本没法看。这道题的设计重心落在停用词表、主题词典和词频归并上,落库、接口参数校验、部署响应速度反而是后期最容易拖垮进度的部分。对应的设计文档也就围绕四条线展开:数据层怎么采,模型层怎么建,展示层怎么画,部署层怎么跑。适合正在做课设或毕设、以及要给旅游平台搭内部舆情看板的人照着重做一遍。

2. 旅游城市关键词分析的数据准备:采集、分词与词表维护

2.1 评论采集的最小脚本:requests加BeautifulSoup,先跑通再扩展

评论采集不是这套题的主线,但没有原始文本,后面的分词和建模全是空的。我一般不会一上来就啃整个页面,先拿一个公开评论列表页,按城市和页码提取正文。下面这个脚本只做一件事:把一页评论的正文抽出来。

import requests import time from bs4 import BeautifulSoup def fetch_comments(city, page=1): url = f"https://example.com/reviews?city={city}&page={page}" headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"} resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() soup = BeautifulSoup(resp.text, "html.parser") nodes = soup.select("div.review-item p.content") return [node.get_text(strip=True) for node in nodes] texts = [] for page in range(1, 6): texts.extend(fetch_comments("hangzhou", page)) time.sleep(1.5)

这段代码里三个细节比较关键。headers里的User-Agent是让请求看起来来自正常浏览器;timeout=10避免某个页面卡住让整个脚本挂掉;time.sleep(1.5)是自己限速,对目标站点和本地数据库都友好。取不到内容时先打印soup.select的结果,而不是反复换selector猜结构。开发环境建议先用vscode配置好python环境,建一个venv,把requests、beautifulsoup4装进去,后面Django也装在这个虚拟环境里,避免依赖混进系统python。采集到的原始文本先按行存成jsonl,后续分词脚本失败重跑时不用再请求一遍网络。

2.2 分词与关键词抽取:jieba的精确模式、TF-IDF和自定义词典

采集完文本,下一步是切词。用jieba时要分清两个入口:统计词频用jieba.cut,抽取关键词用jieba.analyse.extract_tags,后者内部走TF-IDF,对“旅游城市”这个场景比纯词频更能压掉无意义词。

import jieba import jieba.analyse jieba.setLogLevel(20) # 关掉每次初始化时刷屏的INFO日志 jieba.load_userdict("tour_dict.txt") stopwords = {line.strip() for line in open("stopwords.txt", encoding="utf-8")} def clean_cut(text): words = [] for w in jieba.cut(text): if w.strip() and len(w) >= 2 and w not in stopwords: words.append(w) return words tags = jieba.analyse.extract_tags(" ".join(texts), topK=50, withWeight=True)

参数上要解释清楚:len(w) >= 2把单字先滤掉,语气词和量词后面交给停用词表;topK控制返回关键词数量;withWeight=True返回tfidf权重,后面排序和导出都用得上。自定义词典里每行一个词,格式是“词语 词频 词性”,例如西湖 500 n,词频参数越高越倾向被当成独立词,能修正“西湖风景区”被切成“西湖/风景/区”的问题。HMM默认开启,随便一段口语它会拆出“碎词”,自定义词典就是用来压制这种碎词的。

2.3 停用词表和主题词表:两组参数决定分析结果可读性

直接跑出来的结果经常是一堆“感觉”“真的”“还是”“一个”,这些词不进停用词表,词云就是一坨噪音。停用词表按类别维护效果好,不要只堆积常见词。

类别典型词处理建议
语气与代词反正、就是、这么、那里直接过滤
通用评价词不错、还好、可以单独统计,不进词云
城市无关名词酒店、景区、门票、排队按分析目标决定是否过滤
主题词西湖、灵隐寺、苏堤全部保留,且不进停用词表

通用评价词值得单独说明。旅游评论里“不错”出现频率极高,但它回答不了“这个城市有什么特点”,放进词云只会掩盖“断桥”“龙井”“夜游”这些真正的主题词。做法是维护一份场景词表,只参与情感统计,不参与关键词排名。每次换城市也要回看一次停用词表,“景点”“打卡”这类词在多个城市都高频,过滤后剩下才是城市差异。

2.4 词频归并:把同义表达合并成一个分析维度

“西湖风景区”“杭州西湖”“西湖”在词频表里会被当成三个词,分析结论就散了。常见做法是维护别名映射,在统计结束后把低频形态合并进主词条。映射必须放在分词之后,否则“杭州西湖”被切成了“杭州/西湖”,归并目标就不存在了。

ALIAS = {"西湖风景区": "西湖", "杭州西湖": "西湖", "美味": "好吃"} def merge_word(word): return ALIAS.get(word, word)

归并之后再做频次聚合。这里要注意,同一个词在不同评论里切分结果可能不一样,所以合并函数要幂等,跑两遍不能把“西湖”又映射到别处。词表本身建议放进Django项目根目录的data文件夹,和代码一起走版本管理,避免换了环境停用词表丢失。分词阶段产生的词汇表也可以导出,用来检查有没有该归没归的错词。

3. Django后端:MTV模式下的数据建模与关键词分析接口

3.1 用django创建app,先把MTV模式的职责对齐到题目

从项目初始化开始:

django-admin startproject travel_words . python manage.py startapp analysis

Django的MTV是指Model-Template-View,它把Controller的职责拆给了URLconf和View一起处理。放在这个题目里,Model管城市、评论、关键词统计三张表,Template管后台列表和可视化页面,View管参数接收、查询、返回JSON。最容易出现的设计失误是把分词逻辑直接写进View,请求来了现场跑一遍jieba,接口耗时直接飙到两秒以上。正确的分工是:离线脚本把分词结果算好落库,View只做查询和排序。

3.2 城市、评论、关键词统计:三张表把文本和指标分开存

models.py里按聚合粒度拆三张表,不要把所有字段塞进一张表。

from django.db import models class City(models.Model): name = models.CharField(max_length=50, unique=True) pinyin = models.CharField(max_length=50, db_index=True) class Comment(models.Model): city = models.ForeignKey(City, on_delete=models.CASCADE, related_name="comments") source = models.CharField(max_length=20) content = models.TextField() created_at = models.DateField(auto_now_add=True) class KeywordStat(models.Model): city = models.ForeignKey(City, on_delete=models.CASCADE, related_name="keywords") word = models.CharField(max_length=50) freq = models.IntegerField(default=0) tfidf = models.FloatField(default=0.0) batch = models.CharField(max_length=20, db_index=True) class Meta: constraints = [ models.UniqueConstraint( fields=["city", "word", "batch"], name="uniq_city_word_batch" ) ] ordering = ["-freq", "-tfidf"]

字段设计上有几个值得留意的点。说下字段意义:

字段作用注意点
city外键关联城市主数据查询时用select_related或直接用city_id
pinyinURL里用拼音而非中文db_index加速按城市查询
freq词频,展示词云面积不用加索引,连续值范围查询收益低
tfidf权重,排序用按业务情况允许为空
batch区分第几次分析删除和重跑都依赖它

batch字段尤其重要。没有它,重复跑同一批分析要么产生重复行,要么得先手动全表删除。需要清理某一轮数据时,执行批量删除:

KeywordStat.objects.filter(city=city, batch="20250301").delete()

这个delete走的是SQL级别批量删除,不会逐个实例触发save(),所以删完要确保后续写入连到新事务里,否则可能把已提交的结果一起卷进去。

3.3 分析任务封装成服务,不散落在脚本里

把离线分析写成analysis/services.py里的一个函数,不要散落在manage.py shell里。这样定时任务、命令行、测试都能复用。

import jieba import jieba.analyse from collections import Counter from .models import City, KeywordStat def run_keyword_analysis(city_id, texts, batch_no="manual"): city = City.objects.get(pk=city_id) KeywordStat.objects.filter(city=city, batch=batch_no).delete() jieba.setLogLevel(20) jieba.load_userdict("data/tour_dict.txt") stopwords = {line.strip() for line in open("data/stopwords.txt", encoding="utf-8")} counter = Counter() for text in texts: for w in clean_cut(text, stopwords): counter[w] += 1 tags = jieba.analyse.extract_tags(" ".join(texts), topK=200, withWeight=True) weights = {w: weight for w, weight in tags} objs = [ KeywordStat( city=city, batch=batch_no, word=word, freq=counter[word], tfidf=weights.get(word, 0) ) for word in counter if word in weights or counter[word] >= 3 ] KeywordStat.objects.bulk_create(objs, batch_size=500)

这里的关键是“先delete再bulk_create”。同批次重复执行不会叠加词频;bulk_create按500一批写入,减少数据库往返。写入前过滤条件word in weights or counter[word] >= 3的目的是去掉只出现一两次的噪声词。topK=200是权重筛选,不是只保留200个词,后续接口层还能用min_freq和top_n再收敛。批量创建的参数batch_size建议设置在500到1000之间,太大会导致单条SQL过大,太小又失去批量插入的性能优势。

3.4 查询接口的参数校验与JSON返回

接口层直接面对浏览器,参数必须做边界校验。view函数写成这样:

from django.http import JsonResponse from .models import KeywordStat def parse_int(value, default, low, high): try: val = int(value) except (TypeError, ValueError): return default return max(low, min(high, val)) def keyword_list_api(request, city_pinyin): top_n = parse_int(request.GET.get("top_n", 30), 30, 10, 100) min_freq = parse_int(request.GET.get("min_freq", 2), 2, 1, 100000) sort_map = {"freq": "-freq", "tfidf": "-tfidf", "word": "word"} sort_by = sort_map.get(request.GET.get("sort_by", "freq"), "-freq") rows = ( KeywordStat.objects .filter(city__pinyin=city_pinyin, freq__gte=min_freq) .order_by(sort_by)[:top_n] ) return JsonResponse({ "city": city_pinyin, "top_n": top_n, "items": [ {"word": r.word, "freq": r.freq, "tfidf": round(r.tfidf, 4)} for r in rows ], })

sort_by必须走白名单映射,不能直接把请求字符串传给order_by。Django的ORM会拒绝带分号的注入,但如果外部传一个freq__name进来,结果可能返回意外排序或直接报错。top_n夹在10到100之间,避免一次拉几千个词把页面拖死。这个接口里没有select_related,因为只用到city_pinyin,不访问城市其他字段;如果后面要返回城市中文名,再补select_related("city")。JSON返回的tfidf做了round,前端画图和导出时数据体积能小一些。

4. 关键词分析可视化:词云、多城市对比与接口耗时定位

4.1 模板渲染还是前后端分离,按页面复杂度选

列表页、数据管理页用Django Template加Bootstrap就够了,词云图和多城市对比是需要交互的模块,建议用fetch请求JSON在前端渲染。这么划分之后,分析服务、查询接口、展示页面三层各管各的,后面接小程序或移动端也不需要大改。模板里只需要留一个div容器和script入口,页面初始数据从接口拿,而不是塞在模板变量里。

4.2 用ECharts接入旅游城市关键词词云

ECharts核心包不带词云图,需要额外引入echarts和echarts-wordcloud两个script,版本要对应同一大版本,否则会出现注册不到组件的问题。

<script src="/static/js/echarts.min.js"></script> <script src="/static/js/echarts-wordcloud.min.js"></script> <script> const resp = await fetch("/api/city/hangzhou/?top_n=50&sort_by=tfidf"); const data = await resp.json(); const cloud = echarts.init(document.getElementById("wordcloud")); cloud.setOption({ series: [{ type: "wordCloud", shape: "circle", width: "90%", height: "80%", sizeRange: [12, 60], rotationRange: [0, 0], data: data.items.map(d => ({ name: d.word, value: d.freq })) }] }); </script>

参数说明:sizeRange控制字号范围,词频差距大的时候不要直接映射原始数值,ECharts内部会自己归一化;rotationRange设成[0, 0]是为了让中文词全部横排,竖排中文词可读性很差。data里的value用的是freq而不是tfidf,词云按面积展示出现次数最直观,权重指标留给后面的表格排序。fetch请求失败时前端拿不到JSON,setOption会报空数据,接口返回了404时要在then里先判断resp.ok。

4.3 多城市关键词对比接口与参数表

单个城市词云只能回答“这个城市聊什么”,多城市并排才能回答“这个城市和别人有什么不同”。实现上不需要额外算法,给compare接口传城市列表,内部重复调用同一个查询函数,把结果组织成矩阵。

参数示例含义边界
cityhangzhou城市拼音必填
top_n30返回词数10到100
min_freq2最小词频大于等于1
sort_bytfidf排序维度freq / tfidf / word

返回结构保持{ “city”: “hangzhou”, “items”: [...] },前端按城市循环渲染成多个表格或雷达图。多城市对比的关键是保证所有城市用同一套停用词表和相同top_n,否则对比没有意义。接口实现上,批量查询用city__pinyin__in一次取出,不要在循环里逐城市查数据库,否则请求耗时随城市数量线性上涨。

4.4 接口变慢的定位思路:先看ORM再查分词

关键词接口响应慢,最高频的原因是在Python层把KeywordStat逐条实例化,再逐个读字段。只取需要的字段时改用values_list或values。第二个常见问题是按城市反查评论时发生循环查询:

# 错误示例:循环里每次访问comment.city都会发一条SQL for comment in Comment.objects.filter(city__pinyin="hangzhou"): print(comment.city.name) # 正确方式:select_related预取外键 for comment in Comment.objects.select_related("city").filter(city__pinyin="hangzhou"): print(comment.city.name)

分词任务本身慢时,检查是否把jieba.load_userdict放进了for循环,这个操作每次要初始化词典,应该只在进程启动时执行一次。开发期用Django DEBUG为True时,django.db.connection.queries能列出所有SQL,一眼看出有没有重复查询。给接口加耗时统计时,把ORM查询时间和序列化时间分开记录,比凭感觉优化有效。

5. 上线与交付:waitress加nginx部署,CSV导出与响应头参数

5.1 waitress跑Django的启动命令

runserver只适合开发环境,生产环境用waitress作为WSGI容器,它没有额外的C扩展依赖,Windows和Linux都能跑。

pip install waitress waitress-serve --listen=0.0.0.0:8000 travel_words.wsgi:application

--listen绑定对外地址和端口,waitress默认线程数是4,关键词报表场景可以调整到8。waitress是单进程模型,不要指望它自己扩成多进程,要横向扩容就在前面挂多节点负载。

5.2 nginx转发配置与静态资源分离

nginx配置最关键是让静态文件不经过Django。截取关键配置:

server { listen 80; location /static/ { alias /opt/travel/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

/static请求由nginx直接返回磁盘上的静态文件,其他请求转发到waitress监听的127.0.0.1:8000端口。修改配置后必须先执行nginx -t检查语法再reload。注意媒体文件交给Django处理时,不要在location里开proxy_buffering off,否则大响应会长时间占用连接。

5.3 关键词报表CSV导出:StreamingHttpResponse的content_type与Content-Disposition

关键词分析结果经常要导出给运营同学。用普通HttpResponse会把所有KeywordStat先拼成字符串再一次性返回,词量一大内存就上去了。常见做法是用StreamingHttpResponse逐行生成。

import csv from django.http import StreamingHttpResponse from .models import KeywordStat def export_keywords_csv(request, city_pinyin): qs = KeywordStat.objects.filter(city__pinyin=city_pinyin) \ .values_list("word", "freq", "tfidf") def rows(): yield ["word", "freq", "tfidf"] for row in qs.iterator(): yield row response = StreamingHttpResponse( rows(), content_type="text/csv; charset=utf-8-sig", headers={ "Content-Disposition": 'attachment; filename="keywords.csv"' }, ) return response

content_type用了text/csv且charset=utf-8-sig,Excel打开中文才不会乱码;Content-Disposition是下载响应的关键参数,filename里的引号要保留。qs.iterator()让查询结果按游标方式逐行读取,不会一次性把全表数据加载到内存。StreamingHttpResponse是边生成边返回的,生成器里如果又做了查询,连接要保持同一个线程,这里没有新增外键访问,所以是安全的。

上线后给关键词接口加一个耗时字段,把ORM查询时间和序列化时间分别返回,浏览器Network面板里一眼就能看到瓶颈在哪一步。数据量大了以后,把CSV导出任务移到凌晨跑,避免占用白天的高频查询连接。整个项目的设计和实现到这个程度,可以对照需求文档逐项验收了。

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

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

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

立即咨询