1.创建项目
在工作目录下cmd中创建scrapy项目:
scrapy startproject jianshupro
使用pycharm打开项目后。在terminal中传递到/spiders目录下,执行“
scrapy genspider jianshu www.jianshu.com
项目结构:
2.项目配置
1.配置settings:
机器人守则
ROBOTSTXT_OBEY = False
#间隔时间
DOWNLOAD_DELAY = 0.5
管道配置先不设置。
2.配置启动文件
在项目下新建main.py,添加:
from scrapy import cmdline
cmdline.execute('scrapy crawl jianshu'.split())
3.start_request()
在这个函数之前,首先要设置请求头的参数,在
import scrapy
from fake_useragent import UserAgent
class JianshuSpider(scrapy.Spider):
name = 'jianshu'
# allowed_domains = ['www.jianshu.com']
# start_urls = ['http://www.jianshu.com/']
# 基础头
base_headers = {'Accept-Language': 'zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4',
'Host': 'www.jianshu.com',
'Accept-Encoding': 'gzip, deflate, sdch',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'text/html, */*; q=0.01',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36',
'Connection': 'keep-alive',
'Referer': 'http://www.jianshu.com'}
# 生成随机的用户代理
ajax_headers = dict(base_headers, **{"X-PJAX": "true", 'User-Agent': UserAgent().random})
start_requests爬虫生命周期的启动方法,读取起始网址列表,向爬虫发送Request对象:让引擎回调parse().
也就是说这个函数必须yield一个Request对象,包含url,回调函数以及headers参数。
3.1测试start_request()
我们要爬取的数据为简书上的用户信息,所以在简书上的推荐作者页面进行爬取:
在这个页面中没当点击加载更多按钮时,会发起ajax请求,给https://www.jianshu.com/recommendations/users?page=num
num便是页数,返回的数据会以html格式拼接在当前页中。每点一次按钮,num+1。
num =1的时候也是可以展示出来第一页的内容的,与当前的页面显示相同内容。
所以在spiders/jianshu.py:
# start_requests爬虫生命周期的启动方法,读取起始网址列表,向爬虫发送Request对象:让引擎回调parse()
def start_requests(self):
print('执行start_requests...')
yield scrapy.Request(
url='https://www.jianshu.com/recommendations/users?page=2',
callback=self.parse,
headers=self.ajax_headers
)
def parse(self, response):
print('执行parse...')
配置item.py:
import scrapy
class JianshuproItem(scrapy.Item):
# define the fields for your item here like:
nickname = scrapy.Field() #昵称
slug = scrapy.Field() #用户链接接
head_pic = scrapy.Field() #用户头像
gender = scrapy.Field() #性别
is_contract = scrapy.Field()
following_num = scrapy.Field() #关注数
followers_num = scrapy.Field() #粉丝人数
articles_num = scrapy.Field() #文章个数
words_num = scrapy.Field() #字数
be_liked_num = scrapy.Field() #喜欢数
测试运行:
可以看出向该网址发送了请求,并且parse()被执行了。
测试成功
4.多页爬取
在上一步中只测试了第二页的信息,要进行多页爬取
1.首先将其实url,也就是start_request()中的yield中url改为第一页,表示从第一页爬取。
2.声明类变量start_page = 1,要在parse()中累加生成新的url。
def start_requests(self):
print('执行start_requests...')
self.start_page = 1
yield scrapy.Request(
url='https://www.jianshu.com/recommendations/users?page=1',
headers=self.base_headers
)
3.在parse()中解析了当前页的response之后,要生成下一页的url,所以在parse()加上:
# 生成新的url
self.start_page += 1
new_url = f'https://www.jianshu.com/recommendations/users?page={self.start_page}'
if self.start_page < 101:
# 传给调度器,进行循环爬取
yield scrapy.Request(
url= new_url,
callback=self.parse,
headers=self.ajax_headers
)
start_page+1,生成新的url,判断是否该页是否存在(有数据),如果有,yield给调度器进行入队,实现循环爬取。
一.用户列表url的爬取(解析parse中的response),获取用户的id信息,进一步爬取用户主页信息和用户粉丝以及用户动态
首先要在作者列表中爬取每个作者的url,进入该作者的主页,才能进一步爬取想要的信息。
1.首先我们查看用户列表页的元素结构以及用户主页的url结构
parse():
def parse(self, response):
print('执行parse...')
'''解析response'''
# 解析所有作者的主页链接的列表
auth_list = response.xpath('//div[@class="wrap"]/a/@href').extract()
# print(auth_list)
# 生成新的url
self.start_page += 1
new_url = f'https://www.jianshu.com/recommendations/users?page={self.start_page}'
if self.start_page:
# 传给调度器,进行循环爬取
yield scrapy.Request(
url= new_url,
callback=self.parse,
headers=self.ajax_headers,
)
分析:
解析返回的response,获取所有用户跳转链接。
2.根据用户主页链接截取获取用户主页url,用户粉丝页的url,用户动态页的url
1.首先要分析各个页面的url的格式:
- 用户主页url:
- 用户粉丝页url:
- 用户动态页url:
动态页使用的是ajax请求进行展示的:
返回的是html格式。
2.编写获取各个页面url的代码,并yield给响应的函数回调执行并测试(在回调函数中获取当前用户的用户id--slug_id)
for au in auth_list:
# 获取作者id
slug_id = au.split('/')[-1]
# 获取用户主页的url
au_url = f'https://www.jianshu.com/u/{slug_id}'
# 传给调度器,回调函数为解析用户主页信息的回调函数
yield scrapy.Request(
url = au_url,
callback=self.parse_auth,
headers=self.ajax_headers,
meta={'slug':slug_id}
)
# 通过作者url获取粉丝列表页面
fan_url = f'https://www.jianshu.com/users/{slug_id}/followers'
yield scrapy.Request(
url=au_url,
callback=self.parse_fans,
headers=self.ajax_headers,
meta={'slug': slug_id}
)
# 用户的操作动态信息页面url
schedule_url = f'https://www.jianshu.com/users/{slug_id}/timeline'
yield scrapy.Request(
url=schedule_url,
callback=self.parse_schedule,
headers=self.ajax_headers,
meta={'slug': slug_id}
)
'''解析用户主页信息函数'''
def parse_auth(self,response):
print('执行parse_auth...')
# 获取用户id
slug = response.meta['slug']
print(slug)
'''解析作者粉丝的信息'''
def parse_fans(self,response):
print('执行parse_fans...')
slug = response.meta['slug']
print(slug)
'''解析作者动态的信息'''
def parse_schedule(self,response):
print('执行parse_schedule...')
slug = response.meta['slug']
print(slug)
3.代码分析:
-
用户主页url获取:发现用户主页的url的格式为https://www.jianshu.com/u/用户id,与上一步中所解析到的用户跳转链接(/user/用户id)不完全一致,所以主页url要对用户url列表项中截取拼接完成。yield给调度器,回调函数为parse_auth()--处理解析用户主页信息的函数,并将用户id--slug_id作为参数传给回调函数。
-
用户粉丝页url获取:
用户粉丝的页面格式为:
https://www.jianshu.com/users/ 用户id /followers
拼接完成后获取该用户的粉丝列表页面的url,yield给调度器,回调函数为parse_fans()--解析用户粉丝页面的函数,并将用户id--slug_id作为参数传给回调函数。 -
用户动态的url获取:
经实验用户动态的ajax请求地址为:https://www.jianshu.com/users/ef4f2422125f/timeline?page=num,(num为页数)。
拼接完成后获取该用户的动态列表页面的url,yield给调度器,回调函数为parse_schedule()--解析用户粉丝页面的函数,并将用户id--slug_id作为参数传给回调函数。 -
测试运行:在每个回调函数中都打印当前用户的id--slug_id。
运行:
乱七八糟看不懂,但是点击每个连接都能跳转到相应页面,说明测试成功。
二.解析三个页面信息
1.解析用户首页的信息
注意:将parse()中yield给调度器的Request()中headers都改为self.base_headers
在parse_auth()中,编写解析用户主页的函数。
直接上代码:
def parse_auth(self,response):
print('执行parse_auth...')
# print(response.text)
# 获取用户id
slug = response.meta['slug']
# print(slug)
# 实例化item类对象
po = JianshuproItem()
# 获取包含用户信息的div元素
div_main_top = response.xpath('//div[@class="main-top"]')
po['nickname'] = div_main_top.xpath('.//div[@class="title"]/a/text()').extract_first() #昵称
po['slug'] = slug #用户id
po['head_pic'] = div_main_top.xpath('.//a[1]/img/@src').extract_first() # 用户头像
gender = div_main_top.xpath('.//div[@class="title"]/i/@class').extract_first() # 性别
if gender:
po['gender'] = gender.split('-')[-1]
else:
po['gender'] ='NO SEX'
po['following_num'] = int(div_main_top.xpath('.//div[@class="info"]//ul/li[1]/div/a/p/text()').extract_first()) #关注数
po['followers_num'] = int(div_main_top.xpath('.//div[@class="info"]//ul/li[2]/div/a/p/text()').extract_first()) # 粉丝个数
po['articles_num'] = int(div_main_top.xpath('.//div[@class="info"]//ul/li[3]/div/a/p/text()').extract_first()) # 文章个数
po['words_num'] = int(div_main_top.xpath('.//div[@class="info"]//ul/li[4]/div/p/text()').extract_first()) # 字数
po['be_liked_num'] = int(div_main_top.xpath('.//div[@class="info"]//ul/li[5]/div/p/text()').extract_first()) # 喜欢数
yield po
这里边有几个需要注意的点:
- 就是有的用户没有设置性别,这样的话获取到的数据为None,需要进行判断手动设定。
- 获得认证头衔以及资产先不进行爬取,在后边的粉丝列表页的爬取中,我们只是在粉丝列表页进行爬取基本信息,只能爬取到nickname,关注数,粉丝数,文章数,字数,喜欢数。爬取不到头衔以及资产项
那么这时就有一个问题:当一个有资产的用户关注了当前爬取的用户时,我们只爬取到一部分呢信息存入数据库中,在继续爬取时,在遇到该用户时,虽然爬取到器资产项或头衔等完整信息,但是加不进数据库中,因为我们后边设置数据库插入为不重复的插入,这样的话就造成了数据的错误,一旦数据发生错误,爬虫就没有意义。
为了此项目进度,先这样设置,后续的话进行优化,将粉丝列表中的超链接获取出来,循环该列表,截取拼接成新的用户url,yield给调度器,回调函数为parse_auth(),获取完整信息。
记得将settings中的管道放开。
运行:
2.解析用户粉丝页面
因为用户粉丝页的粉丝众多,是分页显示的,也就是说:
www.jianshu.com/users/51b4ef597b53/followers
www.jianshu.com/users/51b4ef597b53/followers?page=1
是等价的。
每当用户划到一定程度时,会自动发起ajax请求给page递增的url。返回的数据为html格式的内容:
返回的内容都是li列表项,拼接在当前页面中d:
我们要获取的数据:
但是经过实验,page不能一味的增加,当超出指定范围后,会出现重新回到前几页数据的页面。
所以解决的方案是在获取到该作者的信息后,将该作者的粉丝数记录下来,在爬取其粉丝时,进行计数,当计数器超过粉丝数时,停止爬取。
有两种方式获取到粉丝的信息,一种与上述内容相结合,另一种单独在最后展示并解释。
- 在start_requests()中声明一些变量用于初始化和存储粉丝数量的变量声明
def start_requests(self):
print('执行start_requests...')
# 因为在start_reuqest中已经读取了第一页
self.start_page = 1
# 存储当前作者粉丝数
self.follow_num = {}
yield scrapy.Request(
url='https://www.jianshu.com/recommendations/users?page=1',
headers=self.base_headers
)
2.获取到作者信息后,要进行初始化工作:
# 作者列表函数
def parse(self, response):
print('执行parse...')
'''解析response'''
# 解析所有作者的主页链接的列表
auth_list = response.xpath('//div[@class="wrap"]/a/@href').extract()
print(auth_list)
for au in auth_list:
# 获取作者id
slug_id = au.split('/')[-1]
# print(slug_id)
# 初始化存储该用户粉丝数的字典
self.follow_num[slug_id] = 0
# 设置粉丝页起始页码
self.fans_start_page = 1
# 设置粉丝数量
self.fans_num = 0
# 动态起始页
self.timeline_start_page = 1
# 获取用户主页的url
au_url = f'https://www.jianshu.com/u/{slug_id}'
# 传给调度器,回调函数为解析用户主页信息的回调函数
yield scrapy.Request(
url = au_url,
callback=self.parse_auth,
headers=self.base_headers,
meta={'slug':slug_id}
)
# 通过作者url获取粉丝列表页面
fan_url = f'https://www.jianshu.com/users/{slug_id}/followers'
yield scrapy.Request(
url=fan_url,
callback=self.parse_fans,
headers=self.base_headers,
meta={'slug': slug_id}
)
# 用户的操作动态信息页面url
schedule_url = f'https://www.jianshu.com/users/{slug_id}/timeline'
yield scrapy.Request(
url=schedule_url,
callback=self.parse_schedule,
headers=self.base_headers,
meta={'slug': slug_id}
)
# 生成新的url
self.start_page += 1
new_url = f'https://www.jianshu.com/recommendations/users?page={self.start_page}'
if self.start_page:
# 传给调度器,进行循环爬取
yield scrapy.Request(
url= new_url,
callback=self.parse,
headers=self.base_headers,
)
3.在用户主页解析之后将该用户的粉丝数存入初始化时创建的字典follow_num中。
po['followers_num'] = int(div_main_top.xpath('.//div[@class="info"]//ul/li[2]/div/a/p/text()').extract_first()) # 粉丝个数
# 存储当前用户的粉丝数
self.follow_num[slug] =po['followers_num']
4.粉丝页解析代码:
'''解析作者粉丝的信息'''
def parse_fans(self, response):
print('执行parse_fans...')
# 解析作者对应粉丝信息
slug = response.meta['slug']
# print(f'解析作者对应粉丝信息,作者标识:{slug}')
# 获取当前粉丝列表信息
ulist = response.xpath('//div[@id="list-container"]/ul[@class="user-list"]/li')
for u in ulist:
# 解析每个粉丝对象,并发送到管道中
item = JianshuproItem()
# 昵称
nick = u.xpath('./div[@class="info"][1]/a/text()').extract_first()
# 获取关注、粉丝数、文章数
temp = u.xpath('./div[@class="info"][1]/div[@class="meta"][1]/span/text()').extract()
arr_temp = []
for t in temp:
arr = t.split(" ")[-1]
arr_temp.append(arr)
following_num = int(arr_temp[0])
followers_num = int(arr_temp[1])
articles_num = int(arr_temp[2])
# print(f'粉丝昵称:{nick}')
item['nickname'] = nick
item['following_num'] = following_num
item['followers_num'] = followers_num
item['articles_num'] = articles_num
# 获取字数和获得喜欢数
meta_text = u.xpath('.//div[@class="meta"]')[1].xpath('text()').extract_first()
meta_num = re.findall('\d+', meta_text)
item['words_num'] = int(meta_num[0])
item['be_liked_num'] = int(meta_num[1])
yield item
# 对应粉丝进行计数
self.fans_num += 1
# 判断粉丝获取是否结束
if self.fans_num > self.follow_num[slug]:
# print(self.fans_num,self.follow_num[slug])
break
# 是否获取下一页的url以及传给调度器
if self.fans_num < self.follow_num[slug]:
# 获取粉丝分页url
self.fans_start_page += 1
xurl = f'https://www.jianshu.com/users/86b81ed8e35c/followers?page={self.fans_start_page}'
yield scrapy.Request(url=xurl, callback=self.parse_fans,
headers=self.base_headers, meta={'slug': slug})
在解析的过程中,如果计数器大于粉丝数,那么跳出循环,停止解析。
如果不大于,解析之后获取新的url发送给调度器进行下一页的数据爬取。
用户动态获取
用户动态页面的相应格式与粉丝大概一致,都是返回li标签,拼接在页面中,但是每次访问的url不仅page是动态的,还有一个max_id也是动态的。
经过发现,这个max_id的值为上一页返回的最后一个元素的li标签的id值-1:
所以我们首先要获取这个max-id值,用于获取下一页新的url:
'''解析作者动态的信息'''
def parse_schedule(self,response):
print('执行parse_schedule...')
slug = response.meta['slug']
# print(slug)
# 获取信息的li列表
li = response.xpath('//ul[@class="note-list"]/li')
if not li:
return
#for it in li:
# # 获取li元素的id属性值
# xid = it.xpath('./@id').extract_first()
# print(xid) # feed-645346514
# 获取下一页的max_id取值
tid = li[-1].xpath('./@id').extract_first()
tid = int(tid.split('-')[-1])-1
# 生成新的url
self.timeline_start_page+=1
xurl = f'https://www.jianshu.com/users/{slug}/timeline?max_id={tid}&page={self.timeline_start_page}'
yield scrapy.Request(
url=xurl,
callback=self.parse_schedule,
headers=self.base_headers,
meta={'slug':slug}
)
由于每次返回的都是li标签拼接在页面中,所以利用response解析到所有的列表项组成li列表。如果该列表不不存在,说明没有获取到元素,说明动态没有了,那么直接return。
如果有值,就获取列表的最后一项,进行解析出id值,转为int后-1,获的max_id。
网友评论