Python词云实战:中文分词、蒙版配色与TF-IDF权重

发布时间:2026/11/2 22:04:54
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, interpolation='bilinear')、plt.axis('off')、plt.show()全是显示逻辑,跟词云生成本身没关系。真正落盘用的是wc.to_file('wordcloud.png'),这一句完全不依赖 matplotlib。

理解这一点之后,选型就清楚了:如果目标是静态报告、PPT、PDF 里的插图,wordcloud 是正解,因为它是服务端离线出图,字体、尺寸、透明通道全可控;如果目标是网页上的可点击词云,那应该去看 pyecharts 的 WordCloud 或者前端 d3-cloud,wordcloud 库在这里帮不上忙。至于 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.3,PyCharm 编辑器。这套版本组合是能对上的,但装的时候有两个地方容易翻车。

第一,wordcloud 是带 C 扩展的包,pip install wordcloud在没有预编译 wheel 的环境(比如某些 32 位 Python、某些精简版容器)会尝试本地编译,然后因为缺 MSVC 或者缺Python.h报一长串红字。血泪经验是:先确认 Python 位数和平台,能装 wheel 就别源码编译;实在装不上,退到wordcloud==1.8.2.2这类老版本通常有更全的预编译包。Python 3.10 以上要用 1.9.x 的新版本,1.8 系列在新解释器上会 import 失败。

# 建议固定版本,避免不同机器上出图效果不一致 pip install wordcloud==1.9.3 matplotlib==3.5.3 jieba==0.42.1 pillow numpy

第二,中文字体。这是整个教程里最容易让人卡住的一步,也是后面三个中文案例能不能跑通的前提。font_path不传或者传了个不存在的路径,wordcloud 不会报错,它会默默用默认字体画,结果就是满屏方块(俗称“豆腐块”)。常见做法是把字体文件路径写死成绝对路径,或者把 ttf 拷到项目目录下用相对路径引用:

平台常用中文字体路径
WindowsC:/Windows/Fonts/simhei.ttf、C:/Windows/Fonts/msyh.ttc
macOS/System/Library/Fonts/Hiragino Sans GB.ttc
Linux/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', encoding='utf-8') as f: text = f.read() # 生成对象:这些参数决定了出图的尺寸、底色和可复现性 wc = WordCloud( width=800, # 画布宽,默认只有 400 height=600, # 画布高,默认只有 200 background_color='white', # 默认是黑底,塞进报告里很难看 max_words=200, # 最多画多少个词,默认 200 random_state=42, # 固定随机种子,保证每次布局一致 ).generate(text) # 保存:to_file 存的是 wc 内部的 PIL Image,不带任何边距 wc.to_file('wordcloud.png') # 显示:这一步只影响你看,不影响文件 plt.imshow(wc, interpolation='bilinear') # bilinear 让缩放时边缘平滑 plt.axis('off') # 关掉坐标轴刻度 plt.show()

逐段说逻辑。第一段open那里,encoding='utf-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_inches='tight', pad_inches=0。

3. 中文词云的分水岭:字体、jieba 分词与停用词表

3.1 案例 02 的真相:不分词也能出图,但出来的是废图

案例 02 叫“中文不分词”,代码跟案例 01 几乎一样,只多传了font_path、width、height、mode='RGBA'、background_color=None。很多人跑完觉得“也能出图啊,挺好看的”,实际上那张图的信息量接近于零。

原因在generate()内部那个默认正则。\w在 Python 的re模块里默认是 Unicode 模式,会匹配中文汉字,但\w[\w']+这个模式匹配的是连续的一串字符。中文句子没有空格,一整段话会被当成一个超长的 token,于是词频表里就只有那么几个“词”,每个都长到画不下,最后被max_words和字号下限挤掉,图里只剩几个孤零零的长串。

# 案例 02 原始写法:能跑,但中文会切成一整段 wc = WordCloud(font_path='Hiragino.ttf', width=800, height=600, mode='RGBA', # 输出带 Alpha 通道 background_color=None # None 表示透明背景,配合 RGBA 才有意义 ).generate(text)

mode='RGBA'加background_color=None这两个参数是配套的,作用是出一张透明底 PNG,方便直接贴在 PPT 或者海报上。这里有个隐藏顺序:mode必须是'RGBA',background_color设None才生效;如果mode是默认的'RGB',background_color=None会被当成黑色处理,你拿到的是一张黑底图。这个坑我在第一次用的时候踩了整整一个下午。

真要救不分词的场景,可以改regexp参数,比如按标点切分,但那属于舍近求远。正确做法是下一节的分词。

3.2 jieba 三种分词模式的取舍

中文词云绕不开 jieba。案例 03 加了一行text = ' '.join(jieba.cut(text)),把分词结果用空格重新拼成字符串,再喂给generate()。这一步的本质是“把中文伪装成英文”,让 wordcloud 的默认正则可以按空格切出一个个词。思路很土,但极其有效。

jieba 提供三种模式,选错了结果差别很大:

import jieba text = open('xyj.txt', 'r', encoding='utf-8').read() # 1. 精确模式(默认):试图把句子最精确地切开,适合文本分析 seg_default = ' '.join(jieba.cut(text)) # 2. 全模式:把所有可能的词都扫出来,速度快但有大量冗余 seg_full = ' '.join(jieba.cut(text, cut_all=True)) # 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', encoding='utf-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] # len>1 顺手干掉单字 text_seg = ' '.join(words) wc = WordCloud(font_path='simhei.ttf', stopwords=stopwords, # 双保险:generate 内部再筛一次 max_words=300, background_color='white').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(mask=arr, font_path='simhei.ttf', mode='RGBA', background_color=None, contour_width=1, # 给轮廓描个边,可选 contour_color='black').generate(text_seg)

这里有个重要副作用:一旦传了 mask,width和height就失效了,输出尺寸完全由蒙版数组的形状决定。很多人传了 800×600 的 mask 又写width=1600,发现没变化,就是这个原因。想要大图,先把蒙版图放大(用Image.resize,注意用最近邻插值保持边缘锐利),而不是改 width。

contour_width和contour_color是 1.9.x 加的描边参数,描的是蒙版轮廓,做海报的时候挺好用,做数据图反而显得花哨,按需开。

4.2 ImageColorGenerator:让词跟着原图配色

案例 05 解决的是“颜色怎么自动好看”的问题。默认配色是 wordcloud 内置的colormap(matplotlib 的色系),跟你的蒙版图毫无关系。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(mask=mask_arr, font_path='simhei.ttf', mode='RGBA', background_color=None).generate(text_seg) # 关键:用彩色原图生成取色器,再 recolor image_colors = ImageColorGenerator(mask_arr) wc.recolor(color_func=image_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_state=None, **kwargs): # HSL 三个分量:色相固定 0(红),饱和度 60~80%,亮度 60~80% # 这样出来的颜色都在同一个色系里,不会花 return 'hsl(0, %d%%, %d%%)' % ( random.randint(60, 80), random.randint(60, 80), ) wc = WordCloud(color_func=random_color, mask=mask_arr, font_path='simhei.ttf', mode='RGBA', background_color=None).generate(text_seg)

这段代码的价值不在“随机颜色”,而在用 HSL 把随机范围锁死在一个色相上。RGB 随机出来的是杂乱色块,HSL 固定色相只抖饱和度和亮度,视觉上就是一套协调的同色系。想换主题色,改hsl(0, ...)里那个 0 就行,120 是绿、210 是蓝。

回调里还能拿到font_size和position,可以做“大词深色、小词浅色”的渐变效果,或者按象限分色。orientation是词的朝向(水平/垂直),想做横竖不同色也在这里判断。

最后一个案例 07 用的是jieba.analyse.extract_tags(text, topK=200, withWeight=True),返回[(词, 权重), ...],转成 dict 后喂给generate_from_frequencies()。这条路的好处是权重来自 TF-IDF 而不是裸词频,能自动把高频虚词压下去,连停用词表都省了一半。缺点是extract_tags默认按整篇文档算,短文本上区分度不如长文本明显。

import jieba.analyse # topK 控制取前多少个词,withWeight=True 才会带上 TF-IDF 权重 freq = jieba.analyse.extract_tags(text, topK=200, withWeight=True) freq_dict = {w: weight for w, weight in freq} wc = WordCloud(mask=mask_arr, font_path='simhei.ttf', mode='RGBA', background_color=None ).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%)这种完整格式。另外mode='RGB'下带 alpha 的颜色会被忽略,要透明底就老老实实设mode='RGBA'。

5.5 保存的图有白边,尺寸也对不上

现象:to_file出来的图比预期大一圈,四周有白边,放进 PPT 里对不齐。

原因:用了plt.savefig而不是wc.to_file,matplotlib 的 figure 默认带 padding 和 dpi 缩放。

解决:静态图一律wc.to_file()。真要走 matplotlib,加plt.savefig('x.png', dpi=100, bbox_inches='tight', pad_inches=0),并且plt.figure(figsize=(w/100, h/100), dpi=100)让画布尺寸和词云尺寸对齐。

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_path='C:/Windows/Fonts/simhei.ttf', use_tfidf=False, topk=200, mask_path=None, color_from_mask=True, stopwords=None, size=(1200, 900)): """统一入口:文本进,PNG 出。 use_tfidf=True 走关键词权重,False 走分词词频。 """ if not os.path.exists(font_path): raise FileNotFoundError('字体不存在:%s' % font_path) stopwords = stopwords or set() if use_tfidf: # 路线 A:TF-IDF 权重,适合长文本,能自动压掉高频虚词 pairs = jieba.analyse.extract_tags(text, topK=topk, withWeight=True) 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_path=font_path, max_words=topk, background_color=None, mode='RGBA', width=size[0], height=size[1], random_state=42) # 蒙版存在时覆盖尺寸参数,并转成灰度 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_func=ImageColorGenerator(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图里词的总数100~300,再多就糊
max_font_size最大词的字号不设则按画布自动算,海报图建议显式给
relative_scaling词频对字号的敏感度0~1,0 只看排名,1 严格按频次
prefer_horizontal水平词占比0.9 默认,想更“词云感”调到 0.7
margin词与词的间距默认 2,密集图调到 4~6
random_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 词表检查”,省掉过好几次“图好看但数据是错的”的尴尬。希望帮到你。

本文还有配套的精品资源,点击获取