告别 403 与空数据!爬虫新手避坑指南:如何优雅地抓取 Ajax 异步加载数据

0 阅读8分钟

引言:为什么你的 requests 总是拿到一堆“空壳”?

在做爬虫时,你一定遇到过这个让人抓狂的场景:
在浏览器里看某个电商网站的商品列表,明明有图有真相、数据满满;但当你信心满满地用 requests.get() 把页面 HTML 抓下来保存到本地打开一看——傻眼了,商品列表那个

标签里空空如也,只有一行“数据加载中...”或者几个闪烁的占位符。
别慌,这并不是你的代码写错了,而是你踩到了爬虫新手的第一个大坑:网页数据的异步加载(Ajax)。

今天,我们就从底层原理聊起,手把手教你如何拆解 Ajax 异步接口,并用最优雅、最高效的方式把这些隐藏在幕后的真实数据“扒”出来!

根因探究:浏览器正常访问 vs requests 直接请求

为什么浏览器能看到,Python 却拿不到?我们先来看一下两者的本质区别:

维度****浏览器正常访问****Python requests 请求
HTML 状态完整渲染后的 DOM 树原始的 HTML 源码文本
JavaScript 引擎执行(V8 等引擎动态解析)不执行(仅当做纯文本下载)
Ajax 异步数据自动触发 JS 脚本并填充彻底忽略,无法触发
对反爬与接口的依赖依赖浏览器黑盒渲染必须直接面对或解析底层接口

典型“空壳”网页源码剖析

当你请求 example.com/products 时,服务器返回给 requests 的往往只是一个前端框架(如 React/Vue)的挂载模板:

<html>
<head><title>商品商城</title></head>
<body>
  <div id="product-list-app">
    <div class="loading-spinner">数据加载中,请稍候...</div>
  </div>
  
  <script src="/js/chunk-vendors.js"></script>
  <script>
    // 浏览器会执行这段代码,但 requests 会直接无视它
    axios.get('/api/v1/products?page=1&limit=20')
         .then(response => { renderToDOM(response.data); });
  </script>
</body>
</html>

由于 requests 没有 JavaScript 解释器,它拿完 HTML 就完事了,根本不会去调用那个 /api/v1/products 接口。这就导致你拿到的永远是“数据加载中”。

要解决这个问题,行业内无非两条路:要么顺藤摸瓜,直接向幕后的 Ajax 接口要数据(硬核高效);要么模拟浏览器环境(简单粗暴)。

方案一:直击死穴,直接逆向分析 Ajax 接口(强烈推荐)

这是最优雅、运行效率最高、也是资深工程师最常用的方案。既然浏览器是通过接口拿数据的,我们为什么不直接去请求那个接口呢?

第一步:利用浏览器 F12 抓包定位

  1. 打开目标网页,按下 F12 或者右键选择“检查”,切换到 Network(网络) 面板。
  2. 核心操作:在过滤器中勾选 Fetch/XHR(部分老网站可能在 Doc 或 JS 中)。
  3. 刷新页面,或者在页面上点击“下一页”触发加载。
  4. 观察左侧的请求列表,重点寻找包含 api、list、query、json、v1 等关键词的 URL。

第二步:伪造请求特征与请求头

进入接口的 Headers 标签页,我们不仅要拿到它的 Request URL,还要识别它的“真实身份认证”。常规的 Ajax 接口往往带有以下三个核心请求头:

  • User-Agent: 浏览器标识,必须伪造。
  • Referer: 告诉服务器你从哪个页面跳转过来的,很多 Ajax 接口会强校验此项。
  • X-Requested-With: 值为 XMLHttpRequest,标识这是一个异步请求。

第三步:Python 代码高精度复现

针对一个标准的带分页的 JSON 接口,标准的复现姿势如下:

import requests
import logging

# 初始化日志输出
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def fetch_ajax_data(page=1):
    # 1. 真实的 Ajax 接口地址,不再是原网页 URL
    api_url = "https://example.com/api/v1/products"
    
    # 2. 精准构造查询参数
    params = {
        "page": page,
        "limit": 20,
        "sort": "sales_desc"
    }
    
    # 3. 严丝合缝的伪造请求头
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Referer": "https://example.com/products",
        "X-Requested-With": "XMLHttpRequest",
        "Accept": "application/json, text/plain, */*"
    }
    
    try:
        logging.info(f"正在抓取第 {page} 页数据...")
        response = requests.get(api_url, params=params, headers=headers, timeout=10)
        
        # 异常状态码直接抛出
        response.raise_for_status()
        
        # Ajax 接口最爽的地方:直接返回结构化的 JSON,不需要用 BeautifulSoup 苦哈哈地解析 HTML 了
        json_data = response.json()
        return json_data
        
    except requests.exceptions.HTTPError as http_err:
        logging.error(f"HTTP 错误 (状态码: {response.status_code}): {http_err}")
    except Exception as err:
        logging.error(f"发生其他错误: {err}")
    return None

if __name__ == "__main__":
    data = fetch_ajax_data(page=1)
    if data:
        print(data)

方案一的进阶试炼:应对接口反爬(会话、高频与 IP 封禁)

当你能够精准找到接口后,往往会遇到对方服务器的“无情毒打”:刚爬了 3 页,接口突然返回 403 Forbidden、429 Too Many Requests 或者干脆直接弹验证码。

针对这些高频痛点,我们有三套标准拳法:

1. 应对“必须登录/权限校验” —— 引入 requests.Session()

很多 Ajax 接口强依赖于用户的登录态(Cookie 或 Authorization Token)。这时候不要每次请求都用 requests.get,而是要用 Session 来自动维持状态。

# 创建一个会话对象,它会自动帮你保存和发送 Cookie
session = requests.Session()

# 模拟登录,获取 Cookie 写入会话
login_url = "https://example.com/api/auth/login"
login_data = {"username": "my_crawler_account", "password": "secure_password"}
session.post(login_url, json=login_data)

# 此时再请求受保护的 Ajax 接口,会自动携带登录成功的 Cookie
protected_url = "https://example.com/api/v1/user/orders"
response = session.get(protected_url)

2. 应对“高频访问封锁 IP” —— 接入动态爬虫代理池

在大规模、高并发地抓取 Ajax 接口时,单 IP 几乎必然会被封。这时候,在代码中引入高质量的动态高匿代理是唯一的破局之道。这里我们以行业内常用的高匿通道为例,展示如何做动态切换:

import requests
import time

# 代理通道配置(以亿牛云爬虫代理服务为例)
proxy_host = "http://http.16yun.cn:16888"  # 代理服务器地址
proxy_user = "your_username"               # 认证用户名
proxy_pass = "your_password"               # 认证密码

proxies = {
    "http": f"http://{proxy_user}:{proxy_pass}@{proxy_host.split('//')[1]}",
    "https": f"http://{proxy_user}:{proxy_pass}@{proxy_host.split('//')[1]}"
}

def crawl_with_proxy(url):
    try:
        # 通过隧道代理发送请求,每次请求在代理服务端会自动切换不同出口 IP
        response = requests.get(url, proxies=proxies, timeout=5)
        
        if response.status_code == 200:
            return response.json()
        elif response.status_code == 429:
            print("⚠️ 触发频控,触发全局规避机制,强行休眠...")
            time.sleep(5)
    except requests.exceptions.ProxyError:
        print("❌ 代理节点异常,等待自动重试")
    except Exception as e:
        print(f"❌ 请求失败: {e}")
    return None

方案二:大巧若拙,使用 Playwright / Selenium 无头浏览器

如果对方的 Ajax 接口参数里带有复杂的、被高度混淆加密的数字签名(比如 _signature=wx12as...),逆向成本极高,该怎么办?_signature=wx12as...

这时候,我们就祭出大杀器:无头浏览器(Headless Browser)。既然 requests 不能执行 JavaScript,那我们就直接开一个真的浏览器去跑,让它自己把数据渲染出来,我们直接下网收鱼。

相比老旧、API 繁琐的 Selenium,这里我强烈推荐微软出品的后起之秀 —— Playwright。它支持异步、速度更快、且不容易被反爬虫特征识别。

Playwright 自动化抓取示例

from playwright.sync_api import sync_playwright
import time

def run_playwright_scraper():
    with sync_playwright() as p:
        # launch 启动浏览器,headless=True 表示后台无界面运行
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        
        print("🌐 正在通过浏览器打开页面...")
        page.goto("https://example.com/products")
        
        # 🔥 关键:显式等待动态组件加载到 DOM 中,完美替代死等 time.sleep()
        # 这里表示等待类名为 'product-item' 的元素出现,最多等 10 秒
        page.wait_for_selector(".product-item", timeout=10000)
        
        # 页面有些数据需要滚动才会触发加载,模拟向下滚动
        page.evaluate("window.scrollTo(0, document.body.scrollHeight);")
        time.sleep(1.5) # 给滚动留一点缓冲渲染时间
        
        # 获取完全渲染结束后的完整 HTML 源码
        rendered_html = page.content()
        print(f"✅ 成功获取渲染后的 HTML,长度为: {len(rendered_html)}")
        
        # 此时你可以用 BeautifulSoup 解析 rendered_html 了
        # ...
        
        browser.close()

if __name__ == "__main__":
    run_playwright_scraper()

终极总结:天下武功,唯快不破,该如何抉择?

在面对异步加载的网页时,我们可以根据以下思维导图和表格来做技术选型:

选型维度****方案一:直击 Ajax 接口****方案二:无头浏览器 (Playwright/Selenium)
开发难度中等(需要具备一点 F12 抓包和分析能力)极低(会点鼠标、会看网页标签就能写)
采集效率极高(毫秒级响应,纯数据传输)极低(需要加载图片、CSS、JS,极耗内存)
数据清洗极简(接口直接返回标准的 Python dict/list)繁琐(仍需要针对复杂的 HTML DOM 进行二次解析)
适用场景接口逻辑清晰、参数常规、大规模批量抓取接口深度加密、多重签名、小规模应急采集

博主的心得: 爬虫的最高境界是**“找到数据的源头,以最小的代价直击要害”。在实际工程中,能用方案一直接抓接口的,绝不用方案二模拟浏览器**。只有当接口被各种魔改过的 Webpack 混淆加密搞到头大、且工期卡死的时候,无头浏览器才是你的低保兜底方案。

掌握了如何处理 Ajax 数据,你已经成功跨过了爬虫新手的门槛。在接下来的文章中,我会继续分享如何“魔改 PySocks 源码”来硬抗一些奇葩的代理限制,感兴趣的兄弟点个关注不迷路!

  • 本文为技术交流,请自觉遵守目标网站的 Robots 协议,合理控制抓取频率,切勿用于非法用途。