Fy J
CS专业扫雷学深造学者互联网冲浪一级选手
FRIENDS
jhn

社会媒体计算:知乎问答信息挖掘

04-14-2021 15:33:25 社会媒体计算
Word count: 3.8k | Reading time: 17min

原创文章,转载、引用请注明出处!


前言

刚好在摸鱼的时候比较喜欢刷知乎,又碰到了这个课程,所以就尝试了做这个内容。以前从来没有接触过文本分析和爬虫这类的技术,就当学新技术了。以及这些在去年12月就写完了,现在闲了才想起搬到这里来。

依稀记得那是为数不多的有心思敲代码的时间,会珍惜的。


说明

要求

从互联网上采集数据,如从 Tewitter、Facebook、新浪微博、B站等采集数据,包括用户基本信息、互相浏览、互相关注等信息,以及对应某一段时间发布的文本内容信息。

对上述数据进行预处理,要求用程序进行预处理。

对上述处理数据进行社团挖掘,包括基本统计信息、社团发现等,对预处理的文本进行情感分析、主题挖掘、分类或聚类等研究。要求用到社会网络计算、文本挖掘等技术。

背景

知乎是一个网络问答社区,用户彼此分享知识、经验和见解,围绕着某一感兴趣的话题进行相关的讨论,同时也可以关注兴趣一致的人。

知乎的基本模式是:用户提问,每一个问题都会有一个独有的id。其他对此问题感兴趣的用户在此问题下进行回答,每一个此问题下的回答也会有一个id。用户可对回答进行点赞、点踩、评论等操作。

选择从知乎采集要用到的数据。所选取的问题是:你打算在 12 月 31 日发什么朋友圈跨年文案?

链接:https://www.zhihu.com/question/360940960

编程环境

Mac OS 10.14.6 + Jupyter notebook + Python 3.6.5

知乎数据采集

数据爬虫

这部分爬虫功能的主要作用就是从知乎的网页上拿数据。

这部分的原理参考最下面唯一的一条引用,包括从网络请求找到数据存放位置、分析请求头的格式和构造代码中所需要的新的请求头。

def crawler(question_num)是该部分功能的组织函数,其他4个函数的具体功能见注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def get_data(url):
# '''
# 功能:访问 url 的网页,获取网页内容并返回
# 参数:
# url :目标网页的 url
# 返回:目标网页的 html 内容
# '''
# 设置多个用户代理
agent=['Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.80 Safari/537.36',
'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/537.75.14',
'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)' ]
# 每次随机抽取一个,防止被封ip
randdom_agent=random.choice(agent)
headers = {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'user-agent': randdom_agent
}
# 爬取数据
try:
r = requests.get(url, headers=headers)
r.encoding = 'UTF8'
r.raise_for_status()
return r.text
except requests.HTTPError as e:
print(e)
print("HTTPError")
except requests.RequestException as e:
print(e)
except:
print("Unknown Error !")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def parse_data(html):
# '''
# 功能:提取 html 页面信息中的关键信息,并整合一个数组并返回
# 参数:
# html:根据 url 获取到的网页内容
# 返回:存储有 html 中提取出的关键信息的数组
# '''
json_data = json.loads(html)['data']
comments = []
try:
for item in json_data:
comment = []
comment.append(item['author']['name']) # 姓名
comment.append(item['author']['gender']) # 性别
comment.append(item['voteup_count']) # 点赞数
comment.append(item['comment_count']) # 评论数
comment.append(item['url']) # 回答链接
comment.append(item['created_time']) # 回答时间
comment.append(item['content']) # 回答时间
comments.append(comment)
return comments
except Exception as e:
print(comment)
print(e)
1
2
3
4
5
6
7
8
9
10
11
12
def get_file(question_num):
# '''
# 功能:根据知乎的问题id构造文件路径和文件名,csv、png都会用到
# 参数:
# question_num:知乎提问的问题id
# 返回:csv文件路径、csv文件名
# '''
current_path = os.getcwd()
file_name=str(question_num)+'_'+str(datetime.datetime.now())+'.csv'
path = current_path+'/'+file_name
print("文件:"+path)
return path,file_name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def save_data(comments,header,path):
# '''
# 功能:将comments中的信息输出到文件/数据库中
# 参数:
# comments:将要保存的数据
# 返回:无
# '''
dataframe = pd.DataFrame(comments)
if header:
dataframe.to_csv(path, mode='a', index=False, sep=',', header=['name','gender','voteup','cmt_count','ans_url','ans_time','ans_content'])
else:
dataframe.to_csv(path, mode='a', index=False, sep=',', header=False)

return 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def crawler(question_num):
# '''
# 功能:爬虫功能的组织函数,爬取数据并存储到csv文件中
# 参数:
# question_num:知乎提问的问题id
# 返回:文件名
# '''
url_start = 'https://www.zhihu.com/api/v4/questions/'+str(question_num)+'/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cis_labeled%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%2A%5D.topics&limit=5&offset='
url_end='&platform=desktop&sort_by=default'
html=get_data(url_start+str(5)+url_end)
totals=json.loads(html)['paging']['totals']
path,file_name=get_file(question_num)

print("总回答数:"+str(totals))
page = 0
while(page < totals):
url = url_start+str(page) +url_end

html = get_data(url)
comments = parse_data(html)
if page==0:
save_data(comments,True,path)
else:
save_data(comments,False,path)
page += 5

return file_name

数据读取

def save_data(comments,header,path)将爬到的数据存到了.CSV中,数据读取函数则将CSV中的内容读到内存里供。

1
2
3
4
5
6
7
8
9
10
11
def read_csv(file_name):
# '''
# 功能:读取爬虫函数得到的csv文件
# 参数:
# file_name:文件名
# 返回:csv文件的list
# '''
with open(file_name, 'r') as f:
reader = csv.reader(f)
result = list(reader)
return result

数据预处理

爬取下来的数据需要预处理。主要是因为所关注的“ans_content”内,除了中文之外,还有一些奇奇怪怪的东西

可以看到,有emoji、有非中文的特殊字符、有h5的标签(由<>括起的部分,主要由于回答中的图片的网页链接需要用这种方式在网页源码内引用)。使用正则表达式去掉这些对中文文本分析没有用的内容。

其他的一些处理还包括秒级时间戳的转换和知乎用户系统中的“匿名用户”进行编号等。这些无关紧要,只是为了看着舒服。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def date_pre_treatment(csv_data):
# '''
# 功能:数据预处理
# 参数:
# csv_data:已经读取好的list
# 返回:经过了预处理的list
# '''
niming_num=0
for j in range(len(csv_data[0])):
if csv_data[0][j]=='name':
for i1 in range(len(csv_data)):
# 将知乎用户、匿名用户的名称统一,加上编号做区分
if csv_data[i1][j]=='知乎用户' or csv_data[i1][j]=='匿名用户':
csv_data[i1][j]='匿名用户'+str(++niming_num)
if csv_data[0][j]=='ans_time':
for i2 in range(len(csv_data)-1):
ltime = time.localtime(int(csv_data[i2+1][j]))
# 将秒级时间戳转化为"年月日时分秒"格式
csv_data[i2+1][j]=time.strftime('%Y-%m-%d %H:%M:%S',ltime)
if csv_data[0][j]=='ans_content':
for i3 in range(len(csv_data)):
# 去除文本信息中的h5标签
csv_data[i3][j] = re.sub(u"\\<.*?\\>", " ", csv_data[i3][j])
# 去除文本信息除中文、英文、ascll字符、常用标点以外的所有内容
csv_data[i3][j] = re.sub("[^\u4e00-\u9fa5 ^a-z ^A-Z ^0-9 ^~!@#$%&*()_+-=:;,.^~!,。?、《》]",'', csv_data[i3][j])
if csv_data[0][j]=='voteup':
for i4 in range(len(csv_data)-1):
# 将投票数转化为int(读取的时候会存储为str)
csv_data[i4+1][j] = int(float(csv_data[i4+1][j]))
return csv_data

词云

使用wordcloud包进行所有回答文本的词云的绘制,并存储为png图片文件。用到的功能为WordCloud,参数和说明见代码注释。

词云的图如果想做的有意义且好看,最主要的参数是stopwords、max_words。

其中,stopwords指的是词云里什么词不能出现,默认为空。毕竟不管是中文还是英文,文本里都有许多没有意义的词,比如语气词“嗯”、“啊”、“呢”这种类似的。所以如果不加限制的话,词云里最大的那个词一定是没什么意义的词(知乎作为一个网络社区,大家的回答肯定是偏向口头语的,那么无意义的词就会多起来,毕竟没人在知乎上写论文是吧)。这里的stopwords是通过txt导入的。stopwords网上能找到许多,我是在GitHub上找到了一份中文停用词,直接拿过来用了。后期又在里面填了一些自己不想看见的词。

max_words就是图片上最多出现多少个词,个人感觉还是多一些好看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from wordcloud import WordCloud
now_time= datetime.datetime.now()
print("2.词云")

# 设置数据,将所有文本放到一个list里方便处理
text = ''
for i in range(len(csv_data)-1):
text+=csv_data[i+1][6]+' '
# 结巴中文分词,生成字符串,默认精确模式,如果不通过分词,无法直接生成正确的中文词云
cut_text = jieba.cut(text)
# 必须给个符号分隔开分词结果来形成字符串,否则不能绘制词云
result = " ".join(cut_text)
# 从文件中获取停用词
stopwords=get_stopwords()
# 词云设置
wc = WordCloud(
font_path="仿宋_GB2312.ttf",# 设置字体(不指定就会出现乱码)
background_color='white',# 设置背景色
width=1500, height=900,# 设置背景宽、高
max_font_size=400, min_font_size=20,# 设置字体大小上下限
stopwords = stopwords,# 停用词
max_words=150 # 图片中显示词的最大数量
)
# 产生词云
wc.generate(result)
# 保存图片
pic_name=re.sub('.csv','',file_name)+'_wordcloud.png'
wc.to_file(pic_name)
# 绘图
plt.figure(figsize=(15, 10))
plt.imshow(wc)
plt.axis("off")
plt.show()

print('Runtime:%d s'%(datetime.datetime.now()-now_time).seconds)

keywords

使用jieba包进行关键词提取。

Jieba提供了两种关键词的提取算法,分别是:

  • 基于TF-IDF(term frequency–inverse document frequency)算法的关键词抽取。函数参数如下:sentence:待提取的文本;topK:返回topK个 TF/IDF 权重最大的关键词;withWeight:是否一并返回关键词权重值,默认值为False;allowPOS:仅包括指定词性的词,默认值为空,即不筛选。

  • 基于TextRank算法的关键词抽取。函数接口同TF-IDF相同。不同的是allowPOS默认指定了一些词性的词。

1
2
3
4
5
6
7
8
9
10
11
import jieba.analyse
now_time= datetime.datetime.now()
print("3.keywords")
print("3.1. TF-IDF")
keywords1=jieba.analyse.extract_tags(text, topK=20, withWeight=True, allowPOS=('ns', 'n', 'vn', 'v'))
print(keywords1)
print("\n")
print("3.2. textrank")
keywords2=jieba.analyse.textrank(text, topK=20, withWeight=True, allowPOS=('ns', 'n', 'vn', 'v'))
print(keywords2)
print('Runtime:%d s'%(datetime.datetime.now()-now_time).seconds)

情感分析

使用snownlp进行情感分析。

snownlp是一个python类库,可以方便的处理中文文本内容,是受到了TextBlob的启发而写的,并且和TextBlob不同的是,这里没有用NLTK,所有的算法都是重新实现的,并且自带了一些训练好的字典。

snownlp: https://github.com/isnowfy/snownlp
TextBlob: https://github.com/sloria/TextBlob

snownlp给出了基于贝叶斯分类的情感分析函数SnowNLP。对于每一条文本,该函数会给出一个0-1之间的评分,越接近1代表积极情绪占比越高。

使用SnowNLP对每一条回答文本进行情感分析,并以评分达到0.6、0.5认定为是积极文本,分别给出积极回答文本在所有文本中的比重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from snownlp import SnowNLP
now_time= datetime.datetime.now()
print("4.情感分析")
s=[]
for i in range(len(csv_data)-1):
a=SnowNLP(csv_data[i+1][6])
s.append(round(a.sentiments,3))
s.sort(reverse=True)
flag06=-1
flag05=-1
for i in range(len(s)):
if s[i]<0.5 :
flag05=i
break
for i in range(len(s)):
if s[i]<0.6 :
flag06=i
break
print("积极(60%)百分比:",round(flag06/(len(s)),2))
print("积极(50%)百分比:",round(flag05/(len(s)),2))
print('Runtime:%d s'%(datetime.datetime.now()-now_time).seconds)

两个评分阈值下的大部分的回答都被认为是积极的,这也符合这个问题的背景:新年。

聚类

对点赞数最多的100条数据进行聚类。

进行涉及文本的向量化表示。sklearn提供了传统的词袋模型。使用sklearn中的TfidfVectorizer计算tf-idf矩阵。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from operator import itemgetter
print('5. 文本聚类')
print('5.1. 准备点赞数最多的100条数据')
user_id=[]
content=[]
csv_data_sorted=csv_data
del(csv_data_sorted[0])
csv_data_sorted=sorted(csv_data,key=itemgetter(2))
# print(csv_data[:10])
for i in range(100):
user_id.append(csv_data_sorted[i][0])
content.append(csv_data_sorted[i][6])

from sklearn.feature_extraction.text import TfidfVectorizer
now_time= datetime.datetime.now()
print('5.2. tfidf matrix')
#max_df: When building the vocabulary ignore terms that have a document frequency strictly higher than the given threshold (corpus-specific stop words). If float, the parameter represents a proportion of documents, integer absolute counts. This parameter is ignored if vocabulary is not None.
#min_df: When building the vocabulary ignore terms that have a document frequency strictly lower than the given threshold. This value is also called cut-off in the literature. If float, the parameter represents a proportion of documents, integer absolute counts. This parameter is ignored if vocabulary is not None.
tfidf_vectorizer = TfidfVectorizer(max_df=0.9, max_features=200000,min_df=0.1, stop_words='english',use_idf=True, tokenizer=segment)
tfidf_matrix = tfidf_vectorizer.fit_transform(content) #fit the vectorizer to synopses
print(tfidf_matrix.shape)
print('Runtime:%d s'%(datetime.datetime.now()-now_time).seconds)

from scipy.cluster.hierarchy import ward, dendrogram, linkage
from sklearn.metrics.pairwise import cosine_similarity
now_time= datetime.datetime.now()
print('5.3. linkage matrix')
dist = 1 - cosine_similarity(tfidf_matrix)
plt.rcParams['font.sans-serif']=['Microsoft YaHei'] #用来正常显示中文标签
linkage_matrix = linkage(dist, method='ward', metric='euclidean', optimal_ordering=False)
print('Runtime:%d s'%(datetime.datetime.now()-now_time).seconds)
print(linkage_matrix)

由于选取了100条回答,tf-idf矩阵的第一维为100。

然后根据tf-idf矩阵进行层次聚类,给出linkage矩阵。使用函数:scipy.cluster.hierarchy.linkage(y, method=’single’, metric=’euclidean’, optimal_ordering=False)。其中,计算新形成的聚类簇u和v之间距离的方法是用的是single,即最近邻点算法。

对上述矩阵进行可视化,并将结果保存为png文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
now_time= datetime.datetime.now()
print('5.4. linkage matrix可视化')
plt.figure(figsize=(40, 15))
plt.title('层次聚类树状图')
plt.xlabel('知乎用户名称')
plt.ylabel('距离(越低表示文本越类似)')
dendrogram(
linkage_matrix,
labels=user_id,
leaf_rotation=-70, # rotates the x axis labels
leaf_font_size=12 # font size for the x axis labels
)
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
plt.savefig(re.sub('.csv','',file_name)+'_linkage.png')
print('Runtime:%d s'%(datetime.datetime.now()-now_time).seconds)
plt.show()

其中,横坐标为知乎用户名,每种的线连起来的用户名代表这些用户的回答可被分为相似的一类。

(看到这里明白为什么只选100个了吧,因为选多了图就画不下了)

其他

在代码中更改需要爬取的知乎问题的问题id,就可以实现对任意知乎问题下的所有回答内容的上述操作。

代码每次运行都会按照以“问题id+时间”为文件名进行各项文件的保存,不会覆盖之前运行所保留的文件。

参考

Python网络爬虫实战:爬取知乎话题下 18934 条回答数据-csdn
https://blog.csdn.net/wenxuhonghe/article/details/86515558

< PreviousPost
Ubuntu16.04虚拟机+Hadoop:伪分布式
NextPost >
想分享给别人看的一些影像
CATALOG
  1. 1. 前言
  2. 2. 说明
    1. 2.1. 要求
    2. 2.2. 背景
    3. 2.3. 编程环境
  3. 3. 知乎数据采集
    1. 3.1. 数据爬虫
    2. 3.2. 数据读取
    3. 3.3. 数据预处理
  4. 4. 词云
  5. 5. keywords
  6. 6. 情感分析
  7. 7. 聚类
  8. 8. 其他
  9. 9. 参考