Python数据API断点续传怎么实现

wen python案例 19

本文目录导读:

Python数据API断点续传怎么实现

  1. 基于Offset/Page的断点续传
  2. 基于Cursor/Token的断点续传
  3. 基于时间范围的断点续传
  4. 通用断点续传框架
  5. 高级特性:并发断点续传

在Python中实现数据API的断点续传,主要涉及到如何记录已获取的数据位置如何从断点处继续请求,以下提供几种常见场景的实现方式:

基于Offset/Page的断点续传

适用于分页API(如list接口),通过记录页码或偏移量实现。

import requests
import json
from pathlib import Path
class OffsetBasedResumableFetcher:
    def __init__(self, checkpoint_file="checkpoint.json"):
        self.checkpoint_file = Path(checkpoint_file)
        self.checkpoint = self.load_checkpoint()
    def load_checkpoint(self):
        """加载检查点"""
        if self.checkpoint_file.exists():
            with open(self.checkpoint_file, 'r') as f:
                return json.load(f)
        return {"next_offset": 0, "completed": False}
    def save_checkpoint(self, offset, completed=False):
        """保存检查点"""
        with open(self.checkpoint_file, 'w') as f:
            json.dump({
                "next_offset": offset,
                "completed": completed
            }, f)
    def fetch_page(self, url, offset, limit=100):
        """获取单页数据"""
        params = {"offset": offset, "limit": limit}
        response = requests.get(url, params=params)
        response.raise_for_status()
        data = response.json()
        return data.get("results", []), data.get("total", 0)
    def fetch_all_data(self, url, batch_size=100):
        """主获取逻辑"""
        offset = self.checkpoint["next_offset"]
        while not self.checkpoint["completed"]:
            try:
                results, total = self.fetch_page(url, offset, batch_size)
                if not results:
                    break
                # 处理数据(示例:保存到文件)
                self.process_data(results)
                # 更新检查点
                offset += len(results)
                self.save_checkpoint(offset)
                # 判断是否完成
                if offset >= total:
                    self.save_checkpoint(offset, completed=True)
                    print("所有数据获取完成")
                    break
            except (requests.RequestException, json.JSONDecodeError) as e:
                print(f"获取数据失败,将在offset={offset}处重试: {e}")
                self.save_checkpoint(offset)
                raise
    def process_data(self, data):
        """处理数据(示例:打印数据条数)"""
        print(f"处理了 {len(data)} 条数据")

基于Cursor/Token的断点续传

适用于游标分页API(如Twitter、某些GraphQL API)。

import requests
import json
from pathlib import Path
class CursorBasedResumableFetcher:
    def __init__(self, checkpoint_file="cursor_checkpoint.json"):
        self.checkpoint_file = Path(checkpoint_file)
        self.checkpoint = self.load_checkpoint()
    def load_checkpoint(self):
        if self.checkpoint_file.exists():
            with open(self.checkpoint_file, 'r') as f:
                return json.load(f)
        return {"next_cursor": None, "completed": False}
    def save_checkpoint(self, cursor, completed=False):
        with open(self.checkpoint_file, 'w') as f:
            json.dump({
                "next_cursor": cursor,
                "completed": completed
            }, f)
    def fetch_page(self, url, cursor=None, limit=100):
        """获取带游标的分页数据"""
        params = {"limit": limit}
        if cursor:
            params["cursor"] = cursor
        response = requests.get(url, params=params)
        response.raise_for_status()
        data = response.json()
        # 假设API返回格式为:
        # { "data": [...], "next_cursor": "xxx", "has_more": true }
        return data.get("data", []), data.get("next_cursor"), data.get("has_more", False)
    def fetch_all_data(self, url, batch_size=100):
        cursor = self.checkpoint["next_cursor"]
        while not self.checkpoint["completed"]:
            try:
                results, next_cursor, has_more = self.fetch_page(url, cursor, batch_size)
                if not results:
                    break
                # 处理数据
                self.process_data(results)
                # 更新游标
                cursor = next_cursor
                self.save_checkpoint(cursor)
                if not has_more:
                    self.save_checkpoint(cursor, completed=True)
                    print("所有数据获取完成")
                    break
            except Exception as e:
                print(f"获取数据失败,将在cursor={cursor}处重试: {e}")
                self.save_checkpoint(cursor)
                raise
    def process_data(self, data):
        print(f"处理了 {len(data)} 条数据")

基于时间范围的断点续传

适用于时间序列数据(如日志、事件流)。

import requests
import json
from datetime import datetime, timedelta
from pathlib import Path
class TimeBasedResumableFetcher:
    def __init__(self, checkpoint_file="time_checkpoint.json"):
        self.checkpoint_file = Path(checkpoint_file)
        self.checkpoint = self.load_checkpoint()
    def load_checkpoint(self):
        if self.checkpoint_file.exists():
            with open(self.checkpoint_file, 'r') as f:
                return json.load(f)
        return {
            "last_fetch_time": (datetime.now() - timedelta(days=1)).isoformat(),
            "completed": False
        }
    def save_checkpoint(self, last_time, completed=False):
        with open(self.checkpoint_file, 'w') as f:
            json.dump({
                "last_fetch_time": last_time,
                "completed": completed
            }, f)
    def fetch_time_range(self, url, start_time, end_time, limit=100):
        """按时间范围获取数据"""
        params = {
            "start_time": start_time,
            "end_time": end_time,
            "limit": limit
        }
        response = requests.get(url, params=params)
        response.raise_for_status()
        data = response.json()
        return data.get("results", []), data.get("has_more", False)
    def fetch_all_data(self, url, batch_size=100, time_window_minutes=60):
        last_time = datetime.fromisoformat(self.checkpoint["last_fetch_time"])
        now = datetime.now()
        while last_time < now and not self.checkpoint["completed"]:
            end_time = min(last_time + timedelta(minutes=time_window_minutes), now)
            try:
                results, has_more = self.fetch_time_range(
                    url, 
                    last_time.isoformat(), 
                    end_time.isoformat(), 
                    batch_size
                )
                if results:
                    self.process_data(results)
                # 更新时间戳
                last_time = end_time
                self.save_checkpoint(last_time.isoformat())
                if last_time >= now:
                    self.save_checkpoint(last_time.isoformat(), completed=True)
                    print("所有数据获取完成")
            except Exception as e:
                print(f"获取时间范围 {last_time} - {end_time} 失败: {e}")
                self.save_checkpoint(last_time.isoformat())
                raise
    def process_data(self, data):
        print(f"处理了 {len(data)} 条数据")

通用断点续传框架

适用于各种场景的通用解决方案:

import requests
import json
import hashlib
from pathlib import Path
from typing import Optional, Dict, Any
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ResumableFetcher:
    def __init__(self, 
                 api_base_url: str,
                 checkpoint_dir: str = "checkpoints",
                 session: Optional[requests.Session] = None):
        self.api_base_url = api_base_url
        self.checkpoint_dir = Path(checkpoint_dir)
        self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
        self.session = session or requests.Session()
        # 生成唯一的检查点文件名(基于API URL的哈希)
        self.checkpoint_name = hashlib.md5(api_base_url.encode()).hexdigest() + ".json"
        self.checkpoint_file = self.checkpoint_dir / self.checkpoint_name
        self.checkpoint = self.load_checkpoint()
    def load_checkpoint(self) -> Dict[str, Any]:
        """加载或初始化检查点"""
        if self.checkpoint_file.exists():
            try:
                with open(self.checkpoint_file, 'r') as f:
                    return json.load(f)
            except (json.JSONDecodeError, IOError) as e:
                logger.error(f"检查点文件损坏,将重新开始: {e}")
                return {}
        return {}
    def save_checkpoint(self, **kwargs):
        """保存检查点"""
        try:
            self.checkpoint.update(kwargs)
            with open(self.checkpoint_file, 'w') as f:
                json.dump(self.checkpoint, f, indent=2, default=str)
            logger.debug(f"检查点已保存: {self.checkpoint}")
        except Exception as e:
            logger.error(f"保存检查点失败: {e}")
    def fetch(self, endpoint: str, params: Optional[Dict] = None,
              pagination_type: str = "offset", **pagination_params):
        """
        通用数据获取方法
        Args:
            endpoint: API端点
            params: 额外的查询参数
            pagination_type: 分页类型 ("offset", "cursor", "time")
            **pagination_params: 分页相关参数
        """
        url = f"{self.api_base_url}/{endpoint.lstrip('/')}"
        paginators = {
            "offset": self._fetch_with_offset,
            "cursor": self._fetch_with_cursor,
            "time": self._fetch_with_time
        }
        paginator = paginators.get(pagination_type)
        if not paginator:
            raise ValueError(f"不支持的分页类型: {pagination_type}")
        return paginator(url, params or {}, **pagination_params)
    def _fetch_with_offset(self, url: str, params: Dict, **kwargs):
        """基于偏移量的分页获取"""
        limit = kwargs.get("limit", 100)
        max_retries = kwargs.get("max_retries", 3)
        offset = self.checkpoint.get("next_offset", 0)
        while True:
            page_params = {**params, "offset": offset, "limit": limit}
            for attempt in range(max_retries):
                try:
                    response = self.session.get(url, params=page_params, timeout=30)
                    response.raise_for_status()
                    data = response.json()
                    break
                except Exception as e:
                    if attempt == max_retries - 1:
                        logger.error(f"获取数据失败,offset={offset}: {e}")
                        raise
                    logger.warning(f"重试 {attempt + 1}/{max_retries}: {e}")
            results = data.get("results", data.get("data", []))
            if not results:
                break
            yield results
            offset += len(results)
            self.save_checkpoint(next_offset=offset)
            total = data.get("total", data.get("count", float('inf')))
            if offset >= total:
                self.save_checkpoint(next_offset=offset, completed=True)
                break
    def _fetch_with_cursor(self, url: str, params: Dict, **kwargs):
        """基于游标的分页获取"""
        limit = kwargs.get("limit", 100)
        cursor_field = kwargs.get("cursor_field", "next_cursor")
        cursor = self.checkpoint.get("next_cursor")
        while True:
            page_params = {**params, "limit": limit}
            if cursor:
                page_params[cursor_field] = cursor
            response = self.session.get(url, params=page_params, timeout=30)
            response.raise_for_status()
            data = response.json()
            results = data.get("results", data.get("data", []))
            if not results:
                break
            yield results
            cursor = data.get("next_cursor", data.get("cursor"))
            self.save_checkpoint(next_cursor=cursor)
            if not data.get("has_more", True):
                self.save_checkpoint(next_cursor=cursor, completed=True)
                break
    def _fetch_with_time(self, url: str, params: Dict, **kwargs):
        """基于时间范围的分页获取"""
        start_time = self.checkpoint.get("last_fetch_time", 
                                         (datetime.now() - timedelta(days=1)).isoformat())
        time_field = kwargs.get("time_field", "start_time")
        page_params = {**params, time_field: start_time}
        response = self.session.get(url, params=page_params, timeout=30)
        response.raise_for_status()
        data = response.json()
        results = data.get("results", data.get("data", []))
        if results:
            yield results
            last_time = results[-1].get("timestamp")
            if last_time:
                self.save_checkpoint(last_fetch_time=last_time)
        self.save_checkpoint(completed=True)
# 使用示例
if __name__ == "__main__":
    # 示例1:基于Offset的分页
    fetcher1 = ResumableFetcher("https://api.example.com")
    for batch in fetcher1.fetch("/users", pagination_type="offset", limit=50):
        print(f"获取到 {len(batch)} 条用户数据")
    # 示例2:基于Cursor的分页
    fetcher2 = ResumableFetcher("https://api.example.com")
    for batch in fetcher2.fetch("/tweets", pagination_type="cursor", limit=100):
        print(f"获取到 {len(batch)} 条推文数据")
    # 示例3:基于时间的分页
    fetcher3 = ResumableFetcher("https://api.example.com")
    for batch in fetcher3.fetch("/events", pagination_type="time", limit=200):
        print(f"获取到 {len(batch)} 条事件数据")

高级特性:并发断点续传

使用aiohttp实现异步并发获取:

import aiohttp
import asyncio
import json
from pathlib import Path
class AsyncResumableFetcher:
    def __init__(self, checkpoint_file="async_checkpoint.json", max_workers=5):
        self.checkpoint_file = Path(checkpoint_file)
        self.checkpoint = self.load_checkpoint()
        self.max_workers = max_workers
        self.semaphore = asyncio.Semaphore(max_workers)
    def load_checkpoint(self):
        if self.checkpoint_file.exists():
            with open(self.checkpoint_file, 'r') as f:
                return json.load(f)
        return {"completed_pages": [], "in_progress": []}
    def save_checkpoint(self):
        with open(self.checkpoint_file, 'w') as f:
            json.dump(self.checkpoint, f, indent=2)
    async def fetch_page(self, session, url, page_num):
        """异步获取单页数据"""
        async with self.semaphore:
            params = {"page": page_num, "per_page": 100}
            async with session.get(url, params=params) as response:
                data = await response.json()
                return page_num, data.get("results", [])
    async def fetch_all_pages(self, url, total_pages):
        """并发获取所有页面"""
        async with aiohttp.ClientSession() as session:
            # 过滤掉已完成的页面
            remaining_pages = [
                p for p in range(1, total_pages + 1) 
                if p not in self.checkpoint["completed_pages"]
            ]
            tasks = []
            for page_num in remaining_pages:
                task = self.fetch_page(session, url, page_num)
                tasks.append(task)
            # 使用gather并发执行,但会捕获异常
            results = await asyncio.gather(*tasks, return_exceptions=True)
            for result in results:
                if isinstance(result, Exception):
                    logger.error(f"页面获取失败: {result}")
                    continue
                page_num, data = result
                if data:
                    self.process_data(data)
                self.checkpoint["completed_pages"].append(page_num)
                self.save_checkpoint()
    def process_data(self, data):
        print(f"处理了 {len(data)} 条数据")
  1. 检查点文件:使用JSON文件保存当前进度,支持程序重启后恢复
  2. 原子性操作:处理完数据后再更新检查点,防止数据重复
  3. 错误处理:捕获网络异常,保存当前进度以便重试
  4. 数据一致性:确保处理完成的数据已持久化后,再更新检查点
  5. 幂等性:API调用和数据处理逻辑应支持重复执行

选择哪种方式取决于API的分页机制、数据量大小和业务需求,对于大规模数据,建议使用异步并发方式提高效率,同时做好错误处理和重试机制。

抱歉,评论功能暂时关闭!