爬取知乎全部话题-子话题

分析

使用Chome浏览器打开知乎的话题广场页面,可以看到页面中有话题分类,每个分类下面又有子话题,但每次默认只显示20个子话题,要获取全部的子话题,需要不断的“点击”下面的“更多”按钮,因此可以有两种方法来实现:

  • 通过selenium来实现(模拟鼠标点击)
  • 通过分析“点击”按钮触发时请求,模拟请求来进行抓取

selenium爬取方式速度慢,因此这里不对该方法的使用做进一步说明。此处针对模拟请求的方式来进行爬取:
首先,在浏览器界面,按下键盘F12,选择Network选项卡(建议勾选"Preseve log")。此时,点击知乎上的“互联网”话题,可以看到如下图所示的两个请求数据包:
img
由上图可知,当点击“互联网”话题时浏览器是通过发送POST请求来取得知乎话题数据。该请求的url地址为https://www.zhihu.com/node/TopicsPlazzaListV2,在POST请求的数据部分:topic_id指话题id(此处对应“互联网”话题的话题id就是99),offset是指偏移量,指每次执行next方法(method:next)加载的子话题数量,hash_id可以为空我们直接忽略。
此时我们点击网页中的更多来查看更多的子话题,并查看网络请求包的变化,如下图所示:
img
可以看出,当每次点击更多加载数据时,在POST请求的数据部分offset偏移递增20(点击多次更多可得道验证)。此外当我们切换话题时(如点击游戏/运动/艺术),POST请求的数据部分的topic_id都会相应改变。
因此为了爬取所有的话题和子话题,我们需要做两个工作:

  • 爬取所有的话题和相应的topic_id(爬取子话题时POST请求的数据部分需要topic_id)
  • 爬取所有的子话题,发送POST请求来进行抓取

爬取话题和对应的topic_id

这一步很简单,直接获取源码,然后通过xpath解析即可:
右键查看网页源代码

1
2
3
4
<li class="zm-topic-cat-item" data-id="253"><a href="#游戏">游戏</a></li>
<li class="zm-topic-cat-item" data-id="833"><a href="#运动">运动</a></li>
<li class="zm-topic-cat-item" data-id="99"><a href="#互联网">互联网</a></li>
...

可知,所有的话题和topic_id都在class="zm-topic-cat-item"<li>标签中,因此,话题和topic_id爬取代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
from lxml import etree

session = requests.session()
headers = {
"User-Agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/56.0.2924.76 Chrome/56.0.2924.76 Safari/537.36"
}

req = session.get("https://www.zhihu.com/topics", headers=headers)
selector = etree.HTML(req.text)
topicList = selector.xpath('//li[@class="zm-topic-cat-item"]')
#print len(topicList)
topicIDList = []
topicNameList = []
for topic in topicList:
topicIDList.append(topic.xpath('./@data-id')[0])
topicNameList.append(topic.xpath('./a/text()')[0])
topicDict = dict(zip(topicIDList, topicNameList))

最后的topicDict就是话题和对应的topic_id

爬取子话题

比较麻烦的就是子话题的抓取。如1.分析中的分析,我们要构建POST请求,主要是其中的数据部分:

1
2
3
4
data = {
"method":"next",
"params": '{"topic_id":' + str(topic_id) + ',"offset":' + str(offset) + ',"hash_id":""}'
}

2.爬取话题和对应的topic_id中我们有了全部的topic_idoffset每次递增20,因此POST请求的数据部分也构建完毕,我们查看能够得到我们想要的数据,得到的响应数据如下:

1
2
3
4
5
6
7
8
9
{
"r":0,
"msg":[
"<div class="item"><div class="blk"> <a target="_blank" href="/topic/19550757"> <img src="https://pic3.zhimg.com/127ee131a4487388e104da2bba7a4df6_xs.jpg" alt="腾讯"> <strong>腾讯</strong> </a> <p>中国最大的互联网综合服务提供公司,主营以腾讯网、QQ、微信、腾…</p> <a id="t::-176" href="javascript:;" class="follow meta-item zg-follow"><i class="z-icon-follow"></i>关注</a> </div></div>",
"<div class="item"><div class="blk"> <a target="_blank" href="/topic/19854644"> <img src="https://pic1.zhimg.com/e9c699fa4_xs.jpg" alt="余额宝"> <strong>余额宝</strong> </a> <p>余额宝创立于2013年6月,是蚂蚁金服旗下的一项余额增值服务和…</p> <a id="t::-103570" href="javascript:;" class="follow meta-item zg-follow"><i class="z-icon-follow"></i>关注</a> </div></div>",
"<div class="item"><div class="blk"> <a target="_blank" href="/topic/19551460"> <img src="https://pic3.zhimg.com/e75e39ed2_xs.jpg" alt="百度"> <strong>百度</strong> </a> <p>中国互联网公司之一,占有中国搜索引擎市场五成以上的份额。旗下有…</p> <a id="t::-413" href="javascript:;" class="follow meta-item zg-follow"><i class="z-icon-follow"></i>关注</a> </div></div>",
...
]
}

显然我们关注的数据都在"msg"字段中,并且"msg"字段对应的value是一个字符串数组(字符串内容是一个xml格式文本,可以直接使用xpath进行提取)。
至此,还有一个问题,每次offset递增20,什么时候不再递增了呢?通过进一步调试我们可以发现当sourceCodeDict["msg"] == []时表示当前话题的子话题都提取到了。

完整代码

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Usage: 话题广场话题和子话题抓取

import pymongo
import urllib2
from publicUtils import generateLogger
import requests
from lxml import etree
import json

errorLog = generateLogger("topicSquareError")
debugLog = generateLogger("topicSquareDebug")

def getTopics():
session = requests.session()
headers = {
"User-Agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/56.0.2924.76 Chrome/56.0.2924.76 Safari/537.36"
}

#爬取话题和对应的topic_id
req = session.get("https://www.zhihu.com/topics", headers=headers)
selector = etree.HTML(req.text)
topicList = selector.xpath('//li[@class="zm-topic-cat-item"]')
#print len(topicList)
topicIDList = []
topicNameList = []
for topic in topicList:
topicIDList.append(topic.xpath('./@data-id')[0])
topicNameList.append(topic.xpath('./a/text()')[0])
topicDict = dict(zip(topicIDList, topicNameList))
#print json.dumps(topicDict, ensure_ascii=False)

#爬取子话题
for topicID in topicDict:
url = "https://www.zhihu.com/topics#" + topicDict[topicID]
debugLog.debug("\n" + "--" * 36 + "\n" + topicDict[topicID] + "\n" + "--" * 36)
index = -20
while 1:
index += 20
#print "index:", index
data = {
"method":"next",
"params": '{"topic_id":' + str(topicID) + ',"offset":' + str(index) + ',"hash_id":""}'
}
try:
sourceCode = session.post("https://www.zhihu.com/node/TopicsPlazzaListV2", data=data, headers=headers).content
sourceCodeDict = json.loads(sourceCode)
subTopicList = sourceCodeDict["msg"]
if subTopicList == []:
break
for subTopic in subTopicList:
selector = etree.HTML(subTopic)
debugLog.debug(selector.xpath("//strong/text()")[0])
except Exception as ce:
break

if __name__ == "__main__":
getTopics()

References