多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于WinCC历史数据的Excel报表系统开发实践

基于WinCC历史数据的Excel报表系统开发实践 干自动化项目的人应该都有同感设备跑了一年数据存了一大堆客户某天突然说“帮我导一份上个月的产量报表按班次统计”或者“把这几台设备的温度曲线导成Excel我要分析”。你要是打开WinCC历史趋势控件截图或者翻变量记录管理器一条条复制一次两次还行次数多了人迟早崩溃。这套基于WINCC的历史数据Excel报表系统就是解决这个问题的。文章围绕实时数据展示、模板化生成、数据处理三条主线展开适合正在做WinCC项目的工程师、需要给客户交付报表功能的实施人员也适合刚接触SCADA数据对接的初学者。我会把从需求拆解到方案选型、从数据库读取到Excel生成的完整思路和代码框架都讲一遍里面涉及的坑和建议都是实际项目中踩过的照着做能少走不少弯路。1. 需求拆解与方案选型先把“客户要什么”翻译成技术语言做这类系统的第一步不是写代码而是把客户的原始需求翻译成明确的技术指标。我见过太多人拿到需求就猛写结果做了两周发现方向错了。所以开篇先讲需求拆解和方案选型这两件事决定了后面80%的开发量。1.1 项目背景与真实痛点这套系统最初是给一条产线做的。现场用WinCC做监控几十个模拟量和开关量都在做历史归档数据存在项目自带的SQL Server数据库里。客户每天都要看日报表各段温度平均值、设备运行时长、产量累计值偶尔还要拉出趋势曲线自己做分析。表面需求是“给我一个报表”实际拆开有三层。第一层是“查得出来”历史数据确实存了但WinCC自带的查询界面并不适合普通车间人员操作更不可能让每个班组长都去学变量记录管理器。第二层是“出得快”手工操作要打开WinCC、定位时间、选变量、导出一套下来至少十分钟碰上数据量大的时候还会卡死。第三层是“看得懂”客户要的不是原始归档记录而是“每小时的均值”“这个班的产量总和”“这台设备今天运行了多久”这种加工过的结果。这三层需求对应到技术上就是数据读取、报表生成、数据处理三个模块。做方案之前先把这个理清楚后面每一步都有明确目标。1.2 三条常见技术路线对比我当时评估了三条路线每条都有各自的使用场景。方案一是纯WinCC内部脚本用VBS或者C脚本在运行画面里操作Excel对象。好处是不需要额外的开发环境直接内嵌部署的时候跟着项目走。缺点是脚本运行在WinCC进程里稍微复杂的操作就会导致画面卡顿而且VBS对Excel格式的控制力很弱做复杂的模板填充和公式追加非常痛苦。这个方案适合偶尔导一次数据的小项目不适合做正式的报表系统。方案二是独立程序直连归档数据库用C#、VB.NET或者Python写一个单独的程序定期或者手动从SQL Server里取出归档数据再生成Excel文件。这条路的优点是程序不依赖WinCC运行状态不会影响画面刷新逻辑可以做得比较复杂而且后续要扩展定时任务、邮件发送都很方便。缺点是需要了解WinCC归档库的表结构还需要装Visual Studio或者Python环境。方案三是用现成的报表工具比如FineReport或者ReportViewer直接用SQL连归档库做报表。开发速度最快格式也漂亮但商业授权费用不低而且如果客户现场没有对应的运行环境部署本身就是一个包袱。三条路线各有优劣适合的场景不太一样我整理了一张对比表供参考。方案开发成本部署成本灵活度稳定性适合场景WinCC内脚本低低低中临时导出、小数据量独立程序直连数据库中中高高正式报表系统、复杂统计第三方报表工具低中高中高项目预算充足、多报表场景1.3 为什么最终选择“独立程序直连归档库”这个项目最终选了第二种方案用C#写独立程序。核心原因有三个。第一是数据量问题。产线上一天能产生几十万条归档记录用WinCC内部脚本一次性读出来处理画面基本就冻住了。独立程序跑在后台服务器上不影响现场操作即使报表程序崩了也不会干扰生产监控。第二是可维护性。客户的需求不会一成不变今天要日报明天可能就要周报、月报甚至要加曲线图。独立程序可以做成参数化的配置结构模板文件也是独立的Excel改表头、改统计口径都不需要动代码。第三是复用性。同一套代码逻辑稍微改一下连接字符串和SQL查询就能用在下一个项目里。我后来在别的项目里反复用这框架也就是改改配置和模板的事情。采用这条路之后架构思路就清晰了下一步要解决的是数据怎么流起来的问题。2. 整体架构与数据流设计从传感器到Excel全链路拆解系统架构看上去不复杂但边界划分很重要。我习惯把整个系统分成三层每层有明确的职责边界这样排查问题的时候一眼就能看出来问题出在哪一层。2.1 三层架构采集层、逻辑层、展示层采集层是WinCC自己完成的。现场传感器通过PLC把信号传到WinCC变量WinCC的变量记录功能按设定的周期把这些变量写入归档数据库。这一层不需要我们操心但要注意归档配置本身要合理采样周期太密数据量大太疏又丢细节一般模拟量归档我们按秒级或者分钟级采集开关量可以按变化存储。逻辑层是报表程序的核心区负责从归档库抓数据、做清洗、做统计、填充模板。这一层处理的是“原始数据到业务结果”的转换比如说把几十万条温度归档记录变成一张24行的“每小时平均温度表”。展示层则是两层输出。实时展示走WinCC画面里的趋势控件和表格控件解决的是“现在设备什么状态”的问题。历史报表走Excel文件解决的是“过去一段时间发生了什么”的问题。这两者虽然都和数据有关但属于完全不同的技术路线下面重点展开实时展示的设计。2.2 实时数据展示画面侧怎么与报表生成配合实时数据展示这块很多做报表的人会忽略其实它在整个系统里的位置很微妙。报表生成是离线的实时展示是在线的两者属于同源数据的不同出口。实时展示我用了WinCC的在线趋势控件和在线表格控件。趋势控件绑定归档变量之后会自动显示最近一段时间的历史曲线不用写任何脚本。表格控件可以绑定变量做实时刷新用来做一个低配版的数字看板。这里要提醒一下WinCC的在线趋势控件在数据量很大的时候刷新会变慢所以画面上的变量不要绑太多按关键参数控制在一屏能看明白的量级。报表程序不参与实时展示它只在后台默默工作按设定时间读取归档数据、生成Excel。有朋友会问既然趋势控件能看历史曲线为什么还要Excel报表。原因很简单趋势控件适合“看”不适合“分析”。客户要在Excel里做对比、算偏差、贴到PPT里汇报这些事WinCC原生做不了得靠我们生成的Excel文件。2.3 模板化生成的核心思想样式与数据分离模板化生成是整个系统里最值得花时间设计的一环。所谓模板化就是预先在Excel里把报表的框架画好程序只负责往里填数据不负责画样式。这样做的好处非常明显。客户改报表格式的频率比你想象的高今天说表头加一行明天说单位换成中文后天说列宽再调一下。如果样式都写在代码里每次改动都要重新编译发布非常痛苦。模板独立出来之后改样式只需要改Excel文件程序代码一行都不用动。实现方式很简单。我在模板文件里用一个专门的Sheet放报表主体表头、标题、单位、边框都画好要填充数据的单元格范围用Excel的“命名区域”功能预先定义好。比如定义一个名为“DataStart”的区域指向第6行第2列程序打开模板后直接找到这个命名区域从对应的行开始循环写数据非常稳定。这里有个重要的原则模板里永远不要用合并单元格作为数据写入起点合并单元格会导致数据写入和样式复制出现很多奇怪的问题后面会专门讲到。3. 关键代码与实操步骤从零搭建可复用的报表生成程序架构清楚了代码就好写了。这一部分我把整个流程拆成四个步骤连接归档库、读取历史数据、填充Excel模板、定时调度。每步都给出可参考的代码框架和操作要点。3.1 连接WinCC归档数据库连接字符串与版本差异WinCC的历史归档数据实际上存放在它自带的SQL Server实例里连接方式跟普通的SQL Server数据库没什么区别。不同版本的WinCC归档表结构会有差异但连接字符串的框架是一致的。string connString Data Sourcelocalhost\\WINCC;Initial CatalogCC_Project_2020;User IDsa;Password******;;先说数据库实例名。WinCC安装的时候会创建一个命名实例类似“WINCC”这种具体看安装时的配置。项目库名通常是项目名加年份后缀例子里我用了CC_Project_2020实际项目中以服务器上能看到的具体库名为准。再说登录方式。SQL Server的登录名和密码在WinCC项目服务器上属于比较敏感的信息建议在配置文件里做加密存储别硬编码在源码里。我见过直接把sa密码写在代码里然后在现场父目录留一堆破解工具的同行这习惯不好安全问题值得重视。连接数据库之前先在服务器上用SQL Server Management Studio确认两点。一是确认目标数据库存在二是确认里面有以“RLG”开头的表这些表就是归档数据表。不同版本的WinCC表结构差异较大但最终的查询思路是一样的都是通过归档表关联变量表和值表取出对应时间范围内的数据。3.2 读取历史数据到DataTableSQL查询与时间过滤读取历史数据是整个系统的关键环节。WinCC归档表里的时间字段有特殊的编码方式在SQL里直接查会得到一串看不懂的数字需要通过转换函数处理成可读的时间格式。不同版本的处理方式不同有些版本提供了系统视图或者用户函数有些版本需要自己写二进制FILETIME转换。实际操作中我一般先用SQL Server的查询分析器验证一下查询结果确保时间和数值都对得上再把SQL粘贴到程序里。一个典型的查询思路是这样的SELECT ValueTime, RealValue FROM ArchiveTable WHERE TagName 温度_1 AND ValueTime BETWEEN StartTime AND EndTime ORDER BY ValueTime在C#程序里用SqlConnection和SqlDataAdapter把结果装进DataTable后续的所有数据清洗和统计都是在DataTable上进行。using (SqlConnection conn new SqlConnection(connString)) { string sql SELECT ...; SqlDataAdapter da new SqlDataAdapter(sql, conn); da.SelectCommand.Parameters.AddWithValue(StartTime, startTime); da.SelectCommand.Parameters.AddWithValue(EndTime, endTime); DataTable dt new DataTable(); da.Fill(dt); }这一步有个非常重要的细节就是时间参数。WinCC服务器的时间格式和本地开发机的时区如果不一致查询出来的数据会出现偏差。项目上线前一定要和现场确认服务器的时区设置最好在程序里统一用DateTime.Now从服务器带出来做基准时间而不是用开发机的本地时间。3.3 模板填充与Excel格式控制COM操作与资源释放数据读出来了接下来就是把它填进Excel模板。这里有两种操作方式一种是用Microsoft.Office.Interop.Excel另一种是用第三方库EPPlus或者NPOI。Interop的优点是模板还原度高、格式控制精细缺点是必须装Office而且COM对象释放很麻烦。EPPlus的优点是轻量、不用装Office、没有进程残留问题缺点是正版授权有要求对老旧模板的一些特殊格式支持有限。我实际项目里前期用的是Interop后期迁移到了EPPlus。如果读者自己选型我建议新项目直接上EPPlus或者NPOI能省掉一大半COM相关的头疼问题。下面以Interop为例讲一下模板填充的基本思路。Excel.Application app new Excel.Application(); app.Visible false; app.DisplayAlerts false; Workbook wb app.Workbooks.Open(templatePath); Worksheet ws wb.Worksheets[日报表]; for (int i 0; i dt.Rows.Count; i) { ws.Cells[startRow i, 1].Value dt.Rows[i][ValueTime].ToString(); ws.Cells[startRow i, 2].Value dt.Rows[i][RealValue]; } wb.SaveAs(outputPath, XlFileFormat.xlOpenXMLWorkbook); wb.Close(); app.Quit();这种写法看起来简单但实际用的时候有几个坑要处理。第一Excel程序实例退出后进程很可能还驻留在任务管理器里。这是因为COM对象引用没完全释放代码里用到的每个Range、Worksheet、Workbook都要显式调用Marshal.ReleaseComObject。第二另存为的时候要指定文件格式参数默认保存格式可能是旧版的.xls要在模板设计阶段就规划好输出格式。第三模板文件最好复制一份再操作防止程序异常时把模板本身写坏。3.4 定时调度与自动化执行Windows计划任务还是服务报表程序不能全靠人工手动点一定要做成自动触发的。两种常见方式一种是Windows计划任务定时调用exe另一种是写成Windows服务常驻。我用Windows计划任务的场景更多原因很简单项目交付之后客户有时候要改生成时间计划任务的配置界面比改服务配置更直观客户自己就能操作。计划任务调用exe的方式很适合这种工具型的程序。把生成逻辑封装在exe里通过命令行参数传入时间范围和报表类型计划任务按班次或者按小时启动。比如早班结束之后6点生成一次日报晚上12点生成一次全天统计。这样既简单又可靠程序崩溃了计划任务也不会被拖挂下次运行照常触发。如果需求复杂比如多个报表、多个工厂目录可以把连接字符串、模板路径、输出目录、定时规则全部放到一个XML配置文件里程序启动时读取配置再干活这样同一个exe在不同的现场只需要改配置不用重新编译。4. 数据处理细节别让报表里全是“坑”这部分其实是报表系统的灵魂。很多人程序写得出来但生成的Excel数据没法看因为原始归档数据里有太多“脏数据”需要加工。我把实际处理中常用的几种数据加工方式梳理一下。4.1 时间戳对齐与采样周期选择WinCC的归档机制有两种模式一种是定时归档比如每秒钟记一次另一种是变化归档只有当变量的值发生变化时才写入一条记录。定时归档的数据时间戳是等间距的比较好处理。变化归档的数据就不等间距了尤其对于开关量一个阀门可能一整天状态都没变数据表里只有一条或几条记录。做报表统计数据的时候不能直接把变化归档的数据当定时数据拿来平均。比如算一天的平均温度如果归档里只有10点、14点、18点三个点直接平均出来的结果没有意义因为每个值代表的时间长度不一样。处理办法是做时间加权平均。简化处理的话可以按采样周期把数据重采样。思路是根据自己的业务把一天切分成固定间隔的窗口比如5分钟一个点每个窗口内取出第一条或者最后一条归档值作为这个窗口的代表值然后再做均值统计。这样算出来的结果至少不会因为采样不均匀而严重失真。WinCC里的数据在SQL里读出来的时候时间是按UTC或者服务器本地时间存的这个也要在处理之前确认清楚不然后面所有的时间窗口分组都会错位查出来莫名奇妙的空白时间。4.2 异常值清洗与数据补数传感器和通讯系统都不是完美无缺的断线、干扰、PLC停机都会让归档值出现异常。常见的有三种异常。第一种是超量程值。比如温度变送器断线会输出一个很大的值或者很小的值超出传感器的量程范围。这种值必须剔除否则一个断路尖峰就能把整天的平均值拉高好几度。处理办法是维护一张量程配置表每个变量设置合理上下限查询数据后先过滤一遍。第二种是固定错误值。很多系统用-9999或者0表示无效数据这种比第一种还要隐蔽因为它在量程范围内直接按正常数据处理的话报表看起来数字挺正常实际上可能全场存了一整天的无效值。对于这种场景建议查一下同一个时间点其他相关变量的状态做联合判断。第三种是跳变毛刺。比如压力值瞬间从1跳到50又跳回1这种往往需要结合上下两个时刻的值做差值判断差值超过物理可能的变化范围就标记为异常。补数策略上我会优先选择“向前填充”也就是用前一个有效值填补空档。这个思路和Excel里“如果为空则返回上一行的值”是完全一致的逻辑在数据清洗阶段先处理一遍后面的统计就轻松很多。你如果是在Excel里手工做这件事可以用IF公式配合OFFSET实现但在报表程序里提前处理更好。4.3 聚合统计的SQL与代码写法日报表、时段统计这类需求本质上就是把DataTable里的数据按时间段分组再做聚合计算。聚合包括了AVG、SUM、MIN、MAX偶尔会有COUNT和STDEV计算标准差。如果需求简单可以直接在SQL里做GROUP BY。比如按天统计每天的平均温度、最高温度、最低温度SELECT CONVERT(date, ValueTime) AS Day, AVG(RealValue) AS AvgTemp, MAX(RealValue) AS MaxTemp, MIN(RealValue) AS MinTemp FROM ArchiveTable WHERE TagName 温度_1 AND ValueTime BETWEEN StartTime AND EndTime GROUP BY CONVERT(date, ValueTime)如果是按班次统计就要把一天分成几个时间段可以用CASE WHEN加DATEPART(hour, ValueTime)来做分段。这里有一个小坑就是SQL的group by在数据量很大的时候性能会很差尤其查询跨月或者跨年的数据。解决方法是把原始DataTable数据先按天拉到内存然后在C#代码里用循环分组统计不要把所有逻辑都压在SQL里。设备运行时间的统计也是一个常见需求。对于Bool型的开关量统计运行时间就是统计值为1的时长。因为归档是变化存储的两条记录之间的时间差乘以状态值就能算出这一段的运行时长把所有段加起来就是总运行时间。double totalMinutes 0; for (int i 1; i dt.Rows.Count; i) { bool status Convert.ToInt32(dt.Rows[i][RealValue]) 1; if (status) { DateTime prevTime Convert.ToDateTime(dt.Rows[i - 1][ValueTime]); DateTime currTime Convert.ToDateTime(dt.Rows[i][ValueTime]); totalMinutes (currTime - prevTime).TotalMinutes; } }这个代码逻辑很直观但要注意如果查询区间开始之前设备已经是运行状态那么这一段运行时间会被漏掉所以统计的时候要考虑区间边界的前后值。5. 实战中的典型问题与排查实录做报表系统最耗时间的往往不是功能开发而是排障。这里把我几次实际项目中遇到的问题整理出来这些都是常规文档里不会写的内容。5.1 Excel进程不释放任务管理器里堆满EXCEL.EXE用Interop做Excel操作最经典的坑就是Excel进程不释放。写的时候明明是调用了app.Quit()但任务管理器里还是挂着好几个EXCEL.EXE时间长了内存越占越多服务器越来越卡。核心原因是COM对象的引用计数没有归零。在C#里除了Application对象之外Workbook、Worksheet、Range甚至Cells属性取出来的对象都被认为是COM引用都必须显式释放。解决办法有两种。一是在代码里逐个Marshal.ReleaseComObject顺序从子对象到父对象最后再Quit和ReleaseComObject(Application)。另一种更彻底的办法是抛弃Interop改用EPPlus这类不需要启动Excel进程的库。写入的逻辑完全一样但因为根本没有Excel进程也就谈不上进程残留。我现在的建议是只要模板格式不是特别复杂全部走EPPlus。Interop的那套代码维护成本太高任何一个小异常都会导致进程卡在内存里遇到即崩溃。5.2 查询归档数据查不到或数据缺失这类问题排查起来比较费劲因为数据确实存了数据库里也有记录但程序查出来的结果跟WinCC画面显示对不上。最常见的原因在于查询方式和WinCC自己取数的逻辑不一致。WinCC本身显示的曲线是通过它的接口按特定规则读的很多时候它会把数据做时间边界处理比如查询的起始时间落在两条记录之间时它会自动把前一条记录带出来。而SQL里如果只查ValueTime StartTime AND ValueTime EndTime很容易漏掉边界的数据。另外还有归档延迟的问题。WinCC写归档不是实时的通常有几秒钟的延迟如果查询的结束时间刚好落在当前时刻附近最后几条数据可能还没写入数据库。所以刚采完数据立刻生成报表结尾总是缺一小段。解决办法是查询结束时间往前推几秒留出归档缓冲时间。这个现象在实时场景下经常会误导初学者以为是程序逻辑写错了实际上是归档机制决定的。5.3 模板样式错乱与公式计算失效模板填充之后最怕看到的结果是数据填进去了但原来的边框、列宽、计算公式全乱了或者SUM公式算出来是0。第一个问题是单元格写入时格式冲突。Excel模板里的数据区有些单元格设了保护程序写入时会直接报错。处理办法是模板设计阶段保留一个“数据填充区”直接不设保护其余区域照常保护。第二个问题是公式区域被覆盖。很多模板会预先在底部放合计公式比如对第10行到第30行做SUM但程序如果正好在模板的公式区域写入了数据把公式覆盖掉了那合计就失效了。设计模板的时候要把数据区域和公式区域彻底分开数据区在最上面公式区固定放在下面这样就算行数变化公式引用范围也要设计成动态的比如用OFFSET函数匹配数据区行数。第三个问题是格式设置为文本导致数值无法计算。Excel单元格如果是“文本”格式输入数字之后虽然看起来是数字但计算时不参与。解决方法是写入之前显式设置NumberFormat为0.00或者General。5.4 32位与64位Office的兼容性差异Interop方式还有一个头疼的问题就是Office位数。客户现场的Office可能是32位也可能是64位同一个程序在这两种环境下运行行为可能完全不一样。比如32位Office下操作正常64位Office下打开模板就报错或者另存为的时候格式参数不识别。这个问题的根治办法就是放弃Interop改用EPPlus或NPOI。这两个库是纯托管代码不依赖Office安装32位和64位环境跑起来都一样。输出文件直接是真正的xlsx文件不经过Excel进程中转稳定性高很多速度也快不少。从我个人的项目经验来看凡是用Interop生成的报表系统交付之后总会被客户打电话问各种奇怪问题换了EPPlus之后清净很多强烈建议新项目直接走这条路。代码层面还更简洁不需要处理COM引用和进程释放那一堆麻烦事。6. 系统的扩展方向从日报表到数据服务这套系统跑顺之后很多时候客户会继续提需求核心还是“数据能不能更方便地被人使用”。我把几个常见的扩展方向整理出来大家可以按自己项目的需求选择。6.1 自动发送与共享报表生成后直达邮箱或共享目录生成Excel只是第一步让它自动达到该去的地方才算是完整的交付。简单的方式是在程序里加一个文件夹复制功能报表生成之后保存到公司共享盘客户打开共享盘就能看到。进阶一点的是邮件发送用SMTP把Excel文件作为附件发给指定收件人清单。发送邮件用.NET的SmtpClient就可以实现注意邮件标题带上日期和报表名称正文可以简单写一下报表的时间范围和内容摘要。如果客户要求更高还可以加一个压缩包循环清理的逻辑日志、周报、月报分别归档设置保留天数超期自动删除避免服务器上的报表文件堆积。6.2 多站点的数据汇聚与统一报表如果客户有多个车间每个车间一套WinCC各自的数据库不在同一台服务器上这时候报表系统要做的是多数据源汇聚。常见做法是每个站点一个数据库连接配置程序按站点逐个读取统计最后汇总生成一张总表。配置管理在这一步就显得很重要把每一台服务器的地址、数据库名、用户名密码、站点名称放到一张配置表或者XML文件里程序循环遍历这样新增一个站点只需要加一行配置不需要改代码。多站点的数据汇聚还要注意时区统一不同服务器如果时区不一样统计数据会出现时间错位这个在实施的时候要特别验证。6.3 从Excel报表升级为Web看板报表系统稳定之后有些客户不满足于每天等Excel他们想要随时点开的实时看板。这个扩展方向可以直接复用报表系统的数据读取和统计逻辑把Web界面换成HTML页面数据用JSON接口的方式输出前端用现成的图表库做渲染。我自己试过把Excel的统计结果导成JSON再通过一个简单的Web服务提供给车间大屏和手机端查看。效果还是不错的相当于把原来报表系统的数据分析能力对外输出成为了一个轻量的数据服务。如果你正打算做这套系统扩展方向一开始不用想太复杂先把核心的“读数据、生成Excel、定时执行”跑通后续按客户的实际要再逐步扩展这样开发成本低交付周期也短。做这个项目我最大的体会是工业报表系统的难点从不在Excel技术本身而在数据处理的严谨程度和对业务需求的理解。客户要的从来不是一个能打开的文件而是一份能直接用来做判断的准确数据。程序里的每一个过滤条件、补数策略、统计口径背后都是现场实际工况的映射这些判断做得越细致交付的报表就越有价值。
返回列表