最近阅读了崔庆才写的《Python3网络爬虫开发实战》,系统地学习一下利用Python写网络爬虫。由于这本书出版时间是2018年,很多书中案例涉及的网站已经改版,基本上每个案例都需要自己再研究一下网站改版后新的结构来爬取数据。这篇博文就来介绍一下如何爬取一下新浪微博用户的微博信息和下载该用户的微博图片,其中涉及到的技术包括Ajax数据爬取,Python和MongoDB的交互以及windows下python的多进程编程。
关于基础的网页前端各个节点的结构,http的get和post请求,requests库中的get函数用法,普通网页header请求头的构建,Beautiful Soup和pyquery解析库的使用这里不做过多介绍,对这方面不是很了解的同学建议先去看一下《Python3网络爬虫开发实战》中的前四章来详细了解一下。这里简要介绍一下什么是Ajax加载,它是一种异步的数据加载方式,原始的页面最初不会包含某些数据,原始页面加载完之后,会再向服务器请求某个接口获取数据,然后数据才被处理从而呈现到网页上,这其实就是发送了一个Ajax请求,本质上来讲Ajax是利用了JavaScript在保证页面不被刷新,页面链接不改变的情况下与服务器交换数据并更新部分网页的技术。
以我的移动端新浪微博页面为例:https://m.weibo.cn/u/6163257669,选择Network下面的XHR选项卡,点击刷新页面之后,在下拉的过程中会发现有getIndex?开头的请求冒出来,这就是Ajax请求,构造Ajax请求主要是提供Requests header和Query String Parameters中的参数。
再看一下该请求返回的数据Previews,如下图所示,其中cards中的绝大部分card对应的是每条微博,点开一个card,若有mblog字段代表这条card对应的是一条微博,mblog字段下面不同字段包含这条微博的所有信息。我们这次主要是爬取每条微博的id,text,attitudes,comments,reposts这几个属性。然后将信息写入MongoDB中,windows下MongoDB的安装可以参考https://www.runoob.com/mongodb/mongodb-window-install.html,为了能够之直接在命令行通过mongo命令启动数据库命令行,可以将mongo安装目录(例如C:\Program Files\MongoDB\Server\4.2\bin)添加到相应的系统环境变量。
关于构造请求的query string parameters参数,其中有一个since_id,这个代表每次Ajax加载时对应的第一条微博id,我们遍历所有微博的方法就是从该用户第一条微博id(有些用户页面上的第一条微博是置顶的,注意不要选该微博作为第一条微博,需要找时间上最晚发的微博)开始作为起始的since_id,返回的数据包含之后的十条card,然后再以最后一个card作为起始的since_id再爬取后续的十条card(下一次返回的结果中要去掉第一条,防止重复爬取该条微博),以此类推最终遍历完所有微博。最终爬取的脚本如下所示:
#author:xfxy
#time:2020/04/18
from urllib.parse import urlencode
from pyquery import PyQuery as pq
import requests
import time
import pymongo
base_url='https://m.weibo.cn/api/container/getIndex?'
def get_all_weibo(since_id): #该函数通过提供since_id参数来爬取cards
if since_id==None:
return None
#请求头的构建
headers={
'Referer': 'https://m.weibo.cn/u/6163257669?from=myfollow_all&is_all=1&sudaref=login.sina.com.cn',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3706.400 SLBrowser/10.0.4040.400',
'X-Requested-With': 'XMLHttpRequest',
}
#Query string parameters的构建
params={
'from': 'myfollow_all',
'is_all': '1',
'sudaref': 'login.sina.com.cn',
'type': 'uid',
'value': '6163257669', #这是我微博的id,如果想爬取某个用户的微博,需要更改value,containerid,headers中的Referer
'containerid': '1076036163257669',
'since_id': since_id,
}
url=base_url+urlencode(params) #通过Query string parameters构造请求url
r=requests.get(url,headers=headers)
return r.json().get('data').get('cards')
items=get_all_weibo('4494845651530130') #since_id为我第一条微博的id
client = pymongo.MongoClient(host='localhost',port=27017) #建立和MongoDB的链接
db=client.weibo #建立数据库weibo
collection=db.xfxy #建立键值对集合collections
while(1): #循环退出条件写在循环体中
for item in items:
if item.get('mblog')!=None:
item=item.get('mblog')
else:
continue
weibo=dict()
weibo['id']=item.get('id')
weibo['text']=pq(item.get('text')).text()
weibo['attitudes']=item.get('attitudes_count')
weibo['comments']=item.get('comments_count')
weibo['reposts']=item.get('reposts_count')
print(weibo)
collection.insert(weibo) #向MongoDB中插入数据
for i in range(len(items)): #寻找最后一条含有mblog字段的card,取其mblog字段中的id作为下一次循环的id
if items[len(items)-1-i].get('mblog')!=None:
since_id=items[-1].get('mblog').get('id')
break
items=get_all_weibo(since_id)
time.sleep(0.5) #设置sleep防止请求过于频繁
if items==None or items[0]==items[-1]: #循环退出条件,只剩最后一条card或者不存在card
break
items=items[1:] #去掉作为请求的since_id对应的微博,防止重复爬取
最后print出的结果和存储在MongoDB中的数据如下所示:
有些时候我们关注了一些喜欢发高清美图的博主,想要爬取该博主发的原创微博的原图。原创微博带有图片的,mblog字段中会有pics字段,如下图所示中最后那行的pics,点开后我们可以看到该条微博内每张图片都单独列了一个条目,如图中的0,0下面large字段字段内的url就是对应的大图的网址,通过get该网址的内容便可下载该图片。
我们首先用单进程的方法爬取微博用户发的大图,脚本如下所示,遍历微博和之前的脚本使用的方法相同,不同的地方在于图片下载url的获取。最后图片都会下载到xfxy文件内,可以通过cnt_pic控制下载图片的数量,如下图所示:
#author:xfxy
#time:2020/04/18
from urllib.parse import urlencode
import requests
import time
import os
base_url='https://m.weibo.cn/api/container/getIndex?'
def get_all_weibo(since_id):
if since_id==None:
return None
headers={
'Referer': 'https://m.weibo.cn/u/6163257669?from=myfollow_all&is_all=1&sudaref=login.sina.com.cn',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3706.400 SLBrowser/10.0.4040.400',
'X-Requested-With': 'XMLHttpRequest',
}
params={
'from': 'myfollow_all',
'is_all': '1',
'sudaref': 'login.sina.com.cn',
'type': 'uid',
'value': '6163257669',
'containerid': '1076036163257669',
'since_id': since_id,
}
url=base_url+urlencode(params)
r=requests.get(url,headers=headers)
return r.json().get('data').get('cards')
items=get_all_weibo('4494845651530130')
start=time.time()
cnt_pic=0
while(1):
for item in items:
if item.get('mblog')!=None:
item=item.get('mblog')
else:
continue
item = item.get('pics') #获取有图片的微博下各个图片信息的list
if item != None:
for pic in item:
if pic.get('large') != None:
url=pic.get('large').get('url') #获取大图下载url
title = url.split('/')[-1] #从url中获取图片名
with open('xfxy\{0}'.format(title),'wb') as f:
f.write(requests.get(url).content)
cnt_pic=cnt_pic+1
if cnt_pic>200: #若下载图片数量大于200,则循环退出
break
for i in range(len(items)): #寻找最后一条含有mblog字段的card,取其mblog字段中的id作为下一次循环的id
if items[len(items)-1-i].get('mblog')!=None:
since_id=items[-1].get('mblog').get('id')
break
items=get_all_weibo(since_id)
time.sleep(0.5)
if items==None or len(items)<=1:
break
items=items[1:]
print("运行时间:",time.time()-start)
由于下载的图片均为大图,如果爬取的图片数量比较多,有大量的时间都会耗费在下载图片上,所以尝试将要爬取的图片下载url整合到一个list之后,通过多进程的方式下载图片,利用该思路修改后的爬取脚本如下所示,通过multiprocessing实现多进程图片下载,大家可以爬取发图片比较多的博主对比一下多进程与单进程的效果,我自己找了一个博主爬取其发过的两百多张图片,单进程与多进程的效果如下面两张图所示,可以看到多进程爬取快了很多:
#author:xfxy
#time:2020/04/18
from urllib.parse import urlencode
import requests
import time
from multiprocessing import Pool,freeze_support
base_url='https://m.weibo.cn/api/container/getIndex?'
def get_all_weibo(since_id):
if since_id==None:
return None
headers={
'Referer': 'https://m.weibo.cn/u/6163257669?from=myfollow_all&is_all=1&sudaref=login.sina.com.cn',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3706.400 SLBrowser/10.0.4040.400',
'X-Requested-With': 'XMLHttpRequest',
}
params={
'from': 'myfollow_all',
'is_all': '1',
'sudaref': 'login.sina.com.cn',
'type': 'uid',
'value': '6163257669',
'containerid': '1076036163257669',
'since_id': since_id,
}
url=base_url+urlencode(params)
r=requests.get(url,headers=headers)
return r.json().get('data').get('cards')
def load_pics(url):
title = url.split('/')[-1]
with open('yaoyao\{0}'.format(title), 'wb') as f:
f.write(requests.get(url).content)
if __name__ =='__main__':
freeze_support() #在windows下使用multiprocess,由于windows下不是通过fork()产生子进程,所以需要加上freeze_support()
items=get_all_weibo('4494883576803454')
start=time.time()
pic_urls=[]
while(1):
for item in items:
if item.get('mblog')!=None:
item=item.get('mblog')
else:
continue
item = item.get('pics')
if item != None:
for pic in item:
if pic.get('large') != None:
url=pic.get('large').get('url')
pic_urls.append(url)
print(len(pic_urls))
if len(pic_urls)>200:
break
for i in range(len(items)): #寻找最后一条含有mblog字段的card,取其mblog字段中的id作为下一次循环的id
if items[len(items)-1-i].get('mblog')!=None:
since_id=items[-1].get('mblog').get('id')
break
items=get_all_weibo(since_id)
time.sleep(0.5)
if items==None or len(items)<=1:
break
items=items[1:]
#
pool=Pool() # 创建进程池
pool.map(load_pics,pic_urls) #第一个参数为函数名,第二个参数为传入函数的参数的迭代器
pool.close() #关闭进程池,不再接受新的进程
pool.join() #阻塞父进程直到所有子进程结束后再执行父进程
print("运行时间:",time.time()-start)