Scrapy爬取知乎用户信息

目标

从一个大V用户开始,通过递归爬取粉丝列表和关注列表,以实现知乎所有用户详细信息的抓取。

(可选)将抓取结果储存到数据库中,并进行去重操作。

环境需求

Python3.6

通过miniconda创建python版本号为3.6的虚拟环境

conda create -n spider python=3.6

conda activate spider

Scrapy

pip install scrapy

注意,在安装scrapy框架前,还需安装下列库至虚拟环境中:

安装lxml库(pip)

安装pyOpenSSL库(pip)

安装Twisted库(wheel+pip)

安装PyWin32库(wheel+pip)

创建项目

在命令行通过以下命令创建一个项目:

scrapy startproject zhihu_user

创建爬虫

通过命令行进入到项目中,运行genspider命令创建一个spider

cd zhihu_user

scrapy genspider zhihu www.zhihu

禁止ROBOTSTXT_OBEY

将settings.py中的ROBOTSTXT_OBEY设为False:

ROBOTSTXT_OBEY = False

其默认为True,表示遵守robots.txt规则。 robots.txt 是遵循 Robot 协议的一个文件,它保存在网站的服务器中,它的作用是,告诉搜索引擎爬虫,本网站哪些目录下的网页不希望你进行爬取收录。在Scrapy启动后,会在第一时间访问网站的 robots.txt 文件,然后决定该网站的爬取范围。

加入请求头

未加入请求头headers中的User-Agent。访问知乎域名下的网页必须指定User-Agent,否则会被服务器检测为爬虫而遭封杀

添加方案:

在settings.py文件中设置,取消DEFAULT_REQUEST_HEADERS的注释,加入如下内容(全局设置):

DEFAULT_REQUEST_HEADERS = {

'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'

}

在创建spider中加入:

headers = {

'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36',

}

在Request的参数中加入。

本次爬取使用方案一。添加后在命令行中运行如下命令:

scrapy crawl zhihu

即可得到返回(200)的结果。未添加时,则会出现返回状态码出错的问题。

同时会出现重定向状态码(301)/(302),这是由于自动创建的爬虫的url开头为http而非https。

爬取流程分析

为探寻获取用户详细信息和关注列表的接口,回到网页并检查网页,打开控制台切换到Network模式。

选取一个知乎大V作为爬取开头,如:

通过观察个人信息页面,确定需要爬取的基本信息,如:姓名,签名,职业,关注数,赞同数等。

注:Ajax,即Asynchronous JavaScript and XML,指异步的JavaScript和XML,指利用JavaScript在保证页面不被刷新、页面链接不改变的情况下与服务器交换数据并更新部分网页的技术。

点击页面内选项卡中的关注,再翻页,可在控制台中发现出现了相应的Ajax请求。这个就是获取关注列表的接口。其形式如:

followees?include=data...

观察其请求结构(headers),请求方法为GET,URL为https://www.zhihu/api/v4/members/excited-vczh/followees?... ,后跟了三个参数,分别为include,offset,limit。可以发现,offset为偏移量,limit表示每页数量,结合这两项即可表示当前的页数。include中则是查询参数。

接下来查看返回结果(Preview),包括有data和paging两个字段。data中包含了关注列表的用户信息,每页有20个内容。paging内容中的字段则可用于请求下一页,其中is_end表示当前翻页是否结束,next则是下一页的链接。

由上,我们即可通过接口获取到获取关注列表了。

若将鼠标放在关注列表中的任意一个头像上,则又会出现新的Ajax请求。可以通过Network控制台中看到该次请求的链接为:

后面同样跟了一个include参数,其包含了一些查询参数。在该请求的返回结果(Preview)中几乎可以获得所有详情。因此我们可以通过该接口获取关注列表中的用户的详细信息。

总结:

有了上两条爬取逻辑后,即可开始构造请求。

构造请求

1.生成第一步请求

第一步即为请求起始用户(excited-vczh)的基本信息,然后再获取其关注列表。首先在之前创建的spider中删除原本的start_urls,新构造一个格式化的url,将其中一些可变参数提取出来,然后重写start_requsets方法,以生成第一步的请求。

同时,还需要实现两个解析方法parse_user和parse_follow。修改后代码如下:

import scrapy

class ZhihuSpider(scrapy.Spider):

name = 'zhihu'

allowed_domains = ['www.zhihu']

# 查询用户信息的url地址

user_url = 'https://www.zhihu/api/v4/members/{user}?include={include}'

user_query = 'allow_message,is_followed,is_following,is_org,is_blocking,employments,answer_count,follower_count,articles_count,gender,badge[?(type=best_answerer)].topics'

# 查询关注列表的url地址

follows_url = 'https://www.zhihu/api/v4/members/{user}/followees?include={include}&offset={offset}&limit={limit}'

follows_query = 'data[*].answer_count,articles_count,gender,follower_count,is_followed,is_following,badge[?(type=best_answerer)].topics'

# 起始用户

start_user = 'excited-vczh'

def start_requests(self):

yield scrapy.Request(self.user_url.format(user=self.start_user, include=self.user_query), callback=self.parse_user)

yield scrapy.Request(self.follows_url.format(user=self.start_user, include=self.follows_query, limit=20, offset=0), callback=self.parse_follows)

def parse_user(self, response):

print(response.text)

def parse_follows(self, response):

print(response.text)

注:在url中&用于转义,表示&

修改完后即可通过在命令行运行下属命令运行,并观察结果:

scrapy crawl zhihu

成功爬取得到结果。

2.编写parse_user

接下来处理爬取得到的用户基本信息,通过查看接口信息所返回的数据,在items.py中新声明一个UserItem:

import scrapy

class UserItem(scrapy.Item):

id = scrapy.Field()

name = scrapy.fleid()

type = scrapy.Field()

gender = scrapy.Field()

answer_count = scrapy.Field()

articles_count = scrapy.Field()

follower_count = scrapy.Field()

is_vip = scrapy.Field()

headline = scrapy.Field()

url_token = scrapy.Field()

url = scrapy.Field()

...

以爬取需要的信息。接下来在spider.py的解析方法里接析我们得到的response,然后转为json对象,依次判断字段是否存在,若存在则赋值:

def parse_user(self, response):

result = json.loads(response.text)

item = UserItem()

for field in item.fields:

if field in result.keys():

item[field] = result.get(field)

yield item

得到item后通过yield即可饭回。这样保存用户基本信息的步骤就完成了。接下来还需要获取该用户的关注列表,因此需要再发起一个获取关注列表的request。在parse_user后面在添加:

yield scrapy.Request(

self.follows_url.format(user=result.get('url_token'),

include=self.follows_query, limit=20, offset=0),

self.parse_follows)

这样就又生成了获取该用户关注列表的请求。

3. 编写prase_follows

同样的步骤处理关注列表。先解析response的文本,然后做两件事:

通过关注列表的每一个用户,对每一个用户发起请求,获取其详细信息

处理分页,判断paging内容,获取下一页的关注列表

改写parse_follows如下:

def parse_follows(self, response):

results = json.loads(response.text)

# 对用户关注列表的接析,json数据中有两个字段,分别为data和page,其中page是分页信息

if 'data' in results.keys():

for result in results.get('data'):

yield scrapy.Request(self.user_url.format(user=result.get('url_token'), include=self.user_query), self.parse_user)

## 判断page是否存在切is_end是否为false,即判断是否最后一页

if 'paging' in results.keys() and results.get('paging').get('is_end') == False:

next_page = results.get('paging').get('next')

## 获取下一页地址并返回request

yield scrapy.Request(next_page, self.parse_follows)

运行爬虫,成功爬取信息。

4. 编写prase_followers

通过获取关系列表实现循环递归爬取后,可以同样的方式获取用户的粉丝列表。经过分析后发现粉丝列表的api也类似,除了将followee换成follower外其他完全相同,所以我们也可通过同样的逻辑添加followers相关信息。

需要改动的位置有:

在zhihu.py中添加followers_url和followers_query

在start_requests中添加yield followers信息

在parse_user中添加yield followers信息

编写parse_followers

如此一来,该spider便完成了,我们便可通过其实现知乎社交网络的递归爬取。

完整的zhihu.py代码如下:

# -*- coding: utf-8 -*-

from zhihu_user.items import UserItem

import json

import scrapy

class ZhihuSpider(scrapy.Spider):

name = 'zhihu'

allowed_domains = ['www.zhihu']

# 查询用户信息的url地址

user_url = 'https://www.zhihu/api/v4/members/{user}?include={include}'

user_query = 'allow_message,is_followed,is_following,is_org,is_blocking,employments,answer_count,follower_count,articles_count,gender,badge[?(type=best_answerer)].topics'

# 查询关注列表的url地址

follows_url = 'https://www.zhihu/api/v4/members/{user}/followees?include={include}&offset={offset}&limit={limit}'

follows_query = 'data[*].answer_count,articles_count,gender,follower_count,is_followed,is_following,badge[?(type=best_answerer)].topics'

# 查询粉丝列表的url地址

followers_url = 'https://www.zhihu/api/v4/members/{iser}/followers?include={include}&offset={offset}&limit={limit}'

followers_query = 'data[*].answer_count,articles_count,gender,follower_count,is_followed,is_following,badge[?(type=best_answerer)].topics'

# 起始用户

start_user = 'excited-vczh'

def start_requests(self):

yield scrapy.Request(self.user_url.format(user=self.start_user, include=self.user_query), callback=self.parse_user)

yield scrapy.Request(self.follows_url.format(user=self.start_user, include=self.follows_query, limit=20, offset=0), callback=self.parse_follows)

yield scrapy.Request(self.followers_url.format(user=self.start_user, include=self.followers_query, limit=20, offset=0), callback=self.parse_followers)

def parse_user(self, response):

result = json.loads(response.text)

item = UserItem()

for field in item.fields:

if field in result.keys():

item[field] = result.get(field)

yield item

yield scrapy.Request(self.follows_url.format(user=result.get('url_token'), include=self.follows_query, limit=20, offset=0), self.parse_follows)

yield scrapy.Request(self.followers_url.format(user=result.get('url_token'), include=self.followers_query, limit=20, offset=9), self.parse_followers)

def parse_follows(self, response):

results = json.loads(response.text)

# 对用户关注列表的接析,json数据中有两个字段,分别为data和page,其中page是分页信息

if 'data' in results.keys():

for result in results.get('data'):

yield scrapy.Request(self.user_url.format(user=result.get('url_token'), include=self.user_query), self.parse_user)

## 判断page是否存在切is_end是否为false,即判断是否最后一页

if 'paging' in results.keys() and results.get('paging').get('is_end') == False:

next_page = results.get('paging').get('next')

## 获取下一页地址并返回request

yield scrapy.Request(next_page, self.parse_follows)

# 编写逻辑同parse_follows

def parse_followers(self, response):

results = json.loads(response.text)

if 'data' in results.keys():

for result in results.get('data'):

yield scrapy.Request(self.user_irl.format(user=result.get('url_token'), include=self.user.query), self.parse_user)

if 'paging' in results.keys() and results.get('paging').get('is_end') == False:

next_page = results.get('paging').get('next')

yield scrapy.Request(next_page, self.parse_followers)

在anaconda虚拟环境命令行中运行:

scrapy crawl zhihu -o zhihu.csv

可将爬取得到的内容保存成csv格式。json,xml,pickle,marshal亦同。

小结

在本次对知乎用户信息的爬取中,通过分析知乎用户的页面结构,对zhihu.py, item.py进行编写并实现了以下逻辑:

start_requests,通过从一个知乎大V用户,开始整个爬取过程

parse_user,实现了对用户详细信息和其关注与粉丝列表的获取

parse_follows,实现了通过关注列表重新请求用户并翻页的功能

parse_followers,实现了通过粉丝列表重新请求用户并翻页的功能

mongoDBpipelines,实现了将粉丝数据保存到数据库的功能

后续可进行的改进:

通过Redis实现分布式爬虫

数据清洗

Cookies池对接

代理池对接

额外信息

配置爬虫关闭的条件

在scrapy的默认配置文件中存在四个配置:

CLOSESPIDER_TIMEOUT = 0

CLOSESPIDER_PAGECOUNT = 0

CLOSESPIDER_ITEMCOUNT = 0

CLOSESPIDER_ERRORCOUNT = 0

该四个配置用于配置爬虫的自动关闭条件,等于0代表不开启。其中:

CLOSESPIDER_TIMEOUT表示指定爬虫运行的秒数

CLOSESPIDER_ITEMCOUNT表示爬虫爬取的条目数

CLOSESPIDER_PAGECOUNT表示爬虫爬取的响应数

CLOSESPIDER_ERRORCOUNT表示爬虫爬取可以接受的最大错误数

当这四个值不为0时,spider的过程中的任意一项参数超过配置数后,爬虫便会被自动关闭。运行时在命令行中设置:

scrapy crawl zhihu -s CLOSESPIDER_ITEMCOUNT=10

scrapy crawl zhihu -s CLOSESPIDER_PAGECOUNT=10

scrapy crawl zhihu -s CLOSESPIDER_TIMEOUT=10

scrapy crawl zhihu -s CLOSESPIDER_ERRORCOUNT=10

reference:

更多推荐

yield python3 知乎_GitHub - yuwenhou/zhihuuser: 爬取知乎user信息