本文,我们以今日头条为例来尝试通过分析Ajax请求来抓取网页数据的方法。这次要抓取的目标是今日头条的街拍图片,抓取并保存。本节代码是参照python3网络爬虫实战的6.4节,但由于现在网页有的东西已经做了更新,所以本文代码也做了相应的修改,使得其可以正常抓取数据。
1.准备工作
首先,请确保安装好requests库。
2.抓取分析
在抓取之前,首先要分析抓取的逻辑。打开今日头条的首页https://www.toutiao.com/,如图 在搜索框输入街拍,结果如图所示
点击图片,切换到图片分类中 这时打开开发者工具,查看所有的网络请求。首先,打开第一个网络请求,这个请求的URL就是当前的连接https://so.toutiao.com/search?keyword=%E8%A1%97%E6%8B%8D&pd=atlas&source=search_subtab_switch&dvpf=pc&aid=4916&page_num=0,打开Preview选项卡查看Response Body。发现页面只有一部分,如图,可以分析后面的图片数据是由Ajax加载,然后用JavaScript渲染出来的。 接下来,我们可以切换到XHR过滤选项卡,查看有没有Ajax请求。 果然,这里出现了一个比较常规的Ajax请求,看看它的结果是否包含了页面中的相关数据。 点击rawData字段展开,可以发现里面有一个data字段,还有count字段为40,意思是这条请求包含40个图片,data字段里包含40条记录,分别是图片的url,以及其他信息。 这就确定了这些数据确实是由Ajax加载的。 我们的目的是要抓取其中的图片,这里一组图就对应前面data字段中的一条数据。如图所示 因此,我们只需要将data中每条数据的img_url字段提取并下载下来就好了。建立一个文件夹保存这些图片。 接下来,我们就可以直接用Python来模拟这个Ajax请求,然后提取相关信息。但是在这之前,我们还需要分析一下URL的规律。 切换回Headers选项卡,观察一下它的请求URL和Headers信息,如图 可以看到,这是一个GET请求,请求的参数有keyword、pd、source、dvpf、aid、page_num、search_json、rawJSON、search_id,我们需要找出这些参数的规律,因为这样才可以方便用程序构造请求。 接下里,滑动界面,多加载一些结果。在加载的同时可以发现,NetWork中又出现了许多Ajax请求,如图: 这里观察一下前后几个连接的请求变化,发现只有page_num参数在变化,每次的变化为1,所以可以发现规律,这个page_num就是偏移量,进而可以推断出count参数就是一次性获取的数据条数。因此我们可以用page_num参数来控制分页。这样就可以通过接口批量获取数据了,然后将数据解析,将图片下载即可。另外,我们发现keyword参数使用的并不是明文,而是经加密后的代码,我们可以借助python中的unquote包来解决。
3.实战演练
我们刚才已经分析了一下Ajax请求的逻辑,下面使用程序来实现。 首先,实现方法get_page()来加载单个Ajax请求的结果。其中唯一的变化就是参数page_num,所以把他当作参数传递,另外需要注意的是这里需要构造请求头,并且需要包含cooike,否则会获取不到Response,信息可以在Headers选项卡中找到: 而构造参数可以在payload选项卡中找到
import requests
from urllib.parse import urlencode,quote,unquote
headers = {
'Host': 'so.toutiao.com',
'Referer': 'https://so.toutiao.com/search?keyword=%E8%A1%97%E6%8B%8D&pd=atlas&dvpf=pc&aid=4916&page_num=0&search_json={%22from_search_id%22:%222022040316335201021218304330C35A48%22,%22origin_keyword%22:%22%E8%A1%97%E6%8B%8D%22,%22image_keyword%22:%22%E8%A1%97%E6%8B%8D%22}',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
'Cookie': 'passport_csrf_token=92a1b4e0108fb8384f5b81554b5b5424; tt_webid=7053044990251386398; _S_DPR=1.25; _S_IPAD=0; MONITOR_WEB_ID=7053044990251386398; ttwid=1%7CoqMwjUw5WGRdjYizT8quhnfpAchk3v_E3YLa1riJgrY%7C1648972603%7Cbaea5aebf3461426baaede977fa55b0efec3033a9f0d6a3e26684ba80f607ee9; _S_WIN_WH=1536_722'
}
def get_page(page_num):
params = {
'keyword':unquote('%E8%A1%97%E6%8B%8D') ,
'pd':'atlas',
'dvpf':'pc',
'aid': '4916' ,
'page_num':page_num,
'search_json':{"from_search_id":"2022040316335201021218304330C35A48","origin_keyword":"街拍","image_keyword":"街拍"},
'rawJSON':'1',
'search_id':'2022040317334901015013503043241DAC'
}
url = 'https://so.toutiao.com/search/?'+urlencode(params,headers)
try:
response = requests.get(url,headers=headers,params=params)
if response.status_code == 200:
return response.json()
except requests.ConnectionError:
return None
这里我们用urlencode()方法构造请求的GET参数,然后用requests请求这个链接,如果返回状态码是200,则调用response的json()方法将结果转为JSON格式,然后返回。 接下来,再实现一个解析方法:提取每条数据的img_url字段中的图片链接,将图片链接返回,此时可以构造一个生成器。实现代码如下:
def get_images(json):
if json.get('rawData'):
images = json.get('rawData').get('data')
for image in images:
link = image.get('img_url')
yield {
'image':image.get('img_url'),
'title':"街拍",
'text':image.get('text')
}
接下来,实现一个保存图片的方法save_image(),其中item就是前面get_images()方法返回的一个字典。在该方法中,首先根据item的title创建文件夹,然后请求这个图片链接,获取图片的二进制数据,以二进制形式写入文件。图片的名称可以使用其内容的MD5值,这样可以去除重复。代码如下:
import os
from hashlib import md5
def save_image(item):
if not os.path.exists(item.get('title')):
os.mkdir(item.get('title'))
try:
response = requests.get(item.get('image'))
if response.status_code == 200:
file_path = '{0}/{1}.{2}'.format(item.get('title'),md5(response.content).hexdigest(),'jpg')
if not os.path.exists(file_path):
with open(file_path,'wb') as f:
f.write(response.content)
else:
print("Already Downloaded",file_path)
except requests.ConnectionError:
print('Failed to Save image')
最后,只需要构造一个数组,遍历,提取图片链接,并将其下载即可:
from multiprocessing.pool import Pool
def main(page_num):
json = get_page(page_num)
for item in get_images(json):
print(item)
save_image(item)
GROUP_START = 1
GROUP_END = 20
if __name__ == '__main__':
for i in range(1,10):
main(i)
由于博主使用线程池出现bug,还未找到原因,所以先使用for循环 运行结果如下: 最后,给出本文代码地址:https://github.com/jiangwenvae/Spider/blob/main/spider/TouTiaoAjax.py
参考文献
[1].Python3网络爬虫开发实战.崔庆才.——6.4
|