Python词云实战:中文分词、蒙版配色与TF-IDF权重 简介面向文本挖掘与数据可视化初学者及中级 Python 用户的词云实践教程围绕个性化词云生成这一目标系统梳理从概念、应用场景到完整实现流程的关键知识。资源包内含 1 个 PDF 文件约 2.07MB将教程正文、代码示例与运行结果整合在一起便于边看边练。内容以 wordcloud 与 matplotlib 为核心覆盖文本预处理、词频权重、图像展示与保存等环节并通过 7 个递进案例演示最简调用、中文分词处理、蒙版形状、自定义配色以及关键词和权重提取等做法。教程还给出 PyCharm、Python 3.7 及对应库版本的环境配置参考可帮助读者快速复现实验并理解不同参数对词云效果的影响。目前已有 109 人学习下载适合用于项目报告、数据分析、社交媒体观察或教学演示等场景。1. 一份 PDF 七个脚本Python 词云这套东西到底值不值得扒上个月有人把三个月的用户反馈导出成一张表甩给我一万两千多条说“你看看大家主要吐槽什么”。我用 Python 词云跑了二十分钟一张图拍下去三个高频词直接顶到最大字号会上一句话就讲完了。这份 PDF 就是我当时翻的那类材料的完整版七个案例从WordCloud().generate(text)一行出图一路做到用 jieba 提取关键词权重再喂给generate_from_frequencies中间把中文分词、蒙版形状、蒙版配色、自定义颜色函数全串了一遍。它适合手头有文本数据、想把可视化塞进周报或数据分析脚本里的初中级 Python 使用者——不需要懂 NLP但得能看懂函数签名和参数。下面按“能不能跑、为什么这么写、怎么改成自己的”这个顺序拆。2. 技术选型与最小可跑环境wordcloud 和 matplotlib 各自管什么2.1 为什么是 wordcloud 而不是前端词云组件先把职责划清楚不然后面容易迷糊。wordcloud这个库干的活是布局计算 出图它拿到一份词频表按字号阶梯默认从大到小max_font_size到min_font_size往画布上填词每个词沿着一条螺旋线找空位找到不重叠的位置就落下去最后直接渲染成一张 PIL Image 对象返回。也就是说WordCloud().generate(text)出来的wc本身就是一张图不是数据。matplotlib在这套流程里其实只干一件事把这张图imshow出来看一眼。案例 01 里那几行plt.imshow(wc, interpolationbilinear)、plt.axis(off)、plt.show()全是显示逻辑跟词云生成本身没关系。真正落盘用的是wc.to_file(wordcloud.png)这一句完全不依赖 matplotlib。理解这一点之后选型就清楚了如果目标是静态报告、PPT、PDF 里的插图wordcloud 是正解因为它是服务端离线出图字体、尺寸、透明通道全可控如果目标是网页上的可点击词云那应该去看 pyecharts 的 WordCloud 或者前端 d3-cloudwordcloud 库在这里帮不上忙。至于 stylecloud 这类封装库本质是在 wordcloud 外面套了一层预设样式内置图标蒙版、渐变配色好处是省事坏处是自定义回调被包住了排查问题的时候反而多一层黑匣子。这份教程走的是原始路线七个案例全部手写参数对二次开发更友好。另外一个常被忽略的点wordcloud 有两条入口。generate(text)吃原始文本内部自己分词、自己统计词频generate_from_frequencies(dict)吃一个{词: 权重}的字典跳过分词和统计直接按你给的权重排字号。案例 07 用的就是后者这是把词云接进已有分析管线的关键——你前端已经有 TF-IDF 结果了没必要让 wordcloud 再算一遍。2.2 环境版本与字体准备3.7 / 1.9.3 / 3.5.3 这套组合的注意点教程给的环境是 Python 3.7、wordcloud 1.9.3、matplotlib 3.5.3PyCharm 编辑器。这套版本组合是能对上的但装的时候有两个地方容易翻车。第一wordcloud 是带 C 扩展的包pip install wordcloud在没有预编译 wheel 的环境比如某些 32 位 Python、某些精简版容器会尝试本地编译然后因为缺 MSVC 或者缺Python.h报一长串红字。血泪经验是先确认 Python 位数和平台能装 wheel 就别源码编译实在装不上退到wordcloud1.8.2.2这类老版本通常有更全的预编译包。Python 3.10 以上要用 1.9.x 的新版本1.8 系列在新解释器上会 import 失败。# 建议固定版本避免不同机器上出图效果不一致 pip install wordcloud1.9.3 matplotlib3.5.3 jieba0.42.1 pillow numpy第二中文字体。这是整个教程里最容易让人卡住的一步也是后面三个中文案例能不能跑通的前提。font_path不传或者传了个不存在的路径wordcloud 不会报错它会默默用默认字体画结果就是满屏方块俗称“豆腐块”。常见做法是把字体文件路径写死成绝对路径或者把 ttf 拷到项目目录下用相对路径引用平台常用中文字体路径WindowsC:/Windows/Fonts/simhei.ttf、C:/Windows/Fonts/msyh.ttcmacOS/System/Library/Fonts/Hiragino Sans GB.ttcLinux/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc注意.ttc是字体集合文件PIL 读它一般没问题但个别老版本 PIL 对 ttc 支持不好稳妥起见优先用.ttf。2.3 案例 01 逐行拆一个 generate 背后发生了什么案例 01 号称“最简调用”代码不到十行但它把整条链路的骨架都摆出来了。我在原代码基础上加了几个后面一定会用到的参数和注释# -*- coding: utf-8 -*- from wordcloud import WordCloud import matplotlib.pyplot as plt # 读文本显式指定编码Windows 默认 GBK读 UTF-8 文件会直接抛 UnicodeDecodeError with open(text.txt, r, encodingutf-8) as f: text f.read() # 生成对象这些参数决定了出图的尺寸、底色和可复现性 wc WordCloud( width800, # 画布宽默认只有 400 height600, # 画布高默认只有 200 background_colorwhite, # 默认是黑底塞进报告里很难看 max_words200, # 最多画多少个词默认 200 random_state42, # 固定随机种子保证每次布局一致 ).generate(text) # 保存to_file 存的是 wc 内部的 PIL Image不带任何边距 wc.to_file(wordcloud.png) # 显示这一步只影响你看不影响文件 plt.imshow(wc, interpolationbilinear) # bilinear 让缩放时边缘平滑 plt.axis(off) # 关掉坐标轴刻度 plt.show()逐段说逻辑。第一段open那里encodingutf-8不是可选项是必须项尤其你在 Windows 上跑的时候。第二段是核心generate(text)内部做了三件事用默认正则r\w[\w]把文本切成 token、统计每个 token 的频次、按频次映射字号然后做螺旋布局。所以英文文本几乎不用预处理就能出图而中文会出问题——这个留到第 3 章讲。random_state这个参数值得单独提一句。不设它同一份文本每次跑出来的布局都不一样词的位置是随机的。你在调参阶段会觉得“怎么改都没变化”或者反过来“明明没改结果变了”都是它闹的。固定成 42 之后出图完全可复现后面做 A/B 对比才有意义。to_file和plt.savefig的区别也得说清。to_file存的是 800×600 的原始画布四周没有留白plt.savefig存的是 matplotlib 的 figure默认会带一圈白边而且受 dpi 影响尺寸会变。要放进文档里的图一律用to_file。如果确实要走 matplotlib 存比如要在图上叠加标题记得加bbox_inchestight, pad_inches0。3. 中文词云的分水岭字体、jieba 分词与停用词表3.1 案例 02 的真相不分词也能出图但出来的是废图案例 02 叫“中文不分词”代码跟案例 01 几乎一样只多传了font_path、width、height、modeRGBA、background_colorNone。很多人跑完觉得“也能出图啊挺好看的”实际上那张图的信息量接近于零。原因在generate()内部那个默认正则。\w在 Python 的re模块里默认是 Unicode 模式会匹配中文汉字但\w[\w]这个模式匹配的是连续的一串字符。中文句子没有空格一整段话会被当成一个超长的 token于是词频表里就只有那么几个“词”每个都长到画不下最后被max_words和字号下限挤掉图里只剩几个孤零零的长串。# 案例 02 原始写法能跑但中文会切成一整段 wc WordCloud(font_pathHiragino.ttf, width800, height600, modeRGBA, # 输出带 Alpha 通道 background_colorNone # None 表示透明背景配合 RGBA 才有意义 ).generate(text)modeRGBA加background_colorNone这两个参数是配套的作用是出一张透明底 PNG方便直接贴在 PPT 或者海报上。这里有个隐藏顺序mode必须是RGBAbackground_color设None才生效如果mode是默认的RGBbackground_colorNone会被当成黑色处理你拿到的是一张黑底图。这个坑我在第一次用的时候踩了整整一个下午。真要救不分词的场景可以改regexp参数比如按标点切分但那属于舍近求远。正确做法是下一节的分词。3.2 jieba 三种分词模式的取舍中文词云绕不开 jieba。案例 03 加了一行text .join(jieba.cut(text))把分词结果用空格重新拼成字符串再喂给generate()。这一步的本质是“把中文伪装成英文”让 wordcloud 的默认正则可以按空格切出一个个词。思路很土但极其有效。jieba 提供三种模式选错了结果差别很大import jieba text open(xyj.txt, r, encodingutf-8).read() # 1. 精确模式默认试图把句子最精确地切开适合文本分析 seg_default .join(jieba.cut(text)) # 2. 全模式把所有可能的词都扫出来速度快但有大量冗余 seg_full .join(jieba.cut(text, cut_allTrue)) # 3. 搜索引擎模式在精确模式基础上对长词再切提高召回 seg_search .join(jieba.cut_for_search(text))精确模式是默认选项绝大多数词云场景用它就够。全模式会把“中华人民共和国”切成“中华”“华人”“人民”“共和”“共和国”一堆重叠片段词频统计直接失真除非你在做召回率优先的检索索引否则别用。搜索引擎模式介于两者之间对长实体词友好做人物名、机构名统计时更合适一点。还有一个实用参数是自定义词典。jieba 自带词典认不出专有名词比如某个产品代号、某个人物绰号会被切碎。常见做法是维护一个userdict.txt一行一个词然后jieba.load_userdict(userdict.txt) # 必须在 cut 之前调用全局生效另外 jieba 首次cut时会加载词典耗时大概一两秒之后走缓存。如果你在循环里反复处理小文件别每次都重新import jieba模块级加载一次就行。3.3 停用词表把“的、了、是”挡在门外分完词直接画你大概率会看到“的”“了”“是”“在”占据最大字号——这不是词云这是虚词展示。wordcloud 内置了一个STOPWORDS集合但那是英文的the、a、is 之类中文一个都没有。处理方式有两种看你的管线形态。第一种走generate()的文本路线在分词之后、拼接之前过滤from wordcloud import WordCloud # 停用词表一行一个词自己维护或者用公开的通用表 stopwords set() with open(stopwords.txt, r, encodingutf-8) as f: for line in f: w line.strip() if w: stopwords.add(w) # 分词 过滤 拼回字符串 words [w for w in jieba.cut(text) if len(w) 1 and w not in stopwords] # len1 顺手干掉单字 text_seg .join(words) wc WordCloud(font_pathsimhei.ttf, stopwordsstopwords, # 双保险generate 内部再筛一次 max_words300, background_colorwhite).generate(text_seg)第二种走generate_from_frequencies()的字典路线在构造词频表的时候就把停用词剔掉。案例 07 走的就是这条下一章会展开。len(w) 1这个过滤条件看起来随意实际很有用。中文单字里除了“我”“你”“他”这类大量是虚词和语气词过滤掉单字能让图里的词更有信息密度。当然代价是会误伤“猫”“狗”这类有效单字看你的语料决定我一般会保留一个白名单。4. 蒙版机制拆解形状、配色与 color_func 回调4.1 mask 参数到底吃什么图案例 04 引入了“蒙版”也就是让词云填进一个特定形状里。这个功能的原理比想象中简单mask接收一个 numpy 数组wordcloud 遍历这个数组把纯白255区域标记为不可绘制非白区域标记为可绘制。词只往非白的地方落于是整体轮廓就跟着蒙版走了。所以制作蒙版图有个硬性要求背景必须是白的主体必须是深色的。很多人拿一张透明背景的 PNG 或者黑底白图的素材直接往里塞结果要么图空白要么填满了整个矩形。稳妥的做法是先转成灰度再阈值化from PIL import Image import numpy as np # 读图 - 转灰度 - 二值化确保只有纯黑(可绘制)和纯白(不可绘制)两种值 img Image.open(mask.png).convert(L) arr np.array(img) # 大于 200 的像素视为背景改成 255其余置 0 arr np.where(arr 200, 255, 0).astype(np.uint8) wc WordCloud(maskarr, font_pathsimhei.ttf, modeRGBA, background_colorNone, contour_width1, # 给轮廓描个边可选 contour_colorblack).generate(text_seg)这里有个重要副作用一旦传了 maskwidth和height就失效了输出尺寸完全由蒙版数组的形状决定。很多人传了 800×600 的 mask 又写width1600发现没变化就是这个原因。想要大图先把蒙版图放大用Image.resize注意用最近邻插值保持边缘锐利而不是改 width。contour_width和contour_color是 1.9.x 加的描边参数描的是蒙版轮廓做海报的时候挺好用做数据图反而显得花哨按需开。4.2 ImageColorGenerator让词跟着原图配色案例 05 解决的是“颜色怎么自动好看”的问题。默认配色是 wordcloud 内置的colormapmatplotlib 的色系跟你的蒙版图毫无关系。ImageColorGenerator的思路是从蒙版原图里按区域取平均色然后每个词落到哪个位置就用那个位置的颜色。from wordcloud import WordCloud, ImageColorGenerator from PIL import Image import numpy as np mask_img Image.open(color_mask.png) mask_arr np.array(mask_img) wc WordCloud(maskmask_arr, font_pathsimhei.ttf, modeRGBA, background_colorNone).generate(text_seg) # 关键用彩色原图生成取色器再 recolor image_colors ImageColorGenerator(mask_arr) wc.recolor(color_funcimage_colors)顺序很重要必须先generate()再recolor()。recolor只改颜色不动布局所以可以反复调用做对比。还有一点ImageColorGenerator传的应该是彩色原图的数组不是前面二值化过的那个——二值图取出来全是黑白灰着色效果等于没做。我一般准备两张图mask_bw.png给mask参数mask_color.png给取色器两张图尺寸必须完全一致。如果原图颜色太杂出来的词会脏。这时候可以给ImageColorGenerator传default_color兜底或者干脆放弃自动取色走下一节的自定义函数。4.3 自定义颜色函数用 HSL 控饱和度案例 06 是最灵活的一个。color_func接收一个回调wordcloud 每画一个词就调用一次你把颜色字符串返回去就行。签名在不同版本略有差异稳妥写法是把多余参数用**kwargs兜住import random def random_color(word, font_size, position, orientation, random_stateNone, **kwargs): # HSL 三个分量色相固定 0(红)饱和度 60~80%亮度 60~80% # 这样出来的颜色都在同一个色系里不会花 return hsl(0, %d%%, %d%%) % ( random.randint(60, 80), random.randint(60, 80), ) wc WordCloud(color_funcrandom_color, maskmask_arr, font_pathsimhei.ttf, modeRGBA, background_colorNone).generate(text_seg)这段代码的价值不在“随机颜色”而在用 HSL 把随机范围锁死在一个色相上。RGB 随机出来的是杂乱色块HSL 固定色相只抖饱和度和亮度视觉上就是一套协调的同色系。想换主题色改hsl(0, ...)里那个 0 就行120 是绿、210 是蓝。回调里还能拿到font_size和position可以做“大词深色、小词浅色”的渐变效果或者按象限分色。orientation是词的朝向水平/垂直想做横竖不同色也在这里判断。最后一个案例 07 用的是jieba.analyse.extract_tags(text, topK200, withWeightTrue)返回[(词, 权重), ...]转成 dict 后喂给generate_from_frequencies()。这条路的好处是权重来自 TF-IDF 而不是裸词频能自动把高频虚词压下去连停用词表都省了一半。缺点是extract_tags默认按整篇文档算短文本上区分度不如长文本明显。import jieba.analyse # topK 控制取前多少个词withWeightTrue 才会带上 TF-IDF 权重 freq jieba.analyse.extract_tags(text, topK200, withWeightTrue) freq_dict {w: weight for w, weight in freq} wc WordCloud(maskmask_arr, font_pathsimhei.ttf, modeRGBA, background_colorNone ).generate_from_frequencies(freq_dict)注意generate_from_frequencies不做任何分词和正则处理词表里是什么就画什么所以脏数据要在这之前清干净。5. 排查清单方块字、空图、蒙版失效这几类翻车5.1 满屏方块一个汉字都认不出现象中文文本跑完图里全是 □□□英文数字却正常。原因font_path没传、传了不存在的路径或者传了个不含中文字形的字体比如某些只带拉丁字符的字体文件。wordcloud 找不到字形时不会抛异常静默降级成默认字体的缺字方块。解决先用os.path.exists(font_path)确认路径真实存在再用ImageFont.truetype(font_path, 20).getmask(测)验证这个字体能不能渲染中文。两步都通过还出方块基本就是路径里有中文或者空格换绝对路径加正斜杠试试。5.2 图里只有寥寥几个词或者干脆全白现象generate()跑完没报错to_file也生成了文件打开一看纯白或者只有三五个词。原因九成是中文没分词整段文本被当成一个超长 token字号被min_font_size卡死后直接丢弃剩下一成是max_words设得太小或者画布尺寸太小放不下。解决先print(len(wc.words_))看内部词表大小个位数就是分词问题。补上 .join(jieba.cut(text))或者换成generate_from_frequencies自己控制词表。5.3 蒙版完全不生效词填满了整个矩形现象传了 mask出来的还是规规矩矩的长方形。原因三种可能。一是mask传的是 PIL Image 对象而不是 numpy 数组二是蒙版图是透明背景convert(L)之后透明区域变成了 0黑被判定为全部可绘制三是蒙版本身就是纯黑图等于没有约束。解决统一np.array(Image.open(p).convert(L))然后print(arr.min(), arr.max())正常应该同时出现 0 和 255。全是 0 说明图有问题重新做一张白底黑主体的图。透明 PNG 要先铺白底再转灰度Image.alpha_composite(Image.new(RGBA, img.size, white), img)。5.4 颜色不生效recolor 之后没变化现象调了wc.recolor(color_func...)出图还是默认配色。原因recolor的调用顺序在generate之前或者color_func返回的颜色字符串格式不被识别比如hsl()里少了百分号。解决严格按generate()→recolor()→to_file()的顺序写。回调里打印一下返回值确认是hsl(0, 70%, 70%)这种完整格式。另外modeRGB下带 alpha 的颜色会被忽略要透明底就老老实实设modeRGBA。5.5 保存的图有白边尺寸也对不上现象to_file出来的图比预期大一圈四周有白边放进 PPT 里对不齐。原因用了plt.savefig而不是wc.to_filematplotlib 的 figure 默认带 padding 和 dpi 缩放。解决静态图一律wc.to_file()。真要走 matplotlib加plt.savefig(x.png, dpi100, bbox_inchestight, pad_inches0)并且plt.figure(figsize(w/100, h/100), dpi100)让画布尺寸和词云尺寸对齐。6. 二次开发把 7 个散装脚本收成一条可复用的词云流程七个案例分开跑没问题但你要是每周都要出一张图复制粘贴七个脚本就是灾难。我的做法是抽成一个函数用配置字典驱动蒙版、分词、配色都做成可选开关。6.1 一个 build_wordcloud 函数收口所有分支import os import jieba import jieba.analyse import numpy as np from PIL import Image from wordcloud import WordCloud, ImageColorGenerator def build_wordcloud(text, out_path, font_pathC:/Windows/Fonts/simhei.ttf, use_tfidfFalse, topk200, mask_pathNone, color_from_maskTrue, stopwordsNone, size(1200, 900)): 统一入口文本进PNG 出。 use_tfidfTrue 走关键词权重False 走分词词频。 if not os.path.exists(font_path): raise FileNotFoundError(字体不存在%s % font_path) stopwords stopwords or set() if use_tfidf: # 路线 ATF-IDF 权重适合长文本能自动压掉高频虚词 pairs jieba.analyse.extract_tags(text, topKtopk, withWeightTrue) freq {w: v for w, v in pairs if w not in stopwords and len(w) 1} else: # 路线 B分词后统计词频控制力更强 freq {} for w in jieba.cut(text): if len(w) 2 or w in stopwords: continue freq[w] freq.get(w, 0) 1 kwargs dict(font_pathfont_path, max_wordstopk, background_colorNone, modeRGBA, widthsize[0], heightsize[1], random_state42) # 蒙版存在时覆盖尺寸参数并转成灰度 ndarray mask_arr None color_img None if mask_path: color_img Image.open(mask_path).convert(RGB) gray np.array(color_img.convert(L)) mask_arr np.where(gray 200, 255, 0).astype(np.uint8) kwargs[mask] mask_arr kwargs.pop(width); kwargs.pop(height) # 传了 mask宽高失效 wc WordCloud(**kwargs).generate_from_frequencies(freq) if mask_arr is not None and color_from_mask: wc.recolor(color_funcImageColorGenerator(np.array(color_img))) wc.to_file(out_path) return out_path, len(freq)函数里的关键决策有两个。一是统一走generate_from_frequencies两条路线TF-IDF 和词频统计最后都收敛成 dict出口只有一个后面加缓存、加过滤都在这一层做不用改两次。二是mask存在时主动pop掉width和height避免“传了没用”的困惑——与其让调用方猜不如在代码里写死规则。6.2 参数与配置对照表把可变项抽成配置之后剩下的就是调参。这张表是我实际用下来最影响出图效果的几个参数影响建议值max_words图里词的总数100300再多就糊max_font_size最大词的字号不设则按画布自动算海报图建议显式给relative_scaling词频对字号的敏感度010 只看排名1 严格按频次prefer_horizontal水平词占比0.9 默认想更“词云感”调到 0.7margin词与词的间距默认 2密集图调到 46random_state布局随机种子固定值保证可复现relative_scaling这个参数经常被忽略但它直接决定图的“说服力”。默认 0.5 时第二名和第一名的字号差距会被压缩看起来都差不多大调到 0.9 以上频次差异才会明显体现在字号上。做汇报用的图我一般拉到 0.8。6.3 怎么验证改对了改完之后别急着看图好不好看先做三个机械检查。第一len(freq)和wc.words_的长度应该一致generate_from_frequencies会丢掉权重为 0 的项差一两个正常差几十个说明过滤逻辑写错了。第二固定random_state连跑两次md5sum两次输出应该完全相同不同就说明有地方引入了额外随机性比如color_func里用了没种子的random。第三把freq的 top20 打印出来人眼扫一遍有没有“的”“了”“是”漏网有没有被切碎的长实体词。至于加交互、批量出图这些延伸需求我的习惯是把上面的函数塞进一个for循环遍历目录输出文件名带上源文件名和max_words这样一次跑几十份文本也不用担心覆盖。从那以后我每次改词云参数都强制先跑一遍“两次 md5 对比 top20 词表检查”省掉过好几次“图好看但数据是错的”的尴尬。希望帮到你。本文还有配套的精品资源点击获取