# 现场收资接入(v0.1,2026-09-17) > 面向:现场交来的 **`F:\temp\如东风场数据`** 这一类目录 → 观澜的 `data/raw/<场站>/`。 > 工具:`scripts/extract_site_data.py`(纯标准库 + 系统 ODBC,**不新增任何 pip 依赖**)。 --- ## 1. 现场交来的东西长什么样(2026-09-17 实测) | 文件 | 体量 | 内容 | 用途 | |---|---|---|---| | `10分钟数据.zip` | 3,760 MB | 38 件 `WTG01.csv … WTG38.csv`,表头 `TimeStamp,WTG,StationId,cnt_wtc_…` | **10 分钟 SCADA**(观澜 `scada_10min` 的正源) | | `1分钟数据.zip` | 1,548 MB | 38 件 `01E.csv … 38B.csv`(机组代号命名),表头 `time_stamp,3#桨电机温度,…`(中文列) | **1 分钟 SCADA**(观澜 `scada_1min` 的正源) | | `25年.zip` | 1,071 MB | `25年/<月>/2025-<月>-<类>.zip` → **`.mdb`(Access)** | 分类深度源(每月 12 类) | | `26年.zip` | 961 MB | `26年/<月>/2026-<月>-<类>.zip` → `.mdb` | 同上 | **年度 zip 内的类目**(每月一套):`cnt din dot flg grd int prs scd std sum tmp tur`。 实测 `std.mdb` 只有一张表 `tblGrid`: ``` TimeStamp | Station | WPSStatus | TimestampStation | CurrentL1..L3 | VoltageL1..L3 | ActivePower | ReActivePower | ActivePowerExport | ReActivePowerExport | …(23 列) ``` `TimeStamp` 为 10 分钟节拍,`Station` 为机组号(如 91/92)。 ## 2. 接入观澜(命令) ```bash # ① 对账:现场 zip ↔ 现有 data/raw(逐件比大小,不解压十几 GB 就能验,幂等) python scripts/extract_site_data.py --src "F:\temp\如东风场数据" --verify-raw # 实测: 10分钟数据.zip → data/raw/如东/scada_10min 38/38 同大小 # 1分钟数据.zip → data/raw/如东/scada_1min 38/38 同大小 ⇒ 现有 raw 输入可追溯 # ② 提取(幂等;已有同大小文件即跳过;--only-missing 只补缺件) python scripts/extract_site_data.py --src "F:\temp\如东风场数据" --extract-csv --only-missing # ③ 年度 MDB(深度源)—— 只看结构 / 解出 / 探针 python scripts/extract_site_data.py --src "F:\temp\如东风场数据" --list python scripts/extract_site_data.py --src "F:\temp\如东风场数据" --extract --year 26 --month 1 python scripts/extract_site_data.py --src "F:\temp\如东风场数据" --probe --class std # ④ 接上观澜既有链路(体检 → 重算) python scripts/raw_data_check.py # 结构/增量/时间空洞体检(R1–R8) python scripts/rebuild_all.py # 或门户「数据重算」 ``` ## 3. 读 Access `.mdb` 的路线(离线、零 pip 依赖) | 平台 | 用什么 | 前提 | |---|---|---| | Windows | **.NET `System.Data.Odbc` + 系统 `Microsoft Access Driver (*.mdb, *.accdb)`** | 装了 Office 或 Access Runtime 的机器自带;**本机实测可用** | | Linux | `mdbtools` 的 `mdb-export` / `mdb-tables` | `apt/yum install mdbtools` | 两条路本器都会**先探针**:`--probe` 缺能力时给补救办法并返回 rc=4,**不静默、不猜**。 (刻意不把 `pyodbc` 之类的包塞进交付包:ODBC 驱动是系统级前提,装了包也解决不了;改成"探针 + 明确补救"更诚实。) ## 4. 从 MDB 转 CSV(`--to-csv`) `python scripts/extract_site_data.py --src <…> --to-csv --class std [--year 26 --month 1] [--turbines WTG01,WTG02]` - 落点按类目映射:`CLASS_TO_RAW`(`std` → `scada_10min`;其余类目**待现场口径确认后才接**,未确认前不硬接); - 机组号映射:`Station` 起点 90 ⇒ `WTG01`(实测 `std.mdb` 里 `Station=91/92`); - 时间列统一改名 `Time`,逐机组落 `data/raw/如东/<类>/WTG.csv`,追加式写入(同一机组多月叠加)。 ## 5. 还没有的输入(如实列) | 观澜要的类 | 现状 | 缺什么 | |---|---|---| | `故障报警` | 16 件 `2025Q1.xls …`(已有) | — | | `风机故障记录` | 10 件(`*.rar` / `*.xlsx`) | — | | `油样报告` | **0 件** | 现场 PDF 报告 | | `windcms` / `m5_cms_tcm` | **0 件** | 振动侧 CMS 导出件(取料单第 7、8 项) | --- ## 6. 落位约定(用户令 2026-09-17:用年度 MDB 归档取代此前的两个 CSV zip) ``` <安装目录>/data/raw/如东/ ├─ scada_mdb/ ← ★ 现场原始收资(Access 按月分类),就地归档 │ ├─ 25年.zip (1,071,376,695 B, sha256 E451FF68…;内层 25年/1-12月/2025-<月>-<类>.zip) │ └─ 26年.zip (1,007,354,282 B, sha256 0B641309…;内层 26年/1-9月/2026-<月>-<类>.zip) ├─ scada_10min/ 38 件 WTG01..WTG38.csv(源: 10分钟数据.zip,已对账 38/38 同大小) ├─ scada_1min/ 38 件 01E..38B.csv(源: 1分钟数据.zip,已对账 38/38 同大小) ├─ 故障报警/ 风机故障记录/ 油样报告/ windcms/ m5_cms_tcm/ ``` - **为什么要单开 `scada_mdb/`**:年度归档里是**每月 12 类**的 Access 库(`cnt din dot flg grd int prs scd std sum tmp tur`),不是"一台一个 CSV",硬塞进 `scada_10min/` 会破坏该目录"平铺 `<机组>.csv`"的约定。 - 目录已登记进两份真源:`src/windscada/config.py::STATION_SUBDIRS` 与 `scripts/raw_data_check.py::EXPECT`(允许 `.zip` 原样归档与 `.mdb` 解出件)⇒ 现场收资体检认它, 不再报"未约定的子目录"。 - 工具直接吃这个目录(不必再指向 `F:\temp`): `python scripts/extract_site_data.py --src data/raw/如东/scada_mdb --list` ⇒ 实测 `25年.zip`:12 个月 · 12 类;`26年.zip`:9 个月 · 12 类。 ⚠️ **尚未做的一步(等你定)**:把年度 zip **解出**(每月 12 个 `.mdb`,约十几 GB), 以及是否把 MDB **转成** `scada_10min/`、`scada_1min/` 的 CSV(即"用 MDB 取代现有 CSV")。 }这个决定会影响现有 27 GB 的 CSV 输入要不要保留, 故先只做"放进去"这一步。 --- ## 7. CSV → MDB(用户令 2026-09-17:仿照 `25年.zip`/`26年.zip` 的形态)—— 探路结果 ### 7.1 目标形态(照现场归档抄) ``` <年>年.zip → <年>年/<月>月/<年>-<月>-<类>.zip → <年>-<月>-<类>.mdb ``` 现场每月 12 类(`cnt din dot flg grd int prs scd std sum tmp tur`),**每类一个 MDB、库内一张表** (如 `tblGrid`,用 `Station` 区分机组)。本器同构产出,类名沿用现场叫法:`10min` / `1min`。 ### 7.2 两条**必须偏离**现场形态的硬限制(Access 引擎,绕不过) | 限制 | 实测冲突 | 处置 | |---|---|---| | **单表 ≤ 255 列** | `scada_10min/WTG01.csv` = **598 列**(`scada_1min/01E.csv` = 79 列) | 每库按列拆表 `t1/t2/t3`(建议每片 250 列),各表首列放 `rid` 行号便于对齐 | | **单 MDB ≤ 2 GB** | `scada_10min` 14.4 GB + `scada_1min` 12.8 GB | **按月分库**(不可按年一个库) | ### 7.3 写 MDB 的能力与路线(本机实测,2026-09-17) - ✅ **建库可用**:`ADOX.Catalog` COM + `Microsoft.ACE.OLEDB.12.0/16.0` 提供程序(64 位 PowerShell 下实测 `可实例化`、`$cat.Create("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=x.mdb;")` 成功建出 180 KB 库)。 - ❌ **`SELECT … INTO [文本源]` 不支持**:实测报「这种对象类型不支持该操作」。 - ❌ **用目标库连接去 `INSERT INTO … SELECT * FROM [text;…]` 也不支持**:同样报该错。 - ✅ **可行方向(下一步要落的就是它)**:**反过来的写法** —— 连接**文本源**, 用 Jet 的链接语法指向目标库:`INSERT INTO [;DATABASE=<目标.mdb>;].[表名] SELECT * FROM [<分片>.csv]` (实测语法已被接受:报的是「INSERT 语句语法错误」而非「不支持该操作」;当时两点没对上 —— 方括号里要**带结尾分号** `[;DATABASE=…;]`,且目标表列数/列序要与 CSV 完全一致)。 - ⚠ **Linux 侧只能读不能写**:`mdbtools` 无写库能力 ⇒ MDB 生成请在 Windows 上做(或让现场直接给 MDB)。 - ⚠ 中文字段名(1 分钟数据是中文列名):文本 ISAM 需配 `schema.ini`(`CharacterSet=65001`,UTF-8), 否则中文列名/值会按 ANSI 代码页解错。 ### 7.4 尚未做的(下次接手的最短路径) 1. 在 `scripts/extract_site_data.py` 增 `--csv-to-mdb --month --class {10min,1min}`: 按月过滤 → 按 250 列切片写临时分片 CSV(首列 `rid`)→ 建库 → 按列生成 DDL(`CREATE TABLE`)→ 按 §7.3 的链接语法 `INSERT … SELECT` → 打成同名 zip → **读回校验行数/首末行与源 CSV 一致**; 2. 试点:`--month 2025-01 --turbines WTG01`(1 台 1 月)跑通并逐值对拍; 3. 全量:38 台 × 21 个月(10min 3 张表/月、1min 1 张表/月),按 §7.2 按月分库。 --- ## 8. SCADA 接入兼容 CSV 与 MDB(用户令 2026-09-19) 用户令: **「scada 数据接入兼容支持 CSV、MDB」**。此前 `src/windscada/data.py::load_10min()` 把路径写死成 `/<台>.csv` ⇒ 只有"已导出 CSV"形态能被摄入, 现场交付的年度归档库只能看不能算。 现在取数统一走 `src/windscada/scada_source.py`: | 形态 | 位置 | 取数方式 | |---|---|---| | CSV(优先) | `data/raw/<场站>/scada_10min/WTG01.csv…` | `pandas.read_csv(engine='pyarrow')`,按契约列取交集(既有行为不变) | | MDB(回落) | `data/raw/<场站>/scada_mdb/<年>年/<月>月/<年>-<月>-<类>.mdb` | ACE(`Microsoft.ACE.OLEDB.12.0`) + PowerShell,逐分片表 `SELECT … WHERE turbine=?`,按 `(rid, turbine)` 拼列 | **规则**: CSV 在位就用 CSV;该台没有 CSV 才回落 MDB;用的哪种记在返回表的 `attrs['scada_source']` 并在首次使用时打一行 —— 不静默换源。两种形态都取不到 ⇒ **响亮报错**(不返回空表,空表会被下游当成"这台没数据")。 ### 8.1 库这一侧的四个坑(2026-09-19 实测逮到) 1. **老库没有机组列**:2026-09-19 之前建的库每行只有 `rid`,而 1min 源件**没有任何机组标识列** (`source_file` 全行同值)⇒ 那份 1min 库**按台取不到数**。10min 老库还能救:源件自带 `WTG` 列,读侧兜底。 2. **老库分片表表头与数据行差一列**:旧 `build_chunks` 表头写 `names[lo:hi+1]`、行写 `vals[lo:hi]` ⇒ 第 250 列之后的**列名整体错位一格**,且相邻分片表共用同一列名(实测 `t1` 末列 == `t2` 首列)。 这种库读出来是"名字对不上数"的数据 —— 比缺数据危险 ⇒ 读侧**响亮拒绝**并给出重建命令。 3. **列名消毒两边必须同一套**:Access 列名不能含 `.` `!` `[` `]`,建库时表头被消毒(非法字符→`_`、 截断 60、撞名加 `_1`)。规则单一实现在 `src/windscada/mdb_names.py`:读侧把契约列名翻成库内列名、 取回后改回契约名 —— 否则从 MDB 取数会**静默少列**。 4. **行序必须按 `rid` 数值排**(`rid` 在库里是文本,直接合并会得到 `1,10,100…` 字典序 —— 首轮对拍 "对不上"就是这个原因,数据其实没错)。 ### 8.2 验收(可重复) ``` python scripts/csv_to_mdb.py --month 2026-07 --class 10min # 用修好的脚本重建一个月 # 逐值对拍: 同一台同一月, CSV 取数 vs MDB 取数 → 全列逐值一致(含 t2/t3 分片里的列) ``` 2026-09-19 实测:**2026-07 库 866 行 × 4 列(含跨分片列)逐值一致**;2025-03 老库被如实拒绝。 `data.load_10min()` 从 CSV 取与从 MDB 取,返回同一张表(`attrs['scada_source']` 分别是 `csv` / `mdb`)。 ### 8.3 重建进度 修好的建库脚本**重跑全部月份**(10min 19 个月 + 1min 18 个月,约 5 h,后台跑;日志 `F:\temp\csv2mdb_rebuild.log`)。1min 目前包内**没有消费者**,所以"1min 的 MDB 形态"属数据层完整性补齐; 10min 才是摄入要用的那一类。