- 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
6.7 KiB
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-A 和 2026-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%"),需要从中提取项目关联和分配比例。
对策: 两阶段匹配:
- 精确匹配:项目全名在文本中
- 关键词回退:
"健康小屋" → "工会健康小屋","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 样式解析 bug(TypeError: 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-calamine(Rust 实现的 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 部门概况一致