资讯动态

停词表与新闻库的文本预处理实战:从词频统计到TF-IDF

发布时间:2026/10/14 12:26:07 来源:尧图企业网站定制
简介一份面向新闻文本分类与预处理的实用资源包内含多个常见中文停用词表与基于朴素贝叶斯算法的新闻分类代码适合自然语言处理初学者、文本挖掘课程实践及需要快速搭建分类流程的开发者。压缩包共8个文件其中5个txt停用词表覆盖哈工大、四川大学机器智能实验室及通用中文停用词库等来源1个py文件为新闻分类任务脚本另有simhei.ttf中文字体与show_Chinese.png结果图可保证中文可视化展示并查看分类效果。资源整体仅9.16MB已有358人学习下载。内容完整贯穿文本清洗、停用词过滤、特征提取如TF-IDF到朴素贝叶斯模型训练与评估的环节直观呈现中文新闻分类的完整链路既适合新手入门也能作为基础代码替换数据集或调整参数进一步探索分类优化。1. 停词表新闻库.zip一个看似普通的数据包为什么值得拆开看做文本挖掘最耗时间的往往不是模型而是数据预处理。你从某个渠道拿到一个压缩包名叫“停词表新闻库.zip”里面大概率是一份新闻语料加一份或多份停词表。这个组合解决的是词频统计、TF-IDF 计算和主题分析里最脏的两个问题语料从哪来、噪声词怎么去。如果你正在做新闻舆情分析、关键词提取、词向量预训练或者只是想跑通一个中文文本分类的基线特征这个包能帮你省掉从爬虫到清洗的整整两步。但要提醒一句停词表从来不是“解压即用”新闻库也不是“打开就能训”。我后面会讲清楚怎么拆、怎么用、以及五个会让你翻车的细节。2. 停词表与新闻库的常见形态先搞清楚包里是什么再动手2.1 停词表的格式与语言范围中文、英文还是混合停词表最常见的形态是一个纯文本文件每行一个词。中文表里通常包含“的、了、是、在、和、就、都、而、及、与、着、或”这类高频虚词英文表里则是“the、a、an、of、to、in、for、on、with、by”这类功能词。如果这个压缩包同时覆盖中英文语料它里面可能会有一个stopwords_zh.txt和一个stopwords_en.txt也可能把两种语言混在同一个文件里用空格或者换行分隔。我一般会先看文件的前 20 行确认它的编码和分隔规则而不是直接写代码读取——因为这一步错了后面整个 Pipeline 都会错。需要注意另一种“伪停词表”有些表除了词本身还带词性标注比如“的话/uj”“等等/ud”。这种表在传统词法分析场景里有价值但如果你只是做词频统计直接把它当纯文本切分会把“/uj”也留着。还有一部分表会把标点符号和数字也放进去比如空格、“。”、“1”这本身不算错反而能省掉你单独过滤标点的步骤。但前提是你要清楚停词表里如果包含空格用splitlines()读取后不小心把一行空串加入集合反而会干扰判断。下面是一段通用的停词表读取代码能应对 BOM、空行和行内空格三种情况import zipfile stop_words set() with zipfile.ZipFile(停词表新闻库.zip, r) as zf: raw zf.read(stopwords.txt).decode(utf-8-sig, errorsignore) for line in raw.splitlines(): line line.strip() if not line: continue # 如果一行里有空格分隔的多个词拆开加入 for word in line.split(): word word.strip() if word: stop_words.add(word)这里utf-8-sig用来吃掉文本开头的\ufeffBOM 字符errorsignore防止个别坏字节导致整个读取中断。line.split()则能同时兼容“每行一个词”和“空格分隔多个词”两种写法。参数上的建议是停词表最后要转成set因为后续在in判断时的时间复杂度是 O(1)而且天然去重。2.2 新闻语料库的三种组织方式按行、按文件、按JSON新闻库的文件结构直接决定你应该怎么遍历它最常见的三种如下组织方式典型形态优点缺点按行存储一个大的.txt每行一条新闻全文读取简单内存可控新闻正文里的换行会破坏结构按文件存储一个目录下多个.txt每个文件一篇新闻并行处理方便文件数多时打开开销大按 JSON/JSONL 存储每行一个 JSON 对象含 title、content、time字段丰富便于过滤需要额外处理结构化字段如果压缩包里的新闻库是按行存储的那么zipfile读出来后可以直接splitlines()切分。但要注意新闻正文中如果包含空行splitlines()会返回若干个空字符串循环里要跳过。如果是按目录里多个文件打包的读取时就不能一次read需要遍历namelist用文件名后缀过滤出文本文件。如果是 JSONL还要用json.loads解析每一行再从content字段里取正文。我遇到过一个坑某份新闻库的 JSONL 里content字段里混入了 HTML 标签splitlines()完全管不了需要先用正则把nbsp;、p这类去掉。所以拿到包后先抽一条样本打印到屏幕上比盲目写代码有用得多。2.3 用Python直接读zip省去解压落盘的麻烦很多人习惯先解压到本地再处理但这会引入两个问题一是解压出来的文件乱码文件名二是磁盘空间被临时文件占满。用zipfile可以直接在内存里读取尤其是只要跑一次词频统计时根本不需要落盘。import zipfile with zipfile.ZipFile(停词表新闻库.zip, r) as zf: # 先打印包内文件列表确认路径 for name in zf.namelist(): print(name)这段代码是拿到包之后第一件该做的事。namelist()返回的是包内所有文件的完整路径你可以在里面看到stopwords.txt、news/2024/01/xxx.txt这样的结构。确认路径后再用zf.read(name)读取具体文件。需要注意如果压缩包创建时的操作系统编码和当前环境不一致某些中文文件名会在namelist()里显示乱码但读取内容仍能正常进行。解决办法是不要用文件名乱码去猜内容而是用文件后缀和路径深度来判断。比如说*.txt一律读取*stopword*当作停词表。这样即使文件名乱码逻辑也不会偏。3. 用停词表新闻库跑通一次词频统计从解压到Top N的完整Pipeline3.1 第一步安全解压先检查路径穿越很多人一拿到 zip 就双击解压我是建议先用代码检查一遍包内路径是否安全。路径穿越是一种常见攻击压缩包里可能包含../../这样的条目解压时会把文件写到目标目录之外。import os import zipfile target_dir ./news_lib with zipfile.ZipFile(停词表新闻库.zip, r) as zf: for name in zf.namelist(): clean_path os.path.normpath(name) if clean_path.startswith(..) or os.path.isabs(clean_path): print(f危险路径已跳过: {name}) continue zf.extract(name, target_dir)这里用os.path.normpath把../解析成真实的相对路径再判断是否越界。zf.extract本身也有路径清理功能但面对构造复杂的 zip 还是自己检查一遍更踏实。如果你只是做词频统计完全不需要解压直接内存读就行这个步骤主要是防止“某天需要把新闻库落盘传给下游同事”时踩坑。3.2 第二步加载停词表统一编码与去空行停词表加载的常见错误是直接read().decode(utf-8)然后在 GBK 编码的文件上爆炸。我建议统一走utf-8-sig并过滤掉空行和空白字符。def load_stopwords(zip_path, member_pathstopwords.txt): with zipfile.ZipFile(zip_path, r) as zf: raw zf.read(member_path).decode(utf-8-sig, errorsignore) stop set() for line in raw.splitlines(): line line.strip() if not line: continue stop.update(line.split()) return stop这个函数返回一个set可以直接用于后续的word in stop判断。参数说明zip_path是压缩包路径member_path是包内停词表路径如果你的包里停词表不叫stopwords.txt把namelist()里打印出的那个真实文件名传进来。errorsignore会把无法解码的字节丢弃对绝大多数文本来说可以接受但如果你发现某条新闻里出现“”说明这个文件本身有坏块建议换用chardet做编码探测。3.3 第三步分词、去停用词、统计词频的完整代码中文分词是词频统计的核心。我不绑定某个具体分词库你可以用自己环境里装好的那个下面代码里from your_segmenter import cut只是占位实际项目中换成你的分词函数即可。import zipfile import re from collections import Counter from your_segmenter import cut # 替换为你本地安装的中文分词库 STOPWORDS load_stopwords(停词表新闻库.zip, stopwords.txt) cn_num_pat re.compile(r[0-9]) cn_punc_pat re.compile(r[。、“”‘’【】《》\s\.\!\?…\-]) def filter_word(token): if not token or len(token.strip()) 1: return None token token.strip() if cn_num_pat.fullmatch(token): return None if token in STOPWORDS: return None return token counter Counter() with zipfile.ZipFile(停词表新闻库.zip, r) as zf: news_text zf.read(news.txt).decode(utf-8-sig, errorsignore) for line in news_text.splitlines(): line re.sub(r[^], , line) # 去掉简单HTML标签 for token in cut(line): clean filter_word(token) if clean: counter[clean] 1 # 输出词频最高的前50个词 for word, freq in counter.most_common(50): print(f{word}\t{freq})逻辑说明filter_word先把长度小于等于 1 的词丢掉因为中文单字大多是没有实义的虚词再用正则把纯数字过滤掉最后才判断是否在停词表里。顺序很重要——先做长度和数字过滤再做停词表匹配能减少集合查询的次数对几十万行语料来说能省出几秒甚至十几秒。cn_punc_pat虽然定义了但在示例里没用到实际项目中你可以在分词前把整行文本里的标点先替换成空格以免分词库把标点保留成独立 token。参数说明cut的分词粒度如果太细会产生大量两字词建议在分词函数里开启“搜索引擎模式”之外的普通模式如果你的分词库支持词典把新闻领域的专有名词加进去能明显减少“特朗普”被切成“特朗”“普”的情况。3.4 第四步调参技巧——Top N怎么选词频下限设多少Top N 不是越大越好。词频统计的输出长度取决于你的下游任务做词云Top 50 到 Top 200 足够超过 200 后词云会变得拥挤。做 TF-IDF 特征不需要手动截断但会把词频小于min_df的词在向量化时过滤掉。做主题模型保留词的词频下限建议设为 5低于 5 的词往往是拼写错误或专有名词碎片。我在处理新闻库时会把词频下限暂时设为 1先跑一遍看看有没有明显无意义的词。比如“据悉”“日前”“记者”这类新闻套话如果频繁出现但不在停词表里我会手动把它们加进停词表再重跑。这个过程不是一次性的通常要迭代两到三轮。4. 新闻库清洗的五个常见坑编码、重复、标点与停词表自身的坑4.1 文本乱码UTF-8与GBK的相爱相杀现象打开新闻文本满眼都是“锟斤拷”“烫烫烫”或者“”。原因文件实际是 GBK 编码但代码用 UTF-8 去解码或者反过来文件是 UTF-8代码用 GBK 解码。中文 Windows 上生成的文本文件尤其容易踩这个坑。解决不要用decode(utf-8)硬写先取一段原始字节做编码探测。import chardet import zipfile with zipfile.ZipFile(停词表新闻库.zip, r) as zf: raw zf.read(news.txt) det chardet.detect(raw[:5000]) # 取前5000字节即可 text raw.decode(det[encoding], errorsreplace) print(det)chardet.detect返回一个字典包含encoding和confidence。如果confidence低于 0.7不要盲信可以再取文件中间一段继续探测。errorsreplace会把无法解码的字节替换成“”比直接中断强但你知道结果里可能有脏字符后续最好用替换规则清理。4.2 词频虚高同一新闻被重复收录现象某个品牌词或人物名词的词频突然高得离谱检查发现是同一篇新闻在语料库里出现了几十遍。原因新闻库在做持久化的时候没有去重爬虫重复抓取或者合并了多个来源但没有做唯一性校验。解决对每条新闻的正文做哈希去重。这里不要用 Python 内置hash()因为每次进程的随机种子不同结果不稳定。用hashlib.md5或sha1。import hashlib seen set() unique_lines [] for line in lines: h hashlib.md5(line.encode(utf-8)).hexdigest() if h in seen: continue seen.add(h) unique_lines.append(line)对按行存储的新闻库直接把每行文本做 MD5。如果新闻库是 JSONL最好对title content的组合做哈希而不是只对content否则不同标题但正文相同的新闻可能被误删。MD5 碰撞概率极低这个场景完全够用。4.3 去词不干净停词表里的空格、空行和隐藏字符现象停词表里有“的”但统计结果里“的”依然出现。原因停词表文件第一行有 BOM 字符读出来变成了“\ufeff的”和聚类出来的“的”字符串不相等。还有一种情况是一行里有两个词比如“的 了”你的代码没有按空格拆开。解决用utf-8-sig读取绕过 BOM并在加载停词表时用split()拆行内多个词。这块代码我在第 2.1 节已经给过实际项目中还要注意一行里既有中文又有英文空格比如“的 the 了”。用split()统一拆开比依赖肉眼判断安全得多。另一个隐藏字符是\r。如果停词表是 Windows 换行符splitlines()能正确处理但如果某一行结尾有空格strip()之前不干净会造成空集里多出一个空串。所以if not line: continue必须在strip()之后判断直接写if not line.strip(): continue更稳。4.4 数字与单字混入标点过滤与长度过滤现象Top 50 里出现“2024”“年”“月”“日”这类词或者“1”“2”“3”这种数字 token。原因分词库默认会把连续数字切成一个词也会把单字保留。在新闻语料里“年”“月”虽然能组成时间短语但单独统计时高频出现却没有太多信息量。解决长度过滤和数字正则过滤。import re num_pat re.compile(r^\d([.,]\d)?$) date_pat re.compile(r^\d{4}年\d{1,2}月\d{1,2}日$) def is_meaningless(token): if not token or len(token) 2: return True if num_pat.match(token): return True if date_pat.match(token): return True return False注意如果你在做时间趋势分析日期本身可能是重要字段那就不要用date_pat把它过滤掉而应该在预处理阶段把“2024年1月1日”整体作为一个特殊标记保留。我这里给出的方案只适用于纯词频统计场景。4.5 分词器版本差异同一句话两次运行结果不同现象昨天统计 Top 10 里有“人工智能”今天重跑变成“人工”“智能”两个词。原因你用的分词库自动更新了词典或者不同环境里装了不同版本。中文分词的词典版本对结果影响非常大尤其是新词、人名、地名。解决固化分词库版本。在requirements.txt里把分词库的版本锁死比如your_segmenter0.10.2具体版本号以你实际安装为准。同时如果你的新闻库里有明显的领域词可以用分词库的自定义词典功能加载这样即使分词库升级也只在新增词典上扩展而不会丢掉已有词。我自己的习惯是每次跑完都把分词结果抽样 100 条存下来下个月重跑时对比一下能快速发现是不是分词逻辑变了。5. 从词频到TF-IDF与词向量停词表在不同任务里的取舍5.1 TF-IDF计算时停用词还要不要继续删在 TF-IDF 里停用词的文档频率极高IDF 值接近 0所以理论上它们对最终特征的影响很小。但实际操作中如果不在分词后去掉停用词CountVectorizer这类工具会把“的”“了”当作合法词进入词表增加内存占用和矩阵维度。所以在构建 TF-IDF 特征之前停词表仍然要删。不过有一个例外如果你的语料里“不”“没有”这类否定词在停词表中删掉它们会丢失语义。比如“我不认为这个方案可行”去掉“不”之后情感分析模型会误判为正向。所以遇到否定词要么不放进停词表要么在特征工程里单独保留一个否定词标志。常见做法是维护一个“弱停词表”只删“的、了、是、在”这类无语义词保留否定词和程度副词。这里给出用CountVectorizer配合停词表的代码它能直接完成分词、去停用词、TF-IDF 计算from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer vectorizer TfidfVectorizer( tokenizercut, # 你的分词函数 stop_wordsSTOPWORDS, # 从zip里加载的停词表 max_features5000, min_df2, max_df0.8 ) tfidf_matrix vectorizer.fit_transform(unique_lines)参数说明max_features5000限制词表大小防止矩阵过宽min_df2表示词至少要出现在 2 个文档里才进入词表能过滤掉只在某一条新闻里出现的专名碎片max_df0.8表示出现在 80% 以上文档中的词会被忽略这等于用另一个视角兜底去掉了高频噪声即使你的停词表漏了一两个词也不会成为主成分。5.2 word2vec训练保留停用词反而能提升词向量质量这里是个反直觉点。训练 word2vec 时我通常不在分词后去掉停用词原因是 word2vec 靠的是“上下文共现”来学习词向量。像“的”“在”这样的词频繁出现在很多词旁边它们起到了桥接不同实词的作用。如果你把“的”去掉原本“北京”“城市”之间因为“的”产生的共现窗口就不存在了词向量质量会下降。正确做法是保留停用词但让子采样机制去处理它们。在 Gensim 的Word2Vec里sample参数表示高频词的下采样阈值默认是1e-3值越小时高频词被主动丢弃的概率越大。我一般设为1e-5这样“的”“了”会以一定概率被跳过但不会完全消失保持了上下文完整性。from gensim.models import Word2Vec sentences [cut(line) for line in unique_lines if line.strip()] model Word2Vec( sentencessentences, vector_size100, window5, min_count2, sample1e-5, sg1, # skip-gram epochs5 )注意sentences不能是一个生成器否则第二次迭代时会空这里直接构造成列表如果语料很大内存吃紧时换用LineSentence从磁盘逐行读取。min_count2依赖语料库本身来过滤低词频词而不是依赖停词表这又是一个和词频统计不同的点。5.3 自定义停词表在新闻库里挖出领域专属噪声词通用停词表只解决“的、了、是”这种普适噪声。新闻库里真正让你头疼的是“据悉”“日前”“记者”“报道”“表示”“认为”这类新闻套话。它们在其他场景里可能是实词但在新闻语料里出现频率极高信息量却很低。这时候就需要从新闻库本身挖掘专属停词。做法很简单先用不带停词表的分词统计看 Top 200 里那些“不像实词但高频”的词把它们加进一份custom_stopwords.txt然后和通用停词表合并使用。我一般会统计这些词的文档频率如果一个词出现在 30% 以上的新闻里大概率是套话或连接词值得进停词表。custom_stopwords {据悉, 日前, 记者, 报道, 表示, 认为, 同时, 以及} STOPWORDS.update(custom_stopwords)更新后重新跑词频统计你会发现剩下的词更聚焦在实际主题上。这一步是纯手工活但效果比换一个更大的停词表都好因为领域专属噪声只有你的语料才能暴露出来。6. 快速验证你的停词表效果一个20行的脚本对比去词前后差异拿到“停词表新闻库.zip”并完成上面所有处理后我建议你做一个 20 行脚本的双跑验证同一份新闻库一次不去停词一次去停词输出两侧的 Top 10肉眼观察差异。这个验证能帮你判断停词表是否匹配当前语料也能暴露漏网之鱼。import re from collections import Counter from your_segmenter import cut def top_words(lines, stopwords, limit10): counter Counter() for line in lines: for token in cut(line): token token.strip() if len(token) 1 or re.match(r^\d$, token): continue if token in stopwords: continue counter[token] 1 return counter.most_common(limit) # 假设 lines 是新闻库按行切分后的列表 raw_top top_words(lines, set(), 10) clean_top top_words(lines, STOPWORDS, 10) print(未去停词 Top10:) for w, c in raw_top: print(w, c) print(\n去停词后 Top10:) for w, c in clean_top: print(w, c)如果结果里第一次的 Top 10 几乎全是“的、了、是、在”第二次变成了“公司、市场、发展、问题”这类实词说明停词表和语料是匹配的。如果第二次的 Top 10 里依然有“表示”“记者”说明领域停词表还没加够如果第二次出现了大量的人名不一定需要过滤除非你做的是主题词提取。这个脚本还可以换一种用法把两次 Top 100 做差集漏掉的词就是停词表需要补的地方。我自己的习惯是每换一批新闻语料就重跑一次因为不同时间段的新闻套话会变。去年“新冠”可能是高频词今年就少了而“高质量发展”这类词在政经新闻里永远高频但不一定需要进停词表。所以验证不是一次性的而是每次数据更新后的固定动作。最后说一个我的教训最开始我直接用包里的停词表跑词频结果“记者”和“报道”占了前十名中的两位我以为是正常现象后来发现主题词全被这些词挤掉了。加了两轮自定义停词表后结果才真正可用。这让我明白停词表是消耗品不是收藏品要随语料变化不断调整。希望这个流程能帮你少走一段弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑