很多现代网站采用前后端分离架构,浏览器访问页面时,服务器只返回一个空的 HTML 骨架,真实数据由 JavaScript 通过 XHR(XMLHttpRequest) 或 Fetch API 向后端发起异步请求获取,再以 JSON 格式返回,最后由前端框架(Vue / React / Angular)动态渲染到页面上。
传统静态网页:
浏览器请求 → 服务器返回完整HTML(含数据) → 渲染显示
动态网页(AJAX):
浏览器请求 → 服务器返回空HTML骨架
→ 浏览器执行JS → JS发起XHR/Fetch请求API
→ 服务器返回JSON数据 → JS渲染到页面因此,用 requests.get() 获取页面源码后,用 BeautifulSoup 解析,往往什么都找不到——因为数据根本不在 HTML 里,而在 API 接口的 JSON 响应中。
解决这类问题的最高效方案是:跳过浏览器渲染环节,直接定位并模拟 AJAX 请求,从数据源头获取 JSON 数据。
F12,切换到 Network(网络) 标签页XHR 或 Fetch,过滤掉图片、CSS 等无关请求User-Agent、Referer、Cookie、Authorization 等)requests 库构造相同的请求,直接获取 JSON 数据以下代码以模拟的公开 API 为例,演示如何抓取异步加载的分页数据。实际使用时,将 URL 和参数替换为你从开发者工具中捕获的真实接口即可。
"""
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 逻辑还原算法,或改用无头浏览器方案 |
如果接口存在复杂的签名验证、设备指纹、滑块验证码等反爬机制,逆向成本过高,可以考虑以下备选方案:
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_datafrom 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 htmltime.sleep),避免触发频率限制(429 Too Many Requests)方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
直接模拟 XHR 请求(推荐) | 速度快、资源占用少、数据格式规整 | 需手动分析接口,可能遇到签名验证 | 接口稳定、无强反爬 |
Playwright / Selenium | 能执行 JS、自动处理渲染 | 速度慢、资源占用大 | 接口加密复杂、需交互触发 |
asyncio + aiohttp | 高并发、吞吐量高 | 开发复杂度稍高 | 大批量数据采集 |
核心原则:能用 requests 直接请求 API 的,就不要启动浏览器;只有当接口逆向成本过高时,才考虑无头浏览器兜底。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。