前言

为什么要写这篇文章?
前段时间帮人写了一个这样的小项目,在网上查找资料的过程中,有不少关于该项目的资料,由于各个博主写的代码不尽相同,且没有一个详尽的分析方法,所以我在完成该项目后,想到可以把该项目的分析方法写出来,供大家学习。

> 2021/6/14日更新:最下面有百度网盘的下载链接和提取码。
> 另外我还上传到了gitee,可以直接下载压缩文件在解压,打开项目即可。

准备工作及环境

工具:pycharm、python 3.8.6
其他:大连理工大学情感词汇本体(excel)、程度副词(excel)、否定词列表(txt)
大连理工大学情感词汇下载地址:https://github.com/ZaneMuir/DLUT-Emotionontology
程度副词采用三个等级:1, 1.5, 2
后续会把项目上传到我的资源,如果没有下载积分可以留邮箱,我可以把整个项目发送过去。

项目分析

相信打开这篇文章的同学对情感分析有一定的了解,这里就简单阐述下中文情感分析的大概思路(基个人思路):

jieba库进行分词 – > 分词后,一个词一个词分析是否在情感词表中 --> 如果存在,则在该情感词前寻找是否有程度副词和否定词 --> 如果有,则对该情感词进行处理。

情感倾向的数学表达式:

情感分析自然语言处理 情感分析词表_极值

要点1:一个情感词可能会有不同的情感极性(消极或是积极),例如情感词:八面玲珑。改成语具有褒贬两个不同的情感极性,在有的句子中是褒义,有的却是贬义,那么该情感词如何确定极性呢?这里我引入了K-近邻算法中的思想:

对于这种具有两种不同极性的词语,它的极性由前后各四个情感值的极性来确定,且前四个占比75%,后四个占比25%,得出两个结果之和,在计算该情感值两个极性(一般都是由两个极性构成,且都是数字,例如0(中性),-1(消极))离这个结果的绝对距离的大小,小的则表示该情感词极性更接近该极性,那么就可以确定该极性。
例如前四个结果为2,后四个结果为-1,相加之和:1,该情感词具有两个极性分别为1和-1,算绝对距离为0和2,0<2,所以可以确定该词的极性为中性(0)
为什么这么做?可以这么理解:在一段文字中,所具有的情感大概率是一致的,就比如你在夸奖某个人,即便你夸奖的词语中出现了一个不确定情感的词,那么因为你大篇幅都是在夸奖ta,所以你这个词大概率也是在夸奖(积极的)他,而不是在阴阳怪气(消极的)。

要点2:双重否定为肯定,或者换个说法:奇数个否定词为否定,偶数个否定词为肯定。所以在计算否定词的时候,不是直接赋值为-1,而是要根据否定词的个数来,所以应该是 W3 *= -1

整个项目代码

import jieba
import re
import math
from openpyxl import load_workbook


negative_file = '否定词.txt'
adv_file = '程度副词.xlsx'
emotional_file = '情感词汇本体.xlsx'
test_file = '测试文章.txt'

alpha = 0.75   # 不确定的情感值极性前后判断因素的比例,默认为0.75

'''
判断情感词极性,传入参数:情感词,情感字典
判断依据:情感分为0, 1, -1, 3,分别表示:中性,积极,消极,褒(积)贬(消)不一四种
1、若一个情感词有两种相同的极性,例如都是1,或者-1,0,那么情感值由第一个[强度,极性]确定
2、若一个情感词有两种相同的极性且都是3,那么该情感值的极性由该情感词前0-4个的情感值极性*0.75(可以修改前后比重), 后0-4个情感值极性*0.25的和共同确定,计算只包括0,1,-1。
    最后结果根据离-1,0,1这三个数的绝对距离确定
3、若一个情感词前后极性不同,那么该情感值的极性同上。
4、若只有一个情感词,则直接返回该数值即可

'''
def anaysisPolarity(word, dict_of_emtion):
    str1 = dict_of_emtion[word][0][0]  # 第一个强度
    plo1 = dict_of_emtion[word][0][1]  # 第一个极性
    if len(dict_of_emtion[word]) > 1:   # 若有两个极性
        str2 = dict_of_emtion[word][1][0]
        plo2 = dict_of_emtion[word][1][1]
        if plo1 == plo2 and plo1 in [-1, 0, 1]:  # 判断依据1
            return [[str1, plo1]]            #  返回第一个强度,极性
        elif (plo1 == plo2 and plo1 == 3) or (plo1 != plo2):  # 判断依据2,3
            return [[str1, plo1], [str2, plo2]]         # 两个都返回
    else:
        return [[str1, plo1]]

'''传入参数:文段,情感字典,副词字典,否定词列表'''

def analysisWords(words, dict_of_emtion, dict_of_adv, list_of_negative, par_W):
    for word in words:
        if word in dict_of_emtion.keys():  # 如果这个词在情感词中,则进行分析
            w3 = 1  # 默认没有否定词
            w4 = 1  # 默认副词为没有,也就是弱,为1
            w1w2 = anaysisPolarity(word, dict_of_emtion)  # 判断极性
            for num in range(1, words.index(word)):
                index = words.index(word) - num
                index_w = words[index]  # 当前下标表示的词语
                if index_w == ',':
                    break
                else:
                    if index_w in list_of_negative:  # 如果在否定词列表中
                        w3 *= -1  # 找到了否定词,置为-1
                    if index_w in dict_of_adv.keys():
                        w4 = dict_of_adv[index_w]  # 副词
            try:
                par_W.append([w1w2, w3, w4])
            except Exception as e:
                print("错误:", e)

def main():
    '''读取情感词汇到字典'''
    wb = load_workbook(emotional_file)
    ws = wb[wb.sheetnames[0]]  # 读取第一个sheet
    dict_of_emtion = {}
    for i in range(2, ws.max_row):
        word = ws['A' + str(i)].value
        strength = ws['F' + str(i)].value  # 一个强度
        polarity = ws['G' + str(i)].value  # 一个极性
        if polarity == 2:
            polarity = -1
        assist = ws['H' + str(i)].value  # 辅助情感分类
        if word not in dict_of_emtion.keys():
            dict_of_emtion[word] = list([[strength, polarity]])
        else:
            dict_of_emtion[word].append([strength, polarity])  # 添加二义性的感情词
        if assist != None:
            str2 = ws['I' + str(i)].value
            pola2 = ws['J' + str(i)].value
            if pola2 == 2:
                pola2 = -1
            dict_of_emtion[word].append([str2, pola2])

    '''读取程度副词副字典'''
    dict_of_adv = {}
    wb = load_workbook(adv_file)
    ws = wb[wb.sheetnames[0]]
    for i in range(2, ws.max_row):
        dict_of_adv[ws['A' + str(i)].value] = ws['B' + str(i)].value
    '''读取否定词列表'''
    list_of_negative = []
    with open(negative_file, "r", encoding='utf-8') as f:
        temps = f.readlines()
    for temp in temps:
        list_of_negative.append(temp.replace("\n", ""))

    para_W = []  # W1*W2*W3*W4参数的值
    with open(test_file, 'r', encoding="utf-8") as f:
        txt = f.readlines()
    for info in txt:
        article = re.findall(r'[^。!?\s]+', info)  # 一句一句分析
        if len(article) != 0:
            for par in article:
                words = jieba.lcut(par)  # 一句一句分词
                analysisWords(words, dict_of_emtion, dict_of_adv, list_of_negative, para_W)
    new_para_W = {}
    index = 1  # 情感参数的数量
    for x in para_W:  # 将只有一个情感极值的列表合并
        if len(x[0]) == 1:
            w = x[0][0][0] * x[0][0][1]
            for numx in x[1:]:
                w *= numx
            new_para_W[index] = w
        else:
            new_para_W[index] = x
        index += 1
    for i in range(1, len(new_para_W) + 1):
        if type(new_para_W[i]) == list:  # 如果该情感值是未计算的
            temp_result = 0
            k = i-1 if i-1 != 0 else i
            index = 1
            while index <= 4:  #  计算0-4个的值
                if type(new_para_W[k]) != list:
                    temp_result += new_para_W[k] * alpha   # 当前值乘以alaph
                else:
                    temp_result += new_para_W[k][0][0][1] * alpha  #如果没有,则默认为第一个极性
                k -= 1
                index += 1
                if k <= 0:
                    break
            k = i + 1 if i+1 < len(new_para_W) else i  #  计算后四个的值
            index = 1
            while index <= 4:
                if type(new_para_W[k]) != list and new_para_W[k] != 3:  # 只考虑后面
                    temp_result += new_para_W[k] * (1-alpha)
                k += 1
                index += 1
                if k > len(new_para_W):  # 如果超出了最长长度,后面则不考虑计算
                    break
            w2 = distance_of_num(temp_result, new_para_W[i][0][0][1], new_para_W[i][0][1][1])  # 求出w2
            dict_of_str_plo = {}  # 存放极值---强度的字典
            str1 = new_para_W[i][0][0][0]
            plo1 = w2 if new_para_W[i][0][0][1] == 3 else new_para_W[i][0][0][1]  # 将褒贬不一的置为求出的局部感情极值
            str2 = new_para_W[i][0][1][0]
            plo2 = w2 if new_para_W[i][0][1][1] == 3 else new_para_W[i][0][1][1]
            dict_of_str_plo[plo2] = str2
            dict_of_str_plo[plo1] = str1
            if w2 == 0:
                new_para_W[i] = 0
            else:
                try:
                    new_para_W[i] = dict_of_str_plo[w2] * new_para_W[i][1] * new_para_W[i][2]
                except Exception as e:
                    print("错误:", e)
    molecular = 0   # 分子
    denominator = 0  # 分母
    for value in new_para_W.values():
        molecular += value
        denominator += math.fabs(value)
    if denominator == 0:
        print("情感倾向 = 0.00")
    else:
        print("情感倾向 = %.4f" % (molecular / denominator))

def distance_of_num(result, x1, x2):
    num1 = math.fabs(result - x1)
    num2 = math.fabs(result - x2)
    if num1 > num2:
        return x2
    else:
        return x1


if __name__ == '__main__':
    main()

文章测试

这里随便找一篇文章进行测试:https://baijiahao.baidu.com/s?id=1698154205016909657&wfr=spider&for=pc

情感分析自然语言处理 情感分析词表_极值_02


可以观察到改文章比较偏消极,本人直观上也感受到改文章偏负面多一点。

另一篇文章:https://www.xcar.com.cn/bbs/viewthread.php?tid=96468962

情感分析自然语言处理 情感分析词表_python_03

其他

否定次txt文本:

不大
不丁点儿
不甚
不怎么
聊
没怎么
不可以
怎么不
几乎不
从来不
从不
不用
不曾
不该
不必
不会
不好
不能
很少
极少
没有
不是
难以
放下
扼杀
终止
停止
放弃
反对
缺乏
缺少
不
甭
勿
别
未
反
没
否
木有
非
无
请勿
无须
并非
毫无
决不
休想
永不
不要
未尝
未曾
毋
莫
从未
从未有过
尚未
一无
并未
尚无
从没
绝非
远非
切莫
绝不
毫不
禁止
忌
拒绝
杜绝
弗

程度副词excel数据,可以先复制到txt,在excel内通过“数据”导入到excel内:

词语	强弱程度
百分之百	2
倍加	2
备至	2
不得了	2
不堪	2
不可开交	2
不亦乐乎	2
不折不扣	2
彻头彻尾	2
充分	2
到头	2
地地道道	2
非常	2
极	2
极度	2
极端	2
极其	2
极为	2
截然	2
尽	2
惊人地	2
绝	2
绝顶	2
绝对	2
绝对化	2
刻骨	2
酷	2
满	2
满贯	2
满心	2
莫大	2
奇	2
入骨	2
甚为	2
十二分	2
十分	2
十足	2
死	2
滔天	2
痛	2
透	2
完全	2
完完全全	2
万	2
万般	2
万分	2
万万	2
无比	2
无度	2
无可估量	2
无以复加	2
无以伦比	2
要命	2
要死	2
已极	2
已甚	2
异常	2
逾常	2
贼	2
之极	2
之至	2
至极	2
卓绝	2
最为	2
佼佼	2
郅	2
綦	2
齁	2
最	2
不过	1.5
不少	1.5
不胜	1.5
惨	1.5
沉	1.5
沉沉	1.5
出奇	1.5
大为	1.5
多	1.5
多多	1.5
多加	1.5
多么	1.5
分外	1.5
格外	1.5
够瞧的	1.5
够戗	1.5
好	1.5
好不	1.5
何等	1.5
很	1.5
很是	1.5
坏	1.5
可	1.5
老	1.5
老大	1.5
良	1.5
颇	1.5
颇为	1.5
甚	1.5
实在	1.5
太	1.5
太甚	1.5
特	1.5
特别	1.5
尤	1.5
尤其	1.5
尤为	1.5
尤以	1.5
远	1.5
着实	1.5
曷	1.5
碜	1.5
大不了	1
多	1
更	1
更加	1
更进一步	1
更为	1
还	1
还要	1
较	1
较比	1
较为	1
进一步	1
那般	1
那么	1
那样	1
强	1
如斯	1
益	1
益发	1
尤甚	1
逾	1
愈	1
愈 ... 愈	1
愈发	1
愈加	1
愈来愈	1
愈益	1
远远	1
越 ... 越	1
越发	1
越加	1
越来越	1
越是	1
这般	1
这样	1
足	1
足足	1
点点滴滴	1
多多少少	1
怪	1
好生	1
还	1
或多或少	1
略	1
略加	1
略略	1
略微	1
略为	1
蛮	1
稍	1
稍稍	1
稍微	1
稍为	1
稍许	1
挺	1
未免	1
相当	1
些	1
些微	1
些小	1
一点	1
一点儿	1
一些	1
有点	1
有点儿	1
有些	1
半点	1
不大	1
不丁点儿	1
不甚	1
不怎么	1
聊	1
没怎么	1
轻度	1
弱	1
丝毫	1
微	1
相对	1
不为过	1.5
超	1.5
超额	1.5
超外差	1.5
超微结构	1.5
超物质	1.5
出头	1.5
多	1.5
浮	1.5
过	1.5
过度	1.5
过分	1.5
过火	1.5
过劲	1.5
过了头	1.5
过猛	1.5
过热	1.5
过甚	1.5
过头	1.5
过于	1.5
过逾	1.5
何止	1.5
何啻	1.5
开外	1.5
苦	1.5
老	1.5
偏	1.5
强	1.5
溢	1.5
忒	1.5

有不足之处欢迎指出。


有时候不能及时通过邮件发送,所以我特意上传到百度网盘了,有需要的小伙伴可以直接下载:
链接: https://pan.baidu.com/s/1TwvgGze_LBzh8DX4Gh13rA
提取码: nkdx
1m多大小,普通用户一分钟也能下载完。

懒的用百度网盘的可以选择从gitee下载:
https://gitee.com/russianready/Sentiment-Analysis


第三次更新:2021/11/8
本次更新,将原先的代码进行了扩展,有的小伙伴可能需要对大量不同的数据进行批次处理,那么使用本代码的话,会极其麻烦,所以我修改了源代码,可以满足上述要求:

import jieba
import re
import math

import openpyxl
from openpyxl import load_workbook


negative_file = '否定词.txt'
adv_file = '程度副词.xlsx'
emotional_file = '情感词汇本体.xlsx'
test_file = '测试文章.txt'

dict_of_emtion = {} # 情感词
dict_of_adv = {}    # 副词
list_of_negative = []  # 否定词列表
alpha = 0.75   # 不确定的情感值极性前后判断因素的比例,默认为0.75

'''
判断情感词极性,传入参数:情感词,情感字典
判断依据:情感分为0, 1, -1, 3,分别表示:中性,积极,消极,褒(积)贬(消)不一四种
1、若一个情感词有两种相同的极性,例如都是1,或者-1,0,那么情感值由第一个[强度,极性]确定
2、若一个情感词有两种相同的极性且都是3,那么该情感值的极性由该情感词前0-4个的情感值极性*0.75(可以修改前后比重), 后0-4个情感值极性*0.25的和共同确定,计算只包括0,1,-1。
    最后结果根据离-1,0,1这三个数的绝对距离确定
3、若一个情感词前后极性不同,那么该情感值的极性同上。
4、若只有一个情感词,则直接返回该数值即可
'''

def anaysisPolarity(word, dict_of_emtion):
    str1 = dict_of_emtion[word][0][0]  # 第一个强度
    plo1 = dict_of_emtion[word][0][1]  # 第一个极性
    if len(dict_of_emtion[word]) > 1:   # 若有两个极性
        str2 = dict_of_emtion[word][1][0]
        plo2 = dict_of_emtion[word][1][1]
        if plo1 == plo2 and plo1 in [-1, 0, 1]:  # 判断依据1
            return [[str1, plo1]]            #  返回第一个强度,极性
        elif (plo1 == plo2 and plo1 == 3) or (plo1 != plo2):  # 判断依据2,3
            return [[str1, plo1], [str2, plo2]]         # 两个都返回
    else:
        return [[str1, plo1]]


'''传入参数:文段,情感字典,副词字典,否定词列表'''
def analysisWords(words, dict_of_emtion, dict_of_adv, list_of_negative, par_W):
    for word in words:
        if word in dict_of_emtion.keys():  # 如果这个词在情感词中,则进行分析
            w3 = 1  # 默认没有否定词
            w4 = 1  # 默认副词为没有,也就是弱,为1
            w1w2 = anaysisPolarity(word, dict_of_emtion)  # 判断极性
            for num in range(1, words.index(word)):
                index = words.index(word) - num
                index_w = words[index]  # 当前下标表示的词语
                if index_w == ',':
                    break
                else:
                    if index_w in list_of_negative:  # 如果在否定词列表中
                        w3 *= -1  # 找到了否定词,置为-1
                    if index_w in dict_of_adv.keys():
                        w4 = dict_of_adv[index_w]  # 副词
            try:
                par_W.append([w1w2, w3, w4])
            except Exception as e:
                print("错误:", e)


# 处理之前加载各种信息
def load_infos():
	'''读取情感词汇到字典'''
    wb = load_workbook(emotional_file)
    ws = wb[wb.sheetnames[0]]  # 读取第一个sheet
    for i in range(2, ws.max_row):
        word = ws['A' + str(i)].value
        strength = ws['F' + str(i)].value  # 一个强度
        polarity = ws['G' + str(i)].value  # 一个极性
        if polarity == 2:
            polarity = -1
        assist = ws['H' + str(i)].value  # 辅助情感分类
        if word not in dict_of_emtion.keys():
            dict_of_emtion[word] = list([[strength, polarity]])
        else:
            dict_of_emtion[word].append([strength, polarity])  # 添加二义性的感情词
        if assist != None:
            str2 = ws['I' + str(i)].value
            pola2 = ws['J' + str(i)].value
            if pola2 == 2:
                pola2 = -1
            dict_of_emtion[word].append([str2, pola2])

    '''读取程度副词副字典'''
    wb = load_workbook(adv_file)
    ws = wb[wb.sheetnames[0]]
    for i in range(2, ws.max_row):
        dict_of_adv[ws['A' + str(i)].value] = ws['B' + str(i)].value

    '''读取否定词列表'''
    with open(negative_file, "r", encoding='utf-8') as f:
        temps = f.readlines()
    for temp in temps:
        list_of_negative.append(temp.replace("\n", ""))


def distanceOfNum(result, x1, x2):
    num1 = math.fabs(result - x1)
    num2 = math.fabs(result - x2)
    if num1 > num2:
        return x2
    else:
        return x1


def cal_res(txt) -> float:
    para_W = []  # W1*W2*W3*W4参数的值
    article = re.findall(r'[^。!?\s]+', txt)  # 一句一句分析
    if len(article) != 0:
        for par in article:
            words = jieba.lcut(par)  # 一句一句分词
            analysisWords(words, dict_of_emtion, dict_of_adv, list_of_negative, para_W)
    new_para_W = {}
    index = 1  # 情感参数的数量
    for x in para_W:  # 将只有一个情感极值的列表合并
        if len(x[0]) == 1:
            w = x[0][0][0] * x[0][0][1]
            for numx in x[1:]:
                w *= numx
            new_para_W[index] = w
        else:
            new_para_W[index] = x
        index += 1
    for i in range(1, len(new_para_W) + 1):
        if type(new_para_W[i]) == list:  # 如果该情感值是未计算的
            temp_result = 0
            k = i - 1 if i - 1 != 0 else i
            index = 1
            while index <= 4:  # 计算0-4个的值
                if type(new_para_W[k]) != list:
                    temp_result += new_para_W[k] * alpha  # 当前值乘以alaph
                else:
                    temp_result += new_para_W[k][0][0][1] * alpha  # 如果没有,则默认为第一个极性
                k -= 1
                index += 1
                if k <= 0:
                    break
            k = i + 1 if i + 1 < len(new_para_W) else i  # 计算后四个的值
            index = 1
            while index <= 4:
                if type(new_para_W[k]) != list and new_para_W[k] != 3:  # 只考虑后面
                    temp_result += new_para_W[k] * (1 - alpha)
                k += 1
                index += 1
                if k > len(new_para_W):  # 如果超出了最长长度,后面则不考虑计算
                    break
            w2 = distanceOfNum(temp_result, new_para_W[i][0][0][1], new_para_W[i][0][1][1])  # 求出w2
            dict_of_str_plo = {}  # 存放极值---强度的字典
            str1 = new_para_W[i][0][0][0]
            plo1 = w2 if new_para_W[i][0][0][1] == 3 else new_para_W[i][0][0][1]  # 将褒贬不一的置为求出的局部感情极值
            str2 = new_para_W[i][0][1][0]
            plo2 = w2 if new_para_W[i][0][1][1] == 3 else new_para_W[i][0][1][1]
            dict_of_str_plo[plo2] = str2
            dict_of_str_plo[plo1] = str1
            if w2 == 0:
                new_para_W[i] = 0
            else:
                try:
                    new_para_W[i] = dict_of_str_plo[w2] * new_para_W[i][1] * new_para_W[i][2]
                except Exception as e:
                    print("错误:", e)
    molecular = 0  # 分子
    denominator = 0  # 分母
    for value in new_para_W.values():
        molecular += value
        denominator += math.fabs(value)
    if denominator == 0:
        return 0.0
    else:
        return (molecular / denominator)

if __name__ == '__main__':
    load_infos()  # 加载情感处理所需要的词汇信息
    # for txt in txts:
    txt = "在这里修改txt,调用 cal_res 方法即可返回结果。如果是有多个数据,写成for的形式,多次调用即可。"
    res = cal_res(txt)
    print(res)