python知乎话题数据爬取及关系图谱可视化

python知乎话题数据爬取及关系图谱可视化

一、前言

学习python大半年,也算基本入门。python作为一个简单易学且具有强大生产力的工具,在工作和生活中都提供了极大的助力,项目中写写脚本、爬爬数据,生活中帮朋友水水论文做做实验都还是能够勉强应付。虽然深知姿势水平还需极大提高,但是方向太多,不知如何着手。一直都对知乎数据很感兴趣,想做点什么,但始终没有静下心来付诸行动,最近忙里偷闲,琢磨着也许可以借此机会逼着自己开始去系列化的学习来提高自己。本次练手项目是基于知乎话题数据的简单可视化,包括知乎数据的搜集(共获取到33586个话题数据,不包含未分类话题以及丢失部分话题)和可视化两个部分。数据在文末。


先放可视化图

二、数据可视化:

可视化部分采用pyecharts包处理,chenjiandongx/pyecharts

1、关注人数维度分析

本次采集到知乎话题总数33586个,所有话题关注人数总量为592,534,475人,平均每个话题关注人数为17642人。其中关注人数最多的话题是【电影】话题,关注人数为17,839,771人;话题关注人数TOP10,如下图所示:

按照关注人数维度,将话题按照关注人数1000W以上、100W~1000W、10W~100W、1W~10W、0~1W、0 六个等级进行划分,其所占比例如下图所示:

从话题关注量来看,90%以上的话题其关注量都在1W以下,这些话题中,又有70%的话题关注量集中在1K以下。

2、话题关系可视化:

话题关系图谱以知乎话题为节点,父话题与子话题关系为连接线,节点越大表示关注度越高,按照节点关注量分布,将话题颜色标识为7个等级。

话题关系网络的产生,是输入一个话题,然后逐级迭代,直至最后一级,关系网络将是该话题下的整个话题树的关系展示。越是顶级的话题,其关系图谱会越复杂

【电影】话题关系图。先直观感受以下,关注量最多的【电影】话题的子话题关系图。点击图片会清晰一点(不知道如何在知乎上传高清大图...o(╯□╰)o)

下面列表是关注度最高的TOP10电影。

【明星】话题关系图

明星关注度列表TOP20,哎哟,周杰伦排第一哟。我渤哥关注量也过万了:-D

【演员】话题关系图,演员节点旁边的仓井优看成了苍井空(o(╯□╰)o)

演员列表top20

【旅行】话题关系图

旅行话题关注列表top20,自助游和穷游名列前茅Σ( ° △ °|||)︴,知乎穷游群体真这么大?

【知乎】和【豆瓣】话题关系图

【数据】话题关系图

【数据库】话题关系图

【数据分析】话题关系图

以及【Python】话题关系图

【男朋友】【女朋友】话题关系图谱

还有一些好玩儿的话题,比如:

【两性关系】话题下,子话题【追求女生】和【女追男】【追求男孩】三个话题,关注量分别是47746、16178、377,果然应了那句老话:

男追女隔座山 女追男隔层纱

(手动围笑)

三、关系图谱部分代码:

话题之间的关系在数据库中已经通过sql语句处理好了,代码部分调用SQL语句即可。


#获取节点信息
def get_nodes(name):
    db=psycopg2.connect(host="localhost",user="postgres",password="postgres",database="test")
    cur=db.cursor()
    nodes=[]
    cur.execute('select * FROM crawler.zh_topic_symbolSize(\''+str(name)+'\')')
    rows=cur.fetchall()
    for row in rows:
        nodes.append(row[0])
        print row[0]
    db.close()
    return nodes

#获取节点之间的链接关系
def get_links(name):
    db=psycopg2.connect(host="localhost",user="postgres",password="postgres",database="test")
    cur=db.cursor()
    links=[]
    cur.execute('select * FROM crawler.zh_topic_relaction(\''+name+'\')')
    rows=cur.fetchall()
    for row in rows:
        links.append(row[0])
        print row[0]
    db.close()
    return links

#画图
def relation_graph(nodes,links,name):
    category=[1,2,3,4,5,6,7,8]
    graph = Graph('知乎【'+name+'】话题关系图',width=1300, height=768,title_pos='left')
    graph.add("", nodes, links,category, label_pos="right", graph_repulsion=50,
          is_legend_show=True, line_curve=0.2,is_random=True,is_more_utils=True,is_label_show=True)
    graph.render('relationGraph_'+unicode(name,'utf8')+'.html')


if __name__ == '__main__':
    name=input('请输入要可视化的话题名称\n')
    nodes=get_nodes(name)
    links=get_links(name)
    relation_graph(nodes,links,name)

sql语句部分:

--找出该话题下的所有子话题,对应zh_topic_relaction函数
with RECURSIVE t1 as(
SELECT topicid,child_topicid,child_topicname from zh_topic_childinfo WHERE child_topicname=$1
UNION
SELECT t2.topicid,t2.child_topicid,t2.child_topicname from zh_topic_childinfo t2,t1 WHERE t2.topicid=t1.child_topicid
)
SELECT row_to_json(t) FROM(
SELECT t3.topicname as source,child_topicname as target from t1,zh_topic_info t3
WHERE t1.topicid=t3.topicid and child_topicname<>$1
)t;

--话题节点信息,对应zh_topic_symbolSize函数
with RECURSIVE t1 as(
SELECT topicid,child_topicid,child_topicname from zh_topic_childinfo WHERE child_topicname=$1
UNION
SELECT t2.topicid,t2.child_topicid,t2.child_topicname from zh_topic_childinfo t2,t1 WHERE t2.topicid=t1.child_topicid
)
,cnt as (select child_topicid from t1 GROUP BY child_topicid)
SELECT row_to_json(t) from(
SELECT topicname as name
,followers as value
,(CASE 
WHEN followers<=100 then 1
WHEN followers>100 and followers<=1000 then 4
WHEN followers>1000 and followers<=10000 then 8
WHEN followers>10000 and followers<=100000 then 12 
WHEN followers>100000 and followers<=100000 then 16 
WHEN followers>100000 and followers<=1000000 then 20 
ELSE 24 END )as "symbolSize"
,(CASE 
WHEN followers<=100 then 1
WHEN followers>100 and followers<=1000 then 2
WHEN followers>1000 and followers<=10000 then 3
WHEN followers>10000 and followers<=100000 then 4 
WHEN followers>100000 and followers<=100000 then 5 
WHEN followers>100000 and followers<=1000000 then 6 
ELSE 7 END )as "category"
--,tj as "symbolSize" 
FROM cnt
LEFT JOIN zh_topic_info 
ON cnt.child_topicid=zh_topic_info.topicid
)t;

四、数据搜集

整体思路:通过模拟登录进入根节点话题入口,采集根节点下子话题数据,再循环获取各子话题下数据,直到采集到全部话题数据。

1.模拟登录:知乎的话题数据需要登录才能看到,但是由于知乎的反爬虫机制,要抓取知乎的数据还需要费一番功夫。模拟登陆中需要解决的是验证码的问题,知乎变态的验证码是需要识别倒立的文字。在知乎上搜索了相关的解决方案,有部分网友给出了一些解决方案,分为手动识别和自动识别。参考资料如下:

python模拟自动登陆知乎和tesseract-ocr自动识别验证码

怎样用Python设计一个爬虫模拟登陆知乎?

blog.csdn.net/hudeyu777

虽然前人已经给咱探好了路,无奈水平太渣,最后还是卡在验证码的问题上,一直报错验证码失败(%>_<%)。

后来发现了这样一篇文章,Python模拟登陆万能法-微博|知乎,采用了selenium的方式模拟登陆,通过手动识别验证码登录,再将cookie值保存下来用requests请求数据。经过几番尝试终于成功登录知乎。

2.话题数据获取:知乎话题数据,一开始找到的数据入口(比如旅行)旅行,这里需要点击加载更多才能继续展开其子话题,通过抓包分析,发现了其点击‘加载更多’ 时的请求地址,并且返回数据为json格式。但是通过尝试发现,直接模拟请求改地址时,会进行短信二次验证,遂放弃该方式。后来发现了另一个话题数据入口:旅行,通过该地址可以获取到完整比较完整的话题数据,但是还是会损失一些话题数据,目前还没找到更好的入口。

3.免费代理IP:知乎反爬虫机制会检测账户或IP流量异常问题,当一段时间同一IP发起大量请求时,会让输入手机验证码。为解决这一问题,可以通过IP代理来处理,通过切换匿名代理IP发起请求。不过免费代理IP的速度死慢死慢的,实在受不了,所以我一般是先通过自己的ip爬数据,被检测到异常后,再切换代理IP

4.数据存储:由于在采集数据的过程中需要知道哪些子话题已经被抓取过,并且采集过程中可能会遇到各种问题导致采集中断,需要有相应的机制应对。

由于平时工作是玩儿数据库,因此喜欢把数据存在数据库中。本项目设计了两张表存储采集到的数据。

话题表存储已采集过的话题数据,包括话题id,话题名称,话题关注人数

子话题表存储已采集话题下的子话题数据,包括话题id,子话题id,子话题名称


针对数据爬取过程中会中断的问题,需要知道哪些子话题还未爬取,因此可以通过话题表的topicid和子话题表的child_topicid关联,不在话题表中的child_topicid即是还未采集的话题。

最后跌跌撞撞,抓到了3W多话题数据,数据总量上和其他网友抓到的数据较为接近。

项目爬虫部分完整代码:

# coding=utf-8
__author__ = 'zyx50'
import re
import time
import random
from bs4 import BeautifulSoup
import requests
from selenium import webdriver
import psycopg2
import traceback

获取动态IP

#获取动态代理IP
def getProxies():
    page = requests.get("http://www.xicidaili.com/nn", headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5)'})
    soup = BeautifulSoup(page.text, 'lxml')

    taglist = soup.find_all('tr', attrs={'class': re.compile("(odd)|()")})
    for trtag in taglist:
        tdlist = trtag.find_all('td')
        proxy = {'http': 'http://'+tdlist[1].string + ':' + tdlist[2].string,
             'https': 'http://'+tdlist[1].string + ':' + tdlist[2].string}
        url = "http://ip.chinaz.com/getip.aspx"  #用来测试IP是否可用的url
        try:
            requests.get(url, proxies=proxy, timeout=5)#测试IP是否可用
            return proxy
        except Exception, e:
            continue

通过查看用户个人信息来判断是否已经登录

def isLogin():
    url = "https://www.zhihu.com/settings/profile"
    login_code = session.get(url, headers=headers, allow_redirects=False).status_code
    if login_code == 200:
        print '登录成功'
    else:
        print '还未登录'

获取待抓取话题id

def getPendingTopic():
    global topicid
    db=psycopg2.connect(host="localhost",user="postgres",password="postgres",database="test")
    cur=db.cursor()
    cur.execute('SELECT childtopic FROM crawler.get_child_topic_uncraw()')
    topic=cur.fetchall()
    # 判断是否还有话题未抓取
    if len(topic)>0:
        topicid=topic[0][0]
        return True
    else:
        topicid=0
        print '所有话题抓取完毕!'
        return False

获取待抓取话题信息

def getTopic(topicId,proxyip):
    url = 'https://www.zhihu.com/topic/'+str(topicId)+'/organize'
    html= session.get(url, headers=headers,proxies=proxyip,timeout=10)
    soup=BeautifulSoup(html.text)

    #get话题关注人数
    if soup.find(class_='zm-topic-side-followers-info').strong is None: #存在关注者为0的情况
        topicinfo='('+str(topicId)+',\''+topicName+'\',0)'
    else:
        fellower=soup.find(class_='zm-topic-side-followers-info').strong.string
        topicinfo='('+str(topicId)+',\''+topicName+'\','+str(fellower)+')'#话题信息字符串

    #get子话题数据
    child_topic_list=soup.find(id='zh-topic-organize-child-editor')
    child_topics=child_topic_list.find_all(class_='zm-item-tag')
    child_topicinfo=''
    for child_topic in child_topics:
        child_topicid=child_topic.get('data-token')
        child_topicname=child_topic.get_text().replace('\n','').replace('\'','')
        child_topicinfo=child_topicinfo+'('+str(topicId)+','+str(child_topicid)+',\''+child_topicname+'\'),'#子话题信息字符串
    return topicinfo,child_topicinfo

保存话题信息

def saveTopic(topicInfo,childInfo):
    db=psycopg2.connect(host="localhost",user="postgres",password="postgres",database="test")
    cur=db.cursor()
    if topicInfo!='':
        topicSqlStr='insert into crawler.zh_topic_info(topicid,topicname,followers) VALUES '+topicInfo
        print '  话题:',topicSqlStr
        cur.execute(topicSqlStr)
    if childInfo!='':
        childSqlStr='insert into crawler.zh_topic_childinfo(topicid,child_topicid,child_topicname) values '+childInfo[:-1]
        print '子话题:',childSqlStr
        cur.execute(childSqlStr)
    db.commit()
    db.close()

采用 selenium调用浏览器进行登录,并把cookie 值传递给requests,来模拟登录抓取数据

if __name__ == '__main__':
    driver = webdriver.Chrome('C:\Program Files (x86)\Google\Chrome\Application\chromedriver.exe')
    driver.get('http://www.zhihu.com/login/email')
    driver.find_element_by_css_selector('[href="#signin"]').click()
    driver.find_element_by_css_selector('[class="signin-switch-password"]').click()
    driver.find_element_by_xpath("//input[@name='account']").send_keys('XXXXXXXXXXXXXX@qq.com')
    driver.find_element_by_xpath("//input[@name='password']").send_keys('XXXXXXXXXXXXXX')
    time.sleep(5)  # 手动输入验证码
    driver.find_element_by_xpath("//button[@class='sign-button submit']").click()
    time.sleep(2)  # 等待页面跳转

    session = requests.Session()
    cookies = driver.get_cookies()
    for cookie in cookies:
        session.cookies.set(cookie['name'],cookie['value'])
        if cookie['name']=='_xsrf':
            xsrf=cookie['value']
    headers = {
    "Host": "www.zhihu.com",
    "Referer": "https://www.zhihu.com/",
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0',
}
    #验证是否登录成功
    isLogin()

    #话题抓取主体程序
    topicid=0#定义初始话题编号
    crawedCount=0#定义初始已抓取数量
    proxyip=''
    while getPendingTopic():
        print '----------本次已抓取话题数:',crawedCount,'个;当前主题ID为:',topicid,'代理IP地址为:',proxyip,'------------'
        try:
            str1,str2,str3=getTopic(topicid,proxyip)
            saveTopic(str1,str2,str3)
        except Exception,e:
            print 'traceback.format_exc():\n%s' % traceback.format_exc()
            # proxyip=getProxies()使用代理IP地址
            continue
        time.sleep(random.randint(0,1))#控制爬取速度
        crawedCount=crawedCount+1#已抓取话题计数

数据地址pan.baidu.com/s/1i46uCZ 密码 2m9p

编辑于 2017-10-04