如果你正在纠结Python入门之后能做什么拿得出手的实战项目,或者想找一套带完整源码和文档的练手作品来打磨简历,这个“基于Python的旅游城市关键词分析”项目值得认真复盘一遍。它不是什么高深算法,但把爬虫采集、文本清洗、关键词提取、数据可视化和文档撰写串成了完整链路,属于那种“麻雀虽小五脏俱全”的综合型项目。我把它从需求拆解到编码实现、再到最后整理源码和文档的全过程都放在这篇里,包括踩过的各种坑,希望能给你一份可以直接参考的实操路线。
这套东西适合谁?正在做课设、毕设或者面试项目的Python学习者,想了解文本分析真实落地流程的初级数据分析师,以及准备做旅游行业舆情或竞品分析但不知从哪下手的从业者。动手之前先把结论说清楚:项目的核心价值不在算法多牛,而在于它演示了“从一堆杂乱评论里提取出城市特色关键词,再让结果可视化、可解释”的完整思路。
1. 为什么做旅游城市关键词分析:先想清楚这几个问题再动手
1.1 这个项目到底在解决什么问题
旅游行业有一个很实际的痛点:游客在携程、马蜂窝、小红书、抖音评论区留下了海量真实文字反馈,但大部分商家和文旅机构根本没有精力去读。比如一个城市有几千条评论,人工看完要两三天,而且看完也说不清“游客最在意的是交通还是住宿、是自然风光还是美食小吃”。关键词分析解决的就是这个信息过载问题——把几千条文本压缩成几十个关键词,每个词后面挂着频次、热度趋势和情感倾向,决策者一眼就知道该优化什么。
把问题再拆细一步,实际操作时有三个层次的需求。最低层次是“词频统计”,就是单纯看哪些词出现得多;中间层次是“关键词权重排序”,要排除“我们”“他们”这类无意义词,还要让“栈桥”“八大关”这种地方特色词脱颖而出;最高层次是“关键词解释力”,比如同样是“海滩”这个词,在青岛评论和厦门评论里的情感语义可能完全不同,需要结合上下文判断。这个项目做的是后两个层次的结合,用TF-IDF和TextRank做权重排序,再叠加情感标签做语义增强。
1.2 面向谁、怎么用:三个典型应用场景
第一个场景是文旅局或景区管理人员做季度舆情分析。把本季度所有游客评论抓下来,跑一遍关键词提取,再按月份对比关键词热度变化,能直观看到新开的商圈有没有被游客注意到、哪个老景点口碑在下滑。
第二个场景是旅游博主或MCN机构做选题参考。分析某个城市的所有热门笔记关键词,就知道大家最关注什么,写攻略时围绕这些关键词组织内容,流量通常会更好。
第三个场景是Python学习者做作品集。这个项目虽然不复杂,但覆盖面广,源码和文档齐全,面试时能完整讲清楚数据流和算法原理,比单纯刷题有说服力得多。
我最初做这个项目的动机就是帮一个文旅行业的朋友分析游客评论,后来发现这套流程抽出来之后可以复用到任何城市、任何行业,干脆整理成了一套标准化工具。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据是分析的根基:采集、清洗与语料库搭建实录
2.1 数据源选择和采集策略
做关键词分析,数据质量决定结果上限。爬虫是这个项目最不稳定的部分,也是最容易被忽略的部分。我建议优先考虑两个渠道:第一个是公开的数据集,比如天池、Kaggle上有一些旅游评论的开放数据,省去采集麻烦,适合前期开发调试;第二个才是自己写爬虫抓取公开平台数据。
如果决定写爬虫,我认为第一原则是控制频率和遵守平台规范。只用requests加BeautifulSoup就能完成基础抓取,没必要一上来就上Scrapy。以抓取某旅游网站的城市攻略为例,核心逻辑就是构造URL列表、请求页面、解析HTML中的评论或攻略正文,然后存入CSV。
python复制import requests
from bs4 import BeautifulSoup
import time
import csv
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
def fetch_articles(city, pages=10):
results = []
for page in range(1, pages + 1):
url = f"https://example-travel.com/{city}/guide?page={page}"
try:
resp = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(resp.text, "html.parser")
for item in soup.select(".article-content"):
text = item.get_text(strip=True)
if len(text) > 50:
results.append(text)
except Exception as e:
print(f"[WARN] 第{page}页抓取失败: {e}")
time.sleep(1.5) # 控制请求间隔,别给服务器添压力
return results
代码本身没什么花样,但有几个细节会直接影响成败。一是time.sleep的间隔不能太短,否则容易触发反爬;二是User-Agent要伪装成真实浏览器,有些网站对Python默认UA直接拒绝;三是异常处理必须做,网络波动和页面结构调整太常见了。
用关键词“旅游城市关键词分析”相关的需求来说,一般至少需要抓取500到1000条文本,覆盖不同时间段,分析结果才有统计意义。数据太少会出现关键词漂移——某个词因为一条评论就冲上榜首,完全没有代表性。
2.2 文本清洗:编码、去重和噪声过滤
采集完的数据绝对不能直接用,里面什么奇怪的东西都有:广告、表情符号、商家回复模板、无意义的语气词、甚至乱码。清洗这一步我建议分成三层来做。
第一层是编码清洗。尽量统一用UTF-8格式读写文件,但Windows环境下经常遇到GBK编码的旧数据,读取时用errors="ignore"或errors="replace"参数来兜底,避免程序直接崩溃。
python复制def safe_read_text(path):
for encoding in ["utf-8", "gbk", "gb18030"]:
try:
with open(path, "r", encoding=encoding) as f:
return f.read()
except UnicodeDecodeError:
continue
return ""
第二层是去重。爬虫或导出的数据经常有大量重复评论,这种重复会严重扭曲词频统计结果。用哈希或者pandas的drop_duplicates都能解决,我倾向于在存入DataFrame时就处理掉,同时保留一条原始记录以便溯源。
第三层是噪声过滤。评论里经常出现“这家店很赞”“路过打卡”这类短句,以及连续的表情符号。我用正则表达式把非中文字符、数字和URL全部替换掉,只保留中文文本主体。这一步做得好不好,直接影响后面分词和关键词提取的效果。
python复制import re
def clean_text(text):
text = re.sub(r"http\S+", "", text) # 去URL
text = re.sub(r"#\S+#", "", text) # 去话题标签
text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z]", " ", text) # 只保留中文和英文
text = re.sub(r"\s+", " ", text) # 合并空白
return text.strip()
清洗完的文本统一存成一份cleaned_comments.csv,每行一条有效评论,后面所有分析都基于这个文件。我试过跳过清洗直接跑关键词,结果“放心的”“真的”这类词占据前列,完全没法用,所以这一层宁可多花时间也别偷懒。
3. 关键词提取实战:jieba分词、TF-IDF和TextRank到底怎么选
3.1 jieba分词与自定义词典的配合方式
分词是整个关键词分析的地基。中文不像英文有天然空格分隔,选错分词工具或词典会让后续结果南辕北辙。我选用jieba,主要看重它的易用性和工业成熟度,Google一下到处都是资料,遇到问题也容易搜到答案。
jieba.lcut()是基础用法,但直接用在旅游文本上效果一般,因为地名和景点的识别是个痛点。比如“栈桥”“八大关”这类专有名词,标准模式可能被拆成“栈”和“桥”,或者“八大”和“关”。这种情况下需要加载自定义词典,把城市内的景点名、商圈名、特色小吃等词预先加入。
python复制import jieba
custom_words = [
"栈桥", "八大关", "崂山", "啤酒博物馆", "劈柴院",
"海肠捞饭", "皮皮虾", "五四广场", "金沙滩", "小麦岛"
]
for word in custom_words:
jieba.add_word(word, freq=10000, tag="ns")
text = "青岛栈桥的海鸥特别多,逛完去了八大关,傍晚在五四广场散步,吃了海肠捞饭。"
words = jieba.lcut(text)
print(words)
# 输出: ['青岛', '栈桥', '的', '海鸥', '特别', '多', ',', '逛完', '去', '了', '八大关', ...]
自定义词典的freq参数有讲究,设低了可能不起作用,设太高又会影响其他词。我通常设为10000,同时把tag标成地名,这样既能保证不拆词,又不会干扰正常分词的统计逻辑。还要注意,词典不是越大越好,每个城市只需要维护100到200个核心词就够了,关键是覆盖高频出现的地点和美食。
3.2 TF-IDF和TextRank:原理区别与代码实现
分词之后进入正题,用哪两种算法来提取关键词?先说TF-IDF。它的核心思想是:一个词在本文档出现频率高,同时在其他文档出现频率低,这个词就具有很好的区分度。直观理解就是,如果一百篇攻略里每篇都写“好吃”,那“好吃”就没鉴别力;反而是“海肠捞饭”“皮皮虾”这类只在特定城市攻略里高频出现的词,更能代表城市特色。
jieba的analyse.extract_tags已经内置了TF-IDF,用起来很方便:
python复制from jieba import analyse
tags_tfidf = analyse.extract_tags(
"\n".join(clean_comments),
topK=30,
withWeight=True,
allowPOS=("ns", "n", "vn", "nr")
)
for word, weight in tags_tfidf:
print(f"{word}\t{weight}")
再看TextRank。它借鉴了PageRank的思路,把词看作是图中的节点,如果两个词在一句话里相邻出现,说明它们之间存在某种联系,词的重要性由相邻词给它“投票”多少决定。TextRank的优势是不依赖外部语料库计算逆文档频率,完全基于当前文本内部结构,特别适合处理短文本。
python复制from jieba import analyse
tags_textrank = analyse.textrank(
"\n".join(clean_comments),
topK=30,
withWeight=True,
allowPOS=("ns", "n", "vn")
)
两个算法结果对比后会发现一个规律:TF-IDF更擅长捕捉“这个小众词只在局部出现”的地域特色,TextRank更擅长提取“贯穿全文的主题词”。比如在青岛数据里,TF-IDF容易把“青啤”“蛤蜊”这类特色词排前,TextRank则会倾向给“青岛”“海边”“攻略”这类贯穿性词高权重。实际操作时我把两个算法的前30名合并去重,再加一个交集加权,既保留特色词又保留主题词。
3.3 旅游场景下的关键词增强:词性过滤和停用词表
只跑默认参数是不够的,还需要给关键词提取做两个定制化增强。第一个是词性过滤,通过allowPOS参数把范围限定在名词、地名、动名词上,自动排除形容词和动词的干扰。之所以这么做,是因为游客评论里“开心”“方便”“好吃”的情绪词对分析城市特色没有直接价值,它们更适合放到情感分析模块去用。
第二个增强是停用词表,这是很多新手容易忽略的。jieba默认有一些停用词,但远远不够,旅游场景里“真的”“觉得”“就是”“一点”这类口语化高频词需要手动添加。我维护了一个调度词表,里面包含三类:无意义语气词(“在呢”“哦哦”)、通用行为词(“去了”“看到”“下来”)和特定平台词(“种草”“打卡”中意思淡化的部分)。注意“打卡”和“种草”这类词虽然虚,但有时也能反映出一定用户行为,是否过滤取决于分析目的。
我个人建议把停用词表放到独立txt文件里,方便不同城市复用时调整,而不是硬编码在代码中。
python复制stop_words = set()
with open("stopwords.txt", "r", encoding="utf-8") as f:
for line in f:
word = line.strip()
if word:
stop_words.add(word)
def extract_keywords(texts, topk=30):
result = []
for tag, weight in analyse.extract_tags(
"\n".join(texts), topK=topk * 2, withWeight=True
):
if tag not in stop_words:
result.append((tag, weight))
if len(result) >= topk:
break
return result
4. 分析结果怎么落地:趋势图、词云与情感标签的联动方案
4.1 时间维度上的关键词热度趋势
静态关键词排名只是第一步,更有价值的分析是看关键词随时间的变化趋势。比如今年五一期间“日出”这个词突然排到前三,说明最近城市在推日出打卡点了;如果“地铁”的权重连续三个月上涨,说明公共交通体验正在成为游客关注重点。
实现思路不复杂:先给每条评论打上发布时间标签,再按月份分组,对每个月单独提取关键词和权重,最后把一个关键词在不同月份的热度连成折线图。这里有一个细节值得注意——单月评论量太少时关键词权重波动非常大,需要做平滑处理。我用的是简单移动平均,窗口设置为3个月,效果在实战中比较稳定。
python复制import pandas as pd
import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
df = pd.read_csv("comments_with_date.csv")
df["month"] = pd.to_datetime(df["post_date"]).dt.to_period("M")
# 按月统计关键词出现次数
keywords_per_month = df.groupby("month")["keywords"].apply(
lambda x: pd.Series(",".join(x).split(",")).value_counts()
).unstack().fillna(0)
# 选择目标关键词并做3个月移动平均
target = keywords_per_month[["栈桥", "崂山", "啤酒"]].rolling(3).mean()
target.plot(figsize=(10, 6))
plt.title("重点关键词月度热度趋势")
plt.show()
4.2 词云图生成:中文字体是绕不过去的坑
词云图是最容易出效果、也最容易出问题的环节。Python里wordcloud库生成词云本身很成熟,但中文显示必须手动指定字体,否则生成出来的全是方块。我第一次跑的时候没指定字体,以为代码写错了,排查了半天才发现是字体问题。
python复制from wordcloud import WordCloud
word_freq = dict(extract_keywords(clean_comments, topk=100))
wc = WordCloud(
font_path="C:/Windows/Fonts/simhei.ttf", # 中文字体路径
width=1200,
height=800,
background_color="white",
max_words=100,
colormap="coolwarm",
)
wc.generate_from_frequencies(word_freq)
wc.to_file("city_keywords_cloud.png")
字体路径在Windows和macOS下完全不同,更省心的做法是把字体文件复制到项目目录,用相对路径引用。词云的背景图也可以用mask参数指定成城市地标轮廓,比如青岛用栈桥或啤酒瓶造型,视觉冲击力会强很多。网上免费素材很多,搜“剪影png”就能找到。
4.3 关键词加情感分析:从“游客说了什么”升级到“游客感觉怎么样”
纯关键词只能知道游客关注什么,想知道他们是对这里满意还是吐槽,必须加一层情感分析。我用的是SnowNLP,它虽然训练语料偏电商评论,但迁移到旅游场景后整体可用,尤其是对中长文本的情感判断准确率在能接受的范围。
python复制from snownlp import SnowNLP
def sentiment_score(text):
return SnowNLP(text).sentiments # 0到1,越接近1越正面
df["sentiment"] = df["text"].apply(sentiment_score)
情感分数和关键词结合之后能做出很有意思的交叉分析。比如“海鲜”这个关键词对应的评论平均情感分是0.82,说明游客对当地海鲜整体满意;而“出租车”对应的平均分只有0.41,说明交通体验拖后腿。这套交叉结果直接输出成Excel表,文旅局的人拿到就能用。
我尝试过把情感分析结果做成散点图,横轴是关键词出现次数,纵轴是平均情感分,气泡大小代表样本量。这样能一眼看出哪些词既是高频又是好评,哪些词是高频差评——前者是城市名片,后者是整改重点。
5. 源码和文档的组织细节:让人拿到项目十分钟就能跑起来
5.1 项目目录结构与模块职责划分
既然标题里写了“源码+文档”,那代码组织和文档质量就格外重要。我看过太多爬虫和数据分析项目的源码,直接把所有逻辑堆在一个main.py里,别人拿到手根本不敢动。这个项目我尽量做了模块化拆分,每个文件只负责一件事,命名一看就懂:
text复制travel-keyword-analysis/
├── README.md
├── requirements.txt
├── config/
│ ├── custom_words.txt
│ └── stopwords.txt
├── data/
│ ├── raw/
│ ├── cleaned/
│ └── output/
├── src/
│ ├── crawler.py
│ ├── cleaner.py
│ ├── keyword_extractor.py
│ ├── sentiment_analyzer.py
│ └── visualizer.py
├── docs/
│ ├── 需求文档.md
│ ├── 设计文档.md
│ └── 使用手册.md
└── main.py
每个模块的职责划分遵循单一功能原则。crawler.py只管采集,返回原始文本列表;cleaner.py负责清洗、去重和编码处理,输出干净的DataFrame;keyword_extractor.py封装了jieba的自定义词典加载和TF-IDF调优;sentiment_analyzer.py负责情感打分;visualizer.py集中管所有图表生成;main.py只负责串联整个流程。
这样做的好处很明显——调整某个环节不用牵一发动全身。比如想换个情感分析模型,只改sentiment_analyzer.py,其他模块完全不用动。团队协作时,不同人也能并行开发不同模块。
5.2 文档应该如何写:README、需求文档和使用手册
“文档”是这个项目交付物的重要组成部分。我的经验是三份文档各司其职,不要全部塞到一个README里。
README.md面向的是第一次拿到项目的人,要让他在5分钟内能跑通。需要写明项目简介、环境依赖、安装步骤、运行命令和输出说明。最重要的是一段可直接复制的命令序列:
bash复制git clone https://github.com/yourname/travel-keyword-analysis.git
cd travel-keyword-analysis
pip install -r requirements.txt
python main.py --city 青岛 --pages 20
需求文档面向自己或客户,记录项目要解决什么问题、需要哪些数据、输出什么成果。不需要太复杂,用表格把功能需求和验收标准列清楚即可。设计文档面向以后要维护代码的人,记录技术选型理由、模块关系、关键算法原理。使用手册面向非技术用户,讲清楚配置文件怎么改、结果文件在哪里、图表怎么解读。
三份文档加起来一万字左右,写作耗时比代码多,但这个投入是值得的。面试或答辩时,完整的文档比代码更有说服力,因为它展示了系统化思考和沟通能力。
5.3 参数配置和扩展性设计
好的项目必须能轻松换城市复用,而不是写死在某一个城市上。所有上海、青岛这样硬编码都是大忌。我建议通过config.ini或命令行参数来配置,让同一套代码处理任意城市。
bash复制python main.py --city 杭州 --platform douban --pages 30
配置内容包括城市名、要加载的自定义词典路径、停用词表路径、爬取页面范围和输出目录。再把关键词提取的topK、词云图大小、情感分析阈值也做成可配置项。这样别人拿着源码既可以跑默认的青岛案例,也可以一键换成成都、西安、厦门的数据,复用性立刻提升了一个档次。
我把本项目的配置精简了一下,核心内容如下:
ini复制[crawler]
city = 青岛
platform = example-travel
pages = 20
interval = 1.5
[analysis]
topk = 30
algorithms = tfidf,textrank
enable_sentiment = true
[visual]
font_path = ./assets/simhei.ttf
cloud_shape = ./assets/mask.png
output_dir = ./output
6. 运行中踩过的五个坑,每一个都真实到肉疼
6.1 Windows下的中文编码错乱
这个坑几乎是每位中文文本分析者都会遇到的。明明在macOS或Linux上跑得好好的,换到Windows就出现各种乱码或者UnicodeEncodeError。原因很直接:Windows默认控制台编码是GBK,而Python 3的字符串默认是UTF-8,print输出中文时偶尔就会冲突。
我之前踩坑后的解决思路是“三步走”:所有文件读写都显式指定encoding="utf-8",绝不依赖系统默认;写CSV文件时用encoding="utf-8-sig",这样Excel打开也不会乱码;控制台输出前把stream重设为UTF-8或用errors="replace"兜底。
python复制import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
6.2 词云图中文全部变方框
前面提过一次,但这里想再强调一下。wordcloud库的默认字体是DroidSansMono,不支持中文,所以不设置font_path就生成中文词云,结果一定是满屏方块。这件事排查起来很浪费时间,因为代码本身可以正常执行、图片也能生成,看着像是wordcloud出bug了,实际只是字体缺失。
解决方法是去C:\Windows\Fonts下找到simhei.ttf或msyh.ttf,复制到项目assets目录,然后在生成时用相对路径引用。macOS用户对应的是系统自带的PingFang.ttc或Arial Unicode.ttf,路径写法有些不同。如果是在服务器上跑,还得先确认中文字体文件已经安装。
6.3 自定义词典词频参数设置不合理
我在自定义词典里给“青岛啤酒”设了freq=100000,想着让它一定保留,结果导致分词结果里“青岛啤酒”频繁出现,把原本的“青啤”“青岛”“啤酒”都挤压掉,关键词权重严重失衡。freq值并不是越大越好,它在jieba动态规划里会起到强引导作用,设置太高会破坏自然语言本身的统计规律。
后来我把所有自定义词的freq统一设置为10000左右,并标注了正确的词性,再配合allowPOS过滤,效果就平衡多了。建议新建自定义词时先用jieba.suggest_freq来检测词频合理性,而不是盲目调大。
6.4 爬虫请求频率过快被限制
这是采集阶段的常见坑。最初测试时没控制好间隔,一个线程连续发了几百个请求,结果IP被目标平台临时限制访问,影响了大半天进度。处理办法也没有多玄学,严格遵守“慢速爬取”规则:每个页面之间至少间隔1.5秒,做好重试机制,随机User-Agent池。如果任务量大,可以把抓取任务拆分成小批次,每隔一段时间运行一次。
说到合规性,我建议项目里明确标注“仅限技术学习和研究用”,不要拿去大规模采集商用数据。尊重目标网站的服务条款,设置robots协议检查逻辑(虽然实现上我们用的是示例URL,但保持这一良好习惯很重要)。
6.5 数据量过小导致的关键词漂移
用500条评论跑出来的关键词和用2000条跑出来的结果差别很大,越少的数据越容易出现“词频失真”。我最初只用200条测试数据,结果“啊啊啊”这种无意义词都被排进前十,自定义词典里的字段覆盖多半靠运气。后来把数据量提升到1500条以上,再用停用词表和词性过滤双管齐下,结果才稳定下来。
如果你实在拿不到大样本,建议采用多轮抽取策略:把数据随机分成几份,分别提取关键词,只看在多个子集里稳定出现的词。这个方法可以显著提高关键词的鲁棒性,也让文档里“分析结论”更有说服力。
这个项目从构思到完成大概花了两周时间,大部分时间消耗在数据清洗和调优上,真正写代码的时间并不多。如果你也准备做类似的项目,我的建议是不要贪大求全,先把一个城市、一个平台的数据跑通,再把功能往外扩展。源码和文档这块投入的精力一定要给足,它们是项目从“能跑”升级成“可用”的关键。
