2023年最新Ajax爬取技术教程

前言

你一定遇到过这样的场景:兴致勃勃地用 requests 请求一个网页(比如微博、抖音网页版、小红书旧版列表),返回的 HTML 却只有空荡荡的骨架,正文、列表数据像是凭空消失了一样。这种情况多半是因为网站采用了 Ajax(Asynchronous JavaScript and XML) 动态加载内容——服务器并不是一开始就把所有数据塞进 HTML,而是等页面加载完成后,前端再悄悄向后端接口发起请求,拿到 JSON 或 XML 数据后渲染到页面上。

这篇教程就带着你从「开发者工具入门抓包」到「实战微博移动端」,覆盖 2023~2024 主流的 Ajax 分析方法和基础反爬应对技巧。全程没有复杂公式,30 分钟就能上手!


1. 现代 Ajax 请求分析技术

Ajax 的核心思想是「前端异步请求后端接口拿数据」,所以我们的第一步就是找到这个隐藏的接口。所有现代浏览器(Chrome、Edge、Firefox 都可以,推荐 Chrome)自带的「开发者工具」就是我们的最佳助手。

1.1 开发者工具快速开启

不用再回忆繁琐的右键菜单顺序,记住这几个快捷键就够了:

  • Windows / LinuxF12Ctrl + Shift + I
  • macOSCmd + Option + I

专业操作流程:

  1. 先打开目标页面(例如微博移动端个人主页:https://m.weibo.cn/u/2830678474)。
  2. 按下快捷键启动开发者工具,切换到顶部的 Network(网络) 面板。
  3. Ctrl + R(Windows / Linux)或 Cmd + R(macOS)强制刷新页面——只有这样才能完整捕获页面加载过程中触发的所有请求,包括静态资源和动态接口。

1.2 快速筛选隐藏的 Ajax 接口

刷新之后,Network 面板会列出密密麻麻的请求(CSS、JS、图片、字体……),直接肉眼寻找接口效率太低。用好下面几个筛选标签,可以瞬间定位目标:

  1. Fetch/XHR:覆盖 99% 的现代动态接口,包括传统的 XMLHttpRequest 和新式的 fetch API
  2. WS:如果网页内容是通过 WebSocket 双向实时通信获取的(比如聊天消息、直播弹幕),就点这个标签。
  3. GraphQL:部分新网站(例如新版 GitHub 部分页面、Notion)会用 GraphQL,你可以在筛选栏手动点击「Filter」并勾选「GraphQL」。

筛选完 Fetch/XHR 之后,剩下的请求基本上就是我们想要的动态接口了。

1.3 快速判断接口是否有效

面对一长串接口,怎么快速识别出「正文列表」「用户信息」这类真正的数据接口呢?试试这三个小技巧:

  1. 看请求方法和 URL 特征
    大多数数据接口使用 GET(获取数据)或 POST(提交复杂参数),URL 中往往会出现 /api//v2//feed//list//user/ 等关键词。

  2. 看响应预览(Preview)
    在 Network 面板中点击某个请求,再切换到右侧的 Preview 标签。如果你看到了熟悉的内容,比如博主的微博正文、用户头像 URL,那么恭喜你,目标接口找到了

  3. 复制 curl 命令辅助调试
    如果担心自己漏看请求头,可以直接右键点击有用的请求,选择 Copy → Copy as cURL (bash),这样就能拿到一份和浏览器完全一致的请求模板,后续转成 Python 代码也非常方便。


2. 2023~2024 主流反爬应对基础

找到接口只是第一步,很多网站会设置反爬机制:直接在浏览器里打开接口地址可能一切正常,但用 Python 一请求就返回 403、401 或者空数据。这里给大家几个入门但非常实用的解决方案。

2.1 完整模拟浏览器请求(最常用)

大部分入门级反爬(比如检查 User-AgentRefererCookie 这些请求头)只要把刚才复制出来的 cURL 命令转换成 Python 代码就能轻松搞定。

推荐使用免费工具一键转换:curlconverter.com

转换时需要注意两点:

  1. 如果请求中存在 Cookie,不要直接把可能很快过期的 Cookie 硬编码到代码里。可以使用 httpxcookiejar 来管理。
  2. 务必加上 http2=True 参数,因为很多新网站已经强制要求 HTTP/2 协议,不开启可能直接 403。

下面是一个通用的完整模拟请求模板,使用支持 HTTP/2 的异步库 httpx,比 requests 快不少:

import httpx

async def fetch_api_data(url: str, method: str = "GET", params: dict = None, headers: dict = None, data: dict = None):
    """
    通用异步模拟请求函数
    :param url: 目标接口地址
    :param method: 请求方法(GET 或 POST)
    :param params: GET 请求的查询参数
    :param headers: 完整请求头(建议从 curlconverter 复制过来)
    :param data: POST 请求的表单数据
    """
    async with httpx.AsyncClient(http2=True, follow_redirects=True) as client:
        try:
            resp = await client.request(
                method=method,
                url=url,
                params=params,
                headers=headers,
                data=data,
                timeout=10.0  # 设置超时,防止卡死
            )
            resp.raise_for_status()  # 遇到 4xx/5xx 自动抛出异常
            return resp.json()       # 大多数接口返回 JSON,直接解析
        except httpx.HTTPStatusError as e:
            print(f"请求失败,状态码:{e.response.status_code}")
        except Exception as e:
            print(f"其他错误:{e}")

2.2 应对动态参数的入门方法

如果模拟了完整请求头之后依然失败,那大概率是接口中包含了动态参数,比如每次请求都会变化的 signtoken_t 等。对于入门级别的动态参数,可以尝试使用 PyExecJS 直接执行页面上的加密 JS 来解决:

  1. 在开发者工具的 Sources(源代码) 面板中,利用 Ctrl + Shift + F 全局搜索参数名(如 sign),找到生成该参数的 JavaScript 函数。
  2. 把这段 JS 函数和相关依赖代码复制出来,注意补全它依赖的其他变量或函数。
  3. 用 PyExecJS 执行这段 JS,计算出当前请求需要的动态参数。

举个简单的例子,假设生成 token 的函数是 getToken(timestamp)

import execjs
import time

# 补全依赖后的 JS 代码
js_code = """
// 假设这是从 Sources 找到的生成 token 的函数
function getRandomStr() {
    return Math.random().toString(36).substr(2, 9);
}
function getToken(t) {
    return getRandomStr() + t.toString().substr(-6);
}
"""

# 编译 JS
ctx = execjs.compile(js_code)
# 调用函数,传入当前时间戳(单位:秒)
timestamp = int(time.time())
token = ctx.call("getToken", timestamp)
print(f"生成的动态 token:{token}")

3. 微博移动端实战(2023 年 12 月亲测有效)

理论讲得差不多了,现在我们就用 微博移动端某公开博主的主页https://m.weibo.cn/u/2830678474,不涉及个人隐私)来做一个完整的实战演练。

3.1 抓包找到目标接口

按照 1.1~1.3 的操作步骤:

  1. 打开目标页面 → 启动开发者工具 → 切换到 Network → 筛选 Fetch/XHR → 强制刷新。
  2. 依次点击几个请求的 Preview,发现 /api/feed/profile 这个接口返回了博主微博列表的 HTML 片段(没错,有些接口虽然长得像 API,但响应的内容却是 HTML,而不是纯 JSON)。
  3. 切换到 Headers 标签,记录下请求的 URL、查询参数和关键请求头。

目标接口的核心信息如下:

  • 请求方法GET
  • URLhttps://m.weibo.cn/api/feed/profile
  • 查询参数uid(博主 ID,必填)、page(页码,从 1 开始)
  • 必要请求头User-Agent(移动端 UA)、Referer(博主主页地址)、X-Requested-WithXMLHttpRequest,标识这是一个 Ajax 请求)

3.2 完整 Python 实现代码

把通用请求模板和抓包结果结合起来,再用 parsel 解析返回的 HTML 片段,就能得到完整的爬取脚本:

import httpx
from parsel import Selector
import asyncio

# ---------------------- 配置参数 ----------------------
UID = "2830678474"          # 目标博主 ID
MAX_PAGE = 2                # 最大爬取页数
HEADERS = {
    "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1",
    "X-Requested-With": "XMLHttpRequest",
    "Referer": f"https://m.weibo.cn/u/{UID}",
}

# ---------------------- 核心函数 ----------------------
async def fetch_weibo_page(uid: str, page: int = 1):
    """爬取单页微博数据"""
    async with httpx.AsyncClient(http2=True, follow_redirects=True) as client:
        try:
            resp = await client.get(
                url="https://m.weibo.cn/api/feed/profile",
                params={"uid": uid, "page": page},
                headers=HEADERS,
                timeout=10.0
            )
            resp.raise_for_status()
            return resp.json()
        except Exception as e:
            print(f"第{page}页爬取失败:{e}")
            return None

def parse_weibo_html(html: str):
    """解析微博 HTML 片段,提取有用信息"""
    selector = Selector(text=html)
    weibo_list = []
    # 遍历每个微博卡片
    for card in selector.css(".card-wrap"):
        # 过滤掉广告卡片(包含 mblog-tag--ad 类名)
        if card.css(".mblog-tag--ad"):
            continue
        weibo_list.append({
            "微博ID": card.css(".card::attr(mid)").get(),
            "发布时间": card.css(".time::text").get(),
            "微博正文": "".join(card.css(".weibo-text::text, .weibo-text a::text").getall()).strip(),
            "点赞数": card.css(".like-count::text").get("0"),
            "评论数": card.css(".comment-count::text").get("0"),
            "转发数": card.css(".repost-count::text").get("0"),
        })
    return weibo_list

async def main():
    """主函数:多页爬取"""
    all_weibos = []
    for page in range(1, MAX_PAGE + 1):
        print(f"正在爬取第{page}页...")
        data = await fetch_weibo_page(UID, page)
        if not data or not data.get("data", {}).get("cards"):
            print(f"第{page}页没有数据,停止爬取")
            break
        # 2023 年 12 月时,接口返回的 cards 中第一个元素是置顶微博(isTop:1),后续才是普通微博
        # 这里把所有卡片中的 HTML 片段拼接起来,统一解析
        full_html = "".join([
            card.get("mblog", {}).get("text", "")
            for card in data["data"]["cards"]
            if card.get("mblog")
        ])
        page_weibos = parse_weibo_html(full_html)
        all_weibos.extend(page_weibos)
        # 加上 3 秒延迟,避免请求过快被封
        await asyncio.sleep(3)

    # 打印最终结果
    print(f"\n爬取完成,共获取 {len(all_weibos)} 条有效微博:")
    for idx, weibo in enumerate(all_weibos, 1):
        print(f"\n【第{idx}条】")
        for key, value in weibo.items():
            print(f"{key}{value}")

if __name__ == "__main__":
    asyncio.run(main())

4. 法律与道德红线(必须看!)

技术本身是中立的,但使用技术的人必须遵守规则,否则可能带来严重的法律风险。以下几点请一定牢记:

  1. 遵守 robots.txt:爬取前先访问目标网站的 https://域名/robots.txt,看看是否明确禁止了你要访问的路径。
  2. 设置合理的爬取间隔:建议至少 3 秒 / 请求,不要给目标服务器造成不必要的压力。
  3. 绝不爬取个人隐私数据:例如手机号、身份证号、私密朋友圈或私密微博等。
  4. 遵守相关法律法规:在国内进行数据采集,务必遵守《数据安全法》《个人信息保护法》《网络安全法》等法律法规。

总结

这篇教程带你从零开始,完成了现代 Ajax 爬取的基础入门:

  1. 用开发者工具找到隐藏的 Ajax 接口(筛选 Fetch/XHR → 查看 Preview)
  2. 完整模拟浏览器请求(curl 一键转换 → 添加 http2=True
  3. 入门级动态参数应对(使用 PyExecJS 执行前端加密逻辑)
  4. 遵守法律与道德红线

如果你在实战中遇到了更复杂的反爬手段(比如 TLS 指纹识别、WebAssembly 加密、行为验证码等),可以关注我们后续的进阶教程,一步一步攻克难题!