Python网络爬虫案例实战:动态网页爬取:什么是Ajax
Ajax(Asynchronous JavaScript and XML)异步JavaScript和XML,即异步的JavaScript 和XML。它不是一门编程语言,而是利用JavaScript在保证页面不被刷新、页面链接不改变的情况下与服务器交换数据并更新部分网页的技术。
对于传统的网页,如果想更新其内容,那么必须要刷新整个页面,但有了Ajax,便可以在页面不被全部刷新的情况下更新其内容。在这个过程中,页面实际上是在后台与服务器进行了数据交互,在获取到数据之后,再利用JavaScript改变网页,这样网页内容就会更新了。
可以到 W3School上体验几个示例来感受一下:http://www.w3school.com.cn/ajax/ajax_example.asp。
1.实例引入
为了帮助理解Ajax的工作原理,创建了一个小型的Ajax应用程序。其实现效果:运行程序,生成初始界面如图4-3(a)所示,当单击界面中的“通过AJAX改变内容”按钮时,即切换界面,如图4-3(b)所示,再次单击该按钮,即返回如图4-3(a)的界面。
下面对该Ajax实例进行解释。
上面的Ajax应用程序包含一个div和一个按钮。div部分用于显示来自服务器的信息。当按钮被单击时,它负责调用名为loadXMLDoc()的函数,代码为:
< html>
< body >
<div id= "myDiv">< h3 > Let AJAX change this text </h3 ></div>
< button type = "button" onclick = "loadXMLDoc()"> Change Content </button >
</body >
</html>
接下来,在页面的head 部分添加一个
< head >
< script type = "text/javascript">
function loadXMLDoc()
{
... script goes here ...
}
</script >
</head >
2.基本原理
初步了解了Ajax之后,下面再来详细了解它的基本原理。发送Ajax请求到网页更新的这个过程可以简单分为3步:发送请求、解析内容、渲染网页。下面分别来详细介绍这几个过程。
1)发送请求
我们知道,JavaScript可以实现页面的各种交互功能,Ajax也不例外,它也是由JavaScript实现的,实际上执行了如下代码:
var xmlhttp:
if(window. XMLHttpRequest){
//code for IE7 + , Firefox, Chorme, Opera, Safari
xmlhttp = new HMLHttpRequest();
} else{//code for IE6, IE5
xmlhttp = new Activexobject("Microsoft. XMLHTTP");
}
xmlhttp. onreadystatechange = function(){
if(xmlhttp. readyState == 4&& xmlhttp. status == 200){
document. getElementById("myDiv"). innerHTML = xmlhttp. responseText;
}
}
xmlhttp. open("POST","/ajax/", true);
xmlhttp. send();
这是JavaScript对 Ajax最底层的实现,实际上就是新建了XMLHttpRequest对象,接着调用onreadystatechange属性设置了监听,然后调用 open()和 send()方法向某个链接 (也就是服务器)发送了请求。前面用 Python实现请求发送之后,可以得到响应结果,但这里请求的发送变成JavaScript来完成。由于设置了监听,所以当服务器返回响应时,onreadystatechange对应的方法便会被触发,然后在这个方法中解析响应内容即可。
2)解析内容
得到响应之后,onreadystatechange属性对应的方法便被触发,此时利用xmlhttp的 responseText属性便可取到响应内容。这类似于Python 中利用request向服务器发起请求,然后得到响应的过程。返回内容可能是HTML,可能是JSON,接着只需要在方法中用JavaScript 进一步处理即可。比如,如果是JSON,则可以进行解析和转化。
3)渲染网页
JavaScript有改变网页内容的能力,解析完响应内容之后,就可以调用JavaScript来针对解析完的网页进行下一步处理了。比如,通过 document.getElementById().innerHTML这样的操作,便可以对某个元素内的源代码进行更改,这样网页显示的内容就改变了,该操作也被称为DOM操作,即对 Document网页文档进行操作,如更改、删除等。
在前面的例子中,document.getElementById(“myDiv”).innerHTML = xmlhttp.responseText便将ID为myDiv的节点内部的HTML代码更改为服务器返回的内容,这样myDiv元素内部便会呈现出服务器返回的新数据,网页的部分内容看上去就更新了。
可以观察到,这3个步骤其实都是由JavaScript完成的,
它完成了整个请求、解析和渲染的过程。真实的数据其实都是一次次Ajax请求得到的,如果想要爬取这些数据,需要知道这些请求到底是怎样发送的,发往哪里,发了哪些参数。如果知道这些,不就可以用Python 模拟这个发送操作,获取到其中的结果了吗?
4.2.1Ajax 分析
我们知道,拖动刷新的内容由 Ajax加载,而且页面的URL没有变化,那么应该到哪里去查看这些Ajax请求呢?下面进行介绍。
1.查看请求
上面内容是借助浏览器的开发者工具,下面以Chrome浏览器为例来介绍。首先,用Chrome浏览器打开https://www.cnblogs.com/python-study/p/6060530 .html,随后在页面中右击,从弹出的快捷菜单中选择“检查”选项,此时便会弹出开发者工具。
此时在 Elements选项卡中便会观察到网页的源代码,右侧便是节点的样式。不过这不是我们想要寻找的内容。切换到Network 选项卡,随后重新刷新页面,可以发现这里出现了非常多的条目,如图4-4所示。
这里其实就是在页面加载过程中浏览器与服务器之间发送请求和接收响应的所有记录。
Ajax其实有其特殊的请求类型,它叫xhr。在图4-5中,可以发现一个名称以Get开头的请求,这就是一个Ajax请求。单击这个请求,可以查看这个请求的详细信息。
在右侧可以观察到 Request Headers.URL 和 Response Headers等信息。其中 Request Headers 中有一个信息为X-Requested-With:XMLHttpRequest,这就标记了此请求是Ajax请求,如图4-6所示。
随后单击Preview,即可看到响应的内容,它是JSON 格式的。这里Chrome为我们自动做了解析,单击下三角按钮即可展开和收起相应内容,如图4-7所示。
另外,也可以切换到 Response选项卡,从中观察到真实的返回数据,如图4-8所示。
接着,切回到另一个请求,观察一下它的Response是什么,如图4-9所示。
这是最原始的链接https://www.cnblogs.com/python-study/p/6060530.html返回的结果,结构相对简单,只是执行了一些JavaScript。
2.过滤请求
接着,再利用 Chrome 开发者工具的筛选功能选出所有的Ajax请求。在请求的上方有一层筛选栏,直接单击XHR,此时在下方显示的所有请求便都是Ajax请求了,如图4-10所示。
随意点开一个条目,都可以清楚地看到其Request URL.Request Headers.Response Headers.Response Body等内容,此时想要模拟请求和提取就非常简单了。
至此,已经可以分析出Ajax请求的一些详细信息了,接下来只需要用程序模拟这些Ajax请求,就可以轻松提取所需要的信息。
4.2.2Ajax结果提取
接下来用 Python来模拟这些Ajax请求,将相关数据爬取下来。
1.分析请求
打开 Ajax的XRH过滤器,选定其中一个请求,分析它的参数信息。单击该请求,进入详情页面,如图4-11所示。
可以发现,这是一个GET类型的请求,请求链接为https://www.cnblogs.com/mvc/Blog/GetBlogSideBlocks.aspx?blogApp = python-study&-showFlag = ShowRecentComment% 2CShowTopViewPosts%2CShowTopFeedbackPosts%2CShowTopDiggPosts。往下滑动,可看到请求的参数有两个,分别为blogApp 和 showFlag。
2.分析响应
随后,观察这个请求的响应内容,如图4-12所示。
4.2.3Ajax爬取今日头条街拍美图
本节以今日头条为例,尝试通过分析Ajax请求爬取网页数据的方法。这次要爬取的目标是今日头条的街拍美图,爬取完成后,将每组图片分文件夹下载到本地并保存下来。
在爬取之前,首先要分析爬取的逻辑。打开今日头条的首页http://www.toutiao.com/,如图4-13所示。
右上角有一个搜索入口,此处尝试爬取街拍美图,所以输入“街拍”二字搜索一下,结果如图4-14所示。
这时打开开发者工具,查看所有的网络请求。首先,打开第一个网络请求,这个请求的URL 就是当前的链接http://www.toutiao.com/search/?keyword=街拍,打开 Preview选项卡查看Response Body。如果页面中的内容是根据第一个请求得到的结果渲染出来的,那么第一个请求的源代码中必然会包含页面结果中的文字。为了验证,可以尝试搜索一下搜索结果的标题,比如“路人”二字,如图4-15所示。
可以发现,网页源代码中并没有包含这两个字,搜索匹配结果数目为0。因此,可以初步判断这些内容是由Ajax加载,然后用JavaScript渲染出来的。接着,可以切换到XHR选项卡,查看一下有没有Ajax。
单击XHR选项卡时,此处出现了一个比较常规的Ajax请求,单击data字段展开,发现这里有许多条数据。单击第一条展开,可以发现有一个title字段,它的值正好就是页面中第一条数据的标题。再检查一下其他数据,也正好是一一对应的,如图4-16所示。
这就确定了这些数据确实是由Ajax加载的。
我们的目的是要爬取其中的美图,这里一组图就对应前面data字段中的一条数据。每条数据还有一个image_list字段,它是列表形式,这其中就包含了组图的所有图片列表,如图4-17所示。
因此,只需要将列表中的url字段提取出来并下载就可以了。对每一组图都会建立一个文件夹,文件夹的名称就为组图的标题。
接着,就可以直接用Python 来模拟这个Ajax请求,然后提取出相关美图链接并下载。但是在这之前,还需要分析一下 URL的规律。
切换回Headers选项卡,观察一下它的请求 URL 和 Headers信息,如图4-18所示。
可以看到,这是一个GET请求,请求 URL的参数有aid、app_name、offset、format、keyword、autoload.count、en_qc等。我们需要找出这些参数的规律,才可以方便地用程序构造出来。
接着,可以滑动页面,多加载一些新结果。在加载的同时可以发现,Network 中又出现了许多Ajax请求,如图4-19所示。
这里观察一下后续链接的参数,发现变化的参数只有offset,其他参数都没有变化,而且第二次请求的offset值为20,第三次为40,第四次为60,所以可以发现规律,这个offset值就是偏移量,进而可以推断出 count参数就是一次性获取的数据条数。因此,可以用offset参数来控制数据分页。这样就可以通过接口批量获取数据了。
下面就用程序来实现美图下载,步骤如下:
(1)首先,实现方法get_page()来加载单个Ajax请求的结果。其中唯一变化的参数就是offset,所以将它当作参数传递,实现代码为:
import requests
from urllib.parse import urlencode
from requests import codes
import os
from hashlib import md5
from multiprocessing.pool import Pool
import re
def get_page(offset):
params = {
'aid': '24',
'offset': offset,
'format': 'json',
#'keyword': '街拍',
'autoload': 'true',
'count': '20',
'cur_tab': '1',
'from': 'search_tab',
'pd': 'synthesis'
}
base_url = 'https://www.toutiao.com/api/search/content/?keyword=%E8%A1%97%E6%8B%8D'
url = base_url + urlencode(params)
try:
resp = requests.get(url)
print(url)
if 200 == resp.status_code:
print(resp.json())
return resp.json()
except requests.ConnectionError:
return None
这里用 urlencode()方法构造请求的GET参数,然后用requests请求这个链接,如果返回状态码为200,则调用response的 json()方法将结果转为JSON 格式,然后返回。
(2)实现一个解析方法:提取每条数据的image_detail 字段中的每一张图片链接,将图片链接和图片所属的标题一并返回,此时可以构造一个生成器,实现代码为:
def get_images(json):
if json.get('data'):
data = json.get('data')
for item in data:
if item.get('cell_type') is not None:
continue
title = item.get('title')
images = item.get('image_list')
for image in images:
origin_image = re.sub("list", "origin", image.get('url'))
yield {
'image': origin_image,
# 'iamge': image.get('url'),
'title': title
}
print('succ')
(3)实现一个保存图片的方法save_image(),其中item就是前面get_images()方法返回的一个字典。在该方法中,首先根据item的title 来创建文件夹,然后请求这个图片链接,获取图片的二进制数据,以二进制的形式写入文件。图片的名称可以使用其内容的MD5值,这样可以去除重复。实现代码为:
def save_image(item):
img_path = 'img' + os.path.sep + item.get('title')
print('succ2')
if not os.path.exists(img_path):
os.makedirs(img_path)
try:
resp = requests.get(item.get('image'))
if codes.ok == resp.status_code:
file_path = img_path + os.path.sep + '{file_name}.{file_suffix}'.format(
file_name=md5(resp.content).hexdigest(),
file_suffix='jpg')
if not os.path.exists(file_path):
print('succ3')
with open(file_path, 'wb') as f:
f.write(resp.content)
print('Downloaded image path is %s' % file_path)
print('succ4')
else:
print('Already Downloaded', file_path)
except requests.ConnectionError:
print('Failed to Save Image,item %s' % item)
(4)最后,只需要构建一个offset数组,遍历offset,提取图片链接,并将其下载即可,实现代码为:
def main(offset):
json = get_page(offset)
for item in get_images(json):
print(item)
save_image(item)
GROUP_START = 0
GROUP_END = 7
if __name__ == '__main__':
pool = Pool()
groups = ([x * 20 for x in range(GROUP_START, GROUP_END + 1)])
pool.map(main, groups)
pool.close()
pool.join()
在以上代码中,定义了分页的起始页数和终止页数,分别为GROUP_START和 GROUP_END,还利用了多进程的进程池,调用其 map()方法实现多进程下载。
运行程序,输出如下:
succ
succ
succ
succ
succ
succ
succ
succ
succ
https://www.toutiao.com/api/search/content/?keyword=%E8%A1%97%E6%8B%8Daid=24&offset=80&format=json&autoload=true&count=20&cur_tab=1&from=search_tab&pd=synthesis
{'resp_type': 'rest_end', 'page_count': 0, 'cur_page_num': 0, 'count': 0, 'return_count': 0, 'query_id': '6655102012775994636', 'has_more': 0, 'request_id': '202408120935116668DD52379BEAF80AC8', 'search_id': '202408120935116668DD52379BEAF80AC8', 'session_id': '', 'cur_ts': 1723426511, 'offset': 90, 'message': 'succ