- main.py: 增加show_banner()启动说明、各阶段[INFO]日志、结果摘要、任意键退出 - file_selector.py: 重写为路径输入→验证→空输入弹窗回退→不存在循环重试 - run.bat: 新建启动脚本(chcp 65001, mode con cols=80 lines=20, color 0B, title固定署名, pause) - Code/docs/modification-assessment.md: 修改需求评估文档
861 lines
28 KiB
Markdown
861 lines
28 KiB
Markdown
# PinMAP → PinList 转换器 — 全局架构设计
|
||
|
||
> **版本**: v1.0
|
||
> **日期**: 2026-05-25
|
||
> **架构师**: 脚本架构师 (Script Architect)
|
||
> **状态**: 待审批
|
||
|
||
---
|
||
|
||
## 1. 项目概述
|
||
|
||
### 1.1 背景
|
||
|
||
将 Excel 格式的 **PinMAP** 文件(方形封装引脚布局图)自动转换为 **PinList** 格式(引脚序号列表),消除手动抄录的低效与错误风险。
|
||
|
||
### 1.2 核心规则
|
||
|
||
- PinMAP 为方形/长方形结构,引脚沿四条边分布,左上角为 1 脚,**逆时针**排序
|
||
- 左上角 = 1 脚,右上角 = 上边最后一个脚,右下角 = 下边最后一个脚,左下角 = 左边最后一个脚
|
||
- 四个角点被相邻两边共享(不重复计数)
|
||
|
||
### 1.3 约束
|
||
|
||
| 约束项 | 说明 |
|
||
|--------|------|
|
||
| 运行平台 | Windows |
|
||
| 技术栈 | Python 标准库,**零第三方依赖** |
|
||
| 输入格式 | `.xls`(必须支持)、`.xlsx`(优先支持) |
|
||
| 输出格式 | `.xlsx` 仅 |
|
||
| 交互方式 | 命令行 + 文件选择对话框 |
|
||
|
||
---
|
||
|
||
## 2. 技术选型
|
||
|
||
### 2.1 为什么不用 openpyxl / xlrd?
|
||
|
||
项目明确要求 **无第三方依赖**,因此必须使用 Python 标准库自行实现 Excel 读写。
|
||
|
||
### 2.2 XLS 读取 — BIFF8 二进制解析
|
||
|
||
`.xls` 是 Microsoft **BIFF8** 二进制格式(复合文档 OLE2 容器)。
|
||
|
||
**实现策略**:
|
||
|
||
```
|
||
1. 使用 struct 模块解析 OLE2 复合文档头
|
||
2. 解析 FAT(文件分配表)定位 MiniFAT 和目录流
|
||
3. 定位 Workbook 流,读取 BIFF8 记录序列
|
||
4. 关键记录类型:
|
||
- 0x0009 (BOF) → 块起始标记
|
||
- 0x00FD (LABELSST) → 共享字符串表中的文本单元格
|
||
- 0x0006 (FORMULA) → 公式/数值单元格
|
||
- 0x0203 (NUMBER) → 数值单元格
|
||
- 0x000C (RK) → RK 数值(压缩整数/浮点)
|
||
- 0x000D (RString) → 内联字符串
|
||
- 0x00FC (STRING) → 字符串结果
|
||
- 0x0034 (SST) → 全局共享字符串表
|
||
- 0x0042 (BOUNDSHEET) → 工作表信息
|
||
5. 提取每个单元格的 (行, 列, 值) 三元组
|
||
```
|
||
|
||
**复杂度评估**:中等。BIFF8 是固定长度记录流,struct 解析直接。需处理 Unicode 编码(BIFF8 默认 UTF-16LE,部分兼容 ASCII)。
|
||
|
||
### 2.3 XLSX 读取 — ZIP + XML
|
||
|
||
`.xlsx` 本质是 ZIP 压缩包,内部为 Office Open XML (OOXML)。
|
||
|
||
**实现策略**:
|
||
|
||
```python
|
||
import zipfile
|
||
import xml.etree.ElementTree as ET
|
||
|
||
1. zipfile.ZipFile 打开 .xlsx
|
||
2. 读取 [Content_Types].xml 确认结构
|
||
3. 读取 xl/workbook.xml 获取工作表关系
|
||
4. 读取 xl/worksheets/sheet1.xml 获取单元格数据
|
||
5. 读取 xl/sharedStrings.xml 获取共享字符串表
|
||
6. 解析单元格坐标(如 "A2" → 列A行2)和值类型
|
||
```
|
||
|
||
**复杂度评估**:低。zipfile 和 xml.etree 均为标准库,XML 结构规范清晰。
|
||
|
||
### 2.4 XLSX 写入 — ZIP + XML 生成
|
||
|
||
**实现策略**:
|
||
|
||
```python
|
||
import zipfile
|
||
import xml.etree.ElementTree as ET
|
||
from io import BytesIO
|
||
|
||
1. 构建 OOXML 目录结构:
|
||
[Content_Types].xml
|
||
_rels/.rels
|
||
xl/workbook.xml
|
||
xl/worksheets/sheet1.xml
|
||
xl/sharedStrings.xml
|
||
xl/_rels/workbook.xml.rels
|
||
|
||
2. 使用 zipfile.ZipFile 写入(ZIP_DEFLATED)
|
||
|
||
3. 关键 XML 构建:
|
||
- sharedStrings.xml: 所有唯一字符串的 SST
|
||
- sheet1.xml: 单元格坐标 + si (SST index) 引用
|
||
- workbook.xml: 工作表引用
|
||
- [Content_Types].xml: MIME 类型声明
|
||
```
|
||
|
||
**复杂度评估**:低。XML 结构固定,模板化生成即可。
|
||
|
||
### 2.5 技术选型总结
|
||
|
||
| 操作 | 格式 | 标准库模块 | 难度 |
|
||
|------|------|-----------|------|
|
||
| 读取 | xls | `struct` + 手动 OLE2/BIFF8 解析 | 中 |
|
||
| 读取 | xlsx | `zipfile` + `xml.etree.ElementTree` | 低 |
|
||
| 写入 | xlsx | `zipfile` + `xml.etree.ElementTree` | 低 |
|
||
| 文件选择 | — | `tkinter.filedialog` | 低 |
|
||
|
||
---
|
||
|
||
## 3. 模块划分
|
||
|
||
```
|
||
pinmap-to-pinlist/
|
||
├── Code/
|
||
│ ├── src/
|
||
│ │ ├── main.py # 入口:流程编排
|
||
│ │ ├── file_selector.py # 模块1:文件选择
|
||
│ │ ├── xls_reader.py # 模块2a:XLS 解析引擎
|
||
│ │ ├── xlsx_reader.py # 模块2b:XLSX 解析引擎
|
||
│ │ ├── pinmap_parser.py # 模块3:PinMAP 结构解析
|
||
│ │ ├── validator.py # 模块4:数据验证
|
||
│ │ ├── pinlist_generator.py # 模块5:PinList 生成
|
||
│ │ └── xlsx_writer.py # 模块6:XLSX 输出引擎
|
||
│ └── docs/
|
||
│ └── architecture-design.md
|
||
├── Test/
|
||
└── Releases/
|
||
```
|
||
|
||
### 3.1 模块职责
|
||
|
||
#### 模块1:`file_selector` — 文件选择
|
||
|
||
```python
|
||
def select_file() -> str | None:
|
||
"""弹出文件选择对话框,返回选中文件路径或 None(取消)"""
|
||
```
|
||
|
||
- 使用 `tkinter.filedialog.askopenfilename`
|
||
- 文件类型过滤:`*.xls;*.xlsx`
|
||
- 无 GUI 环境时回退到命令行参数
|
||
|
||
#### 模块2a:`xls_reader` — XLS 解析引擎
|
||
|
||
```python
|
||
class XLSReader:
|
||
def __init__(self, filepath: str)
|
||
def read_all_cells(self) -> dict[tuple[int, int], str]:
|
||
"""返回 {(row, col): value} 字典,行列从 0 开始"""
|
||
def close(self)
|
||
```
|
||
|
||
**内部结构**:
|
||
|
||
```
|
||
XLSReader
|
||
├── OLE2Parser → 解析复合文档,定位 Workbook 流
|
||
├── BIFF8Parser → 解析 BIFF8 记录流
|
||
│ ├── SSTParser → 共享字符串表
|
||
│ └── CellParser → 单元格记录
|
||
└── CellMap → 组装为 (row, col) → value 映射
|
||
```
|
||
|
||
#### 模块2b:`xlsx_reader` — XLSX 解析引擎
|
||
|
||
```python
|
||
class XLSXReader:
|
||
def __init__(self, filepath: str)
|
||
def read_all_cells(self) -> dict[tuple[int, int], str]:
|
||
"""返回 {(row, col): value} 字典,行列从 0 开始"""
|
||
def close(self)
|
||
```
|
||
|
||
**内部结构**:
|
||
|
||
```
|
||
XLSXReader
|
||
├── ZipExtractor → 解压 .xlsx 到内存
|
||
├── SharedStrings → 解析 sharedStrings.xml
|
||
├── SheetParser → 解析 sheet1.xml
|
||
│ ├── CoordParser → 列字母转索引 (A→0, B→1, ...)
|
||
│ └── CellParser → 提取单元格值
|
||
└── CellMap → 组装为 (row, col) → value 映射
|
||
```
|
||
|
||
#### 模块3:`pinmap_parser` — PinMAP 结构解析
|
||
|
||
```python
|
||
def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
|
||
"""
|
||
解析步骤:
|
||
1. 排除 (0,0) 后扫描非空单元格,确定方形边界
|
||
2. 提取 A1 封装信息
|
||
3. 沿四条边提取引脚序号(边界单元格)和 PinName(相邻内侧单元格)
|
||
4. 逆时针遍历(左→下→右→上),按单元格位置去重(角点共享)
|
||
5. 返回 PinMAP 对象
|
||
"""
|
||
```
|
||
|
||
**解析算法**:
|
||
|
||
```
|
||
Step 1: 确定方形边界
|
||
- 排除 (0,0)(封装信息单元格)
|
||
- 扫描所有非空单元格,找到最小/最大行号和列号
|
||
- width = max_col - min_col + 1
|
||
- height = max_row - min_row + 1
|
||
- 验证:width >= 2 且 height >= 2
|
||
|
||
Step 2: 提取 A1 封装信息
|
||
- cells[(0, 0)] → package_info
|
||
|
||
Step 3: 构建 PinName 查找表
|
||
每条边的 PinName 位于序号单元格的"内侧相邻"位置:
|
||
左边:序号在 (r, min_col), Name 在 (r, min_col+1)
|
||
下边:序号在 (max_row, c), Name 在 (max_row-1, c)
|
||
右边:序号在 (r, max_col), Name 在 (r, max_col-1)
|
||
上边:序号在 (min_row, c), Name 在 (min_row+1, c)
|
||
|
||
Step 4: 逆时针遍历四条边(按单元格位置去重)
|
||
4a. 左边:从上到下 (row: min_row → max_row, col: min_col)
|
||
4b. 下边:从左到右 (row: max_row, col: min_col+1 → max_col)
|
||
4c. 右边:从下到上 (row: max_row-1 → min_row, col: max_col)
|
||
4d. 上边:从右到左 (row: min_row, col: max_col-1 → min_col)
|
||
|
||
角点去重:按 (row, col) 单元格位置去重,而非按 Pin 序号。
|
||
这样如果两个不同单元格恰好有相同序号,validator 能检测到。
|
||
|
||
Step 5: 组装 Pin 列表
|
||
按逆时针顺序:Pin1(左上角) → Pin2 → ... → PinN
|
||
```
|
||
|
||
#### 模块4:`validator` — 数据验证
|
||
|
||
```python
|
||
def validate_pinmap(pinmap: PinMAP) -> ValidationResult:
|
||
"""
|
||
验证项:
|
||
1. Pin序号唯一性(无重复)
|
||
2. Pin序号连续性(1..N 无间隔)
|
||
3. PinName 缺失检测(warning,默认 NC)
|
||
4. 方形结构完整性(width/height >= 2)
|
||
"""
|
||
```
|
||
|
||
#### 模块5:`pinlist_generator` — PinList 生成
|
||
|
||
```python
|
||
class PinListGenerator:
|
||
def __init__(self, pinmap: PinMAP, validation: ValidationResult)
|
||
def generate(self) -> PinList:
|
||
"""
|
||
生成规则:
|
||
- A1 = 封装信息
|
||
- A列 = PinName
|
||
- B列 = Pin序号
|
||
- 按 Pin序号 递增排序
|
||
"""
|
||
```
|
||
|
||
#### 模块6:`xlsx_writer` — XLSX 输出引擎
|
||
|
||
```python
|
||
class XLSXWriter:
|
||
def __init__(self)
|
||
def write_pinlist(self, pinlist: PinList, output_path: str)
|
||
```
|
||
|
||
### 3.2 模块依赖关系
|
||
|
||
```
|
||
main.py
|
||
├── file_selector.py
|
||
├── xls_reader.py ──┐
|
||
├── xlsx_reader.py ─┤
|
||
│ ▼
|
||
│ pinmap_parser.py
|
||
│ ▼
|
||
│ validator.py
|
||
│ ▼
|
||
│ pinlist_generator.py
|
||
│ ▼
|
||
└─────────── xlsx_writer.py
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 数据结构设计
|
||
|
||
### 4.1 Pin(引脚)
|
||
|
||
```python
|
||
@dataclass
|
||
class Pin:
|
||
number: int # 引脚序号(1-based)
|
||
name: str # 引脚名称(缺失时默认为 "NC")
|
||
edge: str # 所在边: "top" | "right" | "bottom" | "left"
|
||
position_on_edge: int # 在该边上的位置(0-based)
|
||
```
|
||
|
||
### 4.2 PinMAP(引脚映射图)
|
||
|
||
```python
|
||
@dataclass
|
||
class PinMAP:
|
||
package_info: str # A1 单元格封装信息
|
||
pins: list[Pin] # 所有引脚(按序号排序)
|
||
width: int # 方形宽度(列数)
|
||
height: int # 方形高度(行数)
|
||
grid_origin: tuple[int, int] # (row, col) 方形左上角
|
||
raw_cells: dict[tuple[int, int], str] # 原始单元格数据(调试用)
|
||
```
|
||
|
||
### 4.3 PinList(引脚列表)
|
||
|
||
```python
|
||
@dataclass
|
||
class PinList:
|
||
package_info: str # 输出 A1 单元格
|
||
rows: list[tuple[str, int]] # [(PinName, Pin序号), ...] 按序号排序
|
||
```
|
||
|
||
### 4.4 ValidationResult(验证结果)
|
||
|
||
```python
|
||
@dataclass
|
||
class ValidationError:
|
||
level: str # "error" | "warning"
|
||
message: str # 错误描述
|
||
details: str # 详细信息(如重复的序号、缺失的Pin等)
|
||
|
||
@dataclass
|
||
class ValidationResult:
|
||
is_valid: bool
|
||
errors: list[ValidationError]
|
||
warnings: list[ValidationError]
|
||
```
|
||
|
||
### 4.5 内部:单元格坐标体系
|
||
|
||
```
|
||
统一使用 (row, col) 元组,0-based:
|
||
- row 0 = Excel 第1行
|
||
- col 0 = Excel A列
|
||
- A1 = (0, 0)
|
||
- A2 = (1, 0)
|
||
- C2 = (1, 2)
|
||
- B4 = (3, 1)
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 异常处理策略
|
||
|
||
### 5.1 异常分类
|
||
|
||
| 级别 | 类型 | 处理方式 | 示例 |
|
||
|------|------|---------|------|
|
||
| FATAL | 文件格式错误 | 终止 + 错误信息 | 非Excel文件、BIFF损坏 |
|
||
| FATAL | 结构错误 | 终止 + 错误信息 | 非方形、缺少A1、无边数据 |
|
||
| ERROR | 数据错误 | 终止 + 详细错误 | 序号不连续、序号重复 |
|
||
| WARN | 数据警告 | 提示 + 继续 | PinName缺失(默认NC) |
|
||
| INFO | 信息提示 | 仅显示 | 转换完成、统计信息 |
|
||
|
||
### 5.2 自定义异常层次
|
||
|
||
```python
|
||
class PinMapError(Exception):
|
||
"""基类异常"""
|
||
|
||
class FileFormatError(PinMapError):
|
||
"""文件格式错误(非xls/xlsx、文件损坏)"""
|
||
|
||
class StructureError(PinMapError):
|
||
"""PinMAP结构错误(非方形、缺少必要数据)"""
|
||
|
||
class ValidationError(PinMapError):
|
||
"""数据验证错误(序号不连续、重复)"""
|
||
|
||
class WarningLevel(PinMapError):
|
||
"""警告级别(PinName缺失等,可继续处理)"""
|
||
```
|
||
|
||
### 5.3 错误信息规范
|
||
|
||
```
|
||
[级别] 错误类别: 具体描述
|
||
详细信息: ...
|
||
建议操作: ...
|
||
```
|
||
|
||
示例:
|
||
```
|
||
[ERROR] 序号不连续: 检测到序号间断
|
||
预期: 1,2,3,4,5,6 实际: 1,2,3,5,6
|
||
缺失序号: 4
|
||
建议: 检查PinMAP文件中是否有遗漏的引脚
|
||
|
||
[WARN] PinName缺失: 检测到 3 个引脚缺少PinName
|
||
缺失引脚: Pin 7, Pin 12, Pin 18
|
||
处理: 已自动设为 "NC"
|
||
```
|
||
|
||
### 5.4 主流程异常处理
|
||
|
||
```python
|
||
def main():
|
||
try:
|
||
filepath = select_file()
|
||
if not filepath:
|
||
return # 用户取消
|
||
|
||
cells = read_excel(filepath)
|
||
pinmap = parse_pinmap(cells)
|
||
result = validate(pinmap)
|
||
|
||
if result.has_errors():
|
||
print_errors(result.errors)
|
||
return
|
||
|
||
if result.has_warnings():
|
||
print_warnings(result.warnings)
|
||
if not confirm_continue():
|
||
return
|
||
|
||
pinlist = generate(pinmap, result)
|
||
output_path = build_output_path(filepath)
|
||
write_xlsx(pinlist, output_path)
|
||
print_success(output_path)
|
||
|
||
except FileFormatError as e:
|
||
print_fatal(f"文件格式错误: {e}")
|
||
except StructureError as e:
|
||
print_fatal(f"结构错误: {e}")
|
||
except ValidationError as e:
|
||
print_fatal(f"数据验证失败: {e}")
|
||
except Exception as e:
|
||
print_fatal(f"未知错误: {e}")
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 文件处理流程图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 主流程 (main.py) │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌───────────────────────────┐
|
||
│ 1. 文件选择 (file_selector) │
|
||
│ - tkinter 文件对话框 │
|
||
│ - 过滤 *.xls, *.xlsx │
|
||
└───────────┬───────────────┘
|
||
│
|
||
┌───────────▼───────────────┐
|
||
│ 2. 读取 Excel 文件 │
|
||
│ ┌─────────────────────┐ │
|
||
│ │ 判断文件格式 │ │
|
||
│ │ .xls → xls_reader │ │
|
||
│ │ .xlsx → xlsx_reader│ │
|
||
│ └─────────┬───────────┘ │
|
||
│ ┌─────────▼───────────┐ │
|
||
│ │ 解析为单元格字典 │ │
|
||
│ │ {(row,col): value} │ │
|
||
│ └─────────┬───────────┘ │
|
||
└────────────┬──────────────┘
|
||
│
|
||
┌────────────▼──────────────┐
|
||
│ 3. PinMAP 解析 (pinmap_parser) │
|
||
│ ┌─────────────────────┐ │
|
||
│ │ ① 定位方形边界 │ │
|
||
│ │ 扫描非空单元格 │ │
|
||
│ │ 确定 width/height│ │
|
||
│ └─────────┬───────────┘ │
|
||
│ ┌─────────▼───────────┐ │
|
||
│ │ ② 提取 A1 封装信息 │ │
|
||
│ └─────────┬───────────┘ │
|
||
│ ┌─────────▼───────────┐ │
|
||
│ │ ③ 沿四边提取引脚 │ │
|
||
│ │ 上边 → 右边 │ │
|
||
│ │ 下边 → 左边 │ │
|
||
│ │ 逆时针排序 │ │
|
||
│ └─────────┬───────────┘ │
|
||
│ ┌─────────▼───────────┐ │
|
||
│ │ ④ 组装 PinMAP 对象 │ │
|
||
│ └─────────┬───────────┘ │
|
||
└────────────┬──────────────┘
|
||
│
|
||
┌────────────▼──────────────┐
|
||
│ 4. 数据验证 (validator) │
|
||
│ ┌─────────────────────┐ │
|
||
│ │ ✓ 序号连续性检查 │ │
|
||
│ │ ✓ 序号唯一性检查 │ │
|
||
│ │ ✓ PinName 缺失检查 │ │
|
||
│ │ ✓ 方形结构完整性 │ │
|
||
│ └─────────┬───────────┘ │
|
||
│ ┌─────────▼───────────┐ │
|
||
│ │ ERROR → 终止流程 │ │
|
||
│ │ WARN → 提示确认 │ │
|
||
│ └─────────┬───────────┘ │
|
||
└────────────┬──────────────┘
|
||
│
|
||
┌────────────▼──────────────┐
|
||
│ 5. PinList 生成 (generator) │
|
||
│ ┌─────────────────────┐ │
|
||
│ │ A1 = 封装信息 │ │
|
||
│ │ A列 = PinName │ │
|
||
│ │ B列 = Pin序号 │ │
|
||
│ │ 按序号递增排序 │ │
|
||
│ └─────────┬───────────┘ │
|
||
└────────────┬──────────────┘
|
||
│
|
||
┌────────────▼──────────────┐
|
||
│ 6. XLSX 输出 (xlsx_writer) │
|
||
│ ┌─────────────────────┐ │
|
||
│ │ 构建 OOXML 结构 │ │
|
||
│ │ [Content_Types].xml │ │
|
||
│ │ xl/workbook.xml │ │
|
||
│ │ xl/sharedStrings.xml│ │
|
||
│ │ xl/worksheets/ │ │
|
||
│ │ sheet1.xml │ │
|
||
│ └─────────┬───────────┘ │
|
||
│ ┌─────────▼───────────┐ │
|
||
│ │ ZIP 打包输出 │ │
|
||
│ └─────────┬───────────┘ │
|
||
└────────────┬──────────────┘
|
||
│
|
||
▼
|
||
┌───────────────────────────┐
|
||
│ 完成!输出 .xlsx 文件 │
|
||
│ 默认命名: {原文件名}_PinList.xlsx │
|
||
└───────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 7. PinMAP 结构详解
|
||
|
||
### 7.1 坐标映射
|
||
|
||
以 4×4 方形为例(width=4, height=4):
|
||
|
||
```
|
||
col A(0) col B(1) col C(2) col D(3)
|
||
row 0 [A1=封装] [PinName] [PinName] [PinName] ← 上边PinName行
|
||
row 1 [1] [2] [3] [4] ← 上边Pin序号行
|
||
row 2 [PinName] [ ] [PinName] ← 中间区域(留空)
|
||
row 3 [PinName] [ ] [PinName] ← 中间区域(留空)
|
||
row 4 [13] [12] [11] [10] ← 下边Pin序号行
|
||
[PinName] [PinName] [PinName] [PinName] ← 下边PinName行(行5)
|
||
↑ ↑ ↑ ↑
|
||
左边 左边 右边 右边
|
||
PinName PinName PinName PinName
|
||
(列前) (列前) (列后) (列后)
|
||
```
|
||
|
||
**实际引脚分布**:
|
||
- 上边:Pin 1(A,row1) → Pin 2(B,row1) → Pin 3(C,row1) → Pin 4(D,row1)
|
||
- 右边:Pin 5(D,row2) → Pin 6(D,row3) → Pin 7(D,row4)
|
||
- 下边:Pin 8(D,row4) ... 等等
|
||
|
||
等等,让我重新理清。根据需求描述:
|
||
|
||
```
|
||
C2 是上边最后一个Pin序号,C3是对应PinName
|
||
A4 是左边第一个Pin序号,B4是对应PinName
|
||
```
|
||
|
||
这说明:
|
||
- 行1(Excel第2行)= 上边Pin序号行
|
||
- 行2(Excel第3行)= 上边PinName行
|
||
- 行3(Excel第4行)= 左边第一个Pin序号行
|
||
|
||
所以方形区域从 row=1 开始(Excel第2行),row=0 是PinName行。
|
||
|
||
### 7.2 四边提取规则(修正版)
|
||
|
||
设方形区域:行范围 [r_top, r_bottom],列范围 [c_left, c_right]
|
||
|
||
```
|
||
上边 (Top Edge):
|
||
Pin序号位置:row=r_top, col=c_left → c_right(从左到右)
|
||
PinName位置:row=r_top-1, col=c_left → c_right
|
||
|
||
右边 (Right Edge):
|
||
Pin序号位置:col=c_right, row=r_top → r_bottom(从上到下)
|
||
PinName位置:col=c_right+1, row=r_top → r_bottom
|
||
|
||
下边 (Bottom Edge):
|
||
Pin序号位置:row=r_bottom, col=c_right → c_left(从右到左)
|
||
PinName位置:row=r_bottom+1, col=c_right → c_left
|
||
|
||
左边 (Left Edge):
|
||
Pin序号位置:col=c_left, row=r_bottom → r_top(从下到上)
|
||
PinName位置:col=c_left-1, row=r_bottom → r_top(即B列,当c_left=0时)
|
||
```
|
||
|
||
### 7.3 角点共享规则
|
||
|
||
```
|
||
左上角 (c_left, r_top) = 上边起点 = 左边终点 → Pin 1
|
||
右上角 (c_right, r_top) = 上边终点 = 右边起点
|
||
右下角 (c_right, r_bottom) = 右边终点 = 下边起点
|
||
左下角 (c_left, r_bottom) = 下边终点 = 左边终点
|
||
|
||
总Pin数 = 2 × width + 2 × height - 4
|
||
```
|
||
|
||
### 7.4 长方形支持
|
||
|
||
```
|
||
非正方形示例:width=6, height=4
|
||
|
||
总Pin数 = 2×6 + 2×4 - 4 = 16
|
||
|
||
上边:6个引脚(1-6)
|
||
右边:3个引脚(7-9)
|
||
下边:5个引脚(10-14)
|
||
左边:3个引脚(15-16,回到Pin 1)
|
||
|
||
验证:6 + 3 + 5 + 2 = 16 ✓(左边排除两个角点)
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 任务拆分建议
|
||
|
||
### 8.1 推荐拆分方案
|
||
|
||
建议拆分为 **3 个子任务**,由 2-3 个编码 Agent 并行开发:
|
||
|
||
#### 任务 A:Excel 读写引擎(最复杂,优先开发)
|
||
|
||
**负责模块**:`xls_reader.py`, `xlsx_reader.py`, `xlsx_writer.py`
|
||
|
||
**工作内容**:
|
||
1. 实现 BIFF8 OLE2 解析器(xls 读取)
|
||
2. 实现 ZIP+XML 解析器(xlsx 读取)
|
||
3. 实现 OOXML 生成器(xlsx 写入)
|
||
4. 统一接口:`read_excel(filepath) → dict[(row,col), str]`
|
||
5. 编写单元测试(用已知 xls/xlsx 文件验证)
|
||
|
||
**预估工作量**:高(BIFF8 解析是最大难点)
|
||
|
||
**关键风险**:
|
||
- BIFF8 变体多(BIFF5/BIFF8 混用、不同 Unicode 编码)
|
||
- 需要大量测试文件验证
|
||
|
||
#### 任务 B:PinMAP 解析与验证(核心业务逻辑)
|
||
|
||
**负责模块**:`pinmap_parser.py`, `validator.py`
|
||
|
||
**工作内容**:
|
||
1. 实现方形边界检测算法
|
||
2. 实现四边引脚提取逻辑
|
||
3. 实现角点共享处理
|
||
4. 实现验证规则(连续性、唯一性、完整性)
|
||
5. 编写单元测试(模拟各种 PinMAP 布局)
|
||
|
||
**预估工作量**:中
|
||
|
||
**关键风险**:
|
||
- 边界条件处理(长方形 vs 正方形、最小尺寸)
|
||
- 角点共享逻辑的正确性
|
||
|
||
#### 任务 C:流程编排与输出(集成层)
|
||
|
||
**负责模块**:`main.py`, `file_selector.py`, `pinlist_generator.py`
|
||
|
||
**工作内容**:
|
||
1. 实现文件选择对话框
|
||
2. 实现 PinList 数据转换
|
||
3. 实现输出文件命名和保存
|
||
4. 实现主流程异常处理和用户提示
|
||
5. 端到端集成测试
|
||
|
||
**预估工作量**:低
|
||
|
||
**关键风险**:
|
||
- tkinter 在 Windows 上的兼容性
|
||
- 用户交互流程的友好性
|
||
|
||
### 8.2 开发顺序
|
||
|
||
```
|
||
第1轮:任务 A(Excel 读写引擎)
|
||
↓ 完成后
|
||
第2轮:任务 B(PinMAP 解析与验证)
|
||
↓ 完成后
|
||
第3轮:任务 C(流程编排与输出)
|
||
↓ 完成后
|
||
集成测试 → 发布
|
||
```
|
||
|
||
### 8.3 接口契约(模块间约定)
|
||
|
||
```python
|
||
# xls_reader / xlsx_reader 统一接口
|
||
def read_excel_cells(filepath: str) -> dict[tuple[int, int], str]:
|
||
"""
|
||
输入: Excel 文件路径
|
||
输出: {(row, col): str} 单元格字典
|
||
约定: row/col 从 0 开始,所有值转为 str
|
||
"""
|
||
|
||
# pinmap_parser 接口
|
||
def parse_pinmap(cells: dict[tuple[int, int], str]) -> PinMAP:
|
||
"""
|
||
输入: 单元格字典
|
||
输出: PinMAP 对象
|
||
约定: 结构错误时抛出 StructureError
|
||
"""
|
||
|
||
# validator 接口
|
||
def validate_pinmap(pinmap: PinMAP) -> ValidationResult:
|
||
"""
|
||
输入: PinMAP 对象
|
||
输出: ValidationResult
|
||
约定: 不抛出异常,所有问题记录在 ValidationResult 中
|
||
"""
|
||
|
||
# pinlist_generator 接口
|
||
def generate_pinlist(pinmap: PinMAP, validation: ValidationResult) -> PinList:
|
||
"""
|
||
输入: PinMAP + ValidationResult
|
||
输出: PinList 对象
|
||
约定: 自动处理 WARN 级别的缺失 PinName(设为 NC)
|
||
"""
|
||
|
||
# xlsx_writer 接口
|
||
def write_pinlist_xlsx(pinlist: PinList, output_path: str):
|
||
"""
|
||
输入: PinList + 输出路径
|
||
输出: 无(写入文件)
|
||
约定: 自动创建父目录
|
||
"""
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 项目目录结构
|
||
|
||
```
|
||
pinmap-to-pinlist/
|
||
├── Code/
|
||
│ ├── src/
|
||
│ │ ├── __init__.py
|
||
│ │ ├── main.py # 入口点
|
||
│ │ ├── file_selector.py # 文件选择
|
||
│ │ ├── xls_reader.py # XLS 读取引擎
|
||
│ │ ├── xlsx_reader.py # XLSX 读取引擎
|
||
│ │ ├── pinmap_parser.py # PinMAP 解析
|
||
│ │ ├── validator.py # 数据验证
|
||
│ │ ├── pinlist_generator.py # PinList 生成
|
||
│ │ ├── xlsx_writer.py # XLSX 写入引擎
|
||
│ │ └── models.py # 数据模型定义
|
||
│ └── docs/
|
||
│ └── architecture-design.md # 本文档
|
||
├── Test/
|
||
│ ├── fixtures/ # 测试用 Excel 文件
|
||
│ │ ├── sample_4x4.xls
|
||
│ │ ├── sample_4x4.xlsx
|
||
│ │ ├── sample_rect.xls
|
||
│ │ └── ...
|
||
│ └── test_*.py # 单元测试
|
||
└── Releases/
|
||
└── pinmap2pinlist.exe # 打包后的可执行文件
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 风险与缓解
|
||
|
||
| 风险 | 影响 | 概率 | 缓解措施 |
|
||
|------|------|------|---------|
|
||
| BIFF8 格式变体导致解析失败 | 高 | 中 | 收集多种 xls 样本测试;优先实现 BIFF8 最常见子集 |
|
||
| tkinter 在无头环境不可用 | 中 | 低 | 回退到命令行参数模式 |
|
||
| xlsx 写入的 XML 结构不兼容老版本 Excel | 中 | 低 | 遵循 OOXML 标准,使用最小兼容集 |
|
||
| 超大文件(>1000引脚)性能问题 | 低 | 低 | 当前场景引脚数通常 <100,无需优化 |
|
||
|
||
---
|
||
|
||
## 11. 附录
|
||
|
||
### A. BIFF8 记录类型速查
|
||
|
||
| 记录码 | 名称 | 说明 |
|
||
|--------|------|------|
|
||
| 0x0009 | BOF | 块起始 |
|
||
| 0x000A | EOF | 文件结束 |
|
||
| 0x00FD | LABELSST | 共享字符串表引用单元格 |
|
||
| 0x0203 | NUMBER | 浮点数单元格 |
|
||
| 0x0006 | FORMULA | 公式单元格 |
|
||
| 0x000C | RK | RK 数值 |
|
||
| 0x00FC | STRING | 公式字符串结果 |
|
||
| 0x0034 | SST | 全局共享字符串表 |
|
||
| 0x0042 | BOUNDSHEET | 工作表信息 |
|
||
| 0x00E0 | EXTSST | 扩展共享字符串表 |
|
||
|
||
### B. OOXML xlsx 目录结构
|
||
|
||
```
|
||
example.xlsx (ZIP)
|
||
├── [Content_Types].xml
|
||
├── _rels/
|
||
│ └── .rels
|
||
├── xl/
|
||
│ ├── workbook.xml
|
||
│ ├── _rels/
|
||
│ │ └── workbook.xml.rels
|
||
│ ├── sharedStrings.xml
|
||
│ ├── styles.xml
|
||
│ └── worksheets/
|
||
│ ├── sheet1.xml
|
||
│ └── sheet2.xml
|
||
└── docProps/
|
||
├── core.xml
|
||
└── app.xml
|
||
```
|
||
|
||
### C. 列字母 ↔ 索引转换
|
||
|
||
```python
|
||
def col_letter_to_index(letter: str) -> int:
|
||
"""A→0, B→1, ..., Z→25, AA→26, AB→27, ..."""
|
||
result = 0
|
||
for ch in letter.upper():
|
||
result = result * 26 + (ord(ch) - ord('A') + 1)
|
||
return result - 1
|
||
|
||
def col_index_to_letter(index: int) -> str:
|
||
"""0→A, 1→B, ..., 25→Z, 26→AA, ..."""
|
||
result = ""
|
||
index += 1
|
||
while index > 0:
|
||
index -= 1
|
||
result = chr(index % 26 + ord('A')) + result
|
||
index //= 26
|
||
return result
|
||
```
|
||
|
||
---
|
||
|
||
*文档结束 — 请审批后进入编码阶段*
|