前言:在用request抓取页面时,有时得到的结果与在浏览器中看到的不一样,浏览器能看到正常页面数据,但使用requests得到的结果并没有。requests获取的是原始的HTML文档,而浏览器中的页面是经过JavaScript处理数据后生成的结果,这些数据来源可能通过Ajax加载的,可能包含在HTML文档中的,也可能是经过JavaScript和特定算法计算后生成的。
对于Ajax加载方式,数据加载是异步加载方式,原始页面不包含某些数据,原始页面加载完后,再向服务器请求某个接口获取数据,数据被处理再呈现在网页上,这就是发送一个Ajax请求。现在的Web页面,这种形式越来越多。原始HTML文档不包含任何数据,数据通过Ajax统一加载后再呈现出来,这样在Web开发上可以做到前后端分离,并且降低服务器直接渲染页面带来的压力。
对于Ajax加载方式的页面,直接用requests库抓取页面时,是不能获取到有效数据的。可用requests模拟Ajax请求,分析网页后台向接口发送的Ajax请求,就可成功抓取。
一 什么是Ajax
Ajax的全称为Asynchronous JavaScript and XML ,异步的JavaScript和XML。它不是编程语言,是利用JavaScript在保证页面不被刷新、页面链接不改变的情况下与服务器交换数据并更新部分网页的技术。
传统网页要更新内容需要刷新整个页面。有了Ajax可在页面不全部刷新情况下更新其内容,在这个过程中,页面实际上是在后台与服务器进行了数据交互,获取到数据后,再利用JavaScript改变网页,从而更新网页内容。
可在W3School体验Ajax更新页面:http://www.w3school.com.cn/ajax/ajax_xmlhttprequest_send.asp
1、 Ajax请求基本原理
Ajax请求网页更新的过程可简单分为这3步:发送请求、解析内容、渲染网页。
1.1.1 发送请求
JavaScript可以实现页面的各种交互功能,Ajax也是这样,它是由JavaScript实现的,实现上执行了下面这段JavaScript代码:
var xmlhttp;
if (window.XMLHttpRequest) {
// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
} else { // code for IE6, IE5
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function () {
if (xmlhttp.readState==4 && xmlhttp.status==200) {
document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
}
}
xmlhttp.open("POST","/ajax/",true);
xmlhttp.send();
上面这段代码是JavaScript对Ajax的底层实现,实际是新建了XMLHttpRequest对象,然后调用onreadystatechange属性设置了监听,接着调用open()和send()方法向某个连接(这里是服务器)发送请求。这里请求的发送是由JavaScript来完成。通过监听,当服务器返回响应时,onreadystatechange对应的方法便会被触发,然后在这个方法里解析响应内容即可。
1.1.2 解析内容
得到响应后,onreadystatechange属性对应的方法会被触发,此时利用xmlhttp的responseText属性可取到响应内容。这相当于Python中利用requests向服务器发起请求,然后得到响应的过程。返回的内容可能HTML,可能是JSON,接下来需要在方法中用JavaScript进一步处理。比如可以对JSON字符串进行解析和转化。
1.1.3 渲染网页
JavaScript可以改变网页,可以调用JavaScript来针对解析完的内容对网页进行下一步处理。这里通过document.getElementById().innerHTML这样的操作,可以对某个元素内的源代码进行更改,这样网页显示的内容也就改变了,这种操作也被称作DOM操作,是对Document网页文档进行操作,比如更改、删除等。
上面代码中的 document.getElementById("myDiv").innerHTML=xmlhttp.responseText; 是将ID为myDiv的节点内部的HTML代码更改为服务器返回的内容,这样myDiv元素内部便会呈现出服务器返回的新数据,网页的部分内容看上去也就在更新。上面这3个步骤都是由JavaScript完成的。
用Python模拟这个发送操作,需要知道Ajax请求到底是怎么发送的,发往哪里,发了哪些参数。
二 Ajax分析方法
1 查看Ajax请求
使用Chrome浏览器,打开一个有Ajax请求的页面,这里以 https://m.weibo.cn/u/5589792153 为例进行说明。在页面中点击右键选择“检查”选项,下边就出现开发者工具。在开发者工具中的Elements选项中可看到网页的源代码,右侧对应是节点的样式。这个Elements选项中的内容不是想要找的内容。点击Network选项卡,重新刷新页面后可以看到下面出现了很多的条目。这就是页面加载过程中浏览器与服务器之间发送请求和接收响应的所有记录。
Ajax有特殊的请求类型,叫作xhr。点击Type为xhr的请求后,右边Headers选项卡可以查看这个请求的详细信息。右侧可以看到URL、Request Headers和Response Headers等信息,在Request Headers中的一个信息为X-Requested-With: XMLHttpRequest就标记了此请求是Ajax请求。
点击Preview选项卡可以看到响应的内容,它是JSON格式的。这里Chrome自动做了解析。这个返回结果中有页面上显示需要的信息,这也是用来渲染页面使用的数据。JavaScript接收到这些数据后,再执行相应的渲染方法,整个页面就渲染出来了。在Response选项卡中,是真实的返回数据。
2 过滤请求
可以在Chrome开发者工具的筛选功能筛选出所有的Ajax请求。在请求的上方有层筛选栏,直接点击XHR,此时在下方显示的请求便都是Ajax请求。在页面不断滑动页面,页面底部有一条条新的微博刷出,开发者工具下方也一个个的出现Ajax请求,这样就可以捕获到所有的Ajax请求。
随意点开一个条目,可以看到其Request URL、Request Headers、Response Headers、Response Body等内容,此时要模拟请求和提取就变得容易了。下面用Python实现Ajax请求的模拟,实现数据的获取。
三 Ajax结果提取
这里以获取微博数据为例,网址是:https://m.weibo.cn/u/5589792153
1 分析请求
在Chrome浏览器中打开该网站,切换到开发者工具,打开Ajax的XHR过滤器。在页面上滑动页面以加载新的微博内容。此时下面会不断有Ajax请求发出。选中一个请求,分析它的参数信息。点击请求进入详情页面。
可以看到请求类型是GET,请求链接是https://m.weibo.cn/api/container/getIndex?type=uid&value=5589792153&containerid=1076035589792153&page=2。这个链接中请求的参数有4个:type、value、containerid和page。接着看其它请求,可以看到它们的type、value、containerid和page都是一样的。type都是uid,value的值是页面链接中的数字,就是用户的id号。还有containerid,它就是107603加上用户id。改变的是就是page,这个参数是用来控制分布的,page=1代表第一页,page=2代表第二页,以此类推。
2 分析响应
观察这个请求的响应内容,查看Preview选项卡的内容。如图3-1所示。
图3-1 Ajax响应内容
内容是JSON格式的,浏览器的开发者工具自动做了解析以方便查看。其中最关键的两部分信息是cardlistInfo和cards:前者包含一个重要信息total,这个就是微博的总数量,可根据这个数字估算分布数;后者是一个列表,包含10个元素,展开其中一个看看,如图3-2所示。
图3-2 列表cards 0的内容
这个元素中比较重要的字段是mblog。展开这项可以发现它包含的是微博的一些信息,如attitudes_count(赞数目)、comments_count(评论数目)、reposts_count(转发数目)、created_at(发布时间)、text(微博正文)等,这些都是格式化的内容。
这样在请求一个接口,就可以得到10条微博,而且请求时只需改变page参数即可。只需要一个简单的循环,就可以获取所有微博。
3 实际操作方法
用Python模拟Ajax请求,将微博前10页全部爬取下来。
第一步:定义一个方法获取每次请求的结果。在请求时,page是一个可变参数,可将其作为方法的参数传递进来,代码如下所示:
from urllib.parse import urlencode
import requests
base_url = \'https://m.weibo.cn/api/container/getIndex?\'
headers = {
\'Host\': \'m.weibo.cn\',
\'Referer\': \'https://m.weibo.cn/u/5589792153\',
\'User-Agent\': \'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 \'
\'(KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36\',
\'X-Requested-With\': \'XMLHttpRequest\',
}
def get_page(page):
params = {
\'type\': \'uid\',
\'value\': \'5589792153\',
\'containerid\': \'1076035589792153\',
\'page\': page,
}
url = base_url + urlencode(params)
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
except requests.ConnectionError as e:
print(\'Error\', e.args)
这里的代码中定义了base_url表示请求的URL前半部分。接着构造参数字典,其中type、value和containerid是固定参数,page是可变参数。接下来调用urlencode()方法将参数转化为URL的GET请求参数,结果类似于:type=uid&value=5589792153&containerid=1076035589792153&page=2 这样的形式。再将base_url与参数拼合成一个新的URL。紧接着用requests请求这个链接,加入headers参数。然后判断响应状态码,如果是200,则调用json()方法将内容解析为JSON返回,否则不返回任何信息。如果出现异常,则捕获并输出异常信息。
第二步:定义一个解析方法,用来从结果中提取想要的信息,比如要保存微博的id、正文、赞数、评论数和转发数这几项内容,可以先遍历cards,然后获取mblog中的各个信息,赋值为一个新的字典返回即可,代码如下:
from pyquery import PyQuery as pq
def parse_page(json):
if json:
items = json.get(\'data\').get(\'cards\')
try:
for item in items:
item = item.get(\'mblog\')
weibo = {}
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\')
yield weibo
except AttributeError as e:
print(\'AttributeError\', e.args)
这里使用pyquery将正文中的HTML标签去掉,获取文本内容。最后遍历page,一共10页,将提取到的结果打印输出:
if __name__ == \'__main__\':
for page in range(1, 11):
json = get_page(page)
results = parse_page(json)
for result in results:
print(result)
运行程序后大致输出内容如下所示:
{\'id\': \'4268170590397998\', \'text\': \'#为人民军队点赞# 致敬最可爱的人,为人民军队点赞\', \'attitudes\': 6026, \'comments\': 145, \'reposts\': 191}
{\'id\': \'4266803473488506\', \'text\': \'多~喝~水~(为什么找不到水杯的表情)\', \'attitudes\': 23500, \'comments\': 1174, \'reposts\': 177}
{\'id\': \'4265392614366035\', \'text\': \'欢迎光临奇迹王诗诗~\', \'attitudes\': 5285, \'comments\': 284, \'reposts\': 47}
还可以再加一个方法将结果保存到MongoDB数据库中:
from pymongo import MongoClient
client = MongoClient() # 使用默认参数,本地连接及端口,db为0
db = client[\'weibo\'] # 指定数据库
collection = db[\'weibo\'] # 指定数据表
def save_to_mongo(result):
if collection.insert(result): # 将数据保存到MongoDB,并获取返回结果
print(\'Saved to Mongo\')
只需要将if __name__ == \'__main__\':中的print(result)改为save_to_mongo(result)即可完成。
这里示例的Ajax请求过程,有很多可完善的地方,如页码动态计算、微博查看全文等。
下面的这段代码可以获取新浪微博上某一个微博账号的相册,只需要将代码中的 1111111111 改为相应的微博账号即可。详细代码如下所示:
import requests, os
from urllib.parse import urlencode
from pyquery import PyQuery as pq
from hashlib import md5
from multiprocessing.pool import Pool
base_url = \'https://m.weibo.cn/api/container/getIndex?\'
headers = {
\'Host\': \'m.weibo.cn\',
\'Referer\': \'https://m.weibo.cn/u/1111111111\',
\'User-Agent\': \'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36\',
\'X-Requested-With\': \'XMLHttpRequest\',
}
def get_page(page):
"""获取页面"""
if page == 0:
params = {
\'type\': \'uid\',
\'value\': \'1111111111\',
\'containerid\': \'1076031111111111\',
}
else:
params = {
\'type\': \'uid\',
\'value\': \'1111111111\',
\'containerid\': \'1076031111111111\',
\'page\': page
}
url = base_url + urlencode(params)
# url = base_url
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
except requests.ConnectionError as e:
print(\'Error\', e.args)
def get_images(json):
"""获取主题及图片的URL"""
if json:
items = json.get(\'data\').get(\'cards\')
#try:
for item in items:
item = item.get(\'mblog\')
try:
# 对于没有pics字段的mblog,不能使用get方法
if item.get(\'pics\'):
title = pq(item.get(\'text\')).text()
#print(title)
pics = item.get(\'pics\')
for pic in pics:
imageURL = pic.get(\'large\').get(\'url\')
yield {
\'title\': title,
\'imageURL\': imageURL
}
else:
print("No Image")
continue
except AttributeError as e:
print(\'AttributeError\', e.args)
continue
def save_image(item):
"""获取图片并保存"""
if not os.path.exists(item.get(\'title\').strip()[:8]):
os.mkdir(item.get(\'title\').strip()[:8])
try:
response = requests.get(item.get(\'imageURL\'))
if response.status_code == 200:
file_path = \'{0}/{1}.{2}\'.format(item.get(\'title\').strip()[:8],
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")
def main(offset):
json = get_page(offset)
for item in get_images(json):
print(item)
save_image(item)
PAGE_START = 0
PAGE_END = 2
if __name__ == \'__main__\':
pool = Pool()
groups = ([x for x in range(PAGE_START, PAGE_END + 1)])
pool.map(main, groups)
pool.close()
pool.join()