前言
本系列将全面涉及本项目从爬虫、数据提取与准备、数据异常发现与清洗、分析与可视化等细节,并将代码统一开源在GitHub:DesertsX/gulius-projects ,感兴趣的朋友可以先行 star 哈。
请先阅读“中国年轻人正带领国家走向危机”,这锅背是不背? 一文,以对“手把手教你完成一个数据科学小项目”系列有个全局性的了解。上一篇文章(1)数据爬取里我讲解了如何用爬虫爬取新浪财经《中国年轻人正带领国家走向危机》一文的评论数据,其中涉及的抓包过程是蛮通用的,大家如果想爬取其他网站,也会是类似的流程,当然介绍的比较粗浅,可自行深入扩展学习。
数据提取
读取数据
读取前10行数据,每行是评论区一页包含的数据:
import pandas as pd df = pd.read_csv('Sina_Finance_Comments_1_20180811.csv',encoding='utf-8') df.head(10)
上回存储数据时存了'page'、'jsons'、'cmntlist'、'replydict' 四列,分别对应页数、每页全部数据(仅存这列也行)、从 jsons 里提取出来的含每页20条的评论主体数据、从 jsons 里提取出来的互相回复的评论数据,这部分后续暂没挖掘,感兴趣的试着将所有节点绘制成网络,虽然效果如何不确定。两篇旧文可供参考:Gephi绘制微博转发图谱:以“@老婆孩子在天堂”为例,配图也是专栏的logo:
374名10万+知乎大V(一):相互关注情况,均用 Gephi 实现
扯远了。将cmntlist
列的元素转换成列表格式(列表嵌套‘列表’,因为每个元素本身也是‘列表’),并打印元素格式发现看起来是‘列表’,其实字符串格式,需要用 eval()
实现将列表样、字典样的字符串转换成列表或字典:
cmntlist = df.cmntlist.values.tolist()print(len(cmntlist))print(type(cmntlist[0])) # str 字符串格式#print(cmntlist[0]) # 篇幅太长占地方print(cmntlist[0][:200]) # 截取前200字符展示
输出结果是,列表共191个元素,对应评论总页数,如果读者重新运行了爬虫,因为新增评论数,此处会不同;每个列表里的元素,也就是表格中该列的每个元素均为字符串;截取前200个字符便于展示:
191<class 'str'> [{'comment_imgs': '', 'parent_mid': '0', 'news_mid_source': '0', 'rank': '0', 'mid': '5B6B9FA6-777B83B3-68B55EBD-8C5-8E4', 'video': '', 'vote': '0', 'uid': '1756716733', 'area': '广东深圳', 'channel_sourc
同理,replydict
列情况相同。
eval() 一下
用eval()
函数将 str 字符串格式转换成 list 或 dict 并由 apply
应用到相应列上:
df['jsons'] = df.jsons.apply(lambda x: eval(x)) df['cmntlist'] = df.cmntlist.apply(lambda x: eval(x)) df['replydict'] = df.replydict.apply(lambda x: eval(x)) df.head()
表格数据看不出变化,但元素格式研究变化了。
准备工作
再次将 cmntlist
列的数据转换成列表格式,方便后面遍历和提取每条评论相关的数据
cmntlists[0][0]
为第一页第一个元素对应的评论数据,是字典形式,每条评论能拿到的数据就是这些,后面要提取的信息也主要是这些字段里筛选。
cmntlists = df['cmntlist'].values.tolist()print(len(cmntlists),len(cmntlists[0]),cmntlists[0][0])
输出总页数,每页评论数,第一页第一个元素对应的评论数据:
191 20 {'comment_imgs': '', 'parent_mid': '0', 'news_mid_source': '0', 'rank': '2', 'mid': '5B6B7C0A-315A56DA-184F9F245-8C5-89E', 'video': '', 'vote': '0', 'uid': '6525940293', 'area': '江苏南京', 'channel_source': '', 'content': '贷款买房你怎么说?[二哈][二哈]', 'nick': '用户6525940293', 'hot': '0', 'status_uid': '1663612603', 'content_ext': '', 'ip': '49.90.86.218', 'media_type': '0', 'config': 'wb_verified=0&wb_screen_name=用户6525940293&wb_cmnt_type=comment_status&wb_user_id=6525940293&wb_description=&area=江苏南京&wb_parent=&wb_profile_img=http%3A%2F%2Ftvax2.sinaimg.cn%2Fdefault%2Fimages%2Fdefault_avatar_male_50.gif&wb_time=2018-08-09 07:26:02&wb_comment_id=4271006493624688', 'channel': 'cj', 'comment_mid': '0', 'status': 'M_PASS', 'openid': '', 'newsid_source': '', 'parent': '', 'status_cmnt_mid': '4271006493624688', 'parent_profile_img': '', 'news_mid': '0', 'parent_nick': '', 'newsid': 'comos-hhkuskt2879316', 'parent_uid': '0', 'thread_mid': '0', 'thread': '', 'level': '0', 'against': '1533770764', 'usertype': 'wb', 'length': '17', 'profile_img': 'http://tvax2.sinaimg.cn/default/images/default_avatar_male_50.gif', 'time': '2018-08-09 07:26:04', 'login_type': '0', 'audio': '', 'agree': '2'}
将上述操作后cmntlists
就是嵌套列表,为了后续遍历提取数据方便,将其整合成一个列表,每个元素就是一条评论的格式,直接用sum()
函数即可,举个栗子,下面的代码结果是[1, 2, 3, 2, 1]
:
sum([[1,2],[3,2,1]], [])
用到 cmntlists
上:
cmntlist = sum(cmntlists, [])print(len(cmntlist)) print(cmntlist[0])
输出总评论数和全部评论里的第一条,准确的说是时间最近的一条评论,为字典格式,所需要提取的数据就都在这里了,可自行选出感兴趣的参数进行提取:
3743 Out[15]: {'against': '1533770764', 'agree': '2', 'area': '江苏南京', 'audio': '', 'channel': 'cj', 'channel_source': '', 'comment_imgs': '', 'comment_mid': '0', 'config': 'wb_verified=0&wb_screen_name=用户6525940293&wb_cmnt_type=comment_status&wb_user_id=6525940293&wb_description=&area=江苏南京&wb_parent=&wb_profile_img=http%3A%2F%2Ftvax2.sinaimg.cn%2Fdefault%2Fimages%2Fdefault_avatar_male_50.gif&wb_time=2018-08-09 07:26:02&wb_comment_id=4271006493624688', 'content': '贷款买房你怎么说?[二哈][二哈]', 'content_ext': '', 'hot': '0', 'ip': '49.90.86.218', 'length': '17', 'level': '0', 'login_type': '0', 'media_type': '0', 'mid': '5B6B7C0A-315A56DA-184F9F245-8C5-89E', 'news_mid': '0', 'news_mid_source': '0', 'newsid': 'comos-hhkuskt2879316', 'newsid_source': '', 'nick': '用户6525940293', 'openid': '', 'parent': '', 'parent_mid': '0', 'parent_nick': '', 'parent_profile_img': '', 'parent_uid': '0', 'profile_img': 'http://tvax2.sinaimg.cn/default/images/default_avatar_male_50.gif', 'rank': '2', 'status': 'M_PASS', 'status_cmnt_mid': '4271006493624688', 'status_uid': '1663612603', 'thread': '', 'thread_mid': '0', 'time': '2018-08-09 07:26:04', 'uid': '6525940293', 'usertype': 'wb', 'video': '', 'vote': '0'}
细心的朋友应该能看到评论数据中不仅有城市数据,而且还有 IP 数据,因为知道只有 ip 查询的网站,所以这次再写个爬虫将 IP 查询返回的数据一并进行存储。并且打算结合地理位置数据和评论时间可以绘制下评论变化的全国热力图等,示例如下,很酷炫,实现方式却很简单,手把手教你完成:(送福利)BDP绘制微博转发动态热力图。看图里的时间也正好是一年前了,时光荏苒.....
再开始写 IP 查询的爬虫前,先输出10条评论里的 IP 和地理数据:
for num,cmnt in enumerate(cmntlist): print(cmnt['ip'], cmnt['area']) if num==10:break
49.90.86.218 江苏南京 59.44.235.75 辽宁铁岭 110.53.17.193 湖南怀化 117.136.32.92 广东广州 49.66.22.166 江苏无锡 117.136.0.149 北京 106.122.202.117 福建厦门 106.122.202.117 福建厦门 221.220.138.231 北京 39.68.204.68 山东济宁 219.232.34.181 北京
网上搜到一个 ip 查询的网站:https://ip.cn/ 换成其他网站亦可。
ip 查询的原理并不了解,拿到的数据是否准确也无从考证,本回也仅是根据此信息来挖掘,不过和原本评论数据里自带的城市可以对照下,发现大多是一致的,数据应该还算靠谱。不过听 @lxghost 说也可能 IP 是假的,自己科学上网时的 IP 也不准确。
后面查询评论里的 ip 时也有不少海外的,不知道是真实的呢,还是科学上网的假象等,无从知晓。
右键“审查元素” -> Network -> ALL -> 复制需查询的 IP 到输入框并点击查询 -> 找到4中的爬虫入口 URL 格式为https://ip.cn/index.php?ip=49.90.86.218
-> 在Preview里找到查询结果的信息在网页源代码里:
于是测试下,先拿到网页源代码:
import requestsdef ip2loc(ip): url = 'https://ip.cn/index.php?ip={}'.format(ip) r = requests.get(url).text return r text = ip2loc(cmntlist[0]['ip']) print(text)
具体查询结果在这部分源代码里:
<div id="result"><div class="well"><p>您查询的 IP:<code>49.90.86.218</code></p><p>所在地理位置:<code>江苏省南京市 电信</code></p><p>GeoIP: Nanjing, Jiangsu, China</p><p>China Telecom</p></div></div>
信息提取可用正则表达式 re 或 BeautifulSoup ,不过这里用的是 xpath,(Python爬虫利器三之Xpath语法与lxml库的用法 ),右键“审查元素 -> 点新窗口左上角的鼠标logo ->然后选中网页内容后会自动定位到源代码里位置 -> 右键 ‘Copy’ -> ‘Copy Xpath’,自动生成路径,复制到代码里即可:
from lxml import etree html = etree.HTML(text) loc = html.xpath('//div[@id="result"]/div/p[2]/code/text()')[0] tele = html.xpath('//div[@id="result"]/div/p[4]/text()')[0] geo_ip = html.xpath('//div[@id="result"]/div/p[3]/text()')[0] print(loc,'*',tele,'*',geo_ip)
能获取到查询结果,表明爬虫代码 OK:
江苏省南京市 电信 * China Telecom * GeoIP: Nanjing, Jiangsu, China
然后测试时发现有些 IP 查询结果可能少些数据,所以异常时返回空字符串,输出前30条测试数据,结果不贴了:
%%time # 耗时 Wall time: 36 simport requestsfrom lxml import etreeimport timeimport randomdef ip2loc(ip): url = 'https://ip.cn/index.php?ip={}'.format(ip) text = requests.get(url).text html = etree.HTML(text) try: loc = html.xpath('//div[@id="result"]/div/p[2]/code/text()')[0] except:loc='' try: geo_ip = html.xpath('//div[@id="result"]/div/p[3]/text()')[0] except:geo_ip='' try: tele = html.xpath('//div[@id="result"]/div/p[4]/text()')[0] except:tele='' return loc+' * '+geo_ip+' * '+telefor num,cmnt in enumerate(cmntlist): ip_loc = ip2loc(cmnt['ip']) print(num, cmnt['ip'], cmnt['area'], ip_loc) if num%5==0: time.sleep(random.randint(0,2)) if num==30:break
数据提取与保存
需查询3千多个 IP 还蛮耗时的(其实后面也没用到这部分数据,大家可以将 IP 查询部分注释掉,并将 DataFrame 的字段删除即可)。
注意设置间隔时间,以免对 IP 查询的网站造成侵扰。最后遍历评论数据,提取感兴趣的数据,并存储到新的 CSV 中方便后续分析挖掘。
%%time import time import random# sinanews_comments = pd.DataFrame(columns = ['No','page','nick','time','content','area', 'ip','ip_loc','length','against','agree', 'channel', 'hot', 'level', 'login_type', 'media_type', 'mid']) page=1for num,cmnt in enumerate(cmntlist): nick = cmnt['nick'] times = cmnt['time'] #命名成 time 会和下面 time.sleep() 冲突 # 所以命名成 times content = cmnt['content'] area = cmnt['area'] ip = cmnt['ip'] ip_loc = ip2loc(cmnt['ip']) length = cmnt['length'] against = cmnt['against'] agree = cmnt['agree'] channel = cmnt['channel'] hot = cmnt['hot'] level = cmnt['level'] login_type = cmnt['login_type'] media_type = cmnt['media_type'] mid = cmnt['mid'] print(num+1,page,times,nick,content,area,ip_loc) sinanews_comments = sinanews_comments.append({'No':num+1,'page':page,'nick':nick,'time':times,'content':content,'area':area, 'ip':ip,'ip_loc':ip_loc,'length':length,'against':against,'agree':agree, 'channel':channel,'hot':hot,'level':level,'login_type':login_type, 'media_type':media_type,'mid':mid},ignore_index=True) if num%30 == 0: time.sleep(random.randint(0,1)) if int((num+1)%20) == 0: page += 1
作者:古柳_Deserts_X
链接:https://www.jianshu.com/p/4426fb622fb3