- 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
178 lines
6.7 KiB
Markdown
178 lines
6.7 KiB
Markdown
# 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 端点**:
|
||
```python
|
||
@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 定义独立的列映射配置,而非通用解析逻辑。
|
||
|
||
```python
|
||
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`。
|
||
|
||
```python
|
||
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 模式**:
|
||
```python
|
||
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 解析器),它完全不解析样式,只读数据。
|
||
|
||
```python
|
||
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 约束失败。
|
||
|
||
```python
|
||
# ❌ 错误: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 中的完整模式**:
|
||
```python
|
||
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 部门概况一致
|