【Python專案】基於文字情感分析的電商評論重排序(以京東為例)(附程式碼)
一、背景
隨著網際網路的普及,網路購物已經成了人們購物的首選。使用者只需在電商平臺搜尋商品名,便可得到成百上千條商品資訊。商品資訊的排序演算法很複雜,但總的說來基本上都是根據與搜尋關鍵詞的關聯度和商品的人氣或商家排名來排序最終對使用者進行展示的。而好評率即是排名中的重要因素。商品的評價分為星級評價和文字評價。星級評價和好評率在排序演算法中佔據重要地位。有的商家為了提升排名,採取“五星好評返現”的方式來誘導顧客好評,但實際上使用者可能對商品並不滿意,因此會在文字評論或者追加評論時說出對商品不滿意的真實評價。因此,對評論文字進行情感分析並使用文字評論好評率來對商品進行重新排序,指導人們根據真實評價選取商品就很顯得有意義。
二、步驟
要獲得某一商品文字評論的好評率,首先需要得到某商品評論資訊,然後再對每一條評論進行文字情感分析,判斷其是好評、中評還是差評,繼而算出商品的文字評論好評率。將所有商品的好評率都計算後進行排序,得出新的排序列表。
因此該專案分為兩個部分。一是評論爬蟲,二是文字情感分析,最後根據情感分析得出的文字評論好評率彙總重排序。
子問題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)