
简介一套基于BosonNLP情感词典的Python情感分析示例代码面向自然语言处理初学者和需要快速实现文本情感判别的开发者。资源围绕读入情感词典与停用词表、使用pandas读取xlsx格式待分析文本、借助jieba分词、去除停用词并计算情感得分、依据正负得分标记积极与消极、最终输出xlsx结果文件这一完整流程展开可直接参考改造用于舆情评论、商品评价等场景该流程覆盖词典法情感分析的主要环节便于调试和二次开发。压缩包以zip格式封装整体大小约1.08MB平台暂未提供文件数量及类型明细从实现逻辑看应包含词典文件、停用词表、Python脚本与示例xlsx数据。目前已有5943人学习下载资源体量轻、步骤清晰适合初学者对照代码理解词典法情感分析的基本套路并迁移到自己的数据集上验证。1. 这不是又一套调包调参的玩具词典法才是小数据情感分析的最优解做舆情监控或者电商评论分析的朋友可能都有过这种体验手里就一两千条没标注的文本张嘴就要上深度学习模型结果训练集连验证集都喂不饱。反直觉的结论是基于BosonNLP情感词典的情感分析示例代码在这类场景下往往比模型方案更靠谱——它不依赖标注数据还能逐词解释打分依据改一个词的情感权重就能立刻影响结果。这份资源解决的是从Excel读入文本、jieba分词、停用词过滤、词典加权打分到结果写回xlsx的整条链路适合刚接触情感分析的初学者也适合需要快速批量处理短文本评论的一线业务分析人员。整份代码量不大但工程闭环完整拿到手改改路径就能跑。2. 读懂BosonNLP情感词典格式、词性与加权逻辑2.1 词典长什么样一行一个词和一个分数BosonNLP情感词典的原始文件是一个纯文本格式每一行包含一个情感词或短语后面跟着一个浮点数情感分值。正分表示积极倾向负分表示消极倾向数值的绝对值大小代表情感强度。比如“喜欢 8.20”和“讨厌 -6.50”这类结构。分词后拿词去词典里查表命中就把分数累加这就是整条情感分析链路最核心的机制。和通用情感词典相比BosonNLP的覆盖面偏向口语化和网络化表达像“给力”“坑爹”“666”这类词都有收录。这对电商评论、外卖评价、短视频文案分析这类场景特别有用。通用词典比如知网HowNet、大连理工情感词汇本体在正式书面语上表现好但遇到“这家店yyds”这种表达就完全失效。选择BosonNLP做基座本质上是把分析场景锚定在短文本、强口语、网络新词频出的真实业务数据上。读取词典的时候有一个细节容易翻车词和分数之间是用空格分隔的但行内可能存在多个连续空格。如果用split( )去切会把空字符串也切出来导致解析出错。我一般直接用split()它会自动把连续空白符当成分隔符处理更稳妥。另外词典里有少量短语带空格这种条目在split时就会被拆成多个字段解析逻辑需要做长度判断只处理恰好两个字段的行其余直接跳过。2.2 停用词表为什么必须配停用词表解决两个问题。第一个是无意义词稀释分数像“的、了、是、在”这类高频率虚词它们本身没有情感倾向但会出现在几乎每一条文本里如果不删掉会在句子长度上干扰情感密度的判断。第二个问题是标点符号jieba分词会把逗号、句号、感叹号当成独立词切出来这些符号在词典里当然匹配不到但会让后续的评分函数在无效词上浪费时间。一个实用的停用词表构建方法是在jieba自带的停用词基础上把中英文标点、全角半角空格、换行符全部加进去。常见做法是手工维护一个stopwords.txt每行一个词读取时转成set集合。集合的查找复杂度是O(1)在批量文本执行时能省下不少时间。注意停用词表要用UTF-8编码保存否则在Windows环境下读进来就是乱码后面过滤全部失效。停用词过滤不只是简单地从分词结果里删词还要注意空字符串的清理。分词结果里有时会出现空串特别是在文本清洗不彻底的情况下。如果不做w.strip()判断空串也会进入后续打分逻辑虽然不影响最终分数但会在命中词列表里留下垃圾数据给人工核查添麻烦。2.3 动手前先做数据自检词典到底加载对没有很多人拿到代码上来就跑结果输出全是0分回头看才发现词典路径没对上或者编码不对导致整个词典没读进来。我习惯在加载完词典后先做一次数据自检打印词典规模、Top积极词和Top消极词各几条确认词典内容解析正确再往下走。import pandas as pd with open(BosonNLP_sentiment_score.txt, encodingutf-8) as f: boson_lines f.readlines() sentiment_dict {} for line in boson_lines: parts line.strip().split() if len(parts) 2: word, score parts sentiment_dict[word] float(score) print(f词典词条数: {len(sentiment_dict)}) print(积极样本:, sorted(sentiment_dict.items(), keylambda x: x[1], reverseTrue)[:5]) print(消极样本:, sorted(sentiment_dict.items(), keylambda x: x[1])[:5])这里的关键是len(parts) 2这个判断它对词典里带空格的短语做了容错避免解析越界。打印排行榜的意义在于快速验证词和分数的映射方向——如果积极榜里出现负分词说明解析顺序搞反了。词条数也能侧面验证是否读到了完整的词典文件如果只有几十条大概率是文件路径或编码有问题需要回头查。这一步虽然简单但能省掉后面排查打分异常的一大半时间。3. 从Excel到情感得分核心流程代码与参数说明3.1 分词前先做文本清洗用pandas读入xlsx之后最常见的坑是文本列里混着空单元格和非字符串类型。Excel里的空单元格在pandas里会成为NaN而NaN是float类型直接传给jieba分词会报AttributeError。数字类型的单元格也要小心比如评论内容是“5分好评”读进来可能已经变成整数或浮点不复原文。我一般会先对目标文本列做一次强制清洗把空值统一填充为空字符串再整体转成str类型。这一步做完后面的分词环节就不会因为类型问题中断。import pandas as pd import jieba import re df pd.read_excel(comments.xlsx, sheet_nameSheet1) df[clean_text] df[评论内容].fillna().astype(str)fillna()把NaN替换成空字符串astype(str)保证所有值都是字符串类型。这里有个细节要注意fillna不影响非NaN值的类型转换如果原来某格是数字8转换成字符串后是“8”跟原文中的“8分好评”语义不同后续分词和打分都不会有问题因为情感词典里本来也不会收录单独的“8”。3.2 逐条打分词典做索引停用词做过滤打分函数是整个脚本的核心。逻辑不复杂先清洗掉标点符号用jieba切词过滤停用词然后遍历每个词去情感词典里查表命中就累加情感分数同时记录命中的词和对应权重。def score_sentence(text, sentiment_dict, stopwords): # 去掉标点符号只保留中英文、数字和汉字 text re.sub(r[^\w\u4e00-\u9fa5], , text) # 精确模式分词 words jieba.lcut(text) # 过滤停用词和空字符串 words [w for w in words if w not in stopwords and w.strip()] score 0.0 hit_words [] for w in words: if w in sentiment_dict: score sentiment_dict[w] hit_words.append((w, sentiment_dict[w])) return score, hit_wordsre.sub这一步把逗号、句号、感叹号等标点全部删除避免它们被jieba切成独立词。正则[^\w\u4e00-\u9fa5]表示匹配所有不是字母、数字、下划线和汉字的字符替换为空串。sentiment_dict[w]直接取词典里的情感分数累加到score上。hit_words列表记录命中的词和权重这一步非常有用——后面排查“为什么这条文本被判成消极”的时候直接看这个列表就能定位是哪些词贡献了负分。3.3 主流程把打分函数应用到整张表有了逐条打分函数主流程就简单了。用pandas的apply方法把函数批量应用到每一行然后把返回的score和hit_words拆成两列最后根据分数正负标上积极/消极标签。results df[clean_text].apply(lambda x: score_sentence(x, sentiment_dict, stopwords)) df[sentiment_score] [r[0] for r in results] df[hit_words] [r[1] for r in results] df[label] df[sentiment_score].apply(lambda s: 积极 if s 0 else 消极) print(df[[评论内容, sentiment_score, hit_words, label]].head())apply返回的results是一个包含元组的Series每个元组第一个元素是情感总分第二个是命中词列表。用列表推导式拆出两列。label列的分类规则是0为积极、0为消极。等于0的情况被分到了消极这在业务上其实有争议——一条文本没有命中任何情感词到底算中立还是消极要看具体场景。如果你想区分出“中性”可以改成三分类大于0是积极小于0是消极等于0是中性。这个参数位留给使用者自己定。4. 把结果写回 Excelpandas 读写细节与批量改造4.1 to_excel 的正确姿势engine 与 float_format打分结果写回Excel看起来是一行to_excel的事但实际跑起来会遇到两个烦人的细节。第一个是engine问题常见的xlwt不支持xlsx格式只支持老版xls如果机器上同时装了多套Excel读写库不指定engine可能会报错或者写出错误格式。第二个是浮点数精度问题情感分数累加之后经常出现0.38000000000000004这种尾巴写进Excel里既难看又显得不专业。output_path comments_result.xlsx df.to_excel(output_path, indexFalse, engineopenpyxl, float_format%.2f)engineopenpyxl明确指定用openpyxl写xlsx文件绕开多库并存时的选择混乱。indexFalse防止pandas把行索引写成一列多余的“Unnamed: 0”。float_format%.2f让情感得分统一保留两位小数输出。注意float_format只影响写Excel时的显示精度不影响内存中的实际值后续如果还要基于得分做统计数据精度不受影响。4.2 多文件批量处理循环与路径拼接单文件跑通之后业务上很快会面临多文件批处理的需求——舆情分析经常是按周、按月分Sheet或者分文件存放数据。批量处理的核心是用glob匹配目录下所有xlsx逐个读入、打分、写结果文件名加上_result后缀避免覆盖原始数据。import glob import os input_dir 待分析/ output_dir 分析结果/ os.makedirs(output_dir, exist_okTrue) for file_path in glob.glob(os.path.join(input_dir, *.xlsx)): df pd.read_excel(file_path) df[clean_text] df[评论内容].fillna().astype(str) results df[clean_text].apply(lambda x: score_sentence(x, sentiment_dict, stopwords)) df[sentiment_score] [r[0] for r in results] df[hit_words] [r[1] for r in results] df[label] df[sentiment_score].apply(lambda s: 积极 if s 0 else 消极) base_name os.path.splitext(os.path.basename(file_path))[0] out_path os.path.join(output_dir, base_name _result.xlsx) df.to_excel(out_path, indexFalse, engineopenpyxl, float_format%.2f) print(f已处理: {base_name})glob.glob按通配符收集所有xlsx文件路径os.path.basename提取文件名os.path.splitext去掉扩展名最后拼出原名_result.xlsx。输出目录用makedirs创建避免目录不存在时报错。这里特别把输出目录和输入目录分开就是为了防止覆盖原始数据。批量跑的时候建议在循环体末尾打印一行进度分析几百个文件时能随时确认走到哪了不至于跑一半不知所措。5. 避坑词典法最容易踩的五个坑5.1 空值单元格让 pandas 直接罢工现象代码跑到apply那一步突然报AttributeError: float object has no attribute decode或者报TypeError: expected string or bytes-like object查看堆栈发现是分词函数接收了NaN。原因Excel原始数据里存在空单元格pandas读进来自动填成NaN。NaN的类型是floatjieba分词函数不接受float输入直接抛异常。解决在调用打分函数之前强制对文本列做清洗df[clean_text] df[评论内容].fillna().astype(str)。这一步把空值变成空字符串数字变成字符串从根上消灭类型问题。5.2 Windows 下读词典报编码错误现象打开词典文件时报UnicodeDecodeError: gbk codec cant decode byte或者读进来之后打印词典词条全是乱码。原因BosonNLP词典文件是UTF-8或UTF-8-sig编码但Windows的open函数默认使用系统本地编码(GBK)读取字节流解析失败或者解析错误。解决所有读取文本文件的地方显式指定编码with open(BosonNLP_sentiment_score.txt, encodingutf-8-sig) as f:用utf-8-sig而不是utf-8的好处是它会自动处理开头的BOM头——有些词典文件带BOM标记用纯utf-8读会把\ufeff当成第一个词的开头导致第一个词条解析异常。5.3 标点符号混入分词结果把分数拉平现象某条文本明明有很强烈的情感词但最终得分接近0命中词列表里空荡荡只看到一些逗号句号。原因jieba分词把标点符号切成了独立词这些符号在情感词典里匹配不到不产生分数。但由于它们占据了分词结果的大头导致真正的情感词占比被稀释。更麻烦的是如果标点前后拼接出意料外的词组合还可能干扰词典匹配。解决在分词前用re.sub(r[^\w\u4e00-\u9fa5], , text)清洗文本把所有非字母、非数字、非汉字字符全删掉。这一步能让分词结果里只留下有意义的词打分效率和质量都会提升。5.4 否定句被整反“不好吃”被判成积极现象输入“这家店不好吃”输出得分竟然是正数label标记为“积极”。原因jieba把“不好吃”切分成“不”和“好吃”“好吃”在BosonNLP词典里的情感分数是正的累加后整体得分变成正数。词典法最经典的缺陷就是没有上下文语义建模否定词被当成无意义词过滤掉或者忽略掉。解决方案在下一章展开核心思路是维护否定词表检测到“不、没、无、未”等词出现在情感词前面时把情感得分符号反转。作为应急手段可以先在结果里保留hit_words列人工复核时优先检查包含否定词的文本。5.5 to_excel 覆盖原始问卷数据没了现象脚本正常运行结束打开文件一看原始数据文件变成了处理后的结果最原始的评论文本和其他字段全部丢失。原因读取和写出用到了同一个文件路径。pd.read_excel(调研数据.xlsx)读取完之后接着df.to_excel(调研数据.xlsx)写回原始文件被彻底覆盖而且是不可恢复的——Excel不是git没有版本回退。解决输出文件一律拼后缀或加时间戳。我习惯写成base_name _result.xlsx并且在文件开头把输入输出目录分开。如果确实需要覆盖写至少先复制一份原始文件到备份目录做保险。这个坑看起来低级但赶工时最容易犯一旦发生了只能自认血泪教训。6. 进阶给词典法加上否定词与程度副词两句改动提升准确率6.1 否定词反转符号针对“不好吃”被判成积极的经典翻车问题最简单有效的修复是维护一个否定词集合遍历分词序列时如果当前词是否定词就把后面N个词的情感分数取负。这里有几个细节值得说明否定词要放在停用词表之外不能提前过滤掉作用范围一般取后2到3个词就够太远会误伤同一个情感词可能同时被“不”和“太”修饰需要先处理程度副词再处理否定词。neg_words {不, 没, 无, 未, 别, 莫, 非, 勿, 毋} def score_sentence_v2(text, sentiment_dict, stopwords, neg_words): text re.sub(r[^\w\u4e00-\u9fa5], , text) words jieba.lcut(text) words [w for w in words if w not in stopwords and w.strip()] score 0.0 hit_words [] for i, w in enumerate(words): if w not in sentiment_dict: continue weight 1.0 # 检查前一个词是否是否定词是则翻转符号 if i 0 and words[i-1] in neg_words: weight -1.0 score sentiment_dict[w] * weight hit_words.append((w, sentiment_dict[w] * weight)) return score, hit_wordsweight默认是1.0检测到前一个词是否定词时变成-1.0。这样“不好吃”中“好吃”的3.6分被翻转成-3.6分整句判为消极符合直觉。这里只检查前一个词因为中文否定结构里否定词紧贴被修饰词是绝大多数情况。美团外卖这类短评论里“配送慢但味道好”反而没有否定词不需要翻转这是另一个问题——整句粒度导致的分数抵消。6.2 程度副词放大缩小权重否定词解决了方向问题但强度问题还没解决。“好吃”和“非常好吃”在原始词典法里得分完全一样这显然不合理。程度副词表的核心价值是让情感强度变得更细腻从而实现情感分析的精准定位。更实用的场景是在同一批文本里排优先级——两句话都是积极但“非常满意”的售后应该优先处理“还行”可以往后放。degree_words {很: 1.5, 非常: 2.0, 极其: 2.0, 太: 1.8, 超级: 2.0, 有点: 0.6, 稍微: 0.6, 略: 0.7, 比较: 0.8} def score_sentence_v3(text, sentiment_dict, stopwords, neg_words, degree_words): text re.sub(r[^\w\u4e00-\u9fa5], , text) words jieba.lcut(text) words [w for w in words if w not in stopwords and w.strip()] score 0.0 hit_words [] for i, w in enumerate(words): if w not in sentiment_dict: continue weight 1.0 if i 0 and words[i-1] in neg_words: weight * -1.0 if i 0 and words[i-1] in degree_words: weight * degree_words[words[i-1]] score sentiment_dict[w] * weight hit_words.append((w, sentiment_dict[w] * weight)) return score, hit_words“太”在degree_words里同时被定义为1.8倍权重而“不太好吃”里“不”把符号翻转让“好吃”变成-3.6紧接着“太”再乘1.8变成-6.48情绪强度被正确放大了。处理顺序是先判断否定词再判断程度副词因为程度副词影响的是情感词的绝对值大小而否定词决定正负方向两者不冲突。6.3 分句粒度短评论整句打分容易互相抵消还有一个隐藏问题如果一条评论是“配送慢但味道好”整句分词打分结果是“慢”的负分和“好”的正分互相抵消最终得分接近0label被判成消极。这在美团情感分析这类场景里非常常见用户对单一维度的不满和对另一维度的认可同时出现整句打分丢失了细节。常见做法是按标点符号把文本切分成短句逐句计算情感得分再做汇总。汇总规则可以根据业务定所有分句分数相加或者只取分句中的最高分和最低分做综合判断。分句粒度能保住“味道好”这个正面信号同时不丢掉“配送慢”的负面信息在后端做维度拆解时也更有价值。def score_clause(text, sentiment_dict, stopwords, neg_words, degree_words): clauses re.split([。;!?], text) total_score 0.0 clause_scores [] for clause in clauses: if not clause.strip(): continue c_score, _ score_sentence_v3(clause, sentiment_dict, stopwords, neg_words, degree_words) clause_scores.append(c_score) total_score c_score return total_score, clause_scores从那以后我每次跑情感分析都会先打印两行自查记录一行是分词结果一行是命中的词典词和权重。看到“不好”被拆成两个词、权重还都是正的就知道该上否定词逻辑了。这套从BosonNLP情感词典出发的示例代码我把否定词表、程度副词表、分句逻辑的参数位都留在了脚本里拿到手改一改阈值就能适配自己的业务数据希望帮到你。本文还有配套的精品资源点击获取