首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python 爬虫实战:抓取 XHR/Fetch 异步加载的动态数据

Python 爬虫实战:抓取 XHR/Fetch 异步加载的动态数据

原创
作者头像
用户12890632
发布于 2026-11-14 21:42:07
发布于 2026-11-14 21:42:07
140
举报

一、为什么页面源码里没有数据?

很多现代网站采用前后端分离架构,浏览器访问页面时,服务器只返回一个空的 HTML 骨架,真实数据由 JavaScript 通过 XHR(XMLHttpRequest) 或 Fetch API 向后端发起异步请求获取,再以 JSON 格式返回,最后由前端框架(Vue / React / Angular)动态渲染到页面上。

代码语言:javascript
复制
传统静态网页:
浏览器请求 → 服务器返回完整HTML(含数据) → 渲染显示

动态网页(AJAX):
浏览器请求 → 服务器返回空HTML骨架
          → 浏览器执行JS → JS发起XHR/Fetch请求API
          → 服务器返回JSON数据 → JS渲染到页面

因此,用 requests.get() 获取页面源码后,用 BeautifulSoup 解析,往往什么都找不到——因为数据根本不在 HTML 里,而在 API 接口的 JSON 响应中。


二、核心思路:绕过页面,直取 API

解决这类问题的最高效方案是:跳过浏览器渲染环节,直接定位并模拟 AJAX 请求,从数据源头获取 JSON 数据。

操作步骤

  1. 打开 Chrome 开发者工具:按 F12,切换到 Network(网络) 标签页
  2. 筛选 XHR/Fetch:点击过滤器中的 XHR 或 Fetch,过滤掉图片、CSS 等无关请求
  3. 触发数据加载:刷新页面、滚动、翻页或点击按钮,观察新出现的请求
  4. 定位数据接口:逐个点击请求,在 Preview / Response 面板中查看返回内容,找到包含目标数据的那个请求
  5. 分析请求细节:在 Headers 面板中记录请求 URL、请求方法(GET/POST)、Query 参数、Request Payload、以及必要的 Headers(如 User-Agent、Referer、Cookie、Authorization 等)
  6. 用 Python 模拟请求:使用 requests 库构造相同的请求,直接获取 JSON 数据

三、完整代码示例

以下代码以模拟的公开 API 为例,演示如何抓取异步加载的分页数据。实际使用时,将 URL 和参数替换为你从开发者工具中捕获的真实接口即可。

代码语言:javascript
复制
"""
Python 爬虫:抓取 XHR/Fetch 异步加载数据
核心思路:分析浏览器 Network 面板中的 XHR 请求,
         用 requests 直接模拟 API 调用,获取 JSON 数据。
"""

import requests
import csv
import time
import random
from typing import Optional


class AjaxSpider:
    """异步数据爬虫"""

    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": (
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/120.0.0.0 Safari/537.36"
            ),
            "Accept": "application/json, text/plain, */*",
            "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
            # 如果接口需要 Referer 校验,取消下面注释并填入真实值
            # "Referer": "https://example.com/list",
            # 如果接口需要登录态,取消下面注释并填入真实 Cookie
            # "Cookie": "your_cookie_here",
        })

    def fetch_page(self, page: int, page_size: int = 20) -> Optional[dict]:
        """
        请求单页数据(GET 方式)

        实际使用时,将 url 和 params 替换为从浏览器
        Network → XHR 中捕获的真实接口地址和参数。
        """
        url = "https://wlt.east-car.com/posts"
        params = {
            "_page": page,
            "_limit": page_size,
        }
        try:
            resp = self.session.get(url, params=params, timeout=10)
            resp.raise_for_status()
            return resp.json()
        except requests.RequestException as e:
            print(f"[错误] 第 {page} 页请求失败: {e}")
            return None

    def fetch_page_post(self, page: int, page_size: int = 20) -> Optional[dict]:
        """
        请求单页数据(POST 方式,JSON 传参)

        很多接口使用 POST + JSON body 传参,例如:
        {
            "page": 1,
            "pageSize": 20,
            "keyword": "python"
        }
        """
        url = "https://example.com/api/data"
        payload = {
            "page": page,
            "pageSize": page_size,
        }
        try:
            resp = self.session.post(
                url,
                json=payload,
                timeout=10
            )
            resp.raise_for_status()
            return resp.json()
        except requests.RequestException as e:
            print(f"[错误] 第 {page} 页请求失败: {e}")
            return None

    def crawl(self, max_pages: int = 5, page_size: int = 20) -> list:
        """
        爬取多页数据,返回所有数据的列表
        """
        all_items = []
        for page in range(1, max_pages + 1):
            print(f"正在抓取第 {page}/{max_pages} 页 ...")
            data = self.fetch_page(page, page_size)

            if data is None:
                print(f"  第 {page} 页获取为空,跳过")
                continue

            # jsonplaceholder 返回的是列表,直接追加
            # 如果接口返回的是 {"code": 0, "data": {"list": [...]}} 结构,
            # 需要根据实际结构调整,例如:
            #   items = data.get("data", {}).get("list", [])
            if isinstance(data, list):
                all_items.extend(data)
            elif isinstance(data, dict):
                # 常见嵌套结构,按需调整路径
                items = (
                    data.get("data")
                    or data.get("result")
                    or data.get("items")
                    or data.get("list")
                    or []
                )
                all_items.extend(items if isinstance(items, list) else [items])
            else:
                print(f"  第 {page} 页返回数据类型异常: {type(data)}")

            # 随机延时,模拟真人浏览节奏,降低被封风险
            time.sleep(random.uniform(0.5, 1.5))

        print(f"\n抓取完成,共获取 {len(all_items)} 条数据")
        return all_items

    def save_to_csv(self, items: list, filename: str = "output.csv") -> None:
        """
        将数据保存为 CSV 文件
        """
        if not items:
            print("没有数据可保存")
            return

        # 自动提取所有字段名作为表头
        fieldnames = list(items[0].keys())
        with open(filename, "w", newline="", encoding="utf-8-sig") as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(items)
        print(f"数据已保存至 {filename}")


def main():
    spider = AjaxSpider()

    # 抓取 5 页数据,每页 20 条
    items = spider.crawl(max_pages=5, page_size=20)

    # 打印前 3 条数据预览
    for i, item in enumerate(items[:3], 1):
        print(f"\n--- 第 {i} 条 ---")
        for key, value in item.items():
            print(f"  {key}: {value}")

    # 保存为 CSV
    spider.save_to_csv(items, "posts.csv")


if __name__ == "__main__":
    main()

四、常见接口类型与适配

实际项目中,XHR 接口的形态各不相同,下面列出几种常见情况:

场景

请求方式

参数传递

适配方法

简单分页列表

GET

Query 参数(?page=1&size=20)

requests.get(url, params={...})

带筛选条件

POST

JSON body

requests.post(url, json={...})

需要登录

GET/POST

Cookie / Token

session.headers["Cookie"] = "..." 或 session.headers["Authorization"] = "Bearer ..."

滚动加载更多

GET

offset / cursor 参数

维护 offset 变量,循环递增请求

接口有签名/加密参数

GET/POST

动态生成的签名

需逆向 JS 逻辑还原算法,或改用无头浏览器方案


五、当 API 无法直接抓取时的备选方案

如果接口存在复杂的签名验证、设备指纹、滑块验证码等反爬机制,逆向成本过高,可以考虑以下备选方案:

方案 A:Playwright 无头浏览器

代码语言:javascript
复制
from playwright.sync_api import sync_playwright
import json

def crawl_with_playwright(url: str):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        # 拦截 XHR 响应,直接捕获 JSON 数据
        api_data = []
        def handle_response(response):
            if "/api/" in response.url and response.request.resource_type == "xhr":
                try:
                    api_data.append(response.json())
                except Exception:
                    pass
        page.on("response", handle_response)
        page.goto(url, wait_until="networkidle")
        browser.close()
    return api_data

方案 B:Selenium 等待元素渲染

代码语言:javascript
复制
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def crawl_with_selenium(url: str):
    options = Options()
    options.add_argument("--headless")
    driver = webdriver.Chrome(options=options)
    driver.get(url)
    # 等待动态内容加载完成
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, ".data-item"))
    )
    html = driver.page_source  # 此时 HTML 已包含渲染后的数据
    driver.quit()
    return html

六、注意事项

  1. 遵守 robots.txt 协议和网站服务条款,不要对目标服务器造成过大压力
  2. 设置合理的请求间隔(time.sleep),避免触发频率限制(429 Too Many Requests)
  3. 妥善处理异常:网络超时、JSON 解析失败、空数据等情况都要有容错机制
  4. 保护个人隐私数据,不要采集和存储敏感个人信息
  5. Cookie 和 Token 具有时效性,长期运行的爬虫需要定期更新认证信息

七、总结

方案

优点

缺点

适用场景

直接模拟 XHR 请求(推荐)

速度快、资源占用少、数据格式规整

需手动分析接口,可能遇到签名验证

接口稳定、无强反爬

Playwright / Selenium

能执行 JS、自动处理渲染

速度慢、资源占用大

接口加密复杂、需交互触发

asyncio + aiohttp

高并发、吞吐量高

开发复杂度稍高

大批量数据采集

核心原则:能用 requests 直接请求 API 的,就不要启动浏览器;只有当接口逆向成本过高时,才考虑无头浏览器兜底。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、为什么页面源码里没有数据?
    • 二、核心思路:绕过页面,直取 API
      • 操作步骤
    • 三、完整代码示例
    • 四、常见接口类型与适配
    • 五、当 API 无法直接抓取时的备选方案
      • 方案 A:Playwright 无头浏览器
      • 方案 B:Selenium 等待元素渲染
    • 六、注意事项
    • 七、总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档