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

178 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 样式解析 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-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 部门概况一致