如何处理数据管道中的错误和异常?

7 阅读5分钟

数据管道错误与异常处理:从入门到生产级实战

在 ETL 数据管道中,异常处理不是可选功能,而是生命线。没有健壮的异常处理,管道会随时崩溃、数据丢失、污染目标库,甚至无法定位问题。

我会用最实用、可直接落地的方式,教你处理 Python 数据管道的所有常见异常,包含:

  • 通用异常处理原则
  • 分阶段(抽取/转换/加载)异常处理方案
  • 生产级代码模板
  • 重试、告警、死信队列、数据校验

一、数据管道异常处理核心原则

  1. 不中断主流程:单个文件/单条数据失败,不导致整个管道崩溃
  2. 精准捕获:不使用裸 ​​except​​,只捕获预期异常
  3. 完整日志:记录时间、阶段、错误信息、数据ID,方便排查
  4. 失败隔离:坏数据进入「死信队列」,不污染正常数据
  5. 可重试:网络/接口类异常自动重试,不直接失败
  6. 强校验:数据问题提前拦截,不让错误进入下游

二、分阶段异常处理(ETL 全流程)

1. Extract 抽取阶段异常

最常见异常:文件不存在、网络超时、API 报错、权限不足、格式损坏

处理方案

  • 捕获特定异常
  • 自动重试(网络类)
  • 空值安全返回
  • 记录详细日志

2. Transform 转换阶段异常

最常见异常:字段缺失、类型错误、空值、格式错误、计算异常

处理方案

  • 逐行/逐批处理
  • 坏数据隔离(死信文件)
  • 数据强校验
  • 类型安全转换

3. Load 加载阶段异常

最常见异常:数据库连接失败、主键冲突、字段超长、事务中断

处理方案

  • 事务回滚
  • 批量写入 + 错误捕获
  • 写入失败不污染历史数据

三、生产级异常处理代码模板(直接可用)

这是企业真实使用的健壮 ETL 异常处理框架,包含日志、重试、死信队列、校验。

import pandas as pd
import requests
import sqlite3
import logging
import time
from datetime import datetime
from requests.exceptions import RequestException

# ====================== 1. 生产级日志配置 ======================
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    handlers=[logging.FileHandler("etl_error.log"), logging.StreamHandler()]
)
logger = logging.getLogger(__name__)

# ====================== 2. 重试装饰器(网络/接口专用) ======================
def retry(max_retries=3, delay=2):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except RequestException as e:
                    logger.warning(f"第 {i+1} 次重试失败:{str(e)}")
                    time.sleep(delay)
            logger.error(f"重试 {max_retries} 次后彻底失败")
            return pd.DataFrame()
        return wrapper
    return decorator

# ====================== 3. Extract 抽取(带异常+重试) ======================
@retry(max_retries=3, delay=2)
def extract_orders_api():
    url = "https://jsonplaceholder.typicode.com/orders"
    response = requests.get(url, timeout=10)
    response.raise_for_status()  # 抛出HTTP错误
    return pd.DataFrame(response.json())

def extract_users_csv(file_path):
    try:
        df = pd.read_csv(file_path)
        logger.info(f"抽取用户数据成功:{len(df)} 条")
        return df
    except FileNotFoundError:
        logger.error(f"文件不存在:{file_path}")
        return pd.DataFrame()
    except pd.errors.EmptyDataError:
        logger.error("CSV 文件为空")
        return pd.DataFrame()
    except Exception as e:
        logger.error(f"抽取未知异常:{str(e)}")
        return pd.DataFrame()

# ====================== 4. Transform 转换(坏数据隔离+校验) ======================
def safe_transform_row(row):
    """安全转换单条数据,失败则返回None"""
    try:
        row["email"] = str(row["email"]).lower().strip()
        row["amount"] = round(float(row["amount"]), 2)
        return row
    except Exception as e:
        logger.error(f"数据转换失败 ID={row.get('order_id')},错误:{str(e)}")
        return None

def transform_data(users_df, orders_df):
    try:
        # 基础清洗
        users_df = users_df.drop_duplicates(subset=["user_id"])
        orders_df = orders_df.drop_duplicates(subset=["order_id"])

        # 合并数据
        merged = pd.merge(users_df, orders_df, on="user_id", how="inner")

        # 安全转换 + 分离坏数据
        valid_data = []
        bad_data = []
        for _, row in merged.iterrows():
            new_row = safe_transform_row(row)
            if new_row is not None:
                valid_data.append(new_row)
            else:
                bad_data.append(row)

        # 保存坏数据(死信队列)
        if bad_data:
            pd.DataFrame(bad_data).to_csv("bad_data/dead_letter.csv", mode="a", index=False, header=False)
            logger.warning(f"隔离坏数据 {len(bad_data)} 条 → 已存入死信文件")

        valid_df = pd.DataFrame(valid_data)
        logger.info(f"转换完成:有效数据 {len(valid_df)} 条")
        return valid_df

    except Exception as e:
        logger.error(f"转换流程失败:{str(e)}")
        return pd.DataFrame()

# ====================== 5. Load 加载(事务+异常) ======================
def load_to_db(df, db_path="etl.db"):
    if df.empty:
        logger.warning("无有效数据,跳过加载")
        return

    try:
        with sqlite3.connect(db_path) as conn:
            # 事务:失败自动回滚
            df.to_sql(name="user_orders", con=conn, if_exists="append", index=False)
        logger.info(f"加载成功:{len(df)} 条")
    except sqlite3.IntegrityError:
        logger.error("主键冲突,加载失败")
    except sqlite3.OperationalError:
        logger.error("数据库连接失败/无权限")
    except Exception as e:
        logger.error(f"加载失败:{str(e)}")

# ====================== 6. 主管道(永不崩溃) ======================
def etl_pipeline():
    logger.info("=== ETL 管道启动 ===")
    try:
        users = extract_users_csv("users.csv")
        orders = extract_orders_api()

        if users.empty or orders.empty:
            logger.warning("原始数据为空,终止流程")
            return

        cleaned = transform_data(users, orders)
        load_to_db(cleaned)
        logger.info("=== ETL 管道全部完成 ===")

    except Exception as e:
        logger.critical(f"管道严重崩溃:{str(e)}")

if __name__ == "__main__":
    etl_pipeline()

四、必须掌握的 6 种异常处理武器

1. 精准异常捕获(不要裸 except)

# 错误
except:
    pass

# 正确
except FileNotFoundError:
except RequestException:
except pd.errors.EmptyDataError:
except sqlite3.IntegrityError:

2. 自动重试(网络/接口必备)

适用:API、数据库、FTP 用 ​​tenacity​​ 或手写装饰器都可以。

3. 死信队列(Dead Letter Queue)

坏数据不丢弃、不污染、可重处理

  • 存入 CSV/MySQL/Redis
  • 标记失败原因、数据ID、时间
  • 支持人工修复后重跑

4. 数据校验(提前拦截错误)

# 非空校验
assert not df["user_id"].isnull().any(), "存在空ID"

# 数值范围校验
assert (df["amount"] >= 0).all(), "金额不能为负"

# 格式校验
assert df["email"].str.contains("@").all(), "邮箱格式错误"

5. 事务保证(加载不脏写)

with sqlite3.connect(db) as conn:
    # 要么全成功,要么全失败
    df.to_sql(...)

6. 告警机制(生产必备)

异常达到阈值自动通知:

  • 企业微信/钉钉机器人
  • 邮件
  • 短信
# 简单告警示例
def send_alert(msg):
    requests.post("https://钉钉机器人URL", json={"msgtype": "text", "text": {"content": f"ETL异常:{msg}"}})

五、异常处理最佳实践(总结)

  1. 抽取:文件不存在/网络超时 → 重试 + 日志 + 安全返回
  2. 转换:数据错误 → 死信队列隔离,不中断流程
  3. 加载:数据库错误 → 事务回滚,不污染数据
  4. 日志:必须记录「时间、阶段、数据ID、错误原因」
  5. 监控:失败条数 > 阈值 → 自动告警
  6. 架构:单个数据失败 ≠ 整个管道崩溃

最终一句话总结

数据管道的异常处理,核心就是:不崩溃、可重试、坏数据隔离、全链路日志、自动告警。

如果你需要,我可以帮你把你现有的 ETL 代码改造成生产级、带完整异常处理的版本