做文本分析绕不开一个最基础也最常用的活儿——词频统计。用Python做词频统计从读入文本到输出结果整个流程可以拆得非常清晰文本清洗、分词、过滤停用词、计数、排序、展示。这篇文章不打算绕弯子直接用一套完整可运行的流程把它讲透帮你从零搭起一个词频统计程序顺带把里面的坑和优化思路都指出来。无论你是刚开始学Python还是想给后端写个简单的文本分析工具这个流程都能直接拿去改。1. 词频统计的整体设计思路1.1 为什么流程要分成这几步词频统计看似简单但直接拿原始文本数词儿肯定不靠谱。比如英文句子里的标点、大小写中文文本里的空格和标点、以及各种虚词都会严重影响统计结果。所以第一步必须先清洗数据把干扰项处理干净第二步按语言规则把文本切成最小的语义单元英文按空格切中文需要分词第三步过滤掉那些只想表达语法结构、不携带实际意义的停用词第四步才是真正的频率统计最后再排序和可视化。每一步都有它存在的理由跳过任何一步结果都可能变得没法看。为什么强调“流程”而不是直接给个函数因为实际工作中你会碰到清洗规则要不断加、分词词典要按领域定制、统计结果要反复调优。把步骤分开封装后面改任何一环都不会牵一发动全身。这篇文章就按这个思路一步步展开。1.2 技术选型标准库还是第三方库Python做词频统计最基础的可以用纯内置的字典类型配合字符串方法几分钟就能写出一个能跑的版本。但要处理真实文本纯手写会累死尤其是中文分词没有现成的词典和算法支撑没法做。所以实际项目里标准做法是用几个固定的第三方库读文本用内置的open或pathlib中文分词用jieba数据整理用pandas可选画图用matplotlib做词云用wordcloud。这些库都很成熟网上资料也多出了问题好排查。这里也顺便说一句不要一上来就搞分布式Spark、Flink那套词频统计是个经典入门任务单机单进程完全够用除非数据量到了几GB甚至更大再考虑用大数据框架。先用Python把逻辑跑通后面迁移到任何框架都只是换个API的问题。2. 环境准备与数据预处理2.1 依赖库安装在写代码前先把库装好。推荐用pip安装如果你在用虚拟环境那就更省心。需要的库pip install jieba matplotlib wordcloud如果还要用pandas做数据整理就加一句pip install pandas如果有朋友用的是Anaconda那么matplotlib和pandas基本是自带的jieba和wordcloud就需要手动安装。这里提醒一句wordcloud在国内下载可能比较慢如果pip安装失败可以尝试用国内镜像源安装比如pip install wordcloud -i https://pypi.tuna.tsinghua.edu.cn/simple安装完之后可以测试一下python -c import jieba, matplotlib, wordcloud; print(ok)如果没报错说明环境没问题。2.2 读取文本文件读取文本的第一步是确认编码。最常见的坑就是Windows下用记事本保存的文件是GBK编码而Python默认读法是UTF-8直接读会报UnicodeDecodeError。所以稳妥的做法是打开文件时显式指定编码with open(news.txt, r, encodingutf-8) as f: text f.read()如果文件是GBK把encoding换成gbk。不确定就用爬虫抓下来的文本大多数是UTF-8但带BOM的情况也见过可以在读取后顺手清理BOMtext text.lstrip(\ufeff)一个更省事的方法是借助pathlibfrom pathlib import Path text Path(news.txt).read_text(encodingutf-8)它和open的效果一样但代码更简洁我现在的项目里基本都用pathlib。2.3 文本清洗规则读到原始字符串后不能急着统计。清洗规则一般包括去掉所有标点符号把英文字母统一转成小写去掉多余空白去掉数字或保留数字看需求如果内容是HTML还需要去掉标签。下面给一个常见的清洗函数import re def clean_text(text): # 去掉HTML标签 text re.sub(r[^], , text) # 去掉数字 text re.sub(r\d, , text) # 去掉标点符号和特殊符号 text re.sub(r[^\w\s\u4e00-\u9fff], , text) # 统一小写主要针对英文 text text.lower() # 合并多余空白 text re.sub(r\s, , text).strip() return text这里正则表达式里的[^\w\s\u4e00-\u9fff]表示把非字母数字非空白非中文的字符统统去掉。\w在默认情况下会匹配数字、字母和下划线所以在用这个正则时要清楚数字已经被上一步去掉了。如果你希望保留数字就去掉去数字那行并把正则改成[^\w\u4e00-\u9fff]注意保留空格时要用剪切出来。实操中按需求调整清洗规则永远是跟随业务走的。3. 中文分词与停用词处理3.1 用jieba完成分词中文分词没有天然的空格边界必须借助分词工具。jieba是纯Python实现的分词库支持精确模式、全模式和搜索引擎模式词频统计一般用精确模式就够了。import jieba words jieba.lcut(cleaned_text)lcut返回一个列表比cut更方便直接。比如一句话我爱Python数据分析lcut之后会得到[我, 爱, Python, 数据, 分析]这样的效果。jieba自带的词典对常见词汇覆盖得不错但对专业领域的人名、机构名、新词可能分不准。比如云计算平台可能被切成云/计算/平台如果你希望它成词可以加载自定义词典jieba.add_word(云计算平台) # 或者从文件加载 jieba.load_userdict(my_dict.txt)自定义词典每行一个词格式是词 词频 词性词频可以不写例如云计算平台 1000 n 词频统计 800 n在我的经验里做金融、法律、医学这类垂直领域的文本分析时自定义词典几乎是必备的。不加载的话分词质量会直接影响后面所有统计结果。3.2 停用词表的获取与使用停用词就是的、了、在、是、我、你、他这类在文本中频繁出现但没啥分析价值的词。统计词频前不删掉它们榜单前排全是这些词啥信息也提取不到。一般做法是准备一个停用词表文件txt每行一个词然后构建一个setdef load_stopwords(path): with open(path, r, encodingutf-8) as f: stopwords set(line.strip() for line in f if line.strip()) return stopwords使用时过滤stopwords load_stopwords(stopwords.txt) words_filtered [w for w in words if w.strip() and w not in stopwords and len(w) 1]注意下面那个len(w) 1这一步会把单字词全干掉。中文里很多单字词在特定语境下有实际含义但如果是通用统计去掉单字词能明显提升结果的可用性。如果你做的是古诗词分析这个条件就要去掉否则风花雪月统计不全。3.3 英文文本的处理差异如果同时统计中英文混合文本英文的分词可以用空格切分。具体做法是在清洗之后words re.findall(r[a-z], cleaned_text)但这样会把缩写如dont拆成不完整的词需要额外的缩写还原规则。如果是纯中文场景就用jieba纯英文场景直接用str.split()或正则都够用。混合场景我会建议尽量把中英文拆开处理不然计数和可视化都会比较混乱。4. 词频统计核心实现4.1 用Counter一行完成计数清洗和分词完成之后统计本身其实没什么可写的。Python内置的collections.Counter就是为这种事情准备的from collections import Counter word_counts Counter(words_filtered)Counter对象本质上是一个字典key是词value是次数。它有一个非常好用的方法most_common(n)可以直接返回频率最高的n个词top_words word_counts.most_common(20) for word, count in top_words: print(word, count)这个输出就很清爽词和次数之间用制表符分隔复制到Excel里也能直接分列。4.2 用字典手动计数的原理为了帮助理解这里也给出一个不用Counter的写法便于初学者看清count的本质word_counts {} for word in words_filtered: if word in word_counts: word_counts[word] 1 else: word_counts[word] 1也可以使用dict.get的默认值写法word_counts[word] word_counts.get(word, 0) 1实际开发用Counter更简洁但是面试或考试时经常问手写实现核心逻辑就是上面这几行。4.3 排序与筛选Counter本身可以排序只需要sorted_counts sorted(word_counts.items(), keylambda x: x[1], reverseTrue)如果只想要前100个词也可以用列表切片top100 sorted_counts[:100]还可以转成DataFrame来做进一步分析import pandas as pd df pd.DataFrame(word_counts.items(), columns[word, count]) df df.sort_values(count, ascendingFalse) df.head(20)用DataFrame的好处是可以很方便地做筛选、合并、导出Excel比如写一句df.to_csv(result.csv, indexFalse)就把结果保存下来了。我在做月度汇报时经常这样把词频结果直接输出成CSV丢给领导比手动整理快得多。5. 可视化与综合示例5.1 用matplotlib画柱状图词频统计的结果光看数字不够直观。画柱状图是最常见的方式这里给一个针对top20的示例import matplotlib.pyplot as plt top_words word_counts.most_common(20) words [item[0] for item in top_words] counts [item[1] for item in top_words] plt.figure(figsize(12, 6)) plt.rcParams[font.sans-serif] [SimHei] # 防止中文乱码 plt.bar(words, counts) plt.xticks(rotation45) plt.xlabel(词语) plt.ylabel(出现次数) plt.title(Top 20 高频词) plt.tight_layout() plt.show()中文画图第一怕乱码第二怕重叠。乱码问题在Windows下用SimHei能解决但在macOS下SimHei不一定有这时候可以改成[Arial Unicode MS]。更通用的办法是直接指定系统里可用字体的路径或者下载一个思源黑体放在项目目录里。词频统计这种图标签重叠也很常见所以rotation45度和tight_layout几乎是标配。5.2 用wordcloud生成词云词云是更直观的展示方式wordcloud库需要配合matplotlib使用。注意wordcloud不支持中文直接显示需要指定字体文件路径否则会显示成方框。示例代码如下from wordcloud import WordCloud # 生成词云对象的字符串形式例如 词1 词2 词1 ... word_string .join(words_filtered) wc WordCloud( font_pathmsyh.ttc, # 微软雅黑路径换成你系统里的中文字体 width800, height600, background_colorwhite, max_words200 ).generate(word_string) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.show()如果运行发现wordcloud报缺少font_path的错误或者词云全是矩形框就说明字体路径没配对。Windows下常见字体文件有C:/Windows/Fonts/simhei.ttf、msyh.ttcmacOS下可能在/System/Library/Fonts/PingFang.ttc。不同系统差异很大建议先用fc-listLinux或者直接列目录的方式查一下。5.3 一个可直接复用的综合示例最后把整个流程串起来给一个完整的脚本。这个脚本放在项目根目录数据文件名为news.txt停用词文件为stopwords.txt运行后会在屏幕上打印前20个高频词并生成柱状图和词云。import re import jieba from collections import Counter import matplotlib.pyplot as plt from wordcloud import WordCloud def load_stopwords(path): with open(path, r, encodingutf-8) as f: return set(line.strip() for line in f if line.strip()) def clean_text(text): text re.sub(r[^], , text) text re.sub(r\d, , text) text re.sub(r[^\w\s\u4e00-\u9fff], , text) text text.lower() text re.sub(r\s, , text).strip() return text def word_frequency(filepath, stopword_path, top_n20): with open(filepath, r, encodingutf-8) as f: raw_text f.read() cleaned clean_text(raw_text) words jieba.lcut(cleaned) stopwords load_stopwords(stopword_path) words_filtered [w for w in words if w.strip() and w not in stopwords and len(w) 1] counter Counter(words_filtered) return counter.most_common(top_n), counter, words_filtered def plot_and_cloud(top_list, counter, words_filtered): # 柱状图 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False words [item[0] for item in top_list] counts [item[1] for item in top_list] plt.figure(figsize(12, 6)) plt.bar(words, counts) plt.xticks(rotation45) plt.tight_layout() plt.savefig(word_freq.png, dpi150) plt.show() # 词云 word_string .join(words_filtered) wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, width800, height600, background_colorwhite, max_words200 ).generate(word_string) plt.figure(figsize(10, 8)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.savefig(wordcloud.png, dpi150) plt.show() if __name__ __main__: top, counter, filtered word_frequency(news.txt, stopwords.txt) for word, count in top: print(f{word}\t{count}) plot_and_cloud(top, counter, filtered)注意这段代码用到了Windows下的SimHei字体路径如果你不是Windows记得改字体路径。综合示例的流程说明指定文件路径和停用词路径读取文本、清洗、分词、过滤用Counter统计词频打印top20生成柱状图和词云并保存到本地。我在实际测试中拿一篇几千字的新闻文本跑下来从清洗到出图整个流程不到两秒钟性能完全够用。如果文本特别大比如10MB以上可以考虑用jieba.enable_parallel()开启并行分词或者改用多线程分批读取但大部分场景不需要。6. 常见问题与排查技巧实录6.1 中文乱码与字体问题词频统计项目里最烦人的问题是乱码出现乱码有三种来源。第一种是文件本身编码不对读进来就是乱码这种可以在open时指定正确的encoding或者用chardet库自动检测编码。第二种是print到控制台乱码这是因为Windows终端默认编码是GBK而Python输出UTF-8可以在代码开头加一句import io, sys; sys.stdout io.TextIOWrapper(sys.stdout.buffer, encodingutf-8)或者直接换成支持UTF-8的终端比如VS Code、Conda Prompt。第三种是matplotlib绘图乱码就是前面提到的字体问题用plt.rcParams[font.sans-serif] [SimHei]解决。如果换成其他字体还是乱码建议把字体文件路径先打印出来确认存在不要凭记忆填路径。6.2 停用词表不生效很多新手在过滤停用词时发现词还是出现了。常见原因有两个一是词表里词和文本里的词带空格或换行不一致比如词表里有的但分词结果是的 所以过滤不掉二是自定义词典和停用词表里同一个词因为某一边带了特殊字符导致set匹配不上。排查方法很简单过滤之前先打印几个待过滤词和停用词的关系比如print(repr(w))看输出里有没有隐藏的空格或换行。还有一种是分词粒度问题。比如我们这个词词表里没有但jieba切出来是我们那停用词表里就必须存在我们如果你在停用词表里写的是我们 的带空格那就没用了。正确做法是每行一个词不要有额外空白。6.3 词频统计结果与预期不符如果统计出来的前几名都是一些奇怪短语或者完全不是预期的词多半是清洗不彻底。举例文本中的URL没去掉正则里没有处理http结果https这个词出现很多次。解决方式是在清洗时加一条text re.sub(rhttps?://\S|www\.\S, , text)还有英文单词的分词如果中文和英文混在一起jieba会把Python数据分析拆成Python和数据分析这没问题。但如果你统计的是连续的大段中文会发现一些标点经过清洗后被替换成空格分词后又产生了一堆单字这时len(w) 1就很有必要。6.4 CPU占用过高有朋友在处理很大的文本时发现跑了一次词频统计程序CPU顿时拉满。jieba在加载词典时CPU和内存开销都不小属正常表现。如果主流程只是分词建议使用精确模式不要开搜索引擎模式和全模式后者的计算量和结果冗余都更大。也可以考虑引入redis缓存或数据库但这已经不是入门范围了先不管。如果是wordcloud生成词云时CPU很高可能是在处理大量高频重复词。没关系跑完就好。6.5 词频统计的性能优化建议当文本量上来之后有几个性价比很高的优化点。第一个点是分词本身jieba的精确模式在速度上已经不错但如果你有明确的领域词典把词典自定义加载好能减少分词错误同时减少后续清洗和过滤的负担。第二个点是数据结构的选用Counter的遍历是O(n)most_common排序是O(m log m)如果n很大可以先过滤掉低频词再排序减少排序开销比如只统计出现次数大于等于2的词。第三个点是把结果序列化用pickle或json保存counter对象下次直接加载避免重复切割文本。我平时会把这些高频问题和对应解法整理成一张速查表遇到问题直接查症状原因解法控制台中文乱码终端编码与Python输出不一致调整sys.stdout编码或换终端绘图中文变成方框matplotlib缺少中文字体指定font.sans-serif或font_path词云全是矩形框wordcloud缺少字体路径设置font_path为系统字体文件停用词过滤不掉文本或词表带隐藏空白用repr检查保证每行一个词高频词里有标点清洗规则不完整补充URL、数字、标点正则分词结果太碎领域词典缺失加载自定义词典细化停用词表结尾一点实操体会这篇文章从整体流程讲到具体实现最后再分享一个小技巧做词频统计时不要只盯着最终的词语排行一定要把清洗和停用词过滤看成核心环节。很多时候跑出来的结果难看不是统计逻辑有问题而是前面的预处理太粗糙。我在接手别人的代码时最常见的情况就是对方拿原始文本直接分词结果高频词全是的、了、在。所以如果你要把这套流程用在真实项目里先花点时间整理停用词表和清洗规则比调任何参数都管用。另外一个建议是最好把功能拆成独立函数每个函数只干一件事这样后续加需求比如情感分析、关键词提取时可以直接复用分词和清洗那两段。词频统计是个很小的任务但把它做成一套标准和流程之后你会发现后面很多文本分析的工作都可以在它的基础上扩展这大概就是这个入门任务最大的价值。最后补充一句这段代码我实际跑过很多次文本长度从几百字到几万字都能稳定运行。你只需要替换文件路径和停用词表基本就能直接用到你自己的数据上。调试时如果报错先看是编码问题、库没装好还是路径不对这三个占了九成以上的坑。