资讯详情 Python中文文本分析实战:从词频统计到词云图生成
📅 2026/10/12 14:50:25
1. 从一个真实需求说起为什么词频图和词云值得单独写一篇做文本分析的朋友大概率都经历过这个场景手里拿到一批文本数据可能是用户评论、工单记录、文章标题、聊天日志老板或者合作方第一句话就是——“能不能先给我看看这批数据里大家都在说什么”这时候你不可能直接甩一个原始文本文件过去最直观的方式就是两张图词频统计图和词云图。词频统计图解决的是“哪些词出现得最多、排名如何”的问题它精确、可量化、方便对比词云图解决的是“一眼看上去整体印象是什么”的问题它直观、有冲击力、适合放进汇报材料。两者搭配使用一个负责严谨一个负责好看基本能覆盖大部分初步文本探索的需求。这篇文章要聊的就是怎么用Python把一份原始文本数据经过清洗、分词、去停用词、统计词频最终生成这两张图。整套流程不依赖什么高深模型纯基础操作但里面的坑一点都不少——编码问题、中文分词、停用词表选择、字体配置、词频归一化每一个环节都能让新手卡半天。我会把完整流程拆开讲包括每一步为什么这么做、参数怎么选、遇到问题怎么排查代码可以直接拿去改改就用。适合谁看如果你已经会一点Python基础语法知道列表和字典是什么但还没完整做过一次文本预处理到可视化的流程那这篇就是写给你的。如果你已经做过几次也可以看看里面关于停用词策略和词云参数调优的部分说不定有你没注意到的细节。2. 整体流程设计与技术选型思路2.1 为什么选这套技术栈做中文文本预处理和可视化Python生态里有好几套方案可选。我最终选的是jieba collections.Counter matplotlib wordcloud这个组合理由如下。jieba是中文分词领域最成熟的开源库之一支持三种分词模式社区活跃文档齐全遇到问题搜索一下基本都有答案。collections.Counter是Python标准库自带的计数器专门用来做频次统计底层是字典性能足够好而且有most_common()这种开箱即用的方法。matplotlib是Python可视化的万金油虽然画出来的图默认风格偏“学术”但胜在可控性强每个元素都能调。wordcloud库专门做词云支持自定义字体、蒙版、配色生成效果比手动用matplotlib拼要好得多。有人可能会问为什么不用pandas做统计pandas当然可以value_counts()一行就出结果。但如果你只是做词频统计引入pandas有点重而且pandas和Counter之间的转换反而多了一步。当然如果你的数据本身就是表格形式用pandas读取和处理是合理的这个后面会提到。2.2 完整流程拆解整个流程可以拆成六个步骤我用一个表格先列出来后面再逐步展开。步骤核心任务关键工具常见坑点1读取原始文本open() / pandas编码格式不统一2文本清洗re正则标点、数字、特殊符号处理3中文分词jieba自定义词典、模式选择4去停用词停用词表停用词表选择与维护5词频统计Counter排序、截断、归一化6可视化输出matplotlib / wordcloud字体、布局、配色这个流程看起来线性但实际操作中经常需要回头调整。比如分词之后发现某些词被切碎了就要回去补充自定义词典词云生成后发现某个词大得离谱就要回去检查停用词表是不是漏了什么。所以不要指望一次跑通迭代两三轮是正常的。2.3 数据形态的预判在动手之前先想清楚你的数据是什么形态。常见的有三种第一种是单个大文本文件比如一篇长文、一份报告直接读取整个文件内容作为一个字符串处理。第二种是多行文本比如每行一条评论、一个标题需要逐行处理后再合并统计。第三种是表格数据中的某一列比如Excel或CSV里有一列是“用户反馈”需要用pandas读取后提取该列。三种形态的处理逻辑略有不同但核心步骤一致。本文的示例代码以第二种多行文本为主因为它最接近实际工作中的日志和评论数据同时会说明另外两种怎么调整。3. 核心细节解析与实操要点3.1 文本读取与编码处理编码问题是中文文本处理的第一道坎。你拿到的文件可能是UTF-8、GBK、GB2312、GB18030甚至混合编码。最稳妥的做法是先用二进制模式读一小段检测编码再决定用什么方式打开。实际操作中我一般先用chardet库检测编码如果没有安装用open()的errorsignore参数兜底。但errorsignore会静默丢弃无法解码的字符可能导致数据缺失所以只作为临时方案。import chardet def detect_encoding(file_path): with open(file_path, rb) as f: raw f.read(10000) result chardet.detect(raw) return result[encoding] encoding detect_encoding(data.txt) with open(data.txt, r, encodingencoding, errorsreplace) as f: lines f.readlines()注意errorsreplace会把无法解码的字符替换成特殊符号比ignore更安全因为你能看到哪些地方出了问题。如果是CSV或Excel文件pandas的read_csv()和read_excel()通常能自动处理编码但遇到GBK编码的CSV时可能需要手动指定encodinggbk。Excel文件一般不需要担心编码问题但要注意.xls和.xlsx格式的区别前者需要xlrd引擎后者需要openpyxl。3.2 文本清洗的策略原始文本里充满了对分析无用的内容标点符号、数字、英文、URL、HTML标签、表情符号等。清洗的目标是保留有意义的中文词汇去掉噪声。但“有意义”的定义取决于你的分析目标。如果你分析的是电商评论那么“好评”“差评”“物流”“客服”这些词很重要如果你分析的是技术论坛帖子那么“报错”“配置”“版本”这些词是关键。所以清洗规则不能一刀切要根据场景调整。我通常的清洗顺序是先去HTML标签如果数据来自网页再去URL和邮箱然后去掉标点和特殊符号最后处理数字和英文。中文标点和英文标点都要处理因为很多文本里混着用。import re def clean_text(text): # 去除HTML标签 text re.sub(r[^], , text) # 去除URL text re.sub(rhttp[s]?://\S, , text) # 去除邮箱 text re.sub(r\S\S, , text) # 去除标点符号保留中文和英文 text re.sub(r[^\w\s\u4e00-\u9fff], , text) # 去除纯数字 text re.sub(r\d, , text) # 去除多余空白 text re.sub(r\s, , text).strip() return text这里有个细节\u4e00-\u9fff是中文Unicode范围保留中文\w保留字母数字下划线\s保留空白。但这样会把英文单词也保留下来如果你只想分析中文可以把\w去掉只留\u4e00-\u9fff和\s。实操心得清洗规则不要写得太死建议把清洗前后的文本各打印几行对比一下确认没有误删重要内容。我曾经因为正则写得太激进把“C”这种词直接删没了后来分析编程语言讨论时发现数据缺失回头排查才发现问题。3.3 中文分词的关键参数jieba的分词模式有三种精确模式、全模式和搜索引擎模式。精确模式适合文本分析它试图把句子最准确地切开全模式会把所有可能的词都列出来速度快但有冗余搜索引擎模式在精确模式基础上对长词再切分适合做搜索索引。做词频统计我一般用精确模式代码是jieba.lcut(text)返回一个列表。如果你需要保留词性信息可以用jieba.posseg但词频统计通常不需要词性。import jieba text 自然语言处理是人工智能领域的一个重要方向 words jieba.lcut(text) print(words) # 输出[自然语言, 处理, 是, 人工智能, 领域, 的, 一个, 重要, 方向]你会发现“自然语言”被正确切出来了但“人工智能”也被正确切出来了这说明jieba的默认词典覆盖了常见术语。但如果你处理的领域有大量专有名词比如医疗、法律、金融默认词典可能不够用需要加载自定义词典。jieba.load_userdict(custom_dict.txt)自定义词典的格式是每行一个词可以附带词频和词性用空格分隔。词频越高这个词被切出来的概率越大。如果你不确定词频可以只写词jieba会用默认值。注意加载自定义词典要在分词之前而且只需要加载一次。如果你在循环里反复加载会严重影响性能。3.4 停用词表的构建与维护停用词是那些对分析没有意义的高频词比如“的”“了”“是”“在”“和”等。中文停用词表网上有很多版本哈工大停用词表、百度停用词表、四川大学机器智能实验室停用词库各有侧重。我一般会合并几个表再根据具体任务补充。但停用词表不是越大越好。有些词在通用场景下是停用词在你的场景下可能是关键词。比如“不”在情感分析里很重要“一”在数量分析里可能有用。所以我的做法是先用通用停用词表跑一遍看看结果里有没有明显不该出现的词再手动补充。def load_stopwords(file_path): with open(file_path, r, encodingutf-8) as f: stopwords set([line.strip() for line in f if line.strip()]) return stopwords stopwords load_stopwords(stopwords.txt) # 补充自定义停用词 stopwords.update([回复, 转发, 点赞, 楼主, 沙发])用set而不是list来存储停用词是因为判断一个词是否在集合中的时间复杂度是O(1)而列表是O(n)。当停用词表有几千个词、待过滤的词有几万个时这个差异非常明显。实操心得停用词表建议单独存成一个文件不要硬编码在脚本里。这样不同项目可以复用也方便版本管理。我习惯在项目目录下建一个resources文件夹专门放停用词表、自定义词典、字体文件这些资源。3.5 词频统计的细节处理用Counter统计词频很简单但有几个细节值得注意。第一过滤单字。中文里单个字往往信息量不足比如“的”“了”“是”是停用词但“人”“事”“物”这种单字可能有用。我的做法是如果分析目标是主题词过滤掉长度为1的词如果分析目标是情感词保留单字可能更好。这个没有标准答案看需求。第二词频归一化。如果两份文本长度差异很大直接比较词频不公平。可以用词频除以总词数得到相对频率。但词云图通常用绝对词频因为视觉上大词就是大词归一化后反而不好看。第三截断Top N。词频统计图一般只展示前20或前30个词太多会挤在一起看不清。词云图可以展示更多但也要控制一般100到200个词比较合适。from collections import Counter def get_word_freq(words, stopwords, min_length2): filtered [w for w in words if w not in stopwords and len(w) min_length] counter Counter(filtered) return counter counter get_word_freq(words, stopwords) top_20 counter.most_common(20) print(top_20)most_common(n)返回前n个高频词及其计数返回的是列表每个元素是(词, 频次)的元组。如果你需要字典形式可以用dict(counter.most_common(n))。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把需要的库装好。我习惯用pip安装如果你用conda也可以但要注意conda源里的版本可能和pip不一样。pip install jieba wordcloud matplotlib chardetmatplotlib在有些系统上需要额外配置后端如果你在服务器上跑没有图形界面需要设置matplotlib.use(Agg)否则会报错。这个坑我踩过好几次尤其是在Docker容器里跑的时候。import matplotlib matplotlib.use(Agg) # 无图形界面时必须 import matplotlib.pyplot as plt字体问题也是高频坑点。matplotlib默认字体不支持中文词云库也需要指定中文字体路径。Windows系统可以用C:/Windows/Fonts/simhei.ttfLinux系统需要找到系统中文字体macOS可以用/System/Library/Fonts/PingFang.ttc。如果你不确定系统里有什么中文字体可以用fc-list :langzh命令查看Linux/macOS。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # Windows # plt.rcParams[font.sans-serif] [PingFang SC] # macOS # plt.rcParams[font.sans-serif] [WenQuanYi Micro Hei] # Linux plt.rcParams[axes.unicode_minus] False # 解决负号显示问题注意axes.unicode_minus这个参数一定要设成False否则坐标轴上的负号会显示成方块。这个问题在画柱状图时特别明显。4.2 完整代码实现下面是一个完整的示例从读取文件到生成两张图。我把它拆成函数方便复用和调试。import re import jieba from collections import Counter import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt from wordcloud import WordCloud import chardet def detect_encoding(file_path): with open(file_path, rb) as f: raw f.read(10000) return chardet.detect(raw)[encoding] def read_text(file_path): encoding detect_encoding(file_path) with open(file_path, r, encodingencoding, errorsreplace) as f: return f.read() def clean_text(text): text re.sub(r[^], , text) text re.sub(rhttp[s]?://\S, , text) text re.sub(r\S\S, , text) text re.sub(r[^\w\s\u4e00-\u9fff], , text) text re.sub(r\d, , text) text re.sub(r\s, , text).strip() return text def load_stopwords(file_path): with open(file_path, r, encodingutf-8) as f: return set([line.strip() for line in f if line.strip()]) def segment(text, stopwords, min_length2): words jieba.lcut(text) return [w for w in words if w not in stopwords and len(w) min_length] def plot_bar(counter, top_n20, save_pathbar.png): top_words counter.most_common(top_n) words, counts zip(*top_words) plt.figure(figsize(12, 6)) bars plt.bar(range(len(words)), counts, color#4C72B0) plt.xticks(range(len(words)), words, rotation45, haright) plt.xlabel(词语) plt.ylabel(出现频次) plt.title(f词频统计图Top {top_n}) for bar, count in zip(bars, counts): plt.text(bar.get_x() bar.get_width()/2, bar.get_height(), str(count), hacenter, vabottom, fontsize9) plt.tight_layout() plt.savefig(save_path, dpi150) plt.close() def plot_wordcloud(counter, font_path, save_pathwordcloud.png): wc WordCloud( font_pathfont_path, width1200, height800, background_colorwhite, max_words150, colormapviridis, random_state42 ) wc.generate_from_frequencies(counter) plt.figure(figsize(12, 8)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.tight_layout() plt.savefig(save_path, dpi150) plt.close() if __name__ __main__: text read_text(data.txt) text clean_text(text) stopwords load_stopwords(stopwords.txt) words segment(text, stopwords) counter Counter(words) plot_bar(counter, top_n20, save_pathbar.png) plot_wordcloud(counter, font_pathC:/Windows/Fonts/simhei.ttf, save_pathwordcloud.png) print(f总词数{len(words)}) print(f唯一词数{len(counter)}) print(fTop 10{counter.most_common(10)})这段代码可以直接跑只需要准备好data.txt、stopwords.txt和字体文件。输出是两张PNG图和一个控制台统计摘要。4.3 参数调优与效果对比词云图的效果很大程度上取决于参数设置。我拿同一份数据做了几组对比用表格展示差异。参数取值A取值B效果差异max_words5015050个词时每个词更大更清晰150个词时信息更丰富但小词难辨认background_colorwhiteblack白色背景适合文档黑色背景适合演示colormapviridisRedsviridis色彩层次丰富Reds单色系更统一width/height800x6001200x800大尺寸适合打印小尺寸适合网页random_stateNone42固定随机种子后每次生成布局一致方便对比我的经验是如果词云是放在报告里作为配图用白色背景、150个词、viridis配色如果是放在PPT里做视觉冲击用黑色背景、80个词、高对比度配色。random_state建议固定否则每次生成的布局都不一样调参时无法对比。词频统计图这边主要调的是top_n和图形尺寸。top_n20适合横向柱状图top_n30适合纵向柱状图但需要旋转标签。我一般用横向柱状图因为中文词的长度不一横向排列更容易阅读。def plot_barh(counter, top_n20, save_pathbarh.png): top_words counter.most_common(top_n) words, counts zip(*top_words) plt.figure(figsize(10, 8)) plt.barh(range(len(words)), counts, color#55A868) plt.yticks(range(len(words)), words) plt.gca().invert_yaxis() plt.xlabel(出现频次) plt.title(f词频统计图Top {top_n}) for i, count in enumerate(counts): plt.text(count, i, str(count), vacenter, fontsize9) plt.tight_layout() plt.savefig(save_path, dpi150) plt.close()横向柱状图用barh()然后用invert_yaxis()把最高的词放在顶部。这个细节很多人不知道默认情况下matplotlib的y轴是从下往上递增的不反转的话最高频的词在最下面看起来别扭。5. 常见问题与排查技巧实录5.1 编码报错与乱码问题问题表现UnicodeDecodeError: utf-8 codec cant decode byte...或者读出来的中文是乱码。排查思路先用chardet检测编码如果检测结果置信度低手动尝试GBK、GB18030、UTF-8-SIG等常见编码。UTF-8-SIG是带BOM的UTF-8Windows下用记事本保存的文件经常是这种格式。解决方案encodings_to_try [utf-8, gbk, gb18030, utf-8-sig, big5] for enc in encodings_to_try: try: with open(data.txt, r, encodingenc) as f: content f.read() print(f成功使用编码{enc}) break except UnicodeDecodeError: continue实操心得如果所有编码都失败用errorsreplace强制读取然后检查替换字符出现的位置通常能定位到问题段落。有些文件是混合编码前半部分GBK后半部分UTF-8这种情况只能分段处理。5.2 分词结果不理想问题表现该切开的词没切开比如“机器学习”被切成“机器”和“学习”或者不该切开的切开了比如“区块链”被切成“区块”和“链”。排查思路先确认jieba版本老版本词典覆盖不够。然后检查是否需要加载自定义词典。最后考虑调整分词模式。解决方案# 添加单个词 jieba.add_word(区块链, freq1000, tagn) # 批量添加 jieba.load_userdict(custom_dict.txt) # 调整词频freq越高越容易被切出来 jieba.suggest_freq(机器学习, True) # True表示强制成词 jieba.suggest_freq((机器, 学习), True) # 强制切开suggest_freq这个方法很实用它动态调整词频不需要修改词典文件。但注意它只对当前会话有效脚本重启后失效。5.3 词云图显示方块问题表现词云图里中文全部显示成方块或空白。原因字体路径不对或者字体文件不支持中文。解决方案确认字体文件存在且支持中文。Windows下simhei.ttf、simsun.ttc、msyh.ttc都支持中文Linux下需要安装中文字体包常见的有wqy-microhei、wqy-zenheimacOS下PingFang.ttc、STHeiti Light.ttc都可以。import os font_candidates [ C:/Windows/Fonts/simhei.ttf, C:/Windows/Fonts/msyh.ttc, /System/Library/Fonts/PingFang.ttc, /usr/share/fonts/truetype/wqy/wqy-microhei.ttc, ] font_path None for fp in font_candidates: if os.path.exists(fp): font_path fp break if font_path is None: raise FileNotFoundError(未找到可用的中文字体请手动指定)注意.ttc是字体集合文件wordcloud库对.ttc的支持在某些版本上有问题如果报错尝试用.ttf格式的字体。5.4 词频统计结果异常问题表现统计结果里出现大量无意义的高频词或者某些明显重要的词频次很低。排查思路先检查停用词表是否覆盖了这些无意义词再检查分词是否正确最后检查清洗步骤是否误删了重要内容。常见问题速查表问题现象可能原因解决方法“的”“了”排在前列停用词表未加载或路径错误检查停用词文件路径和编码某个词频次异常高该词在文本中重复出现如页眉页脚在清洗阶段去重或过滤重要词频次很低被分词切碎或误删检查分词结果和清洗正则统计结果为空清洗后文本为空或停用词过滤过度打印中间结果逐步排查词云图只有几个词max_words设置过小或数据量不足调整max_words检查数据量5.5 性能优化建议当文本量很大时比如几十万条评论分词和统计可能比较慢。几个优化方向第一批量分词。jieba支持并行分词用jieba.enable_parallel(4)开启4个进程速度能提升3倍左右。但注意并行分词在Windows下不支持而且会消耗更多内存。第二减少重复计算。如果多次运行脚本可以把分词结果缓存到文件下次直接读取。用pickle或json都行。第三用生成器代替列表。如果内存吃紧不要一次性把所有词读进列表用生成器逐行处理。def iter_words(file_path, stopwords): with open(file_path, r, encodingutf-8) as f: for line in f: line clean_text(line) for word in jieba.lcut(line): if word not in stopwords and len(word) 2: yield word counter Counter(iter_words(data.txt, stopwords))这样内存占用从O(n)降到O(1)代价是代码稍微复杂一点。6. 进阶技巧与场景扩展6.1 词频趋势对比如果你有多份文本比如不同时间段的评论可以分别统计词频然后对比。最简单的做法是画分组柱状图或者用词频差值排序找出增长最快和下降最快的词。def compare_freq(counter_a, counter_b, top_n15): all_words set(counter_a.keys()) | set(counter_b.keys()) diffs [] for word in all_words: diff counter_b.get(word, 0) - counter_a.get(word, 0) diffs.append((word, diff)) diffs.sort(keylambda x: abs(x[1]), reverseTrue) return diffs[:top_n]这个方法在舆情监控和产品迭代分析里很常用能快速定位用户关注点的变化。6.2 词云蒙版与形状定制wordcloud支持用图片作为蒙版生成特定形状的词云。比如用公司Logo、产品轮廓、地图轮廓作为蒙版视觉效果会好很多。from PIL import Image import numpy as np mask np.array(Image.open(mask.png)) wc WordCloud( font_pathfont_path, maskmask, background_colorwhite, max_words200, contour_width1, contour_colorsteelblue )蒙版图片要求是白底黑图白色区域会被忽略黑色区域会填充词。contour_width和contour_color用来给蒙版轮廓描边效果更精致。实操心得蒙版图片不要太大建议控制在1000x1000像素以内否则生成速度很慢。另外蒙版形状不要太复杂否则小词会挤在狭窄区域看不清。6.3 结合TF-IDF做关键词提取单纯词频统计有个问题高频词不一定是最有信息量的词。比如“用户”这个词在所有文档里都高频但它区分度不高。TF-IDF可以解决这个问题它综合考虑词频和逆文档频率找出真正有区分度的关键词。from sklearn.feature_extraction.text import TfidfVectorizer documents [文档1内容, 文档2内容, 文档3内容] vectorizer TfidfVectorizer(stop_wordsstopwords, max_features100) tfidf_matrix vectorizer.fit_transform(documents) feature_names vectorizer.get_feature_names_out()TF-IDF适合多文档场景单文档场景下退化成词频统计。如果你的数据是单篇长文用词频就够了如果是多篇文档TF-IDF能挖出更有价值的关键词。6.4 输出格式与自动化如果这套流程需要定期跑建议做成命令行工具支持参数传入文件路径和输出目录。import argparse parser argparse.ArgumentParser() parser.add_argument(--input, requiredTrue, help输入文件路径) parser.add_argument(--output, default./output, help输出目录) parser.add_argument(--top_n, typeint, default20, help词频图展示词数) parser.add_argument(--max_words, typeint, default150, help词云最大词数) args parser.parse_args()这样每次只需要改命令行参数不用改代码。配合定时任务就能实现自动化报告生成。7. 我在实际项目中的几点体会这套流程我前后用过不下二十次从最初的磕磕绊绊到现在的肌肉记忆有几个体会特别深。第一数据质量决定上限。再好的分词和可视化也救不了一堆乱码和噪声。我现在的习惯是拿到数据先花十分钟做探索性检查看编码、看长度分布、看有没有异常字符、看重复率。这十分钟能省掉后面两小时的调试。第二停用词表要养。不要指望一个通用停用词表解决所有问题。我维护了一个自己的停用词表每次做新项目就往里加几个词日积月累下来覆盖了我常做的几个领域。这个表比任何公开的停用词表都好用因为它是我自己的数据喂出来的。第三可视化是给人看的不是给机器看的。词频图和词云图的目的是沟通不是炫技。颜色不要超过三种标签要清晰标题要说明数据来源和时间范围。我见过太多词云图词是挺多但根本看不出重点这种图不如不做。第四代码要能复用。我现在的做法是把这套流程封装成一个类初始化时传入文件路径和配置参数调用run()方法一键生成所有输出。这样下次做类似任务改几个参数就能跑不用从头写。最后分享一个小技巧如果你的词云图里某个词大得离谱把其他词都挤没了可以在生成词云前对词频做一次开方或对数变换。counter[word] counter[word] ** 0.5能让词的大小差异更温和视觉效果更均衡。这个技巧在词频分布极度不均匀时特别管用你可以试试。