跳到主要内容

历史数据批量导出

工程师常见需求: 把过去 N 天关键 Tag 的历史拉出来导成 Excel. 关键是按时段分批拉, 一次拉 1 个月可能服务端超时. 用 C# + ClosedXML 示例.

配套示例

依赖

dotnet add package DarraOpcUa
dotnet add package ClosedXML

ClosedXML 是开源 .NET Excel 库, 不依赖 Office.

完整代码

using ClosedXML.Excel;
using DarraOpcUa_Client;
using System;
using System.Collections.Generic;

class Program
{
static void Main()
{
const string endpoint = "opc.tcp://localhost:4840";
var nodes = new[]
{
"ns=2;s=Boiler1.Temperature",
"ns=2;s=Boiler1.Pressure",
};
var startUtc = DateTime.UtcNow.AddDays(-7);
var endUtc = DateTime.UtcNow;
var output = "history.xlsx";

using var ua = new DarraOpcUa(endpoint);
ua.Connect();
Console.WriteLine($"[OK] 已连接 {endpoint}");

using var workbook = new XLWorkbook();

foreach (var nodeId in nodes)
{
Console.WriteLine($"\n拉历史 {nodeId} [{startUtc:O} ~ {endUtc:O}]");
var sheet = workbook.Worksheets.Add(SafeSheetName(nodeId));
sheet.Cell(1, 1).Value = "SourceTimestamp(UTC)";
sheet.Cell(1, 2).Value = "Value";
sheet.Cell(1, 3).Value = "Status";
int row = 2;

// 按 6 小时一段拉, 避免单次返回过大
foreach (var dv in PullByChunks(ua, nodeId, startUtc, endUtc, TimeSpan.FromHours(6)))
{
sheet.Cell(row, 1).Value = dv.SourceTimestamp ?? DateTime.MinValue;
sheet.Cell(row, 2).Value = dv.Value?.ToString();
sheet.Cell(row, 3).Value = dv.Status.ToString();
row++;
dv.Dispose();
}

sheet.Range("A1:C1").Style.Font.Bold = true;
sheet.Column(1).Width = 24;
Console.WriteLine($" 共 {row - 2} 行");
}

workbook.SaveAs(output);
Console.WriteLine($"\n已保存 {output}");
}

static IEnumerable<DataValue> PullByChunks(
DarraOpcUa ua, string nodeId, DateTime startUtc, DateTime endUtc, TimeSpan chunk)
{
var t = startUtc;
while (t < endUtc)
{
var t2 = (t + chunk) > endUtc ? endUtc : t + chunk;
IReadOnlyList<DataValue> dvs;
try { dvs = ua.ReadHistory(nodeId, t, t2, maxValues: 0); }
catch (OpcUaException ex)
{
Console.WriteLine($" [{t:HH:mm} ~ {t2:HH:mm}] 失败: {ex.StatusCode}");
t = t2;
continue;
}
foreach (var dv in dvs) yield return dv;
t = t2;
}
}

static string SafeSheetName(string nodeId)
{
var s = nodeId.Replace(":", "_").Replace(";", "_").Replace("/", "_");
return s.Length > 31 ? s.Substring(0, 31) : s; // Excel sheet name 限 31 字符
}
}

分段说明

第 1 步: 时段分批拉

foreach (var dv in PullByChunks(ua, nodeId, startUtc, endUtc, TimeSpan.FromHours(6)))

关键: 一次拉太大区间服务端可能超时或拒绝. 切成小段 (6 小时一批) 串行拉, 失败的段不影响其他段. SDK 内部已经处理 ContinuationPoint 续拉, 不需要手动循环.

第 2 步: DataValue 必须 Dispose

foreach (var dv in dvs) yield return dv;  // 由调用方负责 Dispose
// 调用方:
dv.Dispose();

每个 DataValue 持有 native 内存. 历史数据动辄上万条, 不释放秒爆内存.

第 3 步: 数据量太大时改用聚合

如果一周原始数据上百万点, Excel 打不开. 改用 ReadHistoryProcessed 拉每小时平均:

var hourly = ua.History.ReadProcessed(nodeId,
startUtc, endUtc,
aggregateNodeId: "i=2342" /* Average */,
processingIntervalMs: 3_600_000);
// 7 天 × 24 小时 = 168 行

注意事项

  • 时间戳一律 UTC, 不要传本地时间. 跨时区报表会算错.
  • BadHistoryOperationUnsupported = 服务端没装 Historian, 这个节点没有历史. 跳过即可.
  • Excel 单 Sheet 上限 ~104 万行, 超过要分多 Sheet 或切 CSV.

相关链接