2023年最新Ajax爬取技术教程

前言

有没有遇到过这样的情况:用 requests 直接请求某个网页(比如微博、抖音网页版或者小红书旧版列表),拿到的 HTML 只有框架,正文、列表数据全是空的?这大概率是因为网站用了 Ajax(异步JavaScript和XML) 动态加载内容——不是服务器一开始就吐给你全量数据,而是页面加载后,前端再偷偷请求后端接口,拿到JSON/XML再渲染到页面上。

这篇教程就带你从「开发者工具入门抓包」到「实战微博移动端」,覆盖2023-2024主流的Ajax分析、反爬应对基础,全程无复杂公式,30分钟内就能上手


1. 现代Ajax请求分析技术

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

1.1 开发者工具快速开启

不用再记繁琐的右键菜单顺序,这几个组合键一键搞定:

  • Windows/Linux:F12Ctrl + Shift + I
  • macOS:Cmd + 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、图片、字体……),直接找接口太麻烦,用这3个筛选标签精准定位:

  1. Fetch/XHR:覆盖99%的现代动态接口(包括老的 XMLHttpRequest 和新的 fetch API
  2. WS:如果是实时聊天、直播弹幕这类「双向实时通信」的内容,找这个标签(WebSocket)
  3. GraphQL:部分新网站(比如GitHub新版部分页面、Notion)会用这个,筛选栏可能需要手动点击「Filter」→ 勾选「GraphQL」

筛选完Fetch/XHR后,剩下的请求基本都是我们要找的动态接口了!

1.3 快速判断接口是否有用

找到了一堆接口,怎么知道哪个是「正文列表」「用户信息」的接口?可以用这3个小技巧:

  1. 看请求方法和URL后缀
    • 大部分数据接口用 GET(获取)或 POST(提交参数较多的复杂请求)
    • URL通常会带 /api/ /v2/ /feed/ /list/ /user/ 这类关键词
  2. 看响应预览(Preview): 在Network面板点击某个请求→切换到右侧的 Preview 标签——如果看到了熟悉的内容(比如刚才打开的微博的正文、用户头像URL),那恭喜你,找到了目标接口!
  3. 复制curl调试逻辑: 怕自己漏看请求头?右键点击有用的请求→CopyCopy as cURL (bash),可以拿到和浏览器完全一致的请求参数,后面转成Python代码也超级方便。

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

找到接口只是第一步,很多网站会加上反爬机制——直接复制接口URL用浏览器打开可能没问题,但用Python请求就403/401/返回空数据。这里给大家几个入门但实用的解决方案

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

大部分入门级反爬(比如检查User-Agent、Referer、Cookie这些请求头),只要把刚才Copy的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: 目标接口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 应对动态参数的入门方法

如果模拟完整请求后还是不行,大概率是接口有动态参数(比如 sign token _t 这类每次请求都变的参数)。

入门级的动态参数可以用 「PyExecJS直接执行前端加密JS」 解决:

  1. 在开发者工具的 Sources(源代码) 标签里,找到生成动态参数的JS函数(可以用 Ctrl + Shift + F 全局搜索参数名,比如 sign
  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
  • URL:https://m.weibo.cn/api/feed/profile
  • 查询参数:uid(博主ID,必填)、page(页码,从1开始)
  • 必要请求头:User-Agent(移动端的)、Referer(博主主页的URL)、X-Requested-With(XMLHttpRequest,标识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),后面是普通微博
        # 这里直接取所有cards的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. 完整模拟浏览器请求(用curlconverter转代码→加http2=True)
  3. 入门级动态参数应对(用PyExecJS执行前端JS)
  4. 遵守法律与道德红线

如果遇到更复杂的反爬(比如TLS指纹、WebAssembly加密、行为验证),可以关注后续的进阶教程!