简介:面向文本分析、信息检索与自然语言处理场景,这份“史上最全中英文停用词”提供了覆盖中英文常用停用词的集合,可解决分词后噪声词干扰关键词提取、情感分析、机器翻译等任务预处理效率的问题,适合高校学生、算法工程师及数据分析初学者使用。包体共11个文件,以txt词表为主(9个txt),另有2个Python脚本用于合并与查看停用词,整体压缩后仅45KB,轻量易部署,可配合jieba或nltk直接加载。目前已有1222人学习下载。资源内包含中文、英文及中英文混合停用词表,并可基于脚本灵活合并或扩充自定义词表,帮助读者在文本预处理中快速过滤“的、了、the、is”等无实质意义词汇,适用于中文分词、英文tokenize、文本分类与情感分析等场景,既可作为日常开发的基础词典,也可作为定制化停用词库的起点。
1. 从检索到训练:为什么 | “史上最全中英文停用词”值得认真对待
做搜索的、做文本分类的、做情感分析的,几乎都会被同一样东西绊倒:停用词。“史上最全中英文停用词”这个词条常年出现在技术社区和分享帖的标题里,但真正的问题从来不是“哪一份词表最全”,而是“拿到一份号称最全的词表之后,怎么让它在你自己的语料和任务里不翻车”。我见过不少团队直接下载一份三千行的停用词表灌进分词管道,然后发现检索召回率不升反降,情感分析的结论反了,分类模型的指标也掉了。这篇笔记想把停用词这件事讲透:一份中英文停用词表到底该覆盖什么、怎么从源头把它清洗成能直接用的词典、不同任务下应该怎么裁剪、以及那些最容易让你踩坑的细节。适合正在搭搜索服务、做文本预处理、或者被分词结果里的“的了在”烦到的从业者。
2. 先把“最全”拆开:覆盖度、纯净度、可控性三个硬指标
“最全”这个词是有诱惑力的。人容易觉得词表越长越好,但一份真正能用的中英文停用词表,评判标准不是行数,而是三个彼此纠缠的指标:覆盖度、纯净度、可控性。这三个指标不掰扯清楚,后面所有“直接套用”的行为都是在给自己埋雷。
2.1 覆盖度:几百、几千还是几万?先明确词表的真实成分
先给一个基本盘:英文经典停用词表大约三百个词,中文常用停用词表大约在五百到一千之间。为什么这么少?因为停用词的定义是“高频但对语义贡献低”的词。英文里的 the、a、of、is、and 属于这一类,中文里的“了、的、在、是、和、与”属于这一类。这些词删掉之后,文本的主体结构依然完整。
那“史上最全”为什么能攒到几万行?因为很多版本把数字、标点、单字符、双字符、甚至常见人名地名品牌名都塞进去了。比如把“123”、“2024”、“http”、“com”、“www”都算成停用词。这类词确实在部分场景属于噪声,但它们不是严格意义上的停用词,而是“噪声词”。把两者混在一起,是覆盖度指标急剧膨胀的根源。
我一般会把一份所谓的“全量停用词表”拆成三层:
第一层是核心高频虚词,中英文各几百个,这是任何任务都建议过滤的;第二层是扩展停用词,包含代词变体、介词短语、时间词、数量词,这层需要按任务选择要不要启用;第三层是噪声词,包含标点、数字、URL 片段、无意义符号,这一层通常只在检索场景启用,在情感分析场景必须慎用。
如果你的目标只是做一个通用的预处理词典,覆盖度盯住前两层就够了。第三层每往里面加一个词,都要问一句:这个词在你的真实语料里出现频率高吗?被它误伤的合法词有多少?
2.2 纯净度:混入的标点和乱码如何悄悄污染向量空间
纯净度是“最全”词表最容易翻车的地方。一份号称几万词的停用词表,里面往往混着全角标点、半角标点、空白字符、BOM 头、零宽字符,甚至还有从 PDF 里复制出来的人工换行碎片。你把这份词表灌进分词器时,很多词表面上看存在,实际上匹配不到任何 token。
举一个我踩过的例子:某个中文词表是从网页直接保存的,里面所有逗号和句号都是全角,但分词器切出来的标点是半角,于是一整批标点停用词全部失效。排查了半天,最后发现词表里根本没有“,”而是“,”这种肉眼几乎不可区分的变体。纯净度的第二层含义是语言纯净:中文词表里不应该有英文词,英文词表里不应该有中文汉字。别笑,我在不少公开词表里都见过这种串味。
所以在合并词表这一步,按语言分拣、按字符集过滤,这两件事比什么都重要。宁可丢掉一部分词,也不要让一份带乱码的词表进入生产管道。
2.3 可控性:一份词典要能按任务切换强度
可控性指的是:停用词表不能是一坨不可拆分的集合。它应该是一组有层级的子集,让你在不同任务里能“开多层还是关多层”。
比如做搜索召回时,你可能需要最激进的过滤,把“的、了、吗、呢”全部丢掉,把标点数字也丢掉,这能让索引小很多。但如果你做的是情感分析,“不、没、无、很、太、最”这些词一旦被划进停用词表,整句的情感倾向会直接反转。同一个词表不能两头通吃,所以可控性的落地方式就是:把词表按类别拆成多个文件,运行时按任务组合。
常见的做法是拆成 stopwords_core.txt、stopwords_extended.txt、stopwords_noise.txt 三个文件,再在配置里指定当前任务加载哪几个。这一层想清楚了,“最全”就变成了一种可选的资源,而不是一个强制的负担。
写到这里我想强调一件事:一套“最全”词表如果没有做分层,它的使用成本极高,因为你永远不知道哪些词被哪个任务误伤了。分层是让“全”变成可用能力的第一步。
3. 从源头词表到生产可用:合并、去重、清洗三步走
当你手里有几份来源不同的停用词表时,第一步永远是合并去重,但合并本身有讲究。不是把文件拼起来就完事,而是要按语言分拣、按字符集过滤、按长度约束,最后输出带统计信息的干净词表。这一节我给出一套可直接跑通的 Python 流程。
3.1 收集与合并:先让同一份词表覆盖多类源头
假设你手头有四个文本文件:两份中文词表、两份英文词表。第一步是把它们按语言归拢,去重,然后输出两份合并后的原始词表。
# merge_stopwords.py from pathlib import Path sources = [ Path("zh_a.txt"), Path("zh_b.txt"), Path("en_a.txt"), Path("en_b.txt"), ] zh_words = set() en_words = set() for src in sources: is_zh = src.name.startswith("zh") with open(src, encoding="utf-8", errors="ignore") as f: # 跳过空行和 # 注释行 lines = [line.strip() for line in f if line.strip() and not line.startswith("#")] if is_zh: zh_words.update(lines) else: en_words.update(lines) # 排序后写出,保证每次生成的文件内容一致 with open("merged_zh.txt", "w", encoding="utf-8") as f: f.write("\n".join(sorted(zh_words))) with open("merged_en.txt", "w", encoding="utf-8") as f: f.write("\n".join(sorted(en_words)))这段代码的逻辑是:用文件名前缀判断语言,用set天然去重,用errors="ignore"跳过无法解码的坏字节。输出前做了排序,这样后续 diff、版本管理都方便。要注意errors="ignore"是双刃剑:它能避免脚本崩溃,但也会悄悄丢弃乱码字符,所以合并完一定要再看一眼输出文件里有没有可疑的行。
3.2 清洗脚本:剔除标点、控制长度、限制字符集
合并完只是第一步,清洗才是让词表可用的关键。中文停用词有一个特点:绝大多数是单字或双字,三字以上的虚词很少。英文停用词绝大多数是两到五个字母的短词,偶尔有带撇号的变体,比如 don't、can't。清洗脚本要基于这些规律做约束。
# clean_stopwords.py import re # 中文只保留 1~4 个汉字 ZH_RE = re.compile(r"^[\u4e00-\u9fa5]{1,4}$") # 英文只保留 2~10 个小写字母,撇号单独处理 EN_RE = re.compile(r"^[a-z]{2,10}$") def clean_zh(raw_path: str, out_path: str): kept = set() with open(raw_path, encoding="utf-8") as f: for line in f: w = line.strip() w = w.replace("\ufeff", "").replace("\u3000", "") if ZH_RE.match(w) and w not in kept: kept.add(w) with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(sorted(kept))) def clean_en(raw_path: str, out_path: str): kept = set() with open(raw_path, encoding="utf-8") as f: for line in f: w = line.strip().lower() w = w.replace("\ufeff", "").replace("\u3000", "") if EN_RE.match(w) and w not in kept: # 单字母在英文停用词里只保留 a, i,其他都丢掉 if len(w) == 1 and w not in ("a", "i"): continue kept.add(w) with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(sorted(kept))) clean_zh("merged_zh.txt", "stopwords_zh.txt") clean_en("merged_en.txt", "stopwords_en.txt")参数怎么调?ZH_RE里的{1,4}表示最多四个汉字。如果你发现语料里偶尔出现五字虚词,比如“在一定程度上”,可以放宽到{1,5},但代价是可能混入五字短语。EN_RE里的[a-z]{2,10}把大写词全部排除,这会让词表更干净,但代价是文本里首字母大写的 The 会被漏掉。常见的做法是在过滤之前先把语料整体转成小写,而不是在词表里去兼容大小写。
单字母的a和i是英文里唯一值得保留的单字母停用词,其他单字母如u、r通常是网络缩写,直接在清洗阶段丢掉更安全。中文侧不需要做这个特殊处理,因为中文单字虚词本身就是主体。
3.3 统计输出:让“最全”变得可度量
清洗完成后,做一次统计,把两份词表的规模、样例输出到一个报告文件里。这不是仪式感,而是后续每次增删词时的基准线。
# stats_stopwords.py from pathlib import Path for lang in ("zh", "en"): path = Path(f"stopwords_{lang}.txt") words = path.read_text(encoding="utf-8").splitlines() sample = words[:20] print(f"{lang}: total {len(words)}, sample {sample}")看输出时重点确认三件事:总数是否符合预期、样例里有没有混入非目标语言字符、有没有奇怪的符号残留。如果中文词表里出现“the”或英文词表里出现“的”,说明源头文件的标注或分拣出了问题,要回头查原始文件而不是在这里硬改。
4. 别急着套用:三种场景的停用词定制造型
词表清洗到能用的程度,只是完成了一半。接下来要解决的问题是:同一份“最全”基础词表,怎么派生出检索、分类、情感分析三个不同任务各自需要的版本。这个环节最考验工程判断力。
4.1 检索场景:保住实体词和单字查询
搜索召回里,停用词的作用是缩小索引、提升查询效率。常见的做法是把“的、了、在、是、和、与”这类高频虚词以及标点、数字全部过滤掉。但有个坑:单字查询不能被停用词表吞掉。
比如用户搜“冰箱”和搜“保鲜冰箱”,后者在分词后可能得到“保鲜”“冰箱”两个词。如果“保”或“鲜”在停用词表里,查询语义就残缺了。所以检索场景的停用词表必须配一份“强制保留词”白名单,把品牌词、产品词、地区词、数字型号放进去。很多搜索系统不是不做这个,而是没意识到停用词表的“过滤”优先级高于查询词的手动保留逻辑。
我一般会在检索配置里加一个 rule:当查询词命中白名单时,跳过停用词过滤;只有未命中白名单的词才走停用词表。这个顺序一旦反了,白名单就形同虚设。
4.2 分类场景:拉大类别区分度的词要进白名单
文本分类任务里,停用词的作用是去掉那些所有类别里都高频出现的词。但问题在于:某些词汇虽然整体频率高,却在某个类别里显著更高,这类词的区分度不能被停用词表抹掉。
举例来说,在做汽车领域评论分类时,“汽车”这个词在很多文档里都会出现,但它恰恰是识别“汽车类”文档的重要特征。把它加进停用词表,等于主动丢弃了判别信息。处理方式是用卡方检验或信息增益算一遍每个词的类别区分度,把区分度排名前 5% 的词从停用词表里摘除。即使这些词是“的、是、有”,只要在某个类别里分布异常,都应该保留。
实践中我也见过反过来的做法:有人为了提升分类模型的泛化能力,故意把类别专属词也过滤掉,让模型只看结构特征。这种思路在短文本上偶尔有效,但会牺牲长文本的准确率。文本分类的停用词表没有标准答案,只有按测试集指标调出来的版本。
4.3 情感分析场景:否定词与程度副词必须保留
情感分析是对停用词最敏感的任务。很多人直接套用通用停用词表,结果“不怎么样”“太差了”“没意思”这类句子里的“不”“太”“没”全部被过滤,剩下的“怎么样”“差”“意思”根本表达不了真实倾向。
这个坑的解法是把否定词、程度副词、转折词单独拎出来,形成一份“情感保留词”,在过滤阶段优先级高于停用词表。我常用的集合包括:不、没、无、未、别、莫、勿、很、太、最、极、更、都、也、还、但、却、虽、仍、依、然。这些词一旦被通用停用词表吞掉,模型的输入就失真了。
还有一个容易被忽略的细节:英文情感分析里的 not、no、never、but、however、too、very 同样要保留。如果你的英文停用词清单一律把 not 当成噪声,那“not good”会被处理成“good”,方向直接反了。
情感分析场景中,停用词表的核心目的不是“删”,而是“选择性删”。那些对情感极性有贡献的词,应该比停用词表拥有更高的优先级。
5. 避坑:停用词表使用中的五个经典翻车现场
这一章把我见过的、自己和同事踩过的坑集中列一遍。每一类都是“现象—原因—解决”的结构,建议你把这些条目当成上线前 checklist 来用。
5.1 误删否定词,情感分析结果直接反转
现象:某次舆情系统里,一条“这家餐厅的服务一点都不好”被识别为正面评价。排查后发现问题出在停用词表里包含了“一点”“都”“不”。
原因:通用停用词表为了检索效率会把“都”“不”这类高频词列为噪声,但情感分析需要它们承载否定语义。
解决:把否定词和程度副词全部移出停用词表,改成白名单机制,并写一条单元测试:输入包含“不”的典型句子,断言情感极性方向正确。每次更新停用词表时,自动跑这批回归用例。
5.2 领域术语被过滤,语义检索召回塌方
现象:某个商品搜索系统上线新版词表后,“全脂牛奶”的召回量掉了一半。查日志发现“全”“脂”“奶”三个词都被停用词表过滤了,只剩一个“牛”。
原因:那份“最全”词表里把大量单字高频词一网打尽,而商品名称里恰恰有大量单字词。
解决:给自己的业务词库建一张白名单,把品牌、品类、属性词都放进去。过滤顺序必须是“白名单优先、停用词表次之”,白名单里的词一律不过滤。
5.3 全角空格和零宽字符导致词表能看不能用
现象:词表文件里明明写着“的”,但程序里token in stopwords永远返回 False。
原因:文件里混入了零宽空格或者从富文本编辑器复制来的不可见字符,肉眼看不出来,但in判断是逐字符精确匹配的。
解决:写清洗脚本时统一处理\ufeff、\u3000以及所有\u200b-\u200d区间的字符;另外用十六进制查看器抽查词表的末尾行和中间行,确认没有残留不可见字符。
5.4 词表越攒越“全”,效果却一路下滑
现象:有人把网上能找到的五六份中英文停用词表全部合并去重,得到将近两万词,灌进系统后各项指标全面下滑。
原因:多份词表存在互相冲突的过滤偏好。一份词表把“信息”过滤掉,另一份把它保留,合并之后“信息”被过滤,文档的关键信息被误伤。
解决:不要追求一次合并所有来源。以一份经过验证的核心词表为基座,只从其他来源逐条挑选确有必要的词,并记录每个词的来源和添加理由。这样词表的每次增删都可追溯。
5.5 没有版本化:词表改动无法回滚
现象:某次测试临时给停用词表加了几十个词,跑完忘了撤销,直接带上线,三天后线上指标开始恶化,但没人记得改过什么。
原因:词表文件没有被纳入版本管理,也没有通过配置中心下发,导致改动不可追溯。
解决:把停用词表当作代码一样维护,每个版本打 tag,发布记录写清改动原因。我在工程里习惯把词表放在独立目录,包含stopwords_zh.txt、stopwords_en.txt、whitelist.txt、changelog.md,每次改动必须同步更新 changelog。
6. 验证你的“最全”停用词表:覆盖率、误杀率、业务指标三件套
一份停用词表到底值不值得用,最终要看三个数字:覆盖率、误杀率、业务指标变化。这里给出最轻量的验证方案。
覆盖率统计的是停用词在语料中的命中比例。抽样一万条文档,分词后统计有多少 token 被停用词过滤,覆盖率不是越高越好,而是要落在合理区间。中英文混合的通用语料里,中文覆盖率通常在 20% 到 35%,英文覆盖率在 15% 到 25%。远超这个区间,优先怀疑词表混入了过多种类词。
误杀率需要人工抽样。随机取 200 个被过滤的 token,逐一判断里面有多少是实体词、业务关键词、情感词。误杀率超过 5%,就该考虑收窄词表范围或加强白名单。
业务指标变化最直接:用同一批训练集和模型,分别跑“有停用词”“无停用词”“有定制停用词”三个版本的实验,对比准确率、召回率或搜索的点击率。这一步能彻底终结“要不要用停用词”的争论。
我现在的习惯是:每份词表上线前,先跑一遍覆盖率脚本做快速筛查,再抽查误杀率,最后用最小业务实验验证收益。这中间,白名单的维护优先级高于停用词表本身的增删。把所有词表改动都落到版本管理里,给自己留好后悔药。我见过太多因为一份“最全”词表而翻车的案例,希望这套验证流程能帮你少踩几个坑。
本文还有配套的精品资源,点击获取