ARTICLE DETAIL

资讯详情

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

旅游城市关键词分析系统的设计与实现——基于Python与Django

旅游城市关键词分析系统的设计与实现——基于Python与Django 简介旅游城市关键词分析系统的完整设计与实现方案采用Python与Django框架构建以B/S架构承载用户输入城市名即可检索景点、美食等旅游信息并通过Python可视化库生成各城市旅游热度与资源分布图谱方便横向比较。docx文档面向计算机类专业完成课程设计、毕业设计的学生也适合Web开发、信息检索、数据分析方向的学习者作为项目参考。文档从旅游信息过载和大数据检索难的实际问题切入梳理了国内外研究现状并围绕Django框架、自然语言处理库、知识图谱、Matplotlib/Seaborn可视化等关键技术依次阐述系统选型、功能设计、关键词匹配搜索与旅游资源图谱展示的实现路径结构完整、目录章节清晰便于直接用于论文写作参照或系统开发的思路复盘。资源包共1个docx文件大小约2.21MB已有131人学习下载适合用于毕业设计选题前的技术可行性调研也可作为旅游类信息检索项目落地时的设计蓝本。1. 旅游城市关键词分析的设计与实现先分清算法、工程和展示三条线一套基于pythonDjango的旅游城市关键词分析拆开看其实是三条独立的线用python把OTA评论、游记文本采集下来并做分词统计用Django建模型、写查询接口再用词云和多城市对比把结果展示出来。最容易踩的误判是觉得算法难实际上把任意一批评论交给jieba跑TF-IDF排最前面的永远是“酒店”“景区”“方便”这类和城市无关的通用词交付时根本没法看。这道题的设计重心落在停用词表、主题词典和词频归并上落库、接口参数校验、部署响应速度反而是后期最容易拖垮进度的部分。对应的设计文档也就围绕四条线展开数据层怎么采模型层怎么建展示层怎么画部署层怎么跑。适合正在做课设或毕设、以及要给旅游平台搭内部舆情看板的人照着重做一遍。2. 旅游城市关键词分析的数据准备采集、分词与词表维护2.1 评论采集的最小脚本requests加BeautifulSoup先跑通再扩展评论采集不是这套题的主线但没有原始文本后面的分词和建模全是空的。我一般不会一上来就啃整个页面先拿一个公开评论列表页按城市和页码提取正文。下面这个脚本只做一件事把一页评论的正文抽出来。import requests import time from bs4 import BeautifulSoup def fetch_comments(city, page1): url fhttps://example.com/reviews?city{city}page{page} headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)} resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) nodes soup.select(div.review-item p.content) return [node.get_text(stripTrue) for node in nodes] texts [] for page in range(1, 6): texts.extend(fetch_comments(hangzhou, page)) time.sleep(1.5)这段代码里三个细节比较关键。headers里的User-Agent是让请求看起来来自正常浏览器timeout10避免某个页面卡住让整个脚本挂掉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, encodingutf-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), topK50, withWeightTrue)参数上要解释清楚len(w) 2把单字先滤掉语气词和量词后面交给停用词表topK控制返回关键词数量withWeightTrue返回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 analysisDjango的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_length50, uniqueTrue) pinyin models.CharField(max_length50, db_indexTrue) class Comment(models.Model): city models.ForeignKey(City, on_deletemodels.CASCADE, related_namecomments) source models.CharField(max_length20) content models.TextField() created_at models.DateField(auto_now_addTrue) class KeywordStat(models.Model): city models.ForeignKey(City, on_deletemodels.CASCADE, related_namekeywords) word models.CharField(max_length50) freq models.IntegerField(default0) tfidf models.FloatField(default0.0) batch models.CharField(max_length20, db_indexTrue) class Meta: constraints [ models.UniqueConstraint( fields[city, word, batch], nameuniq_city_word_batch ) ] ordering [-freq, -tfidf]字段设计上有几个值得留意的点。说下字段意义字段作用注意点city外键关联城市主数据查询时用select_related或直接用city_idpinyinURL里用拼音而非中文db_index加速按城市查询freq词频展示词云面积不用加索引连续值范围查询收益低tfidf权重排序用按业务情况允许为空batch区分第几次分析删除和重跑都依赖它batch字段尤其重要。没有它重复跑同一批分析要么产生重复行要么得先手动全表删除。需要清理某一轮数据时执行批量删除KeywordStat.objects.filter(citycity, batch20250301).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_nomanual): city City.objects.get(pkcity_id) KeywordStat.objects.filter(citycity, batchbatch_no).delete() jieba.setLogLevel(20) jieba.load_userdict(data/tour_dict.txt) stopwords {line.strip() for line in open(data/stopwords.txt, encodingutf-8)} counter Counter() for text in texts: for w in clean_cut(text, stopwords): counter[w] 1 tags jieba.analyse.extract_tags( .join(texts), topK200, withWeightTrue) weights {w: weight for w, weight in tags} objs [ KeywordStat( citycity, batchbatch_no, wordword, freqcounter[word], tfidfweights.get(word, 0) ) for word in counter if word in weights or counter[word] 3 ] KeywordStat.objects.bulk_create(objs, batch_size500)这里的关键是“先delete再bulk_create”。同批次重复执行不会叠加词频bulk_create按500一批写入减少数据库往返。写入前过滤条件word in weights or counter[word] 3的目的是去掉只出现一两次的噪声词。topK200是权重筛选不是只保留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__pinyincity_pinyin, freq__gtemin_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_n50sort_bytfidf); 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请求失败时前端拿不到JSONsetOption会报空数据接口返回了404时要在then里先判断resp.ok。4.3 多城市关键词对比接口与参数表单个城市词云只能回答“这个城市聊什么”多城市并排才能回答“这个城市和别人有什么不同”。实现上不需要额外算法给compare接口传城市列表内部重复调用同一个查询函数把结果组织成矩阵。参数示例含义边界cityhangzhou城市拼音必填top_n30返回词数10到100min_freq2最小词频大于等于1sort_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__pinyinhangzhou): print(comment.city.name) # 正确方式select_related预取外键 for comment in Comment.objects.select_related(city).filter(city__pinyinhangzhou): 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 --listen0.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__pinyincity_pinyin) \ .values_list(word, freq, tfidf) def rows(): yield [word, freq, tfidf] for row in qs.iterator(): yield row response StreamingHttpResponse( rows(), content_typetext/csv; charsetutf-8-sig, headers{ Content-Disposition: attachment; filenamekeywords.csv }, ) return responsecontent_type用了text/csv且charsetutf-8-sigExcel打开中文才不会乱码Content-Disposition是下载响应的关键参数filename里的引号要保留。qs.iterator()让查询结果按游标方式逐行读取不会一次性把全表数据加载到内存。StreamingHttpResponse是边生成边返回的生成器里如果又做了查询连接要保持同一个线程这里没有新增外键访问所以是安全的。上线后给关键词接口加一个耗时字段把ORM查询时间和序列化时间分别返回浏览器Network面板里一眼就能看到瓶颈在哪一步。数据量大了以后把CSV导出任务移到凌晨跑避免占用白天的高频查询连接。整个项目的设计和实现到这个程度可以对照需求文档逐项验收了。本文还有配套的精品资源点击获取
返回列表