docs: explain per-doc .epro2 crawl vs web-export .epro2 ZIP

Colleague-facing explainer at docs/sources/pro_crawl_vs_export.md.
Addresses the "I see 278 .epro2 files but my browser only downloaded
one" confusion: web download is a ZIP container (extension is a UX
choice, not a format), our crawl produces per-doc message streams.
Both carry equivalent EPRO2 data; only real gap is IMAGE/ binary
previews which we don't fetch yet.

Why per-doc and not ZIP: the ZIP path has no public endpoint —
three HARs confirm the export button fires zero HTTP requests, it's
pure client-side JSZip on data already loaded by the editor. Our
crawler hits the same chain endpoints the editor uses internally,
which delivers per-doc streams.

Log entry references the 278 vs 266 doc-count delta for ESP-VoCat
(we walk full history chain, web export is a current snapshot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 00:13:52 +08:00
parent 1e06ba6582
commit fc2a45f658
2 changed files with 182 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
# 为什么爬取产出 N 个 .epro2 文件,而网页端导出只有一个 .epro2
> 给同事的科普向解释。如果你看到我们仓库里某个 Pro 项目目录下躺着几十到上千个 `.epro2`
> 而你自己从网页端"下载工程包"只拿到一个 `.epro2`,你不是抓错了,它们也不是同一种文件,
> 只是名字撞了。
---
## 一句话结论
**网页端下载的那个 `.epro2` 是 ZIP 压缩包**(扩展名误导),里面其实有三样东西:
工程消息流 + 工程元数据 + 嵌入图片。
**我们爬取产出的 N 个 `.epro2` 是工程内每个"文档"各自的消息流**
一个组件一文件、一张原理图一文件、一块 PCB 一文件,没打包。
两者**承载的数据是等价的**,只是打包方式相反——一个是"全压一坨"
一个是"每文档一个文件"。
---
## 拿 ESP-VoCat 做实例
同一个项目(`ba64bd6f...`),两种获取方式得到的东西:
### 网页端"下载工程包" → `ProPrj_ESP-VoCat.epro2`1 个文件1.4 MB
文件名虽然是 `.epro2`**实际是 ZIP**。`unzip -l` 看里面:
```
IMAGE/ (目录)
IMAGE/38cc57d9...webp 60 KB ─┐
IMAGE/450927588f1f...webp 260 KB │
IMAGE/78404fce3de8...webp 134 KB │ 6 张嵌入图片
IMAGE/9904a7de7784...webp 48 KB │ (组件预览/3D 缩略图)
IMAGE/a20de4ce4ca4...webp 141 KB │
IMAGE/b3294b3234d6...webp 286 KB ─┘
project2.json 25 KB 工程元数据
ESP-VoCat 喵伴...epru 6.25 MB 全部 doc 拼成一坨的消息流
```
### 我们爬取产出 → `data/raw/oshwhub/ba64bd6f.../source/`278 个文件7.5 MB
每个 doc 一个独立的 `.epro2`
```
00184cbbad5a8d33.epro2 每个 SYMBOL 一个文件 × 105
0a29a31811039d37.epro2 每个 FOOTPRINT 一个 × 55
037c... .epro2 每个 DEVICE 一个 × 88
... 总共 278 个 .epro2 文件
manifest.json 索引(哪个文件对应哪个 docType
structure.json 工程结构树(哪些 sheet / pcb / board 属于哪些 board
```
按 docType 分布看:
| docType | 我们爬取 | 网页端导出 |
|---|---:|---:|
| SYMBOL原理图符号 | 105 | 105 |
| DEVICE元件 BOM 元数据) | 88 | 88 |
| FOOTPRINTPCB 封装) | 55 | 55 |
| SCH_PAGE原理图分页 | 9 | 6 |
| SCH原理图容器 | 6 | 3 |
| PCBPCB 容器) | 6 | 3 |
| BOARD板子顶层 | 6 | 3 |
| BLOB / FONT / CONFIG | 1 / 1 / 1 | 1 / 1 / 1 |
| **合计** | **278** | **266** |
---
## 那 N 个文件到底是啥?为什么这么多
EasyEDA Pro 是**文档式工程**——跟 KiCad「整个项目两个大文件」的思路不一样
- KiCad1 个 `.kicad_sch` + 1 个 `.kicad_pcb`,组件库走外部引用
- EasyEDA Pro**每用到一种组件,就把它的 symbol / footprint / device 各自快照成独立文档存进工程里**
外加多页原理图、容器层、全局资源……
ESP-VoCat 用了大约 100 种不同的元件ESP32-WROOM、各种规格的电阻、按键、LED、晶振、屏幕模组……
所以**自动产生 248 条都是元件库快照**105 SYMBOL + 55 FOOTPRINT + 88 DEVICE。
剩下 30 条才是工程"主体"9 张原理图 + 6 套 PCB/BOARD/SCH 容器 + FONT/BLOB/CONFIG 全局资源)。
> **直觉对照**:把 EPRO2 想象成**每个文档的 git history**,不是项目压缩包。
> 一个 SYMBOL 文件 ≈ 立创元件库里"这颗 0603 电阻"的快照 + 你在工程里对它的每次微调。
每个 EPRO2 文件内部是**事件流**event-sourced按行排
```
{"type":"DOCHEAD","ticket":4456}||{"docType":"FOOTPRINT","uuid":"a20de4ce..."}|
{"type":"OP","op":"ADD","id":"e123",...}||{...payload...}|
{"type":"OP","op":"UPDATE","id":"e123",...}||{...payload...}|
...
```
回放这些 event 就能拿到文档的当前状态。
---
## 为什么数量对不上278 vs 266
我们多 12 个,全部集中在**容器层文档**SCH_PAGE 多 3、SCH 多 3、PCB 多 3、BOARD 多 3。
原因很简单:
- **网页端导出**是当前快照——只导"这个项目此刻包含哪些 doc"
- **我们的爬虫**走完整 history chain——把工程历史上**演化路径**上出现过的 doc 都拉下来了
(比如项目历史里"加过一张图、又删了",那张图的 SCH_PAGE 在我们这里有,在网页导出里没有)
元件库部分248 条)完全一致,没有差异。
---
## 体积对比
| 形态 | 大小 | 说明 |
|---|---:|---|
| 网页 ZIP 压缩态 | **1.41 MB** | 适合用户下载 |
| 网页 ZIP 解压(.epru + json + 6 图) | 7.20 MB | |
| 我们爬取278 个独立 .epro2 | **7.85 MB** | 多 12 容器 + 缺 IMAGE/ |
> 我们多 0.65 MB 是那 12 个历史容器层;他们多的部分是 6 张 IMAGE/ 预览图(~0.93 MB
> 这是我们当前**确实缺的一块数据**——blob 引用爬到了,二进制图片本体没爬。
> 不影响 EPRO2 → KiCad 转换语料生成KiCad 端没对应字段),但如果要做"原貌可视化还原"会需要补。
---
## 为什么我们走 N 文件而不是 1 ZIP
简单说:**ZIP 这条路根本没有公开端点可爬**。
- 三份 HAR 反复抓过编辑器流量,**网页端"导出"按钮压根不发任何 HTTP 请求**
- 它是纯前端 JS 操作:编辑器把已经加载到内存的数据用 `JSZip` 在浏览器里现拼现压
- 所以服务端没有 `/export` 之类的 endpoint 给爬虫调用
我们走的是**官方编辑器加载工程时用的同一套 API**`/api/v4/projects/<P>/branches/<B>/histories/<H>` 拿 chain
逐段 AES-GCM 解密 + gunzip + 按 DOCHEAD 切分),所以拿到的是"原汁原味"的 per-doc EPRO2 流。
附带好处:
| | per-doc 爬取 | 网页 ZIP |
|---|---|---|
| 单文档 diff | ✅ 容易(每文件独立) | ❌ 全在一坨流里 |
| 增量更新 | ✅ 只重抓变动 doc | ❌ 整包重下 |
| LFS 友好 | ✅ 单文件可独立寻址 | ❌ 一个大 zip 改一字节 LFS 也得重传 |
| 历史完整性 | ✅ 全 chain | ❌ 只有当前快照 |
| 体积 | 7.5 MB 平铺 | 1.4 MB 压缩态 |
---
## 你应该用哪种
| 用途 | 选哪种 |
|---|---|
| 训练 LLM 做 EDA 任务、做 EPRO2 → KiCad 转换、做 BOM 抽取 | 我们爬的 per-doc |
| 给立创的人复刻你的工程(直接拿去用编辑器打开) | 网页端 ZIP |
| 想看项目历史演化、做版本 diff | 我们爬的 per-doc含 history |
| 想拿到组件预览图(原始 webp | 网页端 ZIPIMAGE/ 目录) |
---
## TL;DR
- "网页端 .epro2" = ZIP 容器(叫 .epro2 是 UX 决定,不是格式)
- "爬取的 .epro2" = 工程内每个文档自己的 EPRO2 消息流
- 两者**信息量基本等价**,包装方式相反
- 我们选 per-doc 是因为 ZIP 这条路根本没有服务端端点可爬,并且 per-doc 对下游处理更友好
- 唯一**真实差异**是 IMAGE/ 二进制图(我们暂未爬),其它都对得上

16
log.md
View File

@@ -4,6 +4,22 @@
---
## 2026-04-29 01:00 科普文档:爬取 per-doc .epro2 vs 网页端 .epro2 ZIP 整包
**Claude 会话**
接 chain replay sleep 优化commit `1e06ba6`)后续。同事看到 `data/raw/oshwhub/<uuid>/source/` 下面躺着 278 个 `.epro2` 而不是 1 个,会直觉以为抓错了——他们认知里的 `.epro2` 是网页端"下载工程包"那个 1.4 MB 单文件。
实际上:
- **网页端 `.epro2`** = ZIP 容器(扩展名误导),里面 `.epru`(拼成一坨的 EPRO2 流)+ `project2.json` + `IMAGE/` 6 张组件预览图
- **爬取 `.epro2`** = 工程内每个文档SYMBOL / FOOTPRINT / DEVICE / SCH_PAGE / PCB ……)自己的 EPRO2 消息流per-doc 一文件
- 两者**信息量基本等价**ESP-VoCat 我们 278 vs 网页 266多的 12 个是 history chain 上演化掉的容器层旧版本);唯一真实差异是 IMAGE/ 二进制图(我们 blob 引用爬到了但没拉本体——已知 gap
- 我们走 per-doc 不走 ZIP 的硬约束:**ZIP 那条路服务端没有公开端点**,是纯前端 JS 现拼现压(三份 HAR 验证:导出按钮零 HTTP 流量)
写到 `docs/sources/pro_crawl_vs_export.md`给同事看。结构TL;DR → ESP-VoCat 实例 → docType 分布对比表 → 数量差异解释 → 体积对比 → 选型决策表。
---
## 2026-04-29 00:30 KiCad 导出 Phase 3 hierarchicalroot + global_label + 5-Voltage 电源端口
**Claude 会话**