机构选教务系统,最担心的往往不是功能,而是"我的老数据怎么进去"。

学员名单、剩余课时、缴费记录、课表——这些数据躺在 Excel 里,少则几千行,多则几十万行。导入导出看起来是"小功能",做起来全是坑。

这篇文章把我做教培SaaS导入导出模块踩过的坑完整写出来,从编码乱码、数据校验、到百万行性能优化,都是血泪经验。

一、第一坑:乱码,从编码开始

第一个学员名单导入,就翻车了——所有中文全是乱码。

原因:Excel 文件的编码五花八门。

PYTHON复制# 常见编码探测
import chardet

with open("students.csv", "rb") as f:
    raw = f.read(1024)
    print(chardet.detect(raw))
    # {'encoding': 'GB2312', 'confidence': 0.99}

用户手里传上来的文件:有的 UTF-8、有的 GBK/GB2312、有的带 BOM、有的是 Excel 另存的 CSV。统一按 UTF-8 读,必乱码。

解决方案

  1. 编码自动探测:读前 N 字节用 chardet 探测编码,再解码;
  2. BOM 处理:UTF-8 BOM(\ufeff)要剥掉,否则第一列字段名多一个字符;
  3. 统一转 UTF-8:解码后统一转成内部 UTF-8 存储,后续处理不再碰编码问题。
PYTHON复制def smart_read_csv(path):
    with open(path, "rb") as f:
        raw = f.read()
    enc = chardet.detect(raw[:1024])["encoding"] or "utf-8"
    text = raw.decode(enc, errors="replace").lstrip("\ufeff")
    return text

第二个教训:不要假设用户会给你"标准文件"。 用户给的 Excel 什么鬼样子都有:合并单元格、空格、换行符、看不见的特殊字符——导入模块的第一优先级是"容错",不是"优雅"。

二、第二坑:脏数据的校验,导入前必须过三关

编码解决后,新的问题来了:导入的数据质量参差不齐。

学员表里,手机号有的是 11 位、有的是"138-xxxx-xxxx"、有的直接写"无";剩余课时有的是数字、有的是"50.5课时"、有的带单位;缴费金额有的带千分位逗号。

如果直接入库,后面所有功能(课时扣减、财务对账)都会出错。

解决方案:三关校验

第一关:结构校验——必填字段是否齐全?字段名是否匹配模板?

PYTHON复制REQUIRED = ["name", "phone", "remaining_hours"]
errors = []
for i, row in enumerate(rows, start=2):  # 从第2行开始(跳过表头)
    for col in REQUIRED:
        if not str(row.get(col, "")).strip():
            errors.append(f"第{i}行:{col} 为空")

第二关:格式校验——手机号正则、金额数字化、课时归一化。

PYTHON复制import re
def clean_phone(p):
    p = re.sub(r"[\s\-()()]", "", str(p))
    if re.fullmatch(r"1\d{10}", p):
        return p
    return None  # 格式非法

第三关:业务校验——手机号是否重复?课时是否为负?金额是否超限?

三关校验不通过的行,不进库,而是生成"错误报告"告诉用户错在哪一行、什么原因——用户能改,而不是导入失败让你猜。

导入结果必须可见、可改、可重试:成功 N 条、失败 M 条、失败原因明细——这是导入功能的基本修养。

三、第三坑:性能,从"跑不完"到"秒级"

前两个坑解决了,性能坑来了。

一个 300 人机构的学员数据可能只有几千行,导入几秒钟搞定。但连锁机构、多年积累的数据,动辄几十万行——朴素实现直接卡死。

问题代码:一行一行 INSERT,每行一次数据库往返。

PYTHON复制# 千万不能这么干:逐行 INSERT,几十万行会跑到天荒地老
for row in rows:
    cursor.execute("INSERT INTO students ...", row)

优化方案:批量写入 + 事务控制

PYTHON复制def batch_insert(rows, batch_size=5000):
    conn = get_conn()
    for i in range(0, len(rows), batch_size):
        batch = rows[i:i+batch_size]
        conn.executemany(
            "INSERT INTO students (name, phone, remaining_hours) VALUES (?, ?, ?)",
            batch
        )
        conn.commit()  # 每批提交,避免长事务锁表

实测:5000 行一批,几十万行的导入从"跑不完"降到分钟级。

导出侧的性能:导出几十万行,不能一次性加载到内存再写文件(内存会爆),要用流式导出

PYTHON复制def stream_export():
    writer = csv.writer(...)
    for chunk in query_in_chunks(5000):   # 分页查询
        for row in chunk:
            writer.writerow(row)

一边查一边写,内存占用恒定,大文件导出也不卡。

四、第四坑:导入不是一次性的事,要可回滚

用户导入一批数据,入库后发现模板理解错了,50% 的数据是错的——这时候最怕的是"导入即不可逆"。

解决方案:导入批次 + 回滚机制

  1. 每次导入生成一个 batch_id,所有数据标记批次;
  2. 导入过程先写"临时表"(staging),全部校验通过后一次性"正式表";
  3. 用户发现错误,可以按批次回滚——不是删数据,是"恢复导入前状态"。
SQL复制-- 导入批次表
CREATE TABLE import_batch (
    batch_id VARCHAR(32) PRIMARY KEY,
    file_name VARCHAR(255),
    total_rows INT,
    success_rows INT,
    failed_rows INT,
    status VARCHAR(20),   -- PENDING / DONE / ROLLED_BACK
    created_at DATETIME
);

可回滚的导入,用户才敢大胆用。导入功能做得好,机构迁移系统的阻力就小一半。

五、第五坑:模板设计,决定导入成败

最后说一个"看不见的坑":模板设计。

导入模板不是把字段都列出来就行。设计不好,用户填错率极高。

模板设计原则

  1. 必填和选填分开:必填字段放前面,用红色标注;
  2. 提供示例行:模板里放一行示例数据(用"示例"标记),用户照着填;
  3. 下拉枚举:能下拉的字段(如校区、课程类型)做成下拉,减少乱填;
  4. 字段名和系统一致:别让用户猜"course_name"是什么,模板里写"课程名称";
  5. 备注常见错误:模板底部写"常见错误提醒",比如"手机号请填11位,不要加横线"。

模板设计用心,导入错误率能降一大半。

写在最后

导入导出这个功能,看起来不起眼,却是机构"能不能用起来系统"的关键一环:

导入做好了,机构的老数据能顺利进去,系统就成功了一半;导入做砸了,用户卡在第一步,功能再好也没用。

五个坑总结:

一,编码乱码要自动探测,别假设用户给你标准文件;
二,数据校验要过三关(结构/格式/业务),失败要能告诉用户错在哪;
三,性能要批量写入+流式导出,别逐行操作;
四,导入要可回滚,用户才敢大胆用;
五,模板要用心设计,错误率靠模板降。

这套导入导出的设计,最后也落到了爱耕云的数据迁移模块里:编码自动识别、三关校验+错误报告、批量导入大数据量不卡、按批次回滚、内置标准模板。机构从旧系统/Excel 迁到爱耕云,不用求人,自己照着模板填就能完成迁移。

数据迁移是机构换系统的第一道门,这道门做得顺,后面一切都顺。

更多推荐