
1. 项目缘起与整体设计思路1.1 为什么要在 Python 里折腾 OPC 这件事工厂里跑着的 PLC 五花八门西门子、三菱、欧姆龙、汇川各家协议不一样上位机想统一读写数据最省事的办法就是走 OPC。OPC 全称 OLE for Process Control早期基于 Windows 的 COM/DCOM 技术后来演进成跨平台的 OPC UA。OpenOPC 这个库就是 Python 圈子里用来跟 OPC DA 服务器打交道的一个老牌工具底层封装了 PyWin32 的 COM 调用让你能用几行 Python 代码读写 PLC 里的点位数据。我最初接触它是因为一个产线数据采集的小项目现场有一台老式 OPC DA 服务器跑在工控机上PLC 是某主流品牌的中型机需要把几十个寄存器、线圈的状态定时抓下来存进数据库再做看板。用组态软件当然能搞定但授权费用高、扩展性差而且后期想加个自定义告警逻辑特别别扭。用 Python 就灵活多了OpenOPC 正好能填上“Python 与 OPC DA 服务器通信”这个缺口。这个内容适合谁看如果你手头有 OPC DA 服务器想用 Python 做数据采集、设备监控、MES 对接或者单纯想搞明白 PLC 数据怎么进到 Python 程序里那这篇就是给你写的。不需要你精通 COM 原理但得会基本的 Python 语法知道什么是 PLC 的寄存器地址。1.2 整体架构怎么搭才不踩坑先说清楚整个链路Python 程序 → OpenOPC 库 → OPC DA 服务器 → PLC。OpenOPC 本身不直接跟 PLC 说话它只是个客户端真正干活的是 OPC 服务器。这一点特别重要很多人以为装了 OpenOPC 就能连 PLC结果发现连不上就是因为没搞明白中间还有一层服务器。架构上我建议分三层来设计。第一层是连接管理层负责建立和维持与 OPC 服务器的会话处理断线重连。第二层是数据读写层封装具体的点位读写逻辑把 PLC 地址映射成业务上有意义的变量名。第三层是业务处理层做数据清洗、缓存、入库、告警判断。这样分层的好处是哪天 OPC 服务器换了或者要从 DA 迁到 UA只需要改第一层业务代码基本不动。为什么选 OpenOPC 而不是别的坦白讲如果现场是 OPC UA 服务器直接用opcua或asyncua库更现代、更稳定。但很多老产线跑的还是 OPC DA尤其是 Windows 环境下的老服务器这时候 OpenOPC 几乎是 Python 里唯一成熟的选择。它的 API 设计也简单read、write、list几个方法就能覆盖大部分场景。注意OpenOPC 依赖 Windows 的 COM 组件只能在 Windows 上跑。如果你的采集程序要部署在 Linux 服务器上得考虑用 OPC UA 网关做协议转换或者把采集端放在 Windows 工控机上再通过消息队列把数据转发出去。1.3 环境准备与依赖安装的取舍环境这块Python 版本建议用 3.7 到 3.9太新的版本有时候 PyWin32 的兼容性会出幺蛾子。我实测 3.8 最稳。安装 OpenOPC 有两种方式一种是pip install OpenOPC-Python3x这是社区维护的 Python 3 兼容版本另一种是下载源码手动装。推荐前者省事。装完之后还要确认几件事。第一PyWin32 是否正确安装import win32com.client不能报错。第二OPC 服务器的 ProgID 是什么比如某常见服务器的 ProgID 可能是XXX.OPCServer这种形式具体得查服务器文档或问现场工程师。第三DCOM 权限配置这是最容易卡住的地方后面单独讲。# 验证环境是否就绪 import OpenOPC import win32com.client print(OpenOPC 版本:, OpenOPC.__version__) print(PyWin32 可用)如果这行代码能跑通说明基础环境没问题。接下来就是连服务器了。2. 核心细节解析与实操要点2.1 OpenOPC 的连接机制与 DCOM 配置OpenOPC 连接 OPC 服务器的核心方法就一个opc OpenOPC.client()然后opc.connect(服务器ProgID)。看起来简单但背后走的是 DCOM跨机器访问时权限问题能折腾死人。先说本机连接。如果 OPC 服务器和 Python 程序在同一台 Windows 机器上通常直接连就行不需要额外配置 DCOM。但如果是跨机器比如 Python 跑在办公电脑上OPC 服务器在车间工控机上那就得配 DCOM 权限。具体步骤是在工控机上运行dcomcnfg找到 OPC 服务器对应的组件服务设置身份验证级别为“无”身份模拟级别为“模拟”然后在安全选项卡里给远程用户加上访问和启动权限。这些操作需要管理员权限而且不同 Windows 版本界面略有差异。提示DCOM 配置是 OpenOPC 跨机器连接失败的头号原因。如果连不上先别怀疑代码去检查 DCOM 权限。另外防火墙要放行 135 端口和动态端口范围这个也经常被忽略。连接的时候还可以指定 OPC 服务器的节点名比如opc.connect(ProgID, 192.168.1.100)。如果服务器在远程节点名就是那台机器的 IP 或主机名。连接成功后opc.servers()能列出所有可用的服务器opc.list()能列出所有点位标签这两个方法在调试阶段特别有用。2.2 点位地址的读写逻辑与数据类型PLC 里的数据分很多种布尔量、整数、浮点数、字符串每种在 OPC 里的表示方式不一样。OpenOPC 读出来的数据默认是字符串或元组需要自己转换。比如读一个浮点数寄存器返回的可能是12.34这样的字符串得用float()转一下。点位地址的格式取决于 OPC 服务器常见的有Device1.Register1、S7:[S7 connection_1]DB1,REAL0这种。这个没有统一标准必须查服务器的地址映射表。我一般会先建一个地址映射字典把业务名称和 OPC 地址对应起来这样代码里用业务名称维护起来清晰。# 地址映射示例 tag_map { temperature: Device1.TempRegister, pressure: Device1.PressRegister, motor_status: Device1.MotorCoil, batch_number: Device1.BatchString } # 读取单个点位 value opc.read(tag_map[temperature]) print(温度:, float(value[0]))opc.read()返回的是一个元组格式是(值, 质量, 时间戳)。质量字段很重要它表示这个数据是否可靠比如Good、Bad、Uncertain。做数据采集时一定要判断质量质量不好的数据宁可丢弃也不要入库否则看板上会出现莫名其妙的跳变。批量读取用opc.read(list_of_tags)比循环单个读效率高很多。我实测读 100 个点位批量读比逐个读快 5 到 10 倍。写入用opc.write((tag, value))注意写入的值类型要和 PLC 期望的一致写浮点数寄存器时传字符串可能会失败。2.3 数据质量判断与异常处理OPC 数据质量是很多人忽略的点。质量码一般有Good、Bad、Uncertain几大类细分还有Bad Configuration Error、Bad Not Connected等等。采集程序里必须对质量做判断我的做法是只处理Good的数据其他质量码记录日志并跳过。异常处理方面OpenOPC 在连接断开时会抛OpenOPC.OPCError读写超时会抛TimeoutError。我建议把连接和读写都包在 try-except 里连接断开时自动重连读写失败时重试两到三次。重试间隔不要太短给服务器一点喘息时间一般 1 到 2 秒比较合适。import time import OpenOPC def safe_read(opc, tags, retries3): for i in range(retries): try: return opc.read(tags) except (OpenOPC.OPCError, TimeoutError) as e: print(f读取失败第{i1}次重试: {e}) time.sleep(1.5) return None还有一个坑是 OPC 服务器的缓存机制。有些服务器读的是缓存值不是实时值需要设置opc.read(tags, group...)或者用opc.properties()查看。如果发现数据更新不及时先确认服务器是缓存模式还是设备模式。3. 实操过程与核心环节实现3.1 从零搭建一个采集脚本的完整流程假设现场有一台 OPC DA 服务器ProgID 是Factory.OPCServer我们要采集 5 个模拟量、3 个开关量每 2 秒采集一次存到 CSV 文件。下面是从零开始的完整步骤。第一步建立连接。先在本机测试确认能连上服务器并列出点位。import OpenOPC import time opc OpenOPC.client() try: opc.connect(Factory.OPCServer) print(连接成功) tags opc.list() print(f共发现 {len(tags)} 个点位) for t in tags[:10]: print(t) except OpenOPC.OPCError as e: print(f连接失败: {e})第二步定义采集点位和地址映射。根据上一步列出的点位挑出需要的建立映射字典。collect_tags { temp_1: Device1.Temp1, temp_2: Device1.Temp2, press_1: Device1.Press1, flow_1: Device1.Flow1, level_1: Device1.Level1, motor_1: Device1.Motor1, valve_1: Device1.Valve1, alarm_1: Device1.Alarm1 }第三步写采集循环。每 2 秒批量读一次判断质量转换类型写入 CSV。import csv from datetime import datetime tag_list list(collect_tags.values()) tag_names list(collect_tags.keys()) with open(plc_data.csv, a, newline, encodingutf-8) as f: writer csv.writer(f) if f.tell() 0: writer.writerow([timestamp] tag_names) while True: try: results opc.read(tag_list) row [datetime.now().strftime(%Y-%m-%d %H:%M:%S)] for name, (value, quality, ts) in zip(tag_names, results): if quality Good: row.append(value) else: row.append() print(f{name} 质量异常: {quality}) writer.writerow(row) f.flush() except Exception as e: print(f采集异常: {e}) time.sleep(2) time.sleep(2)这个脚本能跑但还不够健壮。实际部署时我会加上断线重连、日志记录、配置文件读取这些。断线重连的逻辑是捕获异常后关闭连接等待几秒再重新连接连接成功后继续采集。3.2 参数计算与采集频率的确定采集频率不是拍脑袋定的得根据几个因素来算。第一是 PLC 的扫描周期如果 PLC 扫描周期是 50ms你 10ms 读一次也没意义读到的都是重复值。第二是 OPC 服务器的更新周期有些服务器默认 100ms 或 500ms 更新一次缓存。第三是数据变化的实际速率温度这种慢变量 5 秒读一次都嫌快但报警信号可能需要 100ms 级别的响应。我的经验公式是采集周期 max(PLC扫描周期, OPC更新周期) × 2 到 5 倍。比如 PLC 扫描 50msOPC 更新 100ms那采集周期设在 200ms 到 500ms 比较合理。对于看板展示2 秒一次足够了对于报警监控500ms 一次比较稳妥。数据量也要算一下。假设 100 个点位每个点位平均 10 字节2 秒采集一次一天的数据量是 100 × 10 × 30 × 60 × 24 ≈ 43MB。这个量级用 CSV 存没问题但如果点位上千、频率到 100ms一天就是几个 GB那就得上数据库了而且要考虑降采样和过期清理。3.3 写入操作的注意事项与实操记录读和写是两回事写操作风险更高因为写错了可能影响生产。我一般遵循几个原则第一写入前先读一次当前值确认要写的点位和预期一致第二写入后立即回读确认写入生效第三关键点位写入要加确认机制比如写一个“允许写入”标志位。def safe_write(opc, tag, value, verifyTrue): # 写入前读取 before opc.read(tag) print(f写入前 {tag} {before[0]}) # 执行写入 opc.write((tag, value)) time.sleep(0.5) # 回读验证 if verify: after opc.read(tag) print(f写入后 {tag} {after[0]}) if str(after[0]) ! str(value): print(f警告: 写入值 {value} 与回读值 {after[0]} 不一致) return False return True实测下来写入浮点数时最好传字符串比如opc.write((Device1.SetTemp, 25.5))传 float 有时候服务器不认。写入布尔量时传True/False或1/0都行但要看服务器文档。写入字符串时注意长度限制超长会被截断。注意写操作一定要在测试环境验证通过后再上生产。我见过有人直接把写入脚本跑到产线上结果地址映射错了一位把设定值写到了报警阈值寄存器里触发了全线停机。这种教训一次就够了。4. 常见问题与排查技巧实录4.1 连接类问题速查表连接问题是 OpenOPC 使用中最常见的我整理了一个速查表基本能覆盖 90% 的情况。问题现象可能原因排查方法解决方案connect报 OPCErrorProgID 错误用opc.servers()列出可用服务器确认正确的 ProgID跨机器连接超时DCOM 权限不足检查 dcomcnfg 配置配置 DCOM 权限和防火墙连接成功但读不到数据点位地址错误用opc.list()核对地址修正地址映射频繁断线网络不稳定或服务器负载高查看网络和服务器日志加心跳和重连机制读取超时服务器响应慢减少单次读取点位数量分批读取增大超时时间DCOM 配置这块再补充一点如果服务器和客户端不在同一个域需要在两台机器上创建相同的用户名和密码然后在 DCOM 配置里用这个用户身份运行。这个操作比较繁琐但跨域场景下基本绕不开。4.2 数据读写异常的处理经验数据读写异常里最让人头疼的是“读到的值不对”。比如明明 PLC 上显示温度是 25.6读出来却是 2560 或者 0.256。这通常是数据类型或字节序的问题。PLC 里的浮点数可能是按整数传输的需要除以 10 或 100也可能是大小端反了需要做字节交换。我的排查步骤是先用 OPC 服务器的自带客户端读同一个点位对比值是否一致。如果服务器客户端读出来是对的那就是 OpenOPC 这边的问题检查数据类型转换。如果服务器客户端读出来也是错的那就是地址映射或 PLC 程序的问题。还有一种情况是读到的值偶尔跳变比如正常 25.6 突然变成 -3276.8。这通常是通信干扰或数据不同步导致的可以在软件层面加一个滑动平均滤波或者判断值的变化率超过合理范围就丢弃。# 简单的滑动平均滤波 from collections import deque class Filter: def __init__(self, size5): self.buffer deque(maxlensize) def update(self, value): self.buffer.append(value) return sum(self.buffer) / len(self.buffer) temp_filter Filter(5) # 每次读到新值就 update 一下4.3 性能优化与长期运行稳定性采集程序通常要 7×24 小时跑稳定性比性能更重要。我踩过的坑包括内存泄漏导致程序跑几天就崩、日志文件把磁盘写满、连接池耗尽等等。内存泄漏主要来自 COM 对象没释放。OpenOPC 的opc对象在不用时要opc.close()虽然 Python 有垃圾回收但 COM 对象的释放有时候不及时。我的做法是定期重启采集进程比如每天凌晨重启一次用任务计划程序或 supervisor 管理。日志方面一定要加日志轮转单个日志文件不要超过 10MB保留最近 7 天。Python 的logging.handlers.RotatingFileHandler就能搞定。import logging from logging.handlers import RotatingFileHandler handler RotatingFileHandler(opc_collector.log, maxBytes10*1024*1024, backupCount7) handler.setFormatter(logging.Formatter(%(asctime)s - %(levelname)s - %(message)s)) logger logging.getLogger(opc) logger.addHandler(handler) logger.setLevel(logging.INFO)性能优化上批量读写是最大的杠杆。另外把采集和入库解耦采集线程只管读读到数据丢进队列入库线程从队列取数据写数据库这样采集不会被数据库的慢查询拖累。4.4 从 OPC DA 迁移到 OPC UA 的考量如果现场条件允许我其实更推荐直接上 OPC UA。OpenOPC 毕竟是基于 DCOM 的老技术跨平台差、配置复杂、安全性也一般。OPC UA 用asyncua或opcua库Python 支持好Linux 上也能跑还自带加密和认证。迁移的难点在于老设备可能只支持 DA。这时候可以用一个 OPC UA 网关把 DA 转成 UAPython 端只跟 UA 打交道。网关有硬件也有软件方案软件方案一般跑在 Windows 上配置好 DA 服务器地址和 UA 端点就行。这样 Python 代码可以用现代的 UA 库写部署也灵活不用再折腾 DCOM。我在实际项目中的体会是新项目尽量用 UA老项目如果 DA 跑得稳也没必要为了技术而技术。关键是搞清楚自己的需求如果只是本机采集几十个点位OpenOPC 够用了如果要跨平台、要远程、要安全那就值得花时间做迁移。最后分享一个小技巧不管用 DA 还是 UA都建议在采集程序里加一个“心跳点位”定期写一个递增值到 PLC 的某个空闲寄存器PLC 程序里判断这个值是否在变化如果长时间不变就报警说明采集程序挂了。这个反向监控机制在无人值守的场景下特别有用能让你第一时间知道采集端出了问题而不是等看板数据不更新了才发现。