> ## Content Index
> Fetch the complete content index at: https://www.hubwiz.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# 博途批量复制设备程序：用点表替代复制粘贴
- URL: https://www.hubwiz.com/blog/tia-portal-batch-copy-from-spreadsheet/
- Published: 2026-10-27T09:16:11.000Z
- Updated: 2026-10-27T09:29:03.000Z
- Description: 批量复制的成本不在复制，而在四类差异不同步。把差异收到一份点表里，把手工改动变成数据，200 台设备一分钟生成，编译 0 错误。
- Author: admin
- Tags: 工业 AI, 博途, TIA Portal, PLC 编程, TiaLink

一条线上 8 台同样的泵，程序写好了第一台，剩下 7 台怎么办？大多数人会复制粘贴，然后改名字、改地址、改延时——一次两分钟，8 台一个下午，中间漏一处就等着现场调试时发现两台设备抢同一个输入点。

这件事真正贵的地方不是动作多，而是**复制粘贴复制的是"一样的东西"，而工程里要的恰恰是"不一样的东西"**：块名、块号、背景 DB 的归属、IO 地址、参数默认值、注释里的台号，这些每一台都必须不同。手工改，是拿人的注意力去保证这五样东西在 200 次复制里一次不错。

更省事的路径是把差异做成**数据**：一份点表，一段脚本，让"改文本"这件事由程序执行。下面是我在一个 S7-1500 测试工程上把这条路径完整跑通的过程——8 台、200 台、以及把 200 多个对象整体搬到另一个 CPU——连同过程中踩到的 6 个坑，这些坑在官方文档里不会写，报错信息也基本指不到真正的原因。

## 一、先看清：复制粘贴到底失败在哪里

把一台设备"抄"成 8 台，动手前先列出需要改的东西，一共四类：

| 差异类别  | 具体是什么              | 漏了会怎样                         |
| ----- | ------------------ | ----------------------------- |
| 标识    | 块名、块号、背景 DB 名      | 轻则编号混乱，重则编译报"程序中含有地址含混不清的对象"  |
| 引用    | 背景 DB 记着自己属于哪个 FB  | 编译报实例与接口不匹配，或背景 DB 挂到了别的 FB 上 |
| IO 地址 | 点表里不连续、台与台之间夹着别的信号 | **编译只给一条警告**，下载后两台设备抢同一个点     |
| 参数与文本 | 延时、量程、阈值，以及注释里的台号  | 编译 0 错误，现场才发现第 5 台的延时还是模板值    |

第三和第四类是真正的麻烦：**它们不会让编译失败**。地址落在一个没组态的区间里，编译只丢一句"使用了硬件组态中不存在的输入或输出"——一条警告，混在几十条编译消息里；注释里的台号对不上更是没人管，直到有人夜里拿着图纸逐台核。

顺便算一下手工基线的动作数。一台设备：选中块 → 复制 → 粘贴 → 打开改块名 → 改块号（或让它自动编号）→ 复制背景 DB → 处理"实例与接口不匹配"→ 变量表里加 5 个点 → 回到调用处写 5 行实参 → 检查地址有没有和上一台撞。大约 15～20 次交互，8 台就是 130 次以上，200 台是三千次。**每一次都是一次"看错一眼就会错"的机会**，而其中至少一半（改号、改地址、对实参）机器做得比人稳。

所以判断一个批量做法好不好，别看它省了多少次点击，看它能不能保证这四类差异**每一台都对**。

## 二、先问一句：真的需要复制块吗

动手之前有一件事值得花五分钟确认，因为它能砍掉大部分工作量。

同类设备如果逻辑完全一样、只是点位和参数不同，**根本不需要复制 FB**：一个 FB + N 个背景 DB + 一个按点表生成调用段的块就够了。改一次 FB，8 台全部生效；也不用维护 8 份几乎相同的代码。

只有下面三种情况才值得复制块本身：

- **代码要分叉**：第 3 台带变频器、第 6 台要跟隔壁线联锁，逻辑真的不一样了；
- **跨 CPU / 跨项目复用**：设备是别人负责的 CPU，你只想把块搬过去，不想留一个共享依赖；
- **按台拆给不同的人维护**：某些项目组的分工就是这样，块必须独立。

对应的两种生成模式：

|       | 一个 FB + N 个背景 DB    | N 个 FB 副本       |
| ----- | ------------------- | --------------- |
| 每台的差异 | 全部在调用时传入            | 部分固化进块里（默认值、注释） |
| 改模板   | 改一次，全部生效            | 改完要重跑一遍生成       |
| 调试    | 所有台共用一份逻辑，改哪里影响谁很清楚 | 各台可以单独改，也容易改歪   |
| 适合    | 90% 的情况             | 代码真的要分叉时        |

后面两种模式都会跑一遍，数字分开记。

## 三、差异只应该有一个来源：点表

点表的格式很朴素：CSV，第一行的列名用**模板 FB 的接口成员名**，每行一台设备。

```csv
no,name,Start,StopPermit,Fault,RunDelay,Motor1,Motor2
1,Fan01,%I10.0,%I10.1,%I10.2,T#6S,%Q10.0,%Q10.1
2,Fan02,%I10.3,%I10.4,%I10.5,T#9S,%Q10.2,%Q10.3
3,Fan03,%I11.0,%I11.1,%I11.2,T#12S,%Q11.0,%Q11.1
4,Fan04,%I11.4,%I11.5,%I11.6,T#15S,%Q11.2,%Q11.3
5,Fan05,%I12.1,%I12.2,%I12.3,T#18S,%Q12.0,%Q12.1

```

只做两件事就够用了：

- 布尔成员（`Bool`）那一格填**地址**，脚本据此建变量表；
- 非布尔成员（`Time`、`Real`、`String`）那一格填**字面值**，写进副本的默认值或调用实参。

这份样例里的地址是**故意不连续**的，原因很实际：现场的 IO 点表按图纸排，中间夹着液位开关、接近开关、备用手动按钮。第 5 台从 `%I12.1` 开始，因为 `%I12.0` 被别的东西占了。

这正是"循环里 `i * 8` 算地址"这种做法必须被否掉的理由——算得整齐，装不上现场。地址只能从点表来，一个字都不猜。

脚本读进来之后先做两项检查，都是人工复制最容易犯、编译又最容易放过的错：

1. **地址写重**（两台设备同一个点）——直接把重复的地址列出来，不往下走；
2. **块号撞车**（新块抢了别人的号）——报出被占用的号，建议换起始编号。

第二项还有个细节：重跑（覆盖已有同名块）应该被允许，否则改一次模板就得先手工删 8 个块。所以判断穿过的是"名字已存在"这一种情况，只有新块占别人的号才报错。

## 四、生成过程：导出 → 改文本 → 预检 → 导入 → 编译

博途里的块可以原样导出成 **SimaticML**——一种 XML，块名、块号、接口、每一条 SCL 语句、每一条注释都在里面，全是文本。这条路子的关键就在"全是文本"上：既然是文本，就可以用脚本改、可以 diff、可以 code review、可以进 git，也可以重跑。

一次批量生成，五个动作：

```text
1. 导出模板 FB（以及它的背景 DB 作为模板）
2. 按点表逐台生成 XML：换名字、换编号、换默认值、换注释台号、平移内部 ID
3. 预检：只回报"将创建 / 将覆盖哪些对象"，一个字节都不写进工程
4. 一次请求导入全部同种对象；调用段走 SCL 外部源生成
5. 整机编译，把错误和警告原文打出来

```

第 3 步是整个流程里最值得保留的一步。预检回答的是"我要覆盖什么"，这在已经有一批块的项目里比什么都重要——尤其当你手上的点表是从别人那里接过来的。

第 4 步有个效率上的事实：**一个请求可以装很多个对象**。同种根对象（比如一堆 FB，或者一堆实例 DB）放进同一个文档，一次提交，比一个个导快一个数量级——真正的开销在工程事务上，不在传输上。

调用段单独说一句，因为它是最容易漏的一块。变量表建好了、块也复制好了，但没人调用它们，编译照样 0 错误——程序只是不干活。所以生成流程的最后一步是把 N 段调用写进同一个块里（SCL 外部源生成一个功能块，一段一台）：

```text
"DB_Fan01"(Start := "Fan01_Start",
            StopPermit := "Fan01_StopPermit",
            Fault := "Fan01_Fault",
            RunDelay := T#6S,
            Motor1 => "Fan01_Motor1",
            Motor2 => "Fan01_Motor2");

```

200 台就是 200 段这样的代码，一次生成，用时 5 秒。比生成更重要的是：**实参来自同一份点表**，所以变量名、地址、延时不可能和块里对不上——手工写这段时最常见的错误就是第 7 台接了第 8 台的地址，编译不报，仿真不动。

一次真实运行的输出（8 台，复制 FB 副本模式）：

```text
模板 FB_MotorSeq：3 Bool 入 / 2 Bool 出；点表 8 台（Fan01 … Fan08）；准备 1325 ms
  ✓ FB × 8              1920 ms （8 个对象）
  ✓ 背景 DB × 8         1815 ms （8 个对象）
  ✓ 变量表 Fans_IO      2594 ms （+40 变量）
  ✓ 调用段 FB_FanCalls   674 ms
编译 2523 ms：errors=0 warnings=1

```

三条安全默认值从头到尾没变过：**先预检、默认不覆盖同名对象、导入后不自动保存**。最后一条尤其重要——工程只在内存里被改，`/health` 一直显示 `dirty=true`，什么时候保存、要不要保留，仍然是人的决定。改错了直接不保存退出，什么都没有发生。

## 五、把这件事交给 agent：一段可直接用的提示词

前面四节都是脚本。但很多人手上的客户端已经接好了 MCP 网关（Claude Code、Cursor、VS Code 都行），第一反应是"直接让 agent 做"。这条路能走，而且很直观——读工程 → 看接口 → 改 XML → 导入 → 编译，全部在一次对话里完成，不装任何东西。

前提是**提示词把约束写清楚**。放开让它自由发挥，它会把地址排整齐、把块号自作主张重排、把注释的台号漏一半，而且编译照样 0 错误。我用的那段是这样的：

```text
背景：这台机器上装着 TiaLink（默认 http://localhost:8080），博途里开着一个工程，
里面有一个已经调好的模板功能块 FB_MotorSeq（3 个 Bool 输入、1 个 Time 输入、2 个 Bool 输出）。

任务：把 FB_MotorSeq 批量复制成 4 台同类设备，块名 FB_<name>，背景 DB 名 DB_<name>。
每台的差异见点表（CSV，列名 = 接口成员名，Bool 列是地址，其它列是字面值）：

no,name,Start,StopPermit,Fault,RunDelay,Motor1,Motor2
1,Valve01,%I30.0,%I30.1,%I30.2,T#4S,%Q30.0,%Q30.1
2,Valve02,%I30.3,%I30.4,%I30.5,T#6S,%Q30.2,%Q30.3
3,Valve03,%I31.0,%I31.1,%I31.2,T#8S,%Q31.0,%Q31.1
4,Valve04,%I31.4,%I31.5,%I31.6,T#10S,%Q31.2,%Q31.3

约束（按顺序执行，任一条做不到就停下来问，不要猜）：
1. 先只读：报出当前工程的设备、模板块的接口成员，确认点表列名与接口一一对应；
   对不上就停下来说明哪一列没用上。
2. 检查点表：地址有没有重复、块号 300 起有没有被占用。有问题先报，不要继续。
3. 预检一次，把"将创建的对象清单"给我看。等我确认后再真写。
4. 每份副本：块名/块号/标题按台号改；Time 类输入的默认值取点表；
   注释里出现台号的地方按点表改，改不了的地方列出来，不要沉默地留着。
5. 背景 DB 必须指向自己那份 FB（不是模板 FB）。
6. 变量表按点表地址原样建，一个地址都不许自己推算。
7. 全程不要保存工程。最后给我三样：编译结果（错误/警告数 + 每条警告原文）、
   第 1 台与第 4 台的块导出 diff、你认为需要人工复核的点。

```

这段提示词我在测试工程里原样跑了一遍（agent 只使用网关的读写接口，不碰脚本），结果：4 个 FB 用时 1.7 s、4 个背景 DB 1.7 s、20 个变量 1.8 s、整机编译 2.0 s，**0 错误 1 警告**（还是那条 IO 地址不在硬件组态里）。导出 diff 抓到 8 处不同，包括块名、块号、延时默认值、三条注释里的台号——以及那句改不掉的"在 1 号运行"。

换成故意写重的点表再跑一次，它按第 2 条停住了：

```text
错误：点表里地址重复：%Q30.0

```

每条约束都不是凑数的：

| 约束           | 对应的事实                                    |
| ------------ | ---------------------------------------- |
| 只读 + 对列名     | 列名和接口错位时，错位是静默发生的，写进去的是另一个参数             |
| 先查地址重复与块号占用  | 地址重复不会让编译失败，只在下载后表现为两台设备抢一个点             |
| 预检后等确认       | 预检只回报第一个根对象，"看起来只建了一个"是汇报口径，不是事实         |
| 改不了要列出来      | 注释里的半结构化句子没有规则能改全，不要求它就默默留着              |
| DB 指向自己那份 FB | 复制实例 DB 最容易漏的一处，编译错报的却是"实例与接口不匹配"        |
| 地址原样         | 让模型"顺手把地址排整齐"是这类任务里最危险的自作主张              |
| 不要保存 + 三样验收物 | 不保存意味着改错了什么都没发生；diff 报告是唯一能证明"每台真的不同"的东西 |

**什么时候该从提示词换成脚本**：设备数超过 10～20 台，逐块改 XML 的成本按台数线性上涨，脚本不变；要反复重跑（这个月 8 台、下个月 14 台），脚本本来就是幂等的；要进 git、要被 review，脚本加点表是可 review 的文本，对话记录不是。反过来，**一次性、探路、需要判断的活留给 agent**：摸清接口、发现点表里的坑、判断"这台要不要单独处理"——这些是提示词擅长的，脚本不会。

那段提示词我单独存了一份，方便直接复制：[agent 提示词](https://github.com/tia-forge/tialink/blob/main/examples/batch-copy/agent-prompt.md?ref=hubwiz.com)。

## 六、实测数字：8 台、200 台、208 个对象

同一台机器（TIA V19 + S7-1500 + TiaLink 网关），三组实测：

| 场景                | 生成内容                                | 用时                  | 编译结果      |
| ----------------- | ----------------------------------- | ------------------- | --------- |
| 8 台泵，复制副本模式       | 8 个 FB + 8 个背景 DB + 40 个变量 + 1 个调用段 | 约 7 s               | 0 错误 1 警告 |
| 200 台，单 FB 多背景 DB | 200 个背景 DB + 1000 个变量 + 200 段调用     | 约 58 s              | 0 错误 1 警告 |
| 208 个对象跨 CPU 搬运   | 205 个块 + 2 张变量表（486 KB）             | 导出 13.7 s + 导入 59 s | 0 错误 1 警告 |

单次请求能装多少，也测了：**20 个 FB 一次 3.9 s，100 个实例 DB 一次 13 s**。这个比例说明批量处理是有规模效应的——第 101 个块的成本远低于第一个。

这组数字也顺带回答了一个常见疑问：**只有 3 台设备，值不值得写脚本？** 不值得。3 台手工改，半小时能搞定，而且你能在改的过程中把模板本身的毛病看清楚。这条路的门槛大约在 6～8 台以上，或者设备数量会反复增减（今天 8 台、下个月加到 14 台），又或者同一套模板要在多个项目里复用——那时"重跑一遍"的价值才真正体现出来：改模板、改点表、重跑，而不是再手工改 14 次。

200 台那组的分解：背景 DB 24.7 s、1000 个变量 27.7 s、200 段调用 5.1 s、编译 10.4 s。变量表反而是最慢的一步，因为它是一次性写 1000 条记录。

跨 CPU 那组更有意思：把整个设备的块导出成一个 zip，再按"变量类型 → 功能块 → 实例 DB → 变量表"的顺序分批灌到另一个 CPU，**207 个对象、6 个请求**，编译 0 错误。顺序不能乱，原因见下一节的第 5 个坑。

唯一那条警告三组都一样：`Inputs or outputs are used that do not exist in the configured hardware`。原因是测试工程的 CPU 上没组态 IO 模块，点表里的 `%I10.0` 这类地址落在空处。**这就是上一条"编译通过不等于正确"的活例子**——0 错误，但地址确实是虚的。

## 七、六个坑

以下六个都是实测踩过的，报错原文照抄，方便你搜到这篇时不至于白跑一遍。

### 1\. 多个对象放进同一个文档，内部 ID 必须全局唯一

把 8 份导出结果首尾拼起来直接提交，得到：

```text
Duplicate Simatic ML ID '0' found at line number 430 and line position 2.
This ID was previously used at line number 50 and line position 4.

```

每一份导出的 XML 内部 ID 都从 0 开始，拼在一起就撞了。做法是按顺序给第 i 份的所有 ID 加一个偏移量。

### 2\. 但 `ID` 是十六进制，`UId` 是十进制整数

第一版我把两类 ID 统一按十六进制平移，前 8 台居然全过了（偏移量 0x1000～0x8000 的十六进制写法恰好全是数字），到第 10 台（0xA000）立刻失败：

```text
"UId" 特性无效 - 根据数据类型"…int"，值"A003"无效 - 字符串"A003"不是有效的 Int32 值。

```

`ID` 是十六进制字符串，`UId` 是十进制整数——必须分开处理。这个坑的危险之处在于它**不会在第一次就暴露**，8 台以内一切正常，等你按同样的脚本去做 20 台、50 台时才炸。

### 3\. `AutoNumber` 为真时，你写的编号会被忽略

导出的模板里 `<AutoNumber>true</AutoNumber>` 意思是"编号由博途自己分配"。想固定块号，必须把它改成 `false`，否则 XML 里写的 `<Number>200</Number>` 完全不起作用，回头核对"第 5 台是不是 204 号"时对不上。

### 4\. 预检只回报第一个对象

一个文档里装了 20 个块，预检结果里只有 1 条记录。一开始我以为是"只识别了一个"，其实是汇报口径的问题——预检通过之后一次导入，20 个对象全建好了。记住这一点，别被预检的输出骗着改脚本。

### 5\. 导入顺序 = 依赖顺序

先导实例 DB、后导它的 FB，报错是：

```text
Block 'FB_Blower01' does not exist at the object with UID ''.

```

这句话里没有一处提到"顺序"。正确的顺序是：**变量类型（UDT） → FB / FC / OB → 实例 DB → 变量表**。批量搬块时按这个顺序分组发请求，一次就过。

### 6\. 合成实例 DB 时，FB 的 `Temp` 和 `Constant` 段必须删掉

当目标 CPU 里没有现成的背景 DB 可以当模板时，只能拿 FB 的接口现场拼一个实例 DB。这时 FB 接口里的 `Temp`、`Constant` 段会让导入失败：

```text
DB_P001: The Openness import failed: "Temp"部分对此块无效。

```

它连着报错两次（先是 `Constant`，删掉之后再报 `Temp`），每次只说"某部分对此块无效"，不说是哪来的。实例 DB 只接受 `Input` / `Output` / `InOut` / `Static` 这几段。

## 八、编译通过之后：四步验收

0 错误只是一个必要条件。批量生成的代码要过下面四关：

**1）读编译消息，别只看计数。** 整机编译出来的失败块有时只有条数没有文字，这时改用单块编译，博途才能把消息挂到具体对象上。批量生成之后至少跑一次单块编译，确认调用段里引用的每个背景 DB 都真的存在。

**2）查地址歧义。** 如果两个块抢了同一个块号，编译会报"程序中含有地址含混不清的对象"并逐个列出块名——这是唯一能提前发现"编号错位"的信号，别跳过。

**3）导出回读，做 diff。** 这一步最能说明问题：把第 1 台和第 5 台的块分别导出来 diff，**应该不同的行**必须不同，**不该不同的行**必须一样。实测的两份 diff：

```diff
-      <StartValue>T#6S</StartValue>
+      <StartValue>T#18S</StartValue>
-    <MultiLanguageText Lang="zh-CN">1 号风机备电机延时</MultiLanguageText>
+    <MultiLanguageText Lang="zh-CN">5 号风机备电机延时</MultiLanguageText>
-      <Name>FB_Fan01</Name>
+      <Name>FB_Fan05</Name>
-      <Number>200</Number>
+      <Number>204</Number>

```

同时它也验证了地址确实是点表里那些不连续的地址：`Fan05_Start = %I12.1`、`Fan06_Start = %I12.6`、`Fan08_Fault = %I14.7`，一个都没被"算整齐"。

**4）仿真里强制值。** 编译和 diff 证明"生成对了"，仿真证明"逻辑对了"。挑两台在虚拟 PLC 里强制输入，看输出是否按预期走——这一步花的时间比前面所有步骤加起来都值。

具体做法：把两台设备的输入分别置 1，观察输出是不是各自独立——如果第 5 台的输出跟着第 1 台的输入动，说明点表里有地址写重了（脚本本该拦住，但变量表被你手工改过就会绕过去）；再把延时参数改大一点，看输出是不是按各自的 `RunDelay` 起跳，这能一次性验证"参数没串台"。这两条检查加起来不到十分钟，却能挡住批量生成里杀伤力最大的两类错误。

## 九、工具替你改不到的地方

上面那份 diff 里有一行值得单独看：注释里原本写的是"2 号电机在 1 号运行 RunDelay 之后启动"，用替换规则把"1 号电机""2 号电机"改成台号之后，剩下那半句"在 1 号运行"没有被替换——因为它写的是"1 号运行"，不带"电机"两个字。

这不是脚本写得不好，而是**文本替换天生有上限**：模板里的差异一旦散落在自由文本（注释、标题、字符串常量）里，就没有可靠的规则能保证改全。

真正的解法不是把替换规则写得更聪明，而是**别把差异写进注释**：

- 台号、设备名这类东西，要么进接口（一个 `DeviceNo` 输入），要么进块标题，不要撒在 8 条网络注释里；
- 延时、量程这类参数，做成接口成员的默认值，让它出现在 diff 里能被一眼看到；
- 只有确实需要人读的说明文字，才留在注释里，并且接受"复制的注释可能不准"这个事实。

换句话说：**能被点表覆盖的差异是可控的，写在注释里的差异是不可控的**。这条经验对任何批量生成都成立，不只对这个工具。

剩下的边界也说清楚：

- 只处理 PLC 侧的块、变量类型、变量表和 SCL 外部源。**WinCC Unified 的屏幕不能这样复制**——那部分不走这条导出通道。
- 不碰安全程序。F-CPU 的安全块有额外的复制规则和校验，必须人工按规范走。
- 不自动保存，也不替你做 review。工具负责"少抄几遍、少错几个地址"，工艺责任和上线判断仍然在人这边。
- 生成的代码仍然要走项目自己的验证流程：编译 → 仿真 → 现场联调 → 验收。批量生成只是把"抄"这一步做对，不改变验证要求。

## 收尾

回到开头那个问题：8 台泵、20 台风机、200 个工位，是不是只能一次一次复制粘贴？

不是。把差异收进一份点表，把"改文本"交给脚本，剩下的判断留给人——一个模板加一份表格，8 台几秒钟，200 台一分钟出头，每一次生成都能预检、能 diff、能重跑。

这套做法的脚本我做成了零依赖的命令行工具，放在 [GitHub](https://github.com/tia-forge/tialink/tree/main/examples/batch-copy?ref=hubwiz.com)（含 8 台与 200 台两份点表样例，可直接改成自己的），它通过 [TiaLink](https://tia.hubwiz.com/tia-link?ref=hubwiz.com) 的 HTTP 接口驱动本机博途工程：导出模板、按点表生成、预检、导入、编译，全程不保存工程。想先看这条路在别处怎么用，可以接着读《[AI 改博途程序：TIA Openness MCP 的写保护与边界](https://www.hubwiz.com/blog/tia-portal-openness-mcp-write-guards/)》和《[梯形图转 SCL 迁移实战指南](https://www.hubwiz.com/blog/lad-to-scl-migration-guide-tia-portal/)》——前者讲"写之前怎么保证不写坏"，后者讲"哪些转换能自动、哪些必须重构"，和这篇是同一类问题的三个切面。

> **这是「AI × 工业 / PLC」专题的一部分。**完整地图见：[AI × 工业 / PLC 完全指南](https://www.hubwiz.com/blog/ai-industrial-stack/)