
1. 关键词提取到底在解决什么问题做文本处理的人迟早会撞上同一个需求给你一篇文章怎么用程序把里面最核心的几个词揪出来。这件事听起来简单真动手做才发现坑不少。关键词提取是自然语言处理里最基础也最实用的任务之一它的应用场景非常广——内容平台的自动打标签、搜索引擎的索引构建、舆情监控的热点聚类、推荐系统的兴趣建模甚至你写论文时自动生成的关键词候选背后都是同一套逻辑。我刚开始接触这块的时候以为关键词提取就是分词之后数一数哪个词出现得多。后来发现纯词频统计确实是最容易上手的方案但它远不是全部。词频统计法Term Frequency的核心思想很朴素一个词在文章里出现的次数越多它就越可能是这篇文章的重点。这个假设在很多场景下成立但它有两个明显的软肋——一是高频词可能是“的”“了”“是”这类没有实际意义的停用词二是有些词虽然出现次数不多但恰恰是文章的核心概念。所以真正要落地一个关键词提取功能你得同时解决三个问题怎么把文章切成词、怎么过滤掉没用的词、怎么给剩下的词排个合理的优先级。这三个问题环环相扣任何一个环节处理不好最终结果都会让你觉得“这提取的是个啥”。这篇文章我会从零开始把基于词频统计的关键词提取方案完整拆一遍。从分词工具的选择、停用词表的构建、词频统计的实现到最终排序输出的优化每一步都会给出可运行的代码和参数选择的理由。适合刚入门NLP的朋友也适合已经能做但想搞清楚细节的同行。2. 整体方案设计与技术选型2.1 为什么从词频统计入手关键词提取的算法大致可以分成两大类无监督方法和有监督方法。有监督方法需要大量人工标注好的数据来训练模型成本高、周期长而且换一个领域就得重新标注。无监督方法不需要标注数据直接利用文章本身的统计特征来提取关键词词频统计就是其中最基础的一种。词频统计法的优势在于实现简单、计算速度快、可解释性强。你不需要理解复杂的概率图模型也不需要调参调到怀疑人生。对于大部分日常场景——比如给一篇新闻自动打标签、从用户评论里提取产品特征词——词频统计配合合理的过滤规则效果已经够用了。当然它也有局限。纯词频统计无法识别同义词和近义词比如“手机”和“移动电话”会被当成两个独立的词也无法处理一词多义的情况。但这些局限在入门阶段可以先放一放等你把基础流程跑通了再考虑引入TF-IDF、TextRank这些进阶方案。2.2 分词工具怎么选中文和英文的关键词提取流程有一个根本区别英文单词天然由空格分隔分词这一步基本可以跳过中文必须先用分词工具把连续的汉字序列切成有意义的词。所以中文场景下分词工具的选择直接决定了后续所有环节的质量。目前常用的中文分词工具主要有这几类工具特点适用场景jieba纯Python实现安装简单支持三种分词模式快速原型、中小规模文本HanLP功能全面支持分词、词性标注、命名实体识别对精度要求较高的场景pkuseg多领域分词支持自定义词典特定领域文本THULAC清华大学出品分词和词性标注一体化学术研究我个人的建议是如果你刚开始学NLP直接用jieba。它的API设计非常友好三行代码就能完成分词而且社区活跃、文档齐全遇到问题容易找到解决方案。等你对分词效果有更高要求了再考虑换HanLP或pkuseg。2.3 停用词表的构建策略停用词就是那些在文本中高频出现但对理解文章主题没有帮助的词比如“的”“了”“在”“是”“和”这些。如果不把停用词过滤掉词频统计的结果会被它们霸占真正有意义的关键词反而排不上号。停用词表的构建有三种常见策略第一种是直接用现成的停用词表。网上有很多开源的中文停用词表比如哈工大停用词表、百度停用词表、四川大学机器智能实验室停用词库等。这些词表覆盖了大部分通用停用词拿来就能用。第二种是在通用词表的基础上根据你的具体场景做增删。比如你做的是医疗领域的文本分析“患者”“治疗”“症状”这些词可能太泛了不适合作为关键词就可以把它们加进去。反过来如果你做的是法律文书分析“原告”“被告”虽然是高频词但可能恰恰是你需要保留的。第三种是完全自定义。这种方式适合你对业务场景非常熟悉、且通用词表效果不理想的情况。但完全自定义的工作量很大一般不建议从零开始。我的做法是先用通用词表跑一遍看看提取结果里有哪些明显不该出现的词把它们补充进去再看看有哪些被误杀的好词把它们从停用词表里删掉。迭代两三轮停用词表就基本贴合你的场景了。3. 核心细节解析与实操要点3.1 jieba分词的三种模式及选择依据jieba提供了三种分词模式每种模式适用于不同的场景精确模式试图把句子最精确地切开适合文本分析。比如“我来到北京清华大学”会被切成“我/来到/北京/清华大学”。这是最常用的模式关键词提取一般都用它。全模式把句子中所有可能成词的词语都扫描出来速度很快但存在歧义。比如“我来到北京清华大学”会被切成“我/来到/北京/清华/清华大学/华大/大学”。这种模式适合搜索引擎的索引构建因为召回率更高但用于关键词提取会产生大量冗余。搜索引擎模式在精确模式的基础上对长词再次切分提高召回率。比如“中华人民共和国”会被切成“中华/人民/共和国/中华人民共和国”。这种模式适合搜索引擎分词关键词提取一般不用。代码示例import jieba text 我来到北京清华大学 # 精确模式 seg_exact jieba.lcut(text, cut_allFalse) print(精确模式:, seg_exact) # 输出: [我, 来到, 北京, 清华大学] # 全模式 seg_full jieba.lcut(text, cut_allTrue) print(全模式:, seg_full) # 输出: [我, 来到, 北京, 清华, 清华大学, 华大, 大学] # 搜索引擎模式 seg_search jieba.lcut_for_search(text) print(搜索引擎模式:, seg_search) # 输出: [我, 来到, 北京, 清华, 华大, 大学, 清华大学]做关键词提取我建议用精确模式。原因是关键词提取的目标是找到最能代表文章主题的词而不是把所有可能的词都找出来。全模式和搜索引擎模式会产生大量碎片化的词反而干扰后续的统计和排序。3.2 词性过滤为什么只保留名词、动词、形容词分词之后你会得到一堆词但并不是所有词都适合做关键词。代词“他”“她”“它”、介词“在”“从”“对”、连词“和”“但是”“因为”这些词即使出现频率很高也不能作为关键词。所以我们需要用词性标注POS Tagging来过滤掉这些词。jieba提供了posseg模块来做词性标注。常用的词性标记包括n名词v动词a形容词d副词p介词c连词u助词关键词提取一般只保留名词、动词和形容词。名词直接指向实体和概念动词描述动作和行为形容词描述性质和状态这三类词承载了文章的主要信息。副词、介词、连词、助词这些功能词一律过滤掉。代码示例import jieba.posseg as pseg text 自然语言处理是人工智能领域的一个重要方向 words pseg.lcut(text) for word, flag in words: print(f{word}: {flag}) # 输出: # 自然语言: l # 处理: v # 是: v # 人工智能: n # 领域: n # 的: uj # 一个: m # 重要: a # 方向: n这里有个细节需要注意jieba的词性标注并不是100%准确的尤其是对未登录词词典里没有的词的标注。所以词性过滤只能作为一个粗筛手段不能完全依赖它。我的做法是先用词性过滤掉明显不合适的词再用停用词表做二次过滤两层过滤叠加效果会好很多。3.3 词频统计的实现与排序过滤完停用词和功能词之后剩下的词就可以做词频统计了。Python的collections.Counter是最好用的工具一行代码就能完成统计和排序。from collections import Counter # 假设 filtered_words 是过滤后的词列表 word_counts Counter(filtered_words) # 取出现次数最多的10个词 top_words word_counts.most_common(10) for word, count in top_words: print(f{word}: {count})但这里有一个容易被忽略的问题词频高不等于重要性高。一篇文章里“系统”出现了20次“架构”出现了5次但“架构”可能才是文章的核心概念。纯词频统计无法区分这种情况。解决这个问题的思路有几种第一种是引入TF-IDF。TF是词频IDF是逆文档频率用来衡量一个词在整个语料库中的稀有程度。一个词在单篇文章中频率高、在整个语料库中频率低它的TF-IDF值就高说明它既重要又独特。但TF-IDF需要有一个语料库来计算IDF单篇文章场景下用不了。第二种是考虑词的位置信息。标题、首段、末段、段落首句中的词权重应该更高。这个思路实现起来也不复杂给不同位置的词加不同的权重系数就行。第三种是考虑词的长度。中文里两个字以上的词往往比单字词更有信息量。比如“学习”比“学”更具体“人工智能”比“智能”更明确。可以在排序时给长词加一点权重。我在实际项目中的做法是以词频为基础分叠加位置权重和长度权重做一个简单的加权排序。这样既保留了词频统计的简单性又在一定程度上弥补了它的不足。4. 完整实操流程与代码实现4.1 环境准备与依赖安装先把环境搭好。我用的Python版本是3.9jieba的版本是0.42.1。安装命令很简单pip install jieba如果你需要做词性标注jieba自带了这个功能不需要额外安装。如果你打算用HanLP或pkuseg安装方式会复杂一些需要下载模型文件这里不展开。4.2 完整代码实现下面是一个完整的关键词提取脚本包含了分词、词性过滤、停用词过滤、词频统计和加权排序的全部流程。import jieba import jieba.posseg as pseg from collections import Counter import re # 1. 加载停用词表 def load_stopwords(filepath): 从文件加载停用词表每行一个词 stopwords set() with open(filepath, r, encodingutf-8) as f: for line in f: word line.strip() if word: stopwords.add(word) return stopwords # 如果没有现成的停用词文件可以用这个内置的基础版 def get_default_stopwords(): 返回一个基础的中文停用词集合 return { 的, 了, 在, 是, 我, 有, 和, 就, 不, 人, 都, 一, 一个, 上, 也, 很, 到, 说, 要, 去, 你, 会, 着, 没有, 看, 好, 自己, 这, 那, 他, 她, 它, 们, 这个, 那个, 什么, 怎么, 为什么, 因为, 所以, 但是, 然而, 虽然, 如果, 可以, 能够, 应该, 需要, 进行, 通过, 使用, 实现, 以及, 并且, 或者, 还是, 对于, 关于, 根据, 按照, 随着, 为了 } # 2. 文本预处理 def preprocess_text(text): 清洗文本去除特殊字符、多余空白 # 去除HTML标签 text re.sub(r[^], , text) # 去除URL text re.sub(rhttp[s]?://\S, , text) # 去除多余空白 text re.sub(r\s, , text) return text.strip() # 3. 分词与词性过滤 def segment_and_filter(text, stopwords, allowed_posNone): 分词并进行词性过滤和停用词过滤 allowed_pos: 允许保留的词性列表默认保留名词、动词、形容词 if allowed_pos is None: allowed_pos {n, v, a, ns, nt, nz, vn} words pseg.lcut(text) filtered [] for word, flag in words: # 过滤停用词 if word in stopwords: continue # 过滤单字可选根据场景决定 if len(word) 2: continue # 过滤非允许词性 if flag not in allowed_pos: continue # 过滤纯数字和纯标点 if re.match(r^[\d\W]$, word): continue filtered.append(word) return filtered # 4. 词频统计与加权排序 def extract_keywords(text, stopwords, top_n10): 提取关键词的主函数 返回[(关键词, 得分), ...] # 预处理 text preprocess_text(text) # 分词过滤 words segment_and_filter(text, stopwords) if not words: return [] # 基础词频统计 word_counts Counter(words) # 计算加权得分 max_count max(word_counts.values()) scored_words [] for word, count in word_counts.items(): # 基础分归一化词频 base_score count / max_count # 长度权重2字词权重1.03字词1.14字及以上1.2 if len(word) 2: length_weight 1.0 elif len(word) 3: length_weight 1.1 else: length_weight 1.2 # 位置权重出现在前20%文本中的词加权 position_weight 1.0 first_part text[:len(text) // 5] if word in first_part: position_weight 1.15 # 最终得分 final_score base_score * length_weight * position_weight scored_words.append((word, final_score, count)) # 按得分降序排序 scored_words.sort(keylambda x: x[1], reverseTrue) # 返回前top_n个 return [(word, round(score, 4)) for word, score, _ in scored_words[:top_n]] # 5. 主程序 if __name__ __main__: # 示例文本 sample_text 自然语言处理是人工智能领域中的一个重要方向。它研究能实现人与计算机之间 用自然语言进行有效通信的各种理论和方法。自然语言处理是一门融语言学、 计算机科学、数学于一体的科学。因此这一领域的研究将涉及自然语言 即人们日常使用的语言所以它与语言学的研究有着密切的联系。 自然语言处理技术广泛应用于机器翻译、文本分类、情感分析、信息抽取等场景。 近年来随着深度学习技术的发展自然语言处理取得了突破性进展。 # 加载停用词 stopwords get_default_stopwords() # 提取关键词 keywords extract_keywords(sample_text, stopwords, top_n10) print(提取的关键词) for word, score in keywords: print(f {word}: {score})运行这段代码你会得到类似这样的输出提取的关键词 自然语言: 1.38 处理: 1.15 研究: 0.92 语言学: 0.92 技术: 0.69 计算机: 0.69 科学: 0.69 领域: 0.69 机器翻译: 0.46 文本分类: 0.464.3 参数选择与调优过程上面代码里有几个参数需要根据你的具体场景来调整词性过滤的允许列表。我默认保留了n名词、v动词、a形容词以及几个名词子类。如果你做的是技术文档分析可以把nz其他专名和vn动名词也加进去。如果你做的是社交媒体文本分析可以适当放宽到d副词因为口语化文本里副词有时也承载情感信息。最小词长。我设置了len(word) 2就过滤掉也就是只保留两个字及以上的词。这个设置对中文文本是合理的因为单字词在中文里往往信息量不足。但如果你处理的是古文或者诗歌单字词可能恰恰是关键那就需要把这个限制去掉。长度权重的系数。我设置了2字词1.0、3字词1.1、4字及以上1.2。这个系数是我根据经验调的你可以根据实际效果微调。如果你发现提取结果里长词太多、短词太少就把系数差距缩小反之就拉大。位置权重的系数。我设置了前20%文本中的词加权1.15。这个系数也不宜太大否则会导致标题党式的词霸屏。1.1到1.2之间是比较安全的范围。注意这些参数没有绝对的最优值必须根据你的具体数据和业务目标来调。建议先跑一遍看结果然后针对性地调整。调参的过程本身就是理解数据的过程。5. 常见问题与排查技巧实录5.1 提取结果里全是“系统”“问题”“方法”怎么办这是最常见的问题。这些词在技术类文章里出现频率极高但它们太泛了作为关键词几乎没有区分度。解决办法有两个一是把这些词加入停用词表。这是最直接的办法但缺点是治标不治本——换一篇文章可能又会出现新的泛词。二是引入逆文档频率IDF的概念。虽然单篇文章场景下没有完整的语料库但你可以维护一个“泛词表”记录那些在大量文章中都会高频出现的词给它们一个较低的权重。这个泛词表可以手动维护也可以从你的历史数据中自动挖掘。我的做法是两者结合先用停用词表过滤掉最明显的泛词再用一个简单的权重衰减机制对长度较短、词性为普通名词的词做降权处理。5.2 分词结果不准确导致关键词提取错误jieba的分词准确率在通用领域大概在85%到90%之间在垂直领域会更低。比如“深度学习”可能被切成“深度”和“学习”“机器学习”可能被切成“机器”和“学习”。这会导致关键词提取的结果不符合预期。解决办法是加载自定义词典。jieba支持通过load_userdict方法加载用户词典把领域专有名词加进去分词时就会优先按照词典来切分。# 加载自定义词典 jieba.load_userdict(user_dict.txt) # user_dict.txt 的格式每行一个词可以指定词频和词性 # 深度学习 100 n # 机器学习 100 n # 自然语言处理 100 n自定义词典的构建需要你对业务领域有一定的了解。我的建议是先跑一遍不加词典的分词把错误切分的词记录下来整理成词典文件再跑一遍。迭代两三次分词质量会有明显提升。5.3 词频统计结果受文本长度影响太大长文章里每个词的绝对出现次数都会比短文章高这导致词频的绝对值没有可比性。解决办法是做归一化处理把词频除以文章总词数得到相对词频。total_words sum(word_counts.values()) normalized_counts {word: count / total_words for word, count in word_counts.items()}归一化之后不同长度文章的关键词得分就可以横向比较了。如果你要做的是跨文章的关键词对比这一步是必须的。5.4 常见问题速查表问题现象可能原因排查方法解决方案提取结果全是单字未过滤单字词检查过滤逻辑添加len(word)2的过滤提取结果含大量标点未过滤非文字字符检查正则过滤添加re.match(r^[\d\W]$)过滤关键词重复出现未做去重检查Counter使用Counter自动去重检查是否重复添加分词结果不符合预期未加载自定义词典打印分词结果加载领域自定义词典长文章关键词得分普遍偏高未做归一化检查是否除以总词数添加归一化处理提取结果全是泛词停用词表不完善人工检查结果补充停用词或引入IDF权重5.5 几个我踩过的坑第一个坑是忽略了编码问题。中文文本的编码格式五花八门UTF-8、GBK、GB2312都有。如果读取文件时编码指定错误会导致乱码后续所有处理都白费。我的做法是统一转成UTF-8读取时用errorsignore参数忽略无法解码的字符。第二个坑是停用词表加载效率低。我一开始把停用词存在列表里每次判断都用in操作文本量大了之后速度明显变慢。后来改成用set存储查询效率从O(n)降到O(1)处理速度提升了好几倍。第三个坑是过度依赖词性标注。jieba的词性标注对未登录词的准确率不高有些重要的专有名词会被标成其他词性导致被过滤掉。我的做法是对词性过滤设置一个白名单机制某些特定的词即使词性不符合要求也保留比如所有长度大于等于4的词都保留因为长词大概率是有意义的。第四个坑是没有考虑同义词合并。比如“电脑”和“计算机”会被当成两个词分别统计导致每个词的频次都不高排名靠后。解决办法是维护一个同义词映射表把同义词归并到同一个标准词上。这个工作比较繁琐但对于提升关键词提取质量很有帮助。6. 进阶优化与扩展思路6.1 从词频统计到TF-IDF词频统计的最大局限是无法区分“常见词”和“关键词”。TF-IDF通过引入逆文档频率解决了这个问题。IDF的计算公式是IDF log(总文档数 / 包含该词的文档数)一个词在越少的文档中出现它的IDF值越高说明它越稀有、越有区分度。TF-IDF TF × IDF综合了词频和稀有度两个维度。在单篇文章场景下你可以用一个外部的语料库来计算IDF。比如用维基百科的中文语料、新闻语料等。计算好每个词的IDF值之后存起来提取关键词时直接查表就行。6.2 从TF-IDF到TextRankTextRank的思路来源于PageRank它把文本中的词看作图的节点词与词之间的共现关系看作边通过迭代计算每个节点的重要性。TextRank不需要外部语料库完全基于单篇文章的内部结构适合处理单篇长文本。TextRank的实现比词频统计复杂一些但Python有现成的库可以用比如jieba.analyse.textrank。用法很简单import jieba.analyse keywords jieba.analyse.textrank(text, topK10, withWeightTrue) for word, weight in keywords: print(f{word}: {weight})6.3 关键词提取在内容平台的应用如果你做的是内容平台关键词提取可以跟推荐系统结合起来。具体做法是对每篇文章提取关键词构建文章的标签向量对每个用户根据其历史行为构建兴趣向量然后计算两个向量的相似度作为推荐排序的依据。这个方案的关键在于关键词的质量。如果提取的关键词不能准确反映文章主题推荐效果就会大打折扣。所以关键词提取的优化是一个持续的过程需要不断根据线上反馈来调整停用词表、权重参数和分词词典。6.4 一个容易被忽略的细节新词发现网络热词层出不穷jieba的词典更新速度跟不上。比如“内卷”“躺平”“元宇宙”这些词在旧版本的词典里可能不存在分词时会被切碎。解决办法是定期更新jieba版本或者自己维护一个热词词典定期从社交媒体上挖掘新词加进去。新词发现本身也是一个NLP任务常用的方法有基于统计的方法比如左右熵和互信息和基于深度学习的方法。如果你对这个方向感兴趣可以单独作为一个课题来研究。7. 实操心得与建议做了几个关键词提取的项目之后我最大的体会是没有一劳永逸的方案只有不断迭代的流程。你不可能一开始就设计出一个完美的停用词表和权重参数必须根据实际数据不断调整。我的建议是建立一个评估机制。每次调整参数之后人工检查前20个关键词的提取结果标记哪些是好的、哪些是坏的计算一个简单的准确率。这样你就能量化地看到每次调整的效果而不是凭感觉瞎调。另外不要忽视数据预处理的重要性。我见过太多人把精力花在算法调优上结果数据本身就有问题——编码错误、HTML标签没洗干净、特殊字符没处理。这些问题不解决再好的算法也跑不出好结果。最后关键词提取只是NLP的一个入口。把它做扎实了你会自然而然地接触到分词、词性标注、命名实体识别、文本分类这些下游任务。每一步的积累都会成为你后续学习的基础。我在实际操作中的体会是把基础流程跑通一遍比看十篇综述文章都管用。