一、背景
随着互联网的普及,网络购物已经成了人们购物的首选。用户只需在电商平台搜索商品名,便可得到成百上千条商品信息。商品信息的排序算法很复杂,但总的说来基本上都是根据与搜索关键词的关联度和商品的人气或商家排名来排序最终对用户进行展示的。而好评率即是排名中的重要因素。商品的评价分为星级评价和文本评价。星级评价和好评率在排序算法中占据重要地位。有的商家为了提升排名,采取“五星好评返现”的方式来诱导顾客好评,但实际上用户可能对商品并不满意,因此会在文本评论或者追加评论时说出对商品不满意的真实评价。因此,对评论文本进行情感分析并使用文本评论好评率来对商品进行重新排序,指导人们根据真实评价选取商品就很显得有意义。
二、步骤
要获得某一商品文本评论的好评率,首先需要得到某商品评论信息,然后再对每一条评论进行文本情感分析,判断其是好评、中评还是差评,继而算出商品的文本评论好评率。将所有商品的好评率都计算后进行排序,得出新的排序列表。
因此该项目分为两个部分。一是评论爬虫,二是文本情感分析,最后根据情感分析得出的文本评论好评率汇总重排序。
子问题1:评论爬虫
本项目的评论爬虫爬取的目标网页为京东。
Ⅰ、先获取某一页商品信息并存储到文件:
在站内商品搜索框输入:“帽子”,并选择按销量排序,得到目标网页URL。分析网页源代码可知:商品名信息位于标签<div class="p-name">
之后的<em></em>
标签之内,商品评论页面URL信息位于<div class="p-commit">
之后的href=” ”sonclick
标签的双引号之内。因此可以写出匹配两项信息的正则表达式。此外因为商品名内有一些标签信息,因此用sub
语句清除。得到商品名信息及商品评论URL后建立字典并存储到result_jingdong.txt
文件中。文件示例如下:
Ⅱ、获取每一件商品的全部文本评论:
分析商品评论页面。京东的商品评论信息是动态加载的,无法直接在之前获取到的商品评论页面的源码中直接得到。因此需要使用开发者工具network选项监测页面资源传输。
搜索可知评论内容页面(包括文本内容)来自productpagecomment
,评论概括信息(包括评论数量)来自commentsummary
。分析commentsummary
源码可得到评论的数量信息,分析pro-ductpagecomment
可获取评论文本内容。根据所得到的评论数量信息,修改URL中的“page=*”即可得到所有评论页面。将每个商品的评论信息都存储到一个“result_jingdong_comment_n”
中。(n为商品序号,范围为1,2……30)。爬虫运行结果如下:
子问题2:文本情感分析
本项目的文本情感分析使用的是基于情感字典的文本情感分析。
Ⅰ、获取情感字典:
为了能够正确标注一段中文文本的情感。需要如下几个情感字典:
①停用词字典:用于过滤掉一段文本中的噪声词组。
②情感词字典:用于得到一段文本中带有情感色彩的词组及其评分。
③程度副词字典:代表情感词的强烈程度,相当于情感词的权重。
④否定词字典:用于判断其后情感词的意思究竟是好(正极性)还是坏(负极性),若情感词前有否定词,则情感得分*-1。
情感字典以及评分通常由手工标注完成,而标注是一项费时又费力的活,因此这四个字典都是由网络搜集而来。
Ⅱ、情感评分算法的实现:
①分词:将评论逐条从文件导入,使用jieba库进行中文分词。
②评论数据清洗:导入停用词表,将分词后的结果除去停用词,得到清洗后的数据集。
③导入情感字典:将情感词字典,程度副词字典,否定词字典分别导入,生成情感词字典(key:词组,value:得分),程度副词字典(key:词组,value:得分),否定词列表。
④词组定位:将评论分词并清洗后的结果分别在情感词字典,程度副词字典,否定词字典中定位,并得到情感词、程度副词的评分。
⑤计算语句得分:将经过上述步骤的语句分词结果进行语句得分计算,根据公式:
Di为程度副词得分,
Sj为程度副词之后的情感词得分,
Ⅲ、根据评分结果,计算商品文本评论好评率:
根据情感极性分析,大于0表示褒义,等于0表示中性,小于0表示贬义,也即大于0的近似看作好评。计算公式如下:
文本情感分析运行结果:
子问题3(综合得出结论):重排序
将存储商品页面信息(商品名和商品评论URL信息)文件result_jingdong.txt导入,建立为原始排序列表。(写在”基于情感词典的文本情感分析.py”里)
将商品按照好评率重新排序。Good_rates
是一个字典,按照其值排序。得到一个商品序号(原先的排序序号)与好评率的列表。之后将原始排序列表按照新的顺序进行重排并将URL改为商品的好评率。
附代码:
① 京东爬虫
import json
import requests
from requests.exceptions import RequestException
import re
import time
import urllib.request
import time
import random
def write_Product_to_file(content):#商品名+URL写文件
with open('result_jingdong.txt', 'w', encoding='utf-8') as f:
f.write(json.dumps(content, ensure_ascii=False) + '\n')
def write_Comments_to_file(content,k):#评论写文件
file_name='result_jingdong_comment_'+str(k)+'.txt'
with open(file_name, 'a', encoding='utf-8') as f:#注意这里要用'a'
f.write(json.dumps(content, ensure_ascii=False) + '\n')
def GetComment(url,k):#函数返回1说明是空页面,跳出页面++循环
html = urllib.request.urlopen(url).read().decode('gbk','ignore')
jsondata = html
data = json.loads(jsondata)
print(type(data))
#如果匹配不到comments,content,则返回1,否则返回0
#judge=re.findall('.*?\"content\".*?',data,re.S)
#if len(judge)==0:
# return 1
for i in data['comments']:
content = i['content']
print("用户评论内容:{}".format(content))
print("-----------------------------")
write_Comments_to_file(content,k)
def GetProductUrl(url):
headers = {
'User-Agent' : 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'
}#必须加请求头,否则禁止访问
response = requests.get(url,headers=headers)
response.encoding='utf-8'#编码问题!!注意!!
#requests根据内容自动编码,会导致一些意想不到的问题,记得用encoding自定义所需编码
html=response.text
results=re.findall(u'.*?class\=\"p\-name.*?\<em\>(.*?)\<\/em\>',html,re.S)
i=0
for result in results:
result=re.sub('<font.*?>|</font>|<span.*?>|</span>|<img.*?>|…|\n','',result)#用正则表达式sub除去文本中的html格式,注意|的含义
results[i]=result
i=i+1
results2=re.findall('.*?class\=\"p\-commit.*?href\=\"(.*?)\"\sonclick',html,re.S)
dictionary=dict(zip(results,results2))#建立字典,表示商品名和评论URL的关系,为之后排序做准备
#print(dictionary)
write_Product_to_file(dictionary)#将字典写入文件
dictionary=dict(zip(results,results2))#建立字典,表示商品名和评论URL的关系,为之后排序做准备
print(dictionary)
write_Product_to_file(dictionary)#将字典写入文件
def GetProduct_Comments_Number(result_ProID):
url='http://club.jd.com/comment/productCommentSummaries.action?referenceIds='+result_ProID
headers = {
'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'
}#必须加请求头,否则禁止访问
response = requests.get(url,headers=headers)
response.encoding='utf-8'#编码问题!!注意!!
#requests根据内容自动编码,会导致一些意想不到的问题,记得用encoding自定义所需编码
html=response.text
comments_count=re.findall('\"CommentCount\"\:(.*?)\,.*?\"DefaultGoodCount\"\:(.*?)\,',html,re.S)
print(comments_count)
return comments_count
url='https://search.jd.com/Search?keyword=%E5%B8%BD%E5%AD%90&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&wq=%E5%B8%BD%E5%AD%90&psort=3&click=0'#根据销量排序
print("开始获取商品信息:")
GetProductUrl(url)
print("商品信息获取成功,开始写入文件:")
f=open('result_jingdong.txt','r',encoding='utf-8')
txt=f.read()
result_ProIDs=re.findall('com\/(.*?)\.html',txt,re.S)
result_ProIDs=list(result_ProIDs)
print(result_ProIDs)
k=1
for result_ProID in result_ProIDs:
print("正在获取第{}个商品的评论数据!".format(k))
comments_count=GetProduct_Comments_Number(result_ProID)
num=int(comments_count[0][0])-int(comments_count[0][1])
print(num)
for i in range(0,int(num/10)):
print("正在获取第{}页评论数据!".format(i+1))
url = 'http://sclub.jd.com/comment/productPageComments.action?'+'&productId='+result_ProID+'&score=0&sortType=5&page=' + str(i) +'&pageSize=10&isShadowSku=0&fold=1'
print(url)
try:
GetComment(url,k)
except:
print("获取继续")
#设置休眠时间
time.sleep(random.randint(1,3))
k=k+1
② 文本情感分析&&重排序
import re
import time
import random
import jieba
from collections import defaultdict
import chardet
def words():
with open('情感词典\\BosonNLP_sentiment_score.txt','r',encoding='utf-8') as f:
sentiList=f.readlines()
#print(len(sentiList))
SentiDict=defaultdict()
#SentiDict=defaultdict()
for s in sentiList:
#print(s)
s=s.strip('\n')
#print(s)
#print(s.split(' ')[0])
#print(s.split(' ')[1])
#print('\n')
try:
SentiDict[s.split(' ')[0]]=s.split(' ')[1]
except:
pass
print(len(SentiDict))
with open('情感词典\\NotList.txt','r',encoding='gbk') as f:
NotList=f.readlines()
NotList2=[]
for line in NotList:
line=line.strip('\n')
#print(line)
NotList2.append(line)
#print(NotList2)
print(len(NotList2))
with open('情感词典\\Degreelist.txt','r',encoding='gbk') as f:
DegreeList=f.readlines()
DegreeDict=defaultdict()
#DegreeDict=defaultdict()
n=0
Degree=[0,2,1.25,1.2,0.8,0.5,1.5]
for d in DegreeList:
d=d.strip('\n')
#print(d)
cout=re.findall('\”.*?(\d+)',d)
if len(cout):
#print(cout)
n=n+1
continue
if n>0:
DegreeDict[d]=Degree[n]
print(len(DegreeDict))#少了四个!!!
return SentiDict,NotList2,DegreeDict
def classifywords(wordDict,SentiDict,NotList,DegreeDict):
SentiWords=defaultdict()
NotWords=defaultdict()
DegreeWords=defaultdict()
#print(wordDict)
for word in wordDict.keys():
if word in SentiDict.keys() and word not in NotList and word not in DegreeDict.keys():
SentiWords[wordDict[word]] = SentiDict[word]
elif word in NotList and word not in DegreeDict.keys():
NotWords[wordDict[word]] = -1
elif word in DegreeDict.keys():
DegreeWords[wordDict[word]] = DegreeDict[word]
#print(Sentiword)
#print(Notword)
#print(Degreeword)
return SentiWords,NotWords,DegreeWords
def scoreSent(senWord, notWord, degreeWord, segResult):
#print(senWord)
#print(notWord)
#print(degreeWord)
#print(segResult)
W = 1
score = 0
senLoc = senWord.keys()
notLoc = notWord.keys()
degreeLoc = degreeWord.keys()
senloc = -1
for i in range(0, len(segResult)):
if i in senLoc:
senloc += 1
score += W * float(senWord[i])
if senloc < len(senLoc) - 1:
for j in range((list(senLoc))[senloc], (list(senLoc))[senloc + 1]):
if j in list(notLoc):
W *= -1
elif j in list(degreeLoc):
W *= float(degreeWord[j])
if senloc < len(senLoc) - 1:
i = (list(senLoc))[senloc + 1]
return score
good_rates={}
words_value=words()
#print(words_value[0])
#print(words_value[1])
#print(words_value[2])
#print('喵')
comments_sum=0
for i in range(1,31):
score_var=[]
print("\nresult_jingdong_comment_"+str(i))
file_name='评论文档\\result_jingdong_comment_'+str(i)+'.txt'
try:
with open(file_name, 'r', encoding='utf-8',errors='ignore') as f:
for line in f.readlines():
#print(line)
#print(type(line))
segList = jieba.cut(line)
segResult = []
for w in segList:
segResult.append(w)
with open('情感词典\\Stopwordlist.txt', 'r', encoding='GB2312') as f:
stopwords = f.readlines()
#print(stopwords)
newSent = []
for word in segResult:
if word+'\n' in stopwords:
continue
else:
newSent.append(word)
datafen_dist={}
for x in range(0, len(newSent)):
datafen_dist[newSent[x]]=x
#datafen_dist=listToDist(data)
#print(datafen_dist)
data_1=classifywords(datafen_dist,words_value[0],words_value[1],words_value[2])
#print('\n1\n',data_1[0],'\n2\n',data_1[1],'\n3\n',data_1[2])
segResult_P = []
segList_P = jieba.cut(line)
for w in segList_P:
segResult_P.append(w)
data_2=scoreSent(data_1[0],data_1[1],data_1[2],newSent)
#print(data_2)
score_var.append(data_2)
#print(score_var,'\n\n')
good=0
normal=0
bad=0
for score in score_var:
if score>0:
good=good+1
elif score<0:
bad=bad+1
else:
normal=normal+1
print('good_comments:',good,'normal_comments:',normal,'bad_comments:',bad,'Total_comments:',good+normal+bad)
good_comments_rate=good/(good+normal+bad)
print('文本评论好评率:%.2f%%'%(good_comments_rate*100))
comments_sum=comments_sum+good+normal+bad
good_rates[i]=good_comments_rate
#print(good_rates)
except:
print('result_jingdong_comment_'+str(i)+'.txt文件不存在!')
print('总获取的评论数量:',comments_sum)
#原始排序
with open('评论文档\\result_jingdong.txt','r',encoding='utf-8') as f:
txt=f.read()
print(type(txt))
text=re.findall('\"(.*?)\"\:.*?\"(.*?)\"\,',txt,re.S)
i=1
print('原始排序为:')
for line in text:
line=list(line)
line[1]=i
i=i+1
print(line)
#新的排序
sorted_good_rates=sorted(good_rates.items(),key=lambda item:item[1],reverse=True)
print(sorted_good_rates)
print('新的排序为:')
for line1 in sorted_good_rates:
line1=list(line1)
#print(line1)
i=1
for line2 in text:
line2=list(line2)
line2[1]=i
i=i+1
#print(line2)
if line2[1]==line1[0]:
line2[1]=line1[1]
print(line2)