如何用爬虫爬取知乎专栏信息?

知乎专栏和知乎总站不一样,是基于AngularJS架构的,所有专栏网页源代码根本是一样的,请问怎样爬取特定的信息呢? 举个例子,想获取每个专栏的关注数。
关注者
452
被浏览
16,282

6 个回答

知乎专栏API来一发?

举俩栗子:
URI: http://zhuanlan.zhihu.com/api/columns/jixin GET/HTTP 1.1
访问上面的URI,浏览器地址栏里直接粘贴也行,得到的返回JSON数据就包含了专栏关注数。

不管AngularJS还是其它架构,都是服务端的东西,再天花乱坠的服务端架构,到了客户端终究逃不脱HTTP协议,至少目前来说还是如此。

顺便分享一些关于爬知乎的东西。
目前来说还没有官方API的支持,可能最有用的也就是用户的“个性网址”(好别扭,下称UID)了,譬如黄继新老师的UID: jixin,不过可以由用户本人修改,但每个用户一定唯一。

以{{%UID}}代替相应的UID。

1. 获得用户专栏入口:
URI: http://www.zhihu.com/people/{{%UID}}/posts GET/HTTP 1.1
XPATH: //div[@id='zh-profile-list-container']
解析上述内容,可获得该用户所有的专栏入口地址。

2. 获得专栏文章信息:
URI: http://zhuanlan.zhihu.com/api/columns/{{%UID}}/posts?limit={{%LIMIT}}&offset={{%OFFSET}} GET/HTTP 1.1
{{%LIMIT}}: 表示该次GET请求获取数据项的数量,即专栏文章信息数量。我没有具体测试过最大值为多少,但是可以设置为比默认值大。默认值为10。
{{%OFFSET}}: 表示该次GET请求获取数据项的起始偏移。
解析上述内容,可以获得每篇专栏文章的信息,比如标题、题图、专栏文章摘要、发布时间、赞同数等。该请求返回JSON数据。
注意:解析该信息时,可以获得该篇专栏文章的链接信息。

3. 获得专栏文章:
URI: http://zhuanlan.zhihu.com/api/columns/{{%UID}}/posts/{{%SLUG}} GET/HTTP 1.1
{{%SLUG}}: 即为2中获得的文章链接信息,目前为8位数字。
解析上述内容,可以获得专栏文章的内容,以及一些文章的相关信息。该请求返回JSON数据。

上述这些应该足够满足题主的要求了。最重要的还是要善用Chrome调试工具,此乃神器!

* * * * * * * * * *
以下是一些零散的更新,用于记录知乎爬虫的想法。当然,相关实现还是要尊重ROBOTS协议,可以通过zhihu.com/robots.txt查看相关参数。

UID是对应该用户所有信息的入口。

虽然用户信息有修改间隔限制(通常为若干月不等),但考虑到即使是修改用户名的操作也会使得UID变更,进而令先前的存储失效。当然这也是可以突破的:用户hash。这个hash值为32位字符串,对每个账号是唯一且不变的。

通过UID获得hash:
URI: http://www.zhihu.com/people/%{{UID}} GET/HTTP 1.1
XPATH: //body/div[@class='zg-wrap zu-main']//div[@class='zm-profile-header-op-btns clearfix']/button/@data-id
解析上述内容,可获得UID对应的hash值。(没错,这个值就是存在“关注/取消关注”这个按钮里的。)这样即可唯一标识用户。

目前还没有找到方法通过hash_id获得UID,但是有间接方法可以参考:通过关注列表定期检查用户信息是否变更,当然关注/取消关注操作也可以自动化:
关注操作
URI: http://www.zhihu.com/node/MemberFollowBaseV2 POST/HTTP 1.1
Form Data
method: follow_member
params: {"hash_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
_xsrf: <xsrf>

取消关注操作
URI: http://www.zhihu.com/node/MemberFollowBaseV2 POST/HTTP 1.1
Form Data
method: unfollow_member
params: {"hash_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
_xsrf: <xsrf>

知乎爬虫需要一份UID列表才能正常运转,如何获得这份列表是一个需要考虑的问题。目前一个可行的想法是选定若干大V用户,批量爬取其被关注列表。举例来说,张公子目前被关注数达到58W+,通过:

URI: http://www.zhihu.com/node/ProfileFollowersListV2 POST/HTTP 1.1
Form Data
method: next
params: {"offset": {{%OFFSET}}, "order_by": "hash_id", "hash_id": "{{%HASHID}}"}
_xsrf: <xsrf>
每次可以获得20条关注者的用户信息。这些信息中包含hash_id、用户名、UID、关注/被关注数、、提问数、回答数等。
这里以个人专栏做一下示例获取专栏所有文章信息和专栏相关信息(包含代码)
第一步:专栏地址:zhuanlan.zhihu.com/pass
后面的passer是作为专栏的唯一标识。通过任何一工具抓包,得到url:
zhuanlan.zhihu.com/api/
返回的json数据。
抓包出来的数据展示为(用浏览的f12抓包):

对json数据进行解析。
大致代码如下(对于以下代码,如果嫌麻烦可以自行把生成验证码部分的代码删除。请求多次之后会无效):
#encoding=utf8
import time
import requests
from bs4 import BeautifulSoup

Default_Header = {'X-Requested-With': 'XMLHttpRequest',
                  'Referer': 'http://www.zhihu.com',
                  'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; '
                                'rv:39.0) Gecko/20100101 Firefox/39.0',
                  'Host': 'www.zhihu.com'}
_session = requests.session()
_session.headers.update(Default_Header) 

BASE_URL = 'https://www.zhihu.com'
CAPTURE_URL = BASE_URL+'/captcha.gif?r='+str(int(time.time())*1000)+'&type=login'
PHONE_LOGIN = BASE_URL + '/login/phone_num'



def login():
    '''登录知乎'''
    username = ''#你自己的帐号密码
    password = ''
    cap_content = _session.get(CAPTURE_URL).content
    cap_file = open('/root/Desktop/cap.gif','wb')
    cap_file.write(cap_content)
    cap_file.close()
    captcha = raw_input('capture:')
    data = {"phone_num":username,"password":password,"captcha":captcha}
    r = _session.post(PHONE_LOGIN, data)
    print (r.json())['msg']
    
def zhuanlan_info():
    Default_Header = {
                  'Referer': 'https://zhuanlan.zhihu.com/passer',
                  'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; '
                                'rv:39.0) Gecko/20100101 Firefox/39.0',
                  'Host': 'zhuanlan.zhihu.com'}
    _session = requests.session()
    _session.headers.update(Default_Header)
    HtmlContent = _session.get('https://zhuanlan.zhihu.com/api/columns/passer')
    HtmlContent = HtmlContent.json()
    print '专栏名称   :'+HtmlContent['name'].encode('utf-8')
    print '专栏关注人数:'+str(HtmlContent['followersCount'])
    print '专栏文章数量:'+str(HtmlContent['postsCount'])
    print '专栏介绍   :'+HtmlContent['description'].encode('utf-8')
    print '专栏创建者相关信息:'
    print '1、地址::'+HtmlContent['creator']['profileUrl'].encode('utf-8')
    print '2、个签::'+HtmlContent['creator']['bio'].encode('utf-8')
    print '3、昵称::'+HtmlContent['creator']['name'].encode('utf-8')
    print '4、hash::'+HtmlContent['creator']['hash'].encode('utf-8')
    print '5、介绍::'+HtmlContent['creator']['description'].encode('utf-8')
运行抓取的结果是:
专栏名称 :学习编程
专栏关注人数:43387
专栏文章数量:39
专栏介绍 :莫道君行早,更有早行人。
全心敲代码,天道自酬勤。
专栏创建者相关信息:
1、地址::zhihu.com/people/sgai
2、个签::教你如何编程
3、昵称::路人甲
4、hash::eaf435b228ce0b038a4afe8203f59b49
5、介绍::爱编程、跑步、旅游、摄影
学习编程专栏:zhuanlan.zhihu.com/pass
已有女友,勿扰。

至此专栏信息抓取完毕。
第二步:抓文章信息。
zhuanlan.zhihu.com/api/
limit后面的参数值表示:限制每次下拉刷新请求的文章数量,这里无论你怎么修改知乎限定了20,后面的offset值表示从哪一篇文章开始。如果是20表示从第二十20篇文章开始往后请求,请求更久的文章。

如果我们想抓取一个专栏的所有文章,我们不难想到,一次一次请求连接起来获取到所有的文章列表以及内容。
比如这样:
第一次:limit = 20 , offset = 0
第二次:limit = 20 , offset = 20
第三次:limit = 20 , offset = 40
...
如此可以看出每次设定起始位置+20即可,limit不变,这两个参数进行请求,获取json数据,对json数据进行解析。
大致代码如下(再说一点关于帐号密码为手机帐号密码,此处如需邮箱帐号密码登录只需要改一下登录地址和参数名称请自行修改):
#encoding=utf8
import time
import requests
from bs4 import BeautifulSoup

Default_Header = {'X-Requested-With': 'XMLHttpRequest',
                  'Referer': 'http://www.zhihu.com',
                  'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; '
                                'rv:39.0) Gecko/20100101 Firefox/39.0',
                  'Host': 'www.zhihu.com'}
_session = requests.session()
_session.headers.update(Default_Header) 

BASE_URL = 'https://www.zhihu.com'
CAPTURE_URL = BASE_URL+'/captcha.gif?r='+str(int(time.time())*1000)+'&type=login'
PHONE_LOGIN = BASE_URL + '/login/phone_num'
BASE_ZHUANLAN_API = 'https://zhuanlan.zhihu.com/api/columns/'
BASE_ZHUANLAN = 'https://zhuanlan.zhihu.com'



def login():
    '''登录知乎'''
    username = ''#你得帐号密码(此处为手机帐号)
    password = ''
    cap_content = _session.get(CAPTURE_URL).content
    cap_file = open('cap.gif','wb')
    cap_file.write(cap_content)
    cap_file.close()
    captcha = raw_input('capture:')
    data = {"phone_num":username,"password":password,"captcha":captcha}
    r = _session.post(PHONE_LOGIN, data)
    print (r.json())['msg']
    
def zhuanlan_text():
    Default_Header = {
                  'Referer': 'https://zhuanlan.zhihu.com/passer',
                  'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; '
                                'rv:39.0) Gecko/20100101 Firefox/39.0',
                  'Host': 'zhuanlan.zhihu.com'}
    _session = requests.session()
    _session.headers.update(Default_Header)
    TextAPI = BASE_ZHUANLAN_API+'passer/posts?limit=20&offset='
    endFlag = True
    offset = 0
    while endFlag:
        TextContentHTML = (_session.get(TextAPI+str(offset))).json()
        for everText in TextContentHTML:
            print '文章作者相关:'
            print '1、地址::'+everText['author']['profileUrl'].encode('utf-8')
            print '2、个签::'+everText['author']['bio'].encode('utf-8')
            print '3、昵称::'+everText['author']['name'].encode('utf-8')
            print '4、hash::'+everText['author']['hash'].encode('utf-8')
            print '5、介绍::'+everText['author']['description'].encode('utf-8')
            print '文章标题   :'+everText['title'].encode('utf-8')
            print '文章地址    :'+BASE_ZHUANLAN+everText['url'].encode('utf-8')
            print '文章推送时间:'+everText['publishedTime'].encode('utf-8')
            print '文章评论数量:'+str(everText['commentsCount'])
            print '文章点赞数量:'+str(everText['likesCount'])
            print '文章内容   :'+everText['content'].encode('utf-8')
        if(len(TextContentHTML) < 20):
            endFlag = False
        offset = offset + 20 
运行的部分结果截图:

以上~