简介:这是一份面向文本挖掘与数据可视化初学者、中级 Python 用户的词云实战教程 PDF,适合在项目报告、数据分析、社交媒体监控与教学演示中快速应用词云。内容围绕 wordcloud 与 matplotlib 展开,先说明词云的定义、作用、生成原理与文本预处理、词频/TF-IDF 权重计算、展示保存流程,再以 7 个递进案例演示最简调用、中文不分词、中文分词、蒙版形状、自定义配色及关键词与权重提取等做法,每个案例配有代码与运行结果,并给出 PyCharm、Python 3.7、wordcloud 1.9.3、matplotlib 3.5.3 的环境参考。资源包为 1 个 PDF 文件,大小约 2.07MB,目录紧凑,便于按案例查阅和动手复现。已有 109 人学习,适合希望从零掌握词云生成并迁移到实际文本分析场景的读者。
1. 别人交一张图,你交一份能复现的流程
做课程作业或者内部小分享时,最常被派到的一类活儿,就是把一堆文字变成一张一眼能看懂的图。python 词云几乎是性价比最高的选择:不用碰模型,wordcloud 加上 matplotlib 二十来行就能出图。这份 python 课程资源里的七个案例就是照着这条线走的,从三行最简调用,到中文分词、蒙版形状、蒙版取色、自定义颜色函数,最后落到 jieba 的关键词权重上。适合谁?会一点 python 语法、想把文本可视化塞进报告里的初学者;也适合那些已经会抄WordCloud()但每次都被中文方块、空白图、形状反了折腾半天的人。七个案例串起来其实是一条完整链路:文本 → 分词 → 权重 → 布局 → 上色 → 落盘,把这条链路上的每一环拆开看,比收藏十个零散片段管用。
2. 环境与选型:wordcloud、matplotlib、jieba 三件套怎么搭
教程给的环境很明确:PyCharm 编辑器,Python 3.7,wordcloud 1.9.3,matplotlib 3.5.3。这个组合不是随便写的,wordcloud 早期版本在 Windows 上装起来相当玄学,得现装 C 编译工具链;1.9.x 之后官方 wheel 覆盖得比较全,Python 3.7 到 3.11 基本都能直接pip install装好。真正决定成败的不是版本号本身,而是三件事:字体文件能不能被找到、蒙版图能不能被正确转成数组、jieba 分出来的词是不是你想要的那些词。把这三点在环境阶段准备好,后面七个案例改起来才顺手。
2.1 依赖清单与安装顺序
wordcloud 依赖 numpy 和 pillow,matplotlib 自己带一套后端依赖,jieba 是纯 Python 无编译依赖。安装顺序其实无所谓,pip 会自己解依赖,但 numpy 跟着 wordcloud 一起装的时候容易拿到最新版,而在 Python 3.7 上最新版 numpy 可能不兼容,所以更稳的做法是先锁版本。
# 建一个干净环境,别和系统里那堆包混着 python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate # 按教程版本装,numpy 手动降一档避免 3.7 上的兼容问题 pip install "numpy<1.22" pillow pip install wordcloud==1.9.3 matplotlib==3.5.3 jiebavenv这一步很多人省掉,结果是跑案例 01 时报ImportError: cannot import name 'WordCloud',其实是装了个同名但残缺的包,或者装到了另一个解释器里。PyCharm 里对应要在 Settings → Project Interpreter 指向venv/bin/python,不要用系统自带的那个。装完可以在解释器里敲import wordcloud; print(wordcloud.__version__),输出版本号才是真装上了。如果你本地是 Python 3.11 也不用退回 3.7,wordcloud 1.9.3 有对应的 wheel,只是 matplotlib 可以上 3.7 以上。
2.2 中文字体:最容易翻车的一环
七个案例里有六个跟中文有关,而 wordcloud 默认用的字体是 DroidSansMono,里面没有汉字字形,出来的图就是一堆方块。教程里给了一份随包的 ttf 字体并在代码里用font_path='../Hiragino.ttf'指向它,你本地换成系统里的中文字体效果一样。Linux 上常见的是wqy-zenhei.ttc或NotoSansCJK-Regular.ttc,Windows 上是C:/Windows/Fonts/simhei.ttf或msyh.ttc,macOS 上是/System/Library/Fonts/PingFang.ttc。
不知道本机有哪些可选字体,可以用 matplotlib 的字体管理器扫一遍,比翻目录快:
import matplotlib.font_manager as fm # 扫出系统里名字带黑体/宋体/雅黑关键词的字体文件 cands = [f.fname for f in fm.fontManager.ttflist if any(k in f.name for k in ('Hei', 'Song', 'YaHei', 'PingFang', 'Noto'))] for p in dict.fromkeys(cands): # 去重后打印 print(p)打印出来的路径可以直接丢给font_path。要注意.ttc是字体集合文件,wordcloud 对它的支持依赖 pillow 版本,遇到OSError: cannot open resource就换成同目录下的.ttf。我一般会把选定的字体路径写成一个模块级常量FONT,七个案例共用,改一处全改完,比在每个文件里写死路径省事。
2.3 素材目录:一次摆好,案例直接跑
教程里的代码用了../xyj.txt、../black_mask.png、../color_mask.png这种相对路径,说明原始工程是「代码放子目录、素材放上级目录」的结构。自己复现时不必照抄,但建议保持「素材和输出分开」这个习惯,否则跑几轮下来目录里全是 png。
| 文件 | 作用 | 说明 |
|---|---|---|
text_cn.txt | 中文样本文本 | 几万字为宜,UTF-8 编码 |
text_en.txt | 英文样本文本 | 案例 01 用,空格天然分词 |
stopwords.txt | 停用词表 | 一行一个词,可自己增删 |
black_mask.png | 黑白形状蒙版 | 白底黑形,决定字的落点 |
color_mask.png | 彩色蒙版 | 既定形状又定颜色 |
out/ | 输出目录 | to_file统一写这里 |
蒙版图建议控制在 1000×1000 像素以内。两三千像素的大图放进mask参数,wordcloud 内的碰撞检测复杂度会上去,一张图跑几十秒很常见,这个坑后面还会细说。
3. 文本处理链路:jieba 分词、停用词与关键词权重
七个案例里真正拉开差距的不是画图那几行,而是「交给 wordcloud 的到底是什么」。案例 01 和 02 给的是原始文本,案例 03 之后给的是分词结果,案例 07 给的干脆是{词: 权重}字典。三种输入对应 wordcloud 的两个入口:generate(text)和generate_from_frequencies(freq)。把这条输入侧链路搞明白,七个案例基本就通了。
3.1 案例 01 与 02:最简调用和中文不分词
案例 01 是标准的英文路径,读文件、生成对象、显示、保存,四步。这段代码短到没什么可优化,但它是验证环境是否装好的最快方式——能出图,说明 wordcloud、matplotlib、字体、样本文件四样都对了。
# -*- coding: utf-8 -*- from wordcloud import WordCloud import matplotlib.pyplot as plt # 读文本,显式指定编码,避免 Windows 下 GBK 报错 with open('text_en.txt', 'r', encoding='utf-8') as f: text = f.read() # 默认参数先跑通,不传 font_path 时用内置英文字体 wc = WordCloud().generate(text) plt.imshow(wc, interpolation='bilinear') # bilinear 让缩放更平滑 plt.axis('off') # 关掉坐标轴,出图更干净 plt.show() wc.to_file('out/wordcloud.png')interpolation='bilinear'是 matplotlib 的插值方式,词云本身是位图,放大显示时用双线性插值不会出现锯齿块。plt.axis('off')不去掉的话,图四周会带着刻度线和边框,直接放进 PPT 很难看。wc.to_file()这一步和plt.show()是两条独立的输出路径,前者按 wordcloud 对象自身的尺寸存原图,后者受窗口缩放影响,想要高清图以to_file为准,或者在构造时加scale=2。
案例 02 把文本换成了中文,并加了font_path、width、height、mode、background_color五个参数,但没做分词。这一版的实际效果通常不理想:wordcloud 内部靠正则把文本切成 token,中文句子没有空格,很容易整段连续汉字被当成一个超长 token,最后图上只有寥寥几个巨大的词块,甚至一片空白。
# 中文不分词:能出图,但词块数量往往少得可怜 wc = WordCloud(font_path='Hiragino.ttf', width=800, height=600, mode='RGBA', background_color=None).generate(text)mode='RGBA'配上background_color=None是让背景透明的组合,存成 png 后可以直接贴到别的底色上。这个组合在案例 02 到 07 里反复出现,也埋了个坑:很多看图软件在白色背景上显示透明图,看着像「图没出来」,后面排查章节会专门讲。
3.2 案例 03:jieba 分词把长句拆成词
案例 03 只加了两行,却是整个教程的分水岭:
import jieba with open('text_cn.txt', 'r', encoding='utf-8') as f: text = f.read() # cut 返回生成器,用空格 join 成字符串喂给 wordcloud text = ' '.join(jieba.cut(text)) print(text[:100]) # 打印前 100 字符,肉眼确认分词是否正常 wc = WordCloud(font_path=FONT, width=800, height=600, mode='RGBA', background_color=None).generate(text)jieba.cut(text)默认是精确模式,尽量把句子切成最合理的词;cut(text, cut_all=True)是全模式,会把「中华人民共和国」同时切出「中华」「华人」「人民」「共和国」,词多但噪声大;jieba.cut_for_search(text)是搜索引擎模式,在精确模式基础上把长词再切一刀,召回高、适合建索引,做词云反而容易把长词的权重摊薄。做词云我一般就用默认的精确模式,稳。
' '.join(...)这一步不能省。wordcloud 的分词逻辑认空格,jieba 返回的是生成器,直接塞进去会报类型错误;即使转成列表传进去,中间没有空格分隔,等于没分词。print(text[:100])这行看着随意,实际是最高效的自检手段,分词结果不对一眼就能看出来。
3.3 停用词表与自定义词典
教程的七个案例都没处理停用词,但真实文本里「的」「了」「是」「我们」这类词频极高,不滤掉的话它们会霸占词云里最大的几块位置,图就废了。常见做法是准备一份一行一个词的 txt,读成集合后在分词阶段过滤:
import jieba def load_stopwords(path='stopwords.txt'): with open(path, 'r', encoding='utf-8') as f: return {line.strip() for line in f if line.strip()} STOP = load_stopwords() def cut_and_clean(text, min_len=2): words = [] for w in jieba.cut(text): w = w.strip() # 过滤空白、停用词、单字和纯标点 if not w or w in STOP or len(w) < min_len: continue if not w.isalnum() and len(w) < 2: continue words.append(w) return ' '.join(words)min_len=2是为了滤掉单字。中文单字在词云里几乎没有信息量,全是「的」「了」「着」这种高频虚词,留着只会让图变成噪点集合。w.isalnum()那个判断是兜底,把「,」「。」这类标点挡掉。
另一半是个性化词典。人名字、产品名、行业术语在 jieba 的通用词典里往往被切碎,比如某个四字的产品名被拆成两个两字词。这时候用jieba.add_word或load_userdict补进去:
jieba.load_userdict('userdict.txt') # 一行一个词,可带词频和词性 jieba.add_word('词云可视化', freq=200, tag='nz') # 临时补一个词freq是词频,值越大越不容易被切开;tag是词性标注,做关键词提取时会影响结果。加完再统计一次词频,能看到目标词从两个碎片变成一个完整词条,这一步的收益比调颜色参数大得多。
3.4 案例 07:从词频到关键词权重
案例 07 换了输入方式,不再传原始文本,而是先算出{词: 权重}再交给generate_from_frequencies:
import jieba.analyse # topK 取前 200 个关键词,withWeight 让返回值带上 TF-IDF 权重 freq = jieba.analyse.extract_tags(text, topK=200, withWeight=True) print(freq[:20]) # 先看前 20 个,确认关键词合理 freq = {i[0]: i[1] for i in freq} # 列表转字典,符合接口要求 wc = WordCloud(mask=mask, font_path=FONT, mode='RGBA', background_color=None).generate_from_frequencies(freq)extract_tags底层用的是 TF-IDF:一个词在本篇里出现得多、在整个语料里出现得少,权重就高。topK=200控制候选词数量,太大图会挤成一团,太小又显得空,200 个是个常见起点。withWeight=True不传的话返回的是纯词列表,没法直接喂给generate_from_frequencies。
generate_from_frequencies和generate的关键差别在于:前者完全跳过分词和统计,字的大小直接由你给的数值决定,也不受max_words之外的因素干扰;后者内部要先切词再统计词频。当你想精确控制「哪些词上榜、谁大谁小」时,走 frequencies 这条路更可控,比如做两个时期的对比图,可以把两边的权重字典合并后统一生成,避免两次随机布局导致视觉上不可比。
4. 出图链路:mask 蒙版、ImageColorGenerator 与 color_func
案例 04 到 06 三个案例,代码只差几行,但它们是整个教程里最容易被抄错的部分——因为蒙版的语义是反直觉的,颜色的来源也不止一种。把这三个案例放在一起看,其实是同一套机制的三层:先定形状,再定颜色来源,最后完全自定义颜色。
4.1 案例 04:mask 的数组语义
import numpy as np from PIL import Image mask = np.array(Image.open('black_mask.png')) wc = WordCloud(mask=mask, font_path=FONT, mode='RGBA', background_color=None).generate(text)mask要的是 numpy 数组,不是图片路径。Image.open读进来是 PIL 对象,必须np.array转一道,直接传图片对象会报属性错误。
关键在于黑白谁填字。wordcloud 内部会把数组里等于 255 的位置视为「不可放置区」,也就是说白色区域不放词,黑色区域才是字的落点。这和大多数人的直觉相反——做形状蒙版时,很多人画了一个黑色底、白色主体形状的图,结果出来的是「背景上有一圈字、形状里空的」。正确做法是把主体形状涂黑,背景留白。案例里给的black_mask.png就是按这个规则做的。
# 快速自检蒙版:看尺寸、类型和白色像素占比 print(mask.shape, mask.dtype, (mask == 255).mean())(mask == 255).mean()输出去重后白色像素的比例。这个值在 0.3 到 0.7 之间比较理想:太小说明可放字区域太窄,词会挤在一起;太大说明形状几乎不存在,跟不传 mask 差不多。dtype最好是uint8,如果是 RGBA 四通道,wordcloud 只看前三个通道,一般也能用,但保险起见可以Image.open(...).convert('RGB')再转数组。
尺寸也在这里看一眼。蒙版宽度超过 2000 像素时,生成时间会明显拉长,因为布局算法要在这么大的网格上做碰撞检测。常见做法是先缩到 800 到 1200 之间再算,出图需要高清的话用scale=2参数在最终渲染时放大,而不是靠蒙版本身撑大。
4.2 案例 05:ImageColorGenerator 从蒙版图里取色
from wordcloud import ImageColorGenerator mask = np.array(Image.open('color_mask.png')) wc = WordCloud(mask=mask, font_path=FONT, mode='RGBA', background_color=None).generate(text) # 用蒙版图自身的颜色给词上色 image_colors = ImageColorGenerator(mask) wc.recolor(color_func=image_colors)ImageColorGenerator的逻辑是:对每个词所在的矩形区域,取蒙版图对应位置的平均颜色,用它给这个词上色。这意味着蒙版图本身就是调色板——想做成暖色系,就把蒙版画成红橙黄;想做成品牌色,就把蒙版填成对应的几个色块。这也是案例 05 和案例 04 最大的区别:同样是形状,彩色蒙版额外承担了配色的职责。
recolor是原地修改,没有返回值,不需要wc = wc.recolor(...)。这一点抄错了不会报错,但会得到一个None,后面to_file直接崩。顺序也不能反,必须先generate再recolor,因为上色时要知道每个词落在哪个位置。
有一个细节:蒙版里有大量纯黑像素时,取出来的平均色会偏暗,词看起来很脏。常见做法是在蒙版里避免使用纯黑,改用深灰或者饱和度高一点的颜色。
4.3 案例 06:自定义颜色函数
import random def random_color(word, font_size, position, orientation, font_path, random_state): # HSL 里写死色相 0(红),饱和度和亮度随机,整体是同一色系 s = 'hsl(0, %d%%, %d%%)' % (random.randint(60, 80), random.randint(60, 80)) print(s) # 调试期看每次生成的颜色 return s wc = WordCloud(color_func=random_color, mask=mask, font_path=FONT, mode='RGBA', background_color=None).generate(text)这个函数每个词调一次,六个参数分别是词本身、字号、在画布上的坐标、旋转角度、字体路径和一个随机状态。函数只要能返回一个 matplotlib 认识的颜色字符串就行:'hsl(0, 70%, 60%)'、'#ff6600'、(0.2, 0.4, 0.9)都可以。
教程里把色相写死成 0,只让饱和度和亮度波动,得到的是同色系深浅不一的红,视觉上很统一。想看多色就把色相也放开:'hsl(%d, 70%%, 60%%)' % random.randint(0, 360)。参数个数跟着 wordcloud 版本会微调,1.9.3 调用时还会额外传**kwargs,稳妥写法是给函数加一个**kwargs兜底,不然升级版本就报TypeError。
顺带说一句,print(s)这种调试输出放在生成几千个词的场景里会拖慢速度,确认效果后记得删掉。用random_state参数固定随机种子可以让颜色可复现,做对比图的时候很有用。
5. 避坑与排查:跑不起来、出图空白、中文变方块
这七个案例的代码量都不大,但踩坑率相当高,因为错误大多不报异常,只是安静地给你一张错图。下面几条是我自己复现时反复撞上的,按「现象 → 原因 → 解决」列出来,对照着排查能省不少时间。
5.1 生成阶段的问题
现象一:整张图空白,或者只有零星几个巨大的词块。原因通常是中文没分词。wordcloud 内部按正则切 token,连续汉字容易被当成一个超长字符串,导致候选词只有几个,布局算法把它们放大后铺满画布。另一个可能是一次性塞进去的文本太长又没设max_words,词太多互相挤不下,最后只成功放上去几个。 解决是先print(text[:100])看一眼喂进去的字符串有没有空格分隔,没有就补' '.join(jieba.cut(text));再把max_words显式设成 200 左右,配合min_font_size=8防止小词被裁掉。
现象二:中文全是方块或者干脆不显示。原因就一个:font_path指向的字体里没有汉字字形,或者路径写错但被静默忽略了。相对路径在不同工作目录下会指向不同位置,从 PyCharm 直接运行时工作目录是项目根,双击脚本运行时是脚本所在目录,行为不一致。 解决是用绝对路径,或者在启动时os.chdir(os.path.dirname(os.path.abspath(__file__)))固定工作目录。验证办法是先单独用这个字体渲染一个中文字符串看看,能显示再往下走。
现象三:报ValueError: We need at least 1 word to plot a word cloud。原因是大面积停用词过滤加最短长度限制之后,候选词被清空了;也可能是读文件时编码不对,读进来一堆乱码,分词后没有有效词条。 解决是先把过滤关掉跑一遍原始文本,确认能出图后再逐层加过滤条件;读文件统一encoding='utf-8',遇到 GBK 文件用errors='ignore'兜底。
5.2 显示与保存阶段的问题
现象四:to_file存出来的图打开是空白,但plt.show()明明能看到。原因是mode='RGBA'加background_color=None生成的是透明背景图,很多看图软件默认白底,透明区域里的浅色字在白底上完全看不见,视觉上就是一片白。 解决是看图软件里换成深色背景预览,或者临时把background_color设成'white'调试一轮,确认字形布局正常后再改回透明。真正交付时,透明图放深色 PPT 上反而更好看,所以不是参数错了,是预览方式的问题。
现象五:形状蒙版出来的字全在背景上,形状内部是空的。原因是蒙版黑白搞反了。前面说过,wordcloud 把 255(白)视为不可放置区,只有非白区域才落字。 解决是在图像工具里把主体形状填黑、背景留白;或者在代码里做一次反转:mask = 255 - mask,但要注意彩色蒙版不能这么取反,会破坏配色。
现象六:脚本跑完没反应,窗口不弹出来。原因常见于在无图形界面的环境里执行,或者plt.show()后面还有代码但被阻塞住了。matplotlib 的默认后端在服务器上是 Agg,show()什么都不会发生。 解决是改用plt.savefig()或直接wc.to_file()落盘,不依赖窗口。跑批处理脚本时把plt.show()全部注释掉,只保留存图那一行。
6. 二次开发:把七个案例收成一个可复用的生成器
七个案例跑通之后,下一步大概率是要在项目里反复用,这时候把它们复制粘贴到各处就很痛苦了。我的做法是抽一个函数,把「文本清洗 → 分词 → 权重 → 蒙版 → 上色 → 落盘」封进一条链路,七个案例变成它的不同参数组合:
import os, jieba, numpy as np from PIL import Image from wordcloud import WordCloud, ImageColorGenerator def build_cloud(text, out='out/wc.png', font='simhei.ttf', mask_path=None, stopwords=None, use_tfidf=False, topk=200, seed=42): # 1. 过滤停用词和单字 words = [w for w in jieba.cut(text) if w.strip() and len(w) > 1 and w not in (stopwords or set())] clean = ' '.join(words) mask = np.array(Image.open(mask_path).convert('RGB')) if mask_path else None wc = WordCloud(font_path=font, mask=mask, mode='RGBA', background_color=None, max_words=topk, random_state=seed, scale=2) # 2. 两条输入路径:原始文本 或 关键词权重 if use_tfidf: freq = dict(jieba.analyse.extract_tags(clean, topK=topk, withWeight=True)) wc.generate_from_frequencies(freq) else: wc.generate(clean) # 3. 有彩色蒙版就按图取色 if mask_path: wc.recolor(color_func=ImageColorGenerator(mask)) os.makedirs(os.path.dirname(out), exist_ok=True) wc.to_file(out) return outrandom_state固定成 42 是我加后悔药的习惯:词云布局本身带随机性,同一个文本跑两次形状不一样,做前后对比图时很要命。固定种子之后,改动只体现在数据上,视觉差异能直接归因。scale=2让输出放大一倍,配合前面把蒙版压到 1000 像素以内的做法,等于「小画布算得快、大倍数存得清」。
几个常用参数组合,对应的就是原来的七个案例:
| 调用方式 | 等价案例 | 适用场景 |
|---|---|---|
use_tfidf=False, mask_path=None | 案例 01/03 | 快速看词频分布 |
mask_path='black_mask.png' | 案例 04 | 只要形状,不要花色 |
mask_path='color_mask.png' | 案例 05 | 形状和配色都来自图 |
use_tfidf=True, mask_path=... | 案例 07 | 报告配图,突出核心议题 |
再往下做,可以加一层命令行入口,把这些参数暴露成--text、--mask、--topk、--tfidf几个开关,跑批时用 shell 循环喂文件,比改代码快得多。另一个值得做的扩展是加时间维度:把不同月份的权重字典分别算出来,用同一张蒙版和同一个random_state生成一组图,放在一起能看出关键词的迁移,这比单张词云的信息量大得多。
血泪经验是别在封装的时候顺手把plt.show()也包进去。批处理跑十几个文件,每个都弹一个窗口,得一个个手动关,中途还容易卡死。存图归存图,显示归显示,两件事分开。从那以后我每次封这类工具函数,都会先把「有没有交互式窗口」这一条强制过一遍,确认纯命令行也能跑完,再往项目里放。希望帮到你。
本文还有配套的精品资源,点击获取