SkillSpace/archives/dev-pipeline-universal/references/excel-data-import.md
sinohqb 8dafa5bc52 Add archives, import dev-pipeline skill, and complete Hermes Agent spec
- Add archives/ directory for immutable external skill snapshots
  - Strict immutability rule: only README/CHANGELOG can be modified
  - First import: dev-pipeline-universal v1.0.0 (Hermes Agent)
- Import dev-pipeline-universal into hermes-agent/skills/ with CHANGELOG
- Update hermes-agent/publish/registry.json with imported skill
- Rewrite meta-skills/create-hermes-agent-skill/ with full spec:
  - SKILL.md frontmatter fields and validator constraints
  - Recommended body structure (Overview → When to Use → Pitfalls → Verification)
  - Progressive disclosure (3-level loading)
  - 8 writing quality principles
  - Official reference skills from GitHub repo
  - Tool chain docs (skill_manage, /learn, skill_view)
- Update README.md with archives/ in directory structure
2026-07-02 00:46:13 +08:00

6.7 KiB
Raw Permalink Blame History

Excel 数据导入实战参考

适用场景: MVP/里程碑完成后,从业务 Excel 报表批量导入测试数据做效果验证 创建日期: 2026-06-24 更新日期: 2026-06-30


两种导入方式

方式一CLI 脚本(适合首次全量导入)

scripts/import_excel_data.py — 从文件系统读取 Excel一次性导入全部数据。

优点: 无网络开销,可调试,适合大数据量 缺点: 需要服务器文件系统访问权限

方式二Web API适合日常增量导入

POST /api/import/excel — 前端上传 Excel 文件,后端解析导入。

优点: 浏览器操作,无需服务器登录,有导入结果统计 缺点: 文件大小受 HTTP 限制,大文件上传慢

前端页面: /import 路径,拖拽上传 + 导入结果展示

API 端点:

@router.post("/api/import/excel")
async def import_excel(file: UploadFile = File(...), db: Session = Depends(get_db)):
    # 保存临时文件 → openpyxl 解析 → 导入各表 → 删除临时文件

核心挑战

业务 Excel 通常有 15-20 个 sheet列布局因项目而异且存在公式、合并单元格、同名项目等陷阱。一次性全量导入比逐条录入高效百倍但需要处理以下问题。


实战陷阱与对策

1. 列布局因 sheet 而异

每个项目明细 sheet 的列数不同:

Sheet 列布局 人日列 描述列
标准 序号|模块|功能项|人日|负责人|占比|... col 3
工健健康小屋 序号|模块|功能项|说明1|详细说明|人日|负责人|... col 5 col 4
基层社区AI医助 序号|模块|功能项|描述|人日|负责人|... col 4 col 3
汤原县AI医助 序号|功能项|描述|人日|负责人|... col 3 col 2无模块列

对策: 为每个 sheet 定义独立的列映射配置,而非通用解析逻辑。

sheet_configs = [
    ("项目-百度-珠江医院", "2026-BD-1", 1, 2, 3, 4, None, [...]),
    ("项目-工健健康小屋", "2026-YZ-1", 1, 2, 5, 6, 4, [...]),
    ("项目-汤原县AI医助", "2026-AI-2", None, 1, 3, 4, 2, [...]),
]

2. 同名项目(不同项目编号)

Excel 中可能有多个同名项目(如"南方医科大学珠江医院"有 20260205-A2026-BD-1 两个编号)。用 project_name 做 dict key 会覆盖。

对策: 始终用 project_code 做映射,不要用 project_name

projects_by_code = {p.project_code: {"name": p.project_name, "id": p.id} for p in db.query(Project).all()}

3. 人员不在人员概况 sheet 中

新 Excel 可能新增了人员(如"陈天然"),但只出现在收益分析 sheet 中,不在人员概况 sheet 里。

对策: 扫描所有 sheet 发现新人员,手动补加到人员表。

4. 公式字段 openpyxl 读不到

Excel 中的 =SUM(...)=C2/D2 等公式openpyxl 读取时返回公式字符串而非计算结果。

对策:

  • 预期收益从收益分析 sheet 的"总计"行读取(那里是数值)
  • 投产比等计算字段在数据库端用 ROI 计算器重新计算

5. 自由文本 → 结构化关联

"人员项目负荷情况" sheet 的工作描述是自由文本(如"1、百度医院智能体-南方医科大学珠江医院-近一个月工作占比50%"),需要从中提取项目关联和分配比例。

对策: 两阶段匹配:

  1. 精确匹配:项目全名在文本中
  2. 关键词回退:"健康小屋" → "工会健康小屋", "AI医助" → "佳木斯中医院AI医助"

分配比例从文本中 占比(\d+)% 正则提取,无比例时按项目关联人数均分。

6. 增量更新 vs 全量重来

原则: 首次导入用全量清空重来;后续更新用增量 upsert按 project_code/personnel_id 匹配)。

增量 upsert 模式:

existing = db.query(Model).filter(Model.code == code).first()
if existing:
    for key, val in new_data.items():
        setattr(existing, key, val)
else:
    db.add(Model(**new_data))

7. openpyxl 样式解析 bugTypeError: expected Fill

openpyxl 3.1.5 在解析某些 Excel 文件的 styles.xml 时,遇到不兼容的 Fill 样式对象会抛出 TypeError: expected <class 'openpyxl.styles.fills.Fill'>

无效尝试

  • data_only=True — 只影响公式计算,不跳过样式解析
  • read_only=True — 同样需要解析样式表
  • 升级 openpyxl — 3.1.5 是当前最新版bug 尚未修复

根治方案:改用 python-calamineRust 实现的 Excel 解析器),它完全不解析样式,只读数据。

from python_calamine import CalamineWorkbook

wb = CalamineWorkbook.from_path(path)
sheets = wb.sheet_names
ws = wb.get_sheet_by_name("Sheet1")
rows = list(ws.to_python())  # 返回 list[list],每个 cell 是 Python 原生类型

注意事项

  • python-calamine 返回的单元格值是 Python 原生类型float/int/str/None无需额外转换
  • 不解析公式,返回的是缓存的计算结果(与 data_only=True 类似)
  • 不支持写 Excel只用于读取
  • 安装:pip install python-calamine 或加到 requirements.txt

⚠️ employee_id 空值陷阱 python-calamine 返回的空单元格是 None,但 Excel 中写了空字符串的单元格也会被 str(val).strip() 转为空字符串。用 clean() 函数处理时,空字符串会变成 None,导致 NOT NULL 约束失败。

# ❌ 错误clean(row[2]) 对空单元格返回 None
emp_id = clean(row[2]) if len(row) > 2 else f"EMP{name}"

# ✅ 正确:显式检查 clean() 结果是否为空
emp_id = clean(row[2]) if len(row) > 2 and clean(row[2]) else f"EMP{name}"

规律:任何 clean() 的返回值都可能为 None,不能因为列存在就假定值非空。在 NOT NULL 字段上使用 clean() 时,必须加 and clean(val) 二次检查。

Web API 中的完整模式

tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
try:
    content = await file.read()
    tmp.write(content)
    tmp.close()
    wb = CalamineWorkbook.from_path(tmp.name)
    # ... 解析各 sheet ...
finally:
    os.unlink(tmp.name)

导入后验证清单

  • 项目数是否匹配 Excel 项目概况行数
  • 人员数是否覆盖所有出现的人名
  • 预期收益是否与 Excel 收益分析 sheet 的"总计"行一致
  • WBS 任务数是否合理(每个项目应有 >0 条)
  • 人员-项目关联数是否覆盖主要参与关系
  • 收益矩阵 API 返回的数据与 Excel 收益分析 sheet 交叉验证
  • 仪表盘 OKR 完成率是否与 Excel 部门概况一致