原创文章,转载、引用请注明出处!
前言 刚好在摸鱼的时候比较喜欢刷知乎,又碰到了这个课程,所以就尝试了做这个内容。以前从来没有接触过文本分析和爬虫这类的技术,就当学新技术了。以及这些在去年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 ): 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)' ] 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 ): 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 ): 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 ): 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 ): 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 ): 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 ): 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)): csv_data[i3][j] = re.sub(u"\\<.*?\\>" , " " , csv_data[i3][j]) 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 ): 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 WordCloudnow_time= datetime.datetime.now() print("2.词云" ) 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.analysenow_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 SnowNLPnow_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 itemgetterprint('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 )) 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 TfidfVectorizernow_time= datetime.datetime.now() print('5.2. tfidf matrix' ) 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) print(tfidf_matrix.shape) print('Runtime:%d s' %(datetime.datetime.now()-now_time).seconds) from scipy.cluster.hierarchy import ward, dendrogram, linkagefrom sklearn.metrics.pairwise import cosine_similaritynow_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 , leaf_font_size=12 ) 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 条回答数据-csdnhttps://blog.csdn.net/wenxuhonghe/article/details/86515558