Python3 爬虫教程:Ajax 数据爬取实战

开篇小提示
如果你写过爬虫,一定见过这种诡异场面:用 requests 拉下来的网页,代码里只有空荡荡的 <div id="app"></div>,数据呢?跑哪儿去了?
别急,这不是你代码的问题,而是网页用了 Ajax 动态加载——这也是现代前端框架的常规操作。
这次我们就用崔庆才老师的免费靶站 spa1,一步步拆解 Ajax 爬虫的全流程。接口清晰、无复杂反爬,非常适合练手。


1. 准备工作

动手之前,先确保你的电脑上这几样东西都就位:

  • Python 3.6 及以上版本
  • 已安装依赖库:
    pip install requests pymongo
  • MongoDB 服务已启动(本篇会把数据存到 MongoDB,如果暂时不想装数据库,可以先跑到打印数据那一步)
  • 对浏览器开发者工具(F12)的 Network 面板有基本了解即可

2. 目标网站分析

靶站地址:https://spa1.scrape.center/

2.1 网站特点一览

  • 数据全部通过 Ajax 异步加载,不做任何服务端渲染
  • 页面前端框架负责生成内容,返回的 HTML 基本只是“骨架”
  • 支持分页浏览,大约 10 页,每页 10 部电影
  • 点击电影卡片会跳转到详情页,详情数据同样来自 Ajax 接口

2.2 我们要获取的字段

从列表页和详情页的接口响应里,可以拿到完整的电影信息:

  • 电影名称
  • 封面图片链接
  • 类别标签(例如「剧情」「悬疑」)
  • 上映日期
  • 综合评分
  • 剧情简介

3. 初步验证:直接请求页面,能拿到数据吗?

先写几行最简单的代码测一下,看看直接用 requests 下载到的 HTML 里有没有我们想要的内容:

import requests

url = 'https://spa1.scrape.center/'
response = requests.get(url)
print(response.text[:500])  # 只打印前 500 个字符,避免刷屏

运行结果会给你一个“空壳” HTML,里面只有类似 <div id="app"></div> 的容器标签,连“电影”“评分”等关键词的影子都看不到。
这正好印证了我们的判断:数据不是服务端直接输出的,需要去找 Ajax 接口。


4. 爬取列表页

4.1 分析列表页的 Ajax 接口

打开浏览器,按 F12 调出开发者工具,切换到 Network 面板,务必勾选 Preserve log(这样翻页时请求记录不会被清掉),然后点击 XHR/Fetch 过滤,只看 Ajax 请求。

接下来在页面上点击“第 2 页”“第 3 页”,你就会看到 Network 里出现一批格式高度统一的请求:

https://spa1.scrape.center/api/movie/?limit=10&offset=0
https://spa1.scrape.center/api/movie/?limit=10&offset=10
https://spa1.scrape.center/api/movie/?limit=10&offset=20

参数含义

  • limit:固定为 10,代表每页返回多少条电影数据
  • offset:偏移量,第 1 页是 0,第 2 页是 10,第 n 页就是 10 × (n - 1)

4.2 封装列表页爬取函数

为了提高代码复用性,我们先写一个通用的接口请求函数,再专门写一个列表页的调用函数:

import requests
import logging

# 配置日志,方便追踪爬取进度和错误
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s: %(message)s'
)

# 全局常量
INDEX_URL = 'https://spa1.scrape.center/api/movie/?limit={limit}&offset={offset}'
LIMIT = 10  # 每页固定 10 条

def scrape_api(url):
    """通用的 Ajax 接口爬取函数,返回 JSON 数据或 None"""
    logging.info('正在爬取接口:%s', url)
    try:
        response = requests.get(url)
        if response.status_code == 200:
            return response.json()  # 直接返回解析后的 JSON 字典
        logging.error('爬取失败:接口 %s 返回状态码 %d', url, response.status_code)
    except requests.RequestException:
        logging.error('爬取出错:接口 %s 发生请求异常', url, exc_info=True)
    return None

def scrape_index(page):
    """爬取指定页的电影列表"""
    url = INDEX_URL.format(limit=LIMIT, offset=LIMIT * (page - 1))
    return scrape_api(url)

现在,调用 scrape_index(1) 就能拿到第 1 页的 10 部电影数据了。


5. 爬取详情页

5.1 分析详情页的 Ajax 接口

在列表页随便点开一部电影,进入详情页,同样筛选 XHR/Fetch 请求,你会发现一条类似这样的新请求:

https://spa1.scrape.center/api/movie/1/

很明显,这里的 1 就是电影的唯一 ID
而列表页的 JSON 接口早就已经把每部电影的 id 字段返给我们了,所以我们根本不用从 HTML 里解析跳转链接,直接用这个 ID 拼接 URL 就行。

5.2 封装详情页爬取函数

延续刚才的逻辑,复用 scrape_api 函数,三行代码搞定:

DETAIL_URL = 'https://spa1.scrape.center/api/movie/{id}/'

def scrape_detail(movie_id):
    """爬取指定 ID 的电影详情"""
    url = DETAIL_URL.format(id=movie_id)
    return scrape_api(url)

6. 数据存储:存入 MongoDB

靶站的接口返回的就是结构清晰的 JSON 对象,非常适合直接塞进 MongoDB 这种文档型数据库,字段都不需要额外处理。

6.1 配置 MongoDB 连接

先导入 pymongo,并连接本地数据库:

import pymongo

# MongoDB 全局常量
MONGO_URI = 'mongodb://localhost:27017'
MONGO_DB = 'movies_spa1'
MONGO_COLLECTION = 'movies'

# 建立连接并获取集合
client = pymongo.MongoClient(MONGO_URI)
db = client[MONGO_DB]
collection = db[MONGO_COLLECTION]

6.2 封装数据保存函数

update_one 配合 upsert=True 实现数据去重与更新

  • 如果库里已存在同名的电影,就更新字段信息
  • 如果没有,则插入一条新纪录
def save_data(data):
    """将电影数据存入 MongoDB,同名电影去重更新"""
    if not data:
        return
    collection.update_one(
        {'name': data.get('name')},  # 用电影名作为去重条件
        {'$set': data},               # 传入的字段会更新或新增
        upsert=True                    # 若没有匹配记录,则插入新文档
    )
    logging.info('电影《%s》已保存成功!', data.get('name'))

7. 完整爬取流程

最后把所有函数串起来,写一个 main(),一次爬取前 10 页的所有电影:

def main():
    for page in range(1, 11):          # 第 1 页到第 10 页
        index_data = scrape_index(page)
        if not index_data or not index_data.get('results'):
            continue
        # 遍历当前页的每部电影,先拿 ID → 查详情 → 存库
        for item in index_data['results']:
            movie_id = item.get('id')
            detail_data = scrape_detail(movie_id)
            save_data(detail_data)

if __name__ == '__main__':
    main()

运行脚本,你会在控制台看到清晰的日志输出,同时 MongoDB 的 movies_spa1 数据库里就会多出一批电影记录。


8. 改进建议:让爬虫更健壮(简单版)

本篇是入门实战,代码写得很直白,但实际项目中可能需要更稳定、更高效。这里提供几个容易上手的优化思路:

  1. 异步请求提速
    aiohttp 代替 requests,搭配 asyncio 实现并发,爬取速度会明显提升。

  2. 增加错误重试
    scrape_api 加上 tenacity 库的重试装饰器,遇到网络波动时自动重试几次,大幅提高稳定性。

  3. 基础反爬策略
    随机切换 User-Agent,并在请求之间加入 time.sleep(),模拟人类浏览节奏,降低被封风险。

  4. 数据简单校验
    保存前检查 nameid 等关键字段是否存在,避免“坏数据”入库污染集合。


9. 总结

这次实战我们完整走通了 Ajax 动态加载数据的爬虫套路:

  1. 先用 requests 做静态测试,确认数据是异步渲染的
  2. 借助浏览器开发者工具,找到列表页和详情页的 Ajax 接口
  3. 分析接口参数规律,封装通用的请求函数
  4. 把流程串起来,加上日志并存入数据库

完整代码可以直接复制运行,动手之前别忘了先启动 MongoDB 哦!