目录
[1.1 具体举例](#1.1 具体举例)
[1.2 基本原理](#1.2 基本原理)
[2.1 查看请求](#2.1 查看请求)
[2.2 过滤请求](#2.2 过滤请求)
[三、解析提取Ajax 结果](#三、解析提取Ajax 结果)
[3.1 分析请求](#3.1 分析请求)
[3.2 分析响应](#3.2 分析响应)
前言
咱们在使用requests抓取网页的时候,常常会发现,得到的结果和在浏览器中看到的网页不太一样。在浏览器里,页面上的数据都能正常显示,可通过requests获取到的内容里,却没有这些数据。原因就在于,requests获取的只是最原始的HTML文档,而浏览器中呈现出来的页面,是对数据进行JavaScript处理之后才生成的。这些数据来源比较复杂,有的是通过Ajax加载过来的,有的原本就包含在HTML文档里,还有的是通过JavaScript结合特定算法算出来的。
先讲讲第一种来源,也就是通过Ajax加载数据的情况。一开始,原始页面里并不包含某些数据。等原始页面加载完了,它会再向服务器的某个接口发送请求,获取数据,然后这些数据经过处理,才会在网页上显示出来。这个过程,本质上就是发送了一个Ajax请求。
随着Web技术不断发展,现在这种类型的页面越来越常见了。网页的原始HTML文档里根本没有数据,所有的数据都是通过Ajax统一加载之后,才展示到页面上的。这样做,在Web开发中可以实现前后端分离,还能减少服务器直接渲染页面所产生的压力。
所以啊,要是遇到这种页面,直接用requests这类库去抓取原始页面,是没办法获取到有效数据的。这个时候,就得去分析网页在后台向接口发送的Ajax请求。要是能用requests模拟出这个Ajax请求,那我们就可以成功抓取到想要的数据了。
正因为如此,接下来,我们主要来了解一下什么是Ajax,以及如何对Ajax请求进行分析和抓取。
一、什么是Ajax
Ajax,全称为Asynchronous JavaScript and XML(异步JavaScript和XML),是一种用于在网页不刷新的情况下与服务器交换数据并更新部分页面内容的技术。它不是一种编程语言,而是通过使用JavaScript来实现的。
传统网页需要刷新整个页面才能更新内容,而Ajax技术允许只更新页面的一部分,从而提供更流畅的用户体验。这个过程是通过在后台与服务器进行数据交互完成的,获取到的数据再由JavaScript处理以修改页面内容。
要体验Ajax的工作原理,可以访问W3School网站上的几个示例(http://www.w3school.com.cn/ajax/ajax_xmlhttprequest_send.asp)。这些示例展示了如何使用XMLHttpRequest对象向服务器发送请求,包括GET和POST方法的区别,以及如何处理响应数据。通过这些实例,你可以看到如何在不重新加载整个页面的情况下更新网页内容。
总之,Ajax使得Web应用程序能够更加动态和互动,提高了用户的浏览体验。
1.1 具体举例
浏览网页时,我们会发现好多网页都有下滑查看更多的选项。就拿这个微博( https://m.weibo.cn/u/2008333573)来说[](https://m.weibo.cn/u/2008333573 " ")。切换到微博页面,一直往下滑,下滑几条微博后,再往下就没了,接着会出现一个加载动画。过一会儿,下方就会继续出现新的微博内容。这个过程,就是Ajax加载的过程。
我们注意到,页面并没有整个刷新,也就是说页面链接没变化,可网页却多了新内容,也就是后面刷出来的新微博。这就是通过Ajax获取新数据并展示出来的过程。
1.2 基本原理
在对Ajax有了一个初步的认识之后,我们进一步深入探究它的基本原理。从发出Ajax请求一直到网页完成更新,整个过程大体上可以划分成三个步骤:
(1)发送请求
(2)解析内容
(3)渲染网页
接下来,我们就对这几个步骤逐一展开,进行详细的讲解。
(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.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里的requests库向服务器发送请求并获得响应的过程比较类似。服务器返回的内容形式多样,有可能是HTML格式,也有可能是JSON格式。
获取到响应内容后,接下来只需要在这个被触发的方法里,使用JavaScript对内容做进一步的处理即可。举个例子,如果返回的内容是JSON格式,我们就可以对它进行解析,将其转换成JavaScript能够方便处理的数据结构,比如对象或者数组等。
(3)渲染网页
JavaScript具备对网页内容进行修改的功能。当我们把服务器响应的内容解析完成后,就能够调用JavaScript,依据解析后的内容对网页做进一步的处理。比如说,利用`document.getElementById().innerHTML`这样的操作,就可以实现对网页中某个元素内部源代码的更改,如此一来,网页上显示的内容也就随之发生变化了。这种操作其实就是DOM操作,也就是对Document(网页文档)进行诸如更改、删除之类的操作。
就拿上面提到的例子来讲,`document.getElementById("myDiv").innerHTML=xmlhttp.responseText` 这行代码,将ID为myDiv的节点内部的HTML代码替换成了服务器返回的内容。这样一来,myDiv元素里面就会呈现出服务器新返回的数据,网页的部分内容也就实现了更新。
可以发现,发送请求、解析内容、渲染网页这三个步骤全部是由JavaScript来完成的,它完整地实现了整个Ajax请求、解析以及渲染的流程。
我们再回顾一下微博的下拉刷新功能,本质上就是JavaScript向服务器发送了一个Ajax请求,获取到新的微博数据后,对数据进行解析,然后把数据渲染到网页上展示出来。
由此可知,我们看到的微博页面上那些真实的数据,都是通过一次次的Ajax请求获取到的。要是我们想要抓取这些数据,就必须弄清楚这些请求是如何发送出去的,发送给了哪个服务器,以及发送请求时携带了哪些参数。一旦知道了这些信息,不就可以使用Python来模拟这个请求发送的操作,进而获取到相应的结果了吗?
那么接下来,我们就来探究一下从什么地方能够查看到这些后台的Ajax操作,深入了解它具体是怎样发送的,以及发送请求时具体携带了哪些参数。
二、Ajax的分析
还是以之前说的微博为例。咱们都清楚,微博页面拖动刷新后出现的内容是通过Ajax加载的,并且页面的URL没有改变。那要到哪里才能查看这些Ajax请求呢?
2.1 查看请求
这得靠浏览器的开发者工具,下面我以Chrome浏览器为例详细说说。
先用Chrome浏览器打开微博链接https://m.weibo.cn/u/2008333573 。打开后,在页面上任意位置点击鼠标右键,从弹出来的快捷菜单里找到"检查"选项并点击,这时开发者工具就弹出来了,具体样子参照下图 。

打开开发者工具后,能看到Elements选项卡,这里面展示的是网页的源代码,右侧显示的是节点的样式。
但这些并不是我们要找的内容。我们需要切换到Network选项卡,然后重新刷新页面,这时就能看到选项卡里出现了好多条目,如下图所示。

之前提到过,这里记录的其实就是页面加载的时候,浏览器和服务器之间发送请求以及接收响应的所有情况。
Ajax有专门的请求类型,叫xhr。从下图能看到,有个请求名称是以getIndex开头的,它的Type显示为xhr,这就是一个Ajax请求。用鼠标点击这个请求,就能查看它的详细信息。
在请求详情页面的右侧,可以看到Request Headers、URL和Response Headers等信息。在Request Headers里面,有一项是X-Requested-With:XMLHttpRequest,这就说明这个请求属于Ajax请求,具体可看下图 。

接着,点击一下Preview,就能看到响应内容了,格式是JSON。这里Chrome浏览器自动帮我们解析好了,页面上有箭头,点击箭头就能展开或收起相应内容,具体情况如下图所示。

仔细看能发现,这里返回的是我的个人信息,比如昵称、简介、头像等,这些就是用来渲染个人主页的数据。JavaScript接收到这些数据后,就会执行相应的渲染方法,进而把整个页面渲染出来。
此外,我们还能切换到Response选项卡,在里面能看到服务器真实返回的数据,具体如下图所示。

接下来,回到第一个请求,看看它的Response内容,参考下图。

这是最开始的链接 ( https://m.weibo.cn/u/2008333573 ) 返回的结果,代码不到50行,结构很简单,主要就是执行了一些JavaScript代码。
所以说,我们看到的微博页面的真实数据,不是原始页面直接返回的,而是原始页面执行JavaScript后,又向后台发送Ajax请求,浏览器获取到数据后再进一步渲染出来的 。
2.2 过滤请求
接下来,再用Chrome开发者工具的筛选功能,把所有的Ajax请求筛选出来。在请求的上方有一层筛选栏,直接点击XHR,这时下方显示的所有请求就都是Ajax请求了,如下图所示。

接着,不断滑动页面,可以看到页面底部一条条新微博被刷出来,开发者工具下方也一个个地出现Ajax请求,这样我们就能捕获到所有的Ajax请求了。
随便点开一个条目,都能清楚看到它的Request URL、Request Headers、Response Headers、Response Body等内容。这时,想模拟请求和提取数据就很简单了。下图所示的内容,就是我的某一页微博的列表信息。

到现在,我们已经能分析出Ajax请求的一些详细信息了。接下来,只要用程序模拟这些Ajax请求,就能轻松提取我们需要的信息。
在下一节中,我们用Python实现Ajax请求的模拟,进而实现数据的抓取。
三、解析提取Ajax 结果
还是以微博为例,下面我们要用 Python 来模拟那些 Ajax 请求,把我发布过的微博抓取下来。
3.1 分析请求
先打开 Ajax 的 XHR 过滤器,然后不断滑动微博页面,让新的微博内容加载出来。这时候能看到,会不断有 Ajax 请求发送出去。
从这些请求里选一个,分析它的参数信息。点击这个请求,进入到详情页面。
我们发现,这是一个 GET 类型的请求,请求的链接是(https://m.weibo.cn/api/container/getIndex?type=uid&value=2008333573&containerid=1076032008333573&since_id=3514220849071579),请求带着四个参数,分别是 type、value、containerid、since_id。
再看看其他的请求,会发现它们的 type、value 和 containerid 这三个参数的值一直都不变。type 始终是 uid,value 的值就是页面链接里的数字,其实这个数字就是用户的 id。containerid 呢,是由 107603 加上用户 id 组成的。会变化的参数只有 since_id,很明显,这个参数是用来控制微博分页的,为了防止被爬虫,人家做了另外的处理。
3.2 分析响应
接着,看看刚才选的这个请求的响应内容,如下图所示。

响应内容是 JSON 格式的,浏览器的开发者工具自动帮我们解析好了,方便查看。这里面最关键的两部分信息是 cardlistInfo 和 cards:cardlistInfo 里面有个重要信息叫 total,观察后发现,它表示的是微博的总数量,我们可以根据这个数字来大概估算一下微博有多少页;cards 是一个列表,里面有 10 个元素,展开其中一个元素看看,如下图所示。


mblog内容如下:
{
"visible": {
"type": 0,
"list_id": 0
},
"created_at": "Mon Nov 19 23:30:04 +0800 2012",
"id": "3514220849071579",
"mid": "3514220849071579",
"can_edit": false,
"text": "转发微博",
"source": "360安全浏览器",
"favorited": false,
"pic_ids": [],
"is_paid": false,
"mblog_vip_type": 0,
"user": {
"id": 2008333573,
"screen_name": "林Tangos",
"profile_image_url": "https://tva1.sinaimg.cn/crop.13.13.313.313.180/77b4bd05jw8f44h2k95r8j20990dw0u4.jpg?KID=imgbed,tva\&Expires=1744647665\&ssig=u%2BVA9Wm0be",
"profile_url": "https://m.weibo.cn/u/2008333573?luicode=10000011\&lfid=1076032008333573",
"close_blue_v": false,
"description": "",
"follow_me": false,
"following": false,
"follow_count": 274,
"followers_count": "122",
"cover_image_phone": "https://tva1.sinaimg.cn/crop.0.0.640.640.640/549d0121tw1egm1kjly3jj20hs0hsq4f.jpg",
"avatar_hd": "https://ww1.sinaimg.cn/orj480/77b4bd05jw8f44h2k95r8j20990dw0u4.jpg",
"badge": {
"unread_pool": 1,
"unread_pool_ext": 1,
"user_name_certificate": 1
},
"statuses_count": 90,
"verified": false,
"verified_type": -1,
"gender": "m",
"mbtype": 0,
"svip": 0,
"urank": 11,
"mbrank": 0,
"followers_count_str": "122",
"verified_reason": "",
"like": false,
"like_me": false,
"special_follow": false
},
"retweeted_status": {
"visible": {
"type": 0,
"list_id": 3,
"list_idstr": "3"
},
"created_at": "Mon Nov 19 15:32:58 +0800 2012",
"id": "3514100782679935",
"mid": "3514100782679935",
"text": "抱歉,根据作者设置的微博可见时间范围,此微博已不可见。 ",
"edit_config": {
"edited": false
},
"user": null,
"bid": "z5UPcbfaL",
"source": ""
},
"reposts_count": 0,
"comments_count": 0,
"reprint_cmt_count": 0,
"attitudes_count": 0,
"mixed_count": 0,
"pending_approval_count": 0,
"isLongText": false,
"show_mlevel": 0,
"darwin_tags": [],
"ad_marked": false,
"mblogtype": 0,
"item_category": "status",
"rid": "0_0_50_165731835786324711_0_0_0",
"extern_safe": 0,
"number_display_strategy": {
"apply_scenario_flag": 19,
"display_text_min_number": 1000000,
"display_text": "100万+"
},
"content_auth": 0,
"is_show_mixed": false,
"comment_manage_info": {
"comment_manage_button": 1,
"comment_permission_type": 0,
"approval_comment_type": 0,
"comment_sort_type": 0
},
"pic_num": 0,
"mlevel": 0,
"detail_bottom_bar": 0,
"analysis_extra": "mblog_rt_mid:3514100782679935",
"mblog_menu_new_style": 0,
"edit_config": {
"edited": false
},
"reads_count": 0,
"raw_text": "转发微博",
"bid": "z5XWQC3VN"
}
可以看到,这个元素里有个重要的字段叫 mblog。展开 mblog 能发现,里面包含了微博的一些信息,像 attitudes_count(点赞数)、comments_count(评论数)、reposts_count(转发数)、created_at(发布时间)、text(微博正文)等,而且这些信息都是格式化好的。
这样一来,我们访问一次这个接口,就能得到 10 条微博的数据,而且每次请求时,只需要改变 since_id 参数的值就行。
但是想获取到所有的微博数据, since_id是变化的,这个要深入了解 since_id的动态变化算法才行。(有反爬虫限制)
四、Ajax抓取实战
现在,我们用程序来模拟这些 Ajax 请求,把我的某一页10条的微博都爬取下来。我们先把请求头的信息拷贝下来,请求数据时需要按照此格式内容构造。请求头如下:
bash
Request URL:
https://m.weibo.cn/api/container/getIndex?type=uid&value=2008333573&containerid=1076032008333573&since_id=3514220849071579
Request Method:
GET
Status Code:
200 OK (from service worker)
Referrer Policy:
strict-origin-when-cross-origin
content-encoding:
gzip
content-security-policy:
upgrade-insecure-requests
content-type:
application/json; charset=utf-8
date:
Mon, 14 Apr 2025 13:21:05 GMT
lb:
49.7.37.75
proc_node:
mweibo-h5-v8-1-web-hj4-5cf9d75bd9-w9pnz
server:
SHANHAI-SERVER
ssl_node:
msre-10-185-8-138.yf.intra.weibo.cn
vary:
Accept-Encoding
x-log-uid:
2008333573
x-powered-by:
PHP/7.2.1
accept:
application/json, text/plain, */*
mweibo-pwa:
1
referer:
https://m.weibo.cn/u/2008333573
sec-ch-ua:
"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"
sec-ch-ua-mobile:
?0
sec-ch-ua-platform:
"Windows"
user-agent:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
x-requested-with:
XMLHttpRequest
x-xsrf-token:
e8e409
首先,定义一个函数,用来获取每次请求的结果。因为请求时since_id 参数是会变化的,所以我们把它作为函数的参数传进去。
先定义了 base_url,它是请求 URL 的前半部分。然后构造了一个参数字典,其中 type、value 和 containerid 这几个参数的值是固定的,page 是会变化的参数。接着调用 urlencode 方法,把参数字典转化成 URL 的 GET 请求参数形式。再把 base_url 和参数拼起来,得到一个完整的新 URL。之后,用 requests 库去请求这个链接,并且带上 headers 参数。请求后判断响应的状态码,如果是 200,就调用 json 方法把响应内容解析成 JSON 格式并返回,否则不返回任何东西。要是请求过程中出了异常,就捕获异常并输出异常信息。
然后,我们要定义一个解析函数,从请求结果里提取出我们想要的信息。比如这次我们想保存微博的 id、正文、点赞数、评论数和转发数这些内容。可以先遍历 cards 列表,然后从 mblog 里获取各个信息,再赋值给一个新的字典返回。
这里借助 pyquery 库,把微博正文里的 HTML 标签去掉了。
最后,我们特定爬取某一页内容,把提取到的结果打印输出,完整代码如下:
python
import requests
from urllib.parse import urlencode
from pyquery import PyQuery as pq
base_url = 'https://m.weibo.cn/api/container/getIndex?'
headers = {
'Host': 'm.weibo.cn',
'Referer': 'https://m.weibo.cn/u/2008333573',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json, text/plain, */*',
'Sec-Ch-Ua': '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Mweibo-Pwa': '1',
'X-Xsrf-Token': 'e8e409'
}
def get_page(page):
params = {
'type': 'uid',
'value': '2008333573',
'containerid': '1076032008333573',
'since_id': page
}
url = base_url + urlencode(params)
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json(), page
except requests.ConnectionError as e:
print('Error', e.args)
def parse_page(json, page: int):
if json:
items = json.get('data').get('cards')
for index, item in enumerate(items):
if page == 1 and index == 1:
continue
else:
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
if __name__ == '__main__':
json = get_page(3514220849071579)
results = parse_page(*json)
for result in results:
print(result)
这样,我们通过分析 Ajax 请求,编写爬虫程序,成功把微博列表数据爬取下来了。

这一节主要是为了演示如何模拟 Ajax 请求,爬取到的结果不是重点。这个程序还有很多可以改进的地方,比如动态计算页码、处理微博查看全文的情况等等。要是你有兴趣,可以自己动手试试看。通过这个例子,我们学会了怎么分析 Ajax 请求,以及怎么用程序模拟请求来抓取数据。掌握了这些原理,下一节的 Ajax 实战操作就会更轻松了。
参考学习书籍:Python 3网络爬虫开发实战