优化了xml修改和表格逆生成功能

This commit is contained in:
lnk
2026-07-14 15:14:54 +08:00
parent 70c1534d96
commit 6bfaa32db4
74 changed files with 15935 additions and 1804129 deletions

View File

@@ -22,26 +22,57 @@
#include "pq_app.h"
#include "tinyxml2.h" // tinyxml2
/*
* main.cpp
*
* 本文件是“映射解析 + 报表生成”的核心,也保留了命令行入口。
*
* 主流程:
* 1. loadValueMetas() 读取 ICD/XML 模板,把每个 <Value> 的 name、desc、DO、DA、
* Topic、DataType、Item、Sequence 等上下文整理成 ValueMeta。
* 2. readJsonDataFile() 读取 DATATopic 的 jsondataflattenJson() 压平成
* Value_... -> value 的键值对mergeSequences() 把连续编号合并成数组。
* 3. buildItemsForSheet() 按 SheetSpec 生成每张工作表的 OutItem 行。
* 4. addOriginFileToContext() 读取 TraceTopic 的 origin/MMS 数据matchOrigin()
* 用 DO + DA + 相位 + 序列范围匹配最终值和原始值。
* 5. writeExcelWorkbook() 输出 Excel 2003 XML 格式的 .xls供 Excel/GUI 预览打开。
*
* 标志/转换要点:
* - Sequence/@value == 7 代表星形接线 star。
* - Sequence/@value == 112 代表角形接线 delta。
* - DATA_TYPE "1" 会规范成 "01",用于匹配统计/闪变/实时 Sheet。
* - 指标名前缀 G_/MAX_/MIN_ 分别转换成 95值/最大值/最小值;无前缀默认平均值。
* - DA 中 phs* 是相位占位符,星形转换为 phsA/phsB/phsC角形转换为
* phsAB/phsBC/phsCA。
* - Value 名称中的 %start,end% 是 XML 序列模板;运行时显示成 [start-end]。
*/
using json = nlohmann::json;
namespace fs = std::filesystem;
/*
* XML <Value> 节点的完整业务元信息。
*
* 输入loadValueMetas() 在递归遍历 XML 时填充。
* 输出:后续用于把 jsondata 的 key 映射成 DO/DA、中文描述、接线方式、Topic/DataType。
*/
struct ValueMeta {
std::string name;
std::string desc;
std::string doPath;
std::string daPath;
std::string wiring = "common"; // star/delta/common
std::string topic;
std::string dataType;
std::string itemName;
std::string seqValue;
bool hasOffset = false;
int offset = 0;
std::string name; // Value/@name可能是普通指标 V1也可能是序列模板 MAX_V_%2,50%。
std::string desc; // Value/@desc最终表格“名称”列主要来自这里。
std::string doPath; // Value/@DOIEC 61850 MMS 对象路径前半段。
std::string daPath; // Value/@DAIEC 61850 MMS 数据属性路径后半段。
std::string wiring = "common"; // star/delta/common:由 Sequence/@value 推断。
std::string topic; // 父级 Topic/@name区分 HISDATA/RTDATA。
std::string dataType; // 父级 DataType/@value区分 01/02/03。
std::string itemName; // 父级 Item/@name如 V/I/PQ/F_S/F_L。
std::string seqValue; // 父级 Sequence/@value后续判断 T 相或接线方式会用到。
bool hasOffset = false; // Value/@Offset 是否存在。
int offset = 0; // 最终值偏移量,用于把最终值数组范围挪到 origin 对齐范围。
bool isSequence = false;
int seqStart = 0; // 例如 %2,50% 里的 2
int seqEnd = 0; // 例如 %2,50% 里的 50
std::string seqPrefixName; // 例如 MAX_V
bool isSequence = false; // name 是否匹配 %start,end% 序列模板。
int seqStart = 0; // 例如 %2,50% 里的 2
int seqEnd = 0; // 例如 %2,50% 里的 50
std::string seqPrefixName; // 例如 MAX_V_%2,50% 的前缀 MAX_V_。
};
static std::string readAll(const fs::path& p) {
@@ -58,6 +89,7 @@ static std::string ltrimBOM(std::string s) {
return s;
}
// DO/DA 中有时会出现 A||A||B 这种重复候选;这里保持顺序并去重,减少表格噪声。
static std::string collapseRepeatedAlternatives(const std::string& s) {
std::vector<std::string> parts;
std::unordered_set<std::string> seen;
@@ -100,6 +132,14 @@ static std::string toStr(const json& v) {
return v.dump();
}
/*
* 把嵌套 JSON 压平成下划线连接的 key。
*
* 输入示例:{"Value":{"V":{"A":{"MAX_V1":12}}}}
* 输出示例Value_V_A_MAX_V1 -> 12
*
* 数组会直接转成逗号分隔值,这样后续单元格可以拆成多列展示。
*/
static void flattenJson(const json& j, const std::string& prefix,
std::vector<std::pair<std::string, std::string>>& out) {
if (j.is_object()) {
@@ -121,7 +161,20 @@ static void flattenJson(const json& j, const std::string& prefix,
out.push_back({prefix, toStr(j)});
}
// 读取 XMLValue/@name -> 完整元信息desc/DO/DA
/*
* 读取 XMLValue/@name -> 完整元信息列表。
*
* 输入xmlPath通常是下载的 ICD/XML 映射文件。
* 输出unordered_map<name, vector<ValueMeta>>。同一个 name 可能在不同 Topic、
* DataType 或接线方式下出现,所以值是 vector。
*
* 递归 walk 的上下文参数含义:
* wiring 当前 Sequence 推断出的接线方式,默认 common
* topic 最近一个父级 Topic/@name
* dataType 最近一个父级 DataType/@value
* itemName 最近一个父级 Item/@name
* seqValue 最近一个父级 Sequence/@value。
*/
static std::unordered_map<std::string, std::vector<ValueMeta>> loadValueMetas(const fs::path& xmlPath) {
std::unordered_map<std::string, std::vector<ValueMeta>> mp;
tinyxml2::XMLDocument doc;
@@ -148,6 +201,7 @@ static std::unordered_map<std::string, std::vector<ValueMeta>> loadValueMetas(co
std::string nextDataType = dataType;
std::string nextItemName = itemName;
std::string nextSeqValue = seqValue;
// Topic/DataType/Item/Sequence 不是最终数据项,而是给子孙 Value 提供上下文。
if (nodeName == "Topic") {
const char* name = e->Attribute("name");
nextTopic = name ? name : "";
@@ -161,6 +215,7 @@ static std::unordered_map<std::string, std::vector<ValueMeta>> loadValueMetas(co
if (nodeName == "Sequence") {
const char* seqValue = e->Attribute("value");
nextSeqValue = seqValue ? seqValue : "";
// 项目约定7=星形接线112=角形接线;其他 Sequence 保持 common。
if (seqValue && std::string(seqValue) == "7") nextWiring = "star";
else if (seqValue && std::string(seqValue) == "112") nextWiring = "delta";
}
@@ -189,6 +244,7 @@ static std::unordered_map<std::string, std::vector<ValueMeta>> loadValueMetas(co
}
std::smatch m;
// name 中的 %start,end% 表示一个序列模板,后面要和 jsondata 的 [start-end] 对齐。
if (std::regex_match(vm.name, m, seqRe)) {
vm.isSequence = true;
vm.seqPrefixName = m[1].str(); // 例如 MAX_V
@@ -270,7 +326,19 @@ static bool parseSequenceKey(const std::string& key, std::string& groupId, int&
return false;
}
// ✅ 合并序列:但如果 metricName 在 xmlNames 里存在(比如 V1/MAX_V1/G_V1则禁止合并
/*
* 合并 jsondata 中的序列项。
*
* 输入flattenJson() 得到的 key/value 列表,例如:
* Value_V_A_MAX_V_2=...
* Value_V_A_MAX_V_3=...
*
* 输出:合并成 Value_V_A_MAX_V[2-3]=v2,v3。
*
* 保护规则:
* 如果相位后的 metricName 已经是 XML 中定义的真实指标名,例如 V1/MAX_V1/G_V1
* 就不能把它误当成“序列编号 1”合并否则会把普通指标压成数组。
*/
static std::vector<std::pair<std::string, std::string>>
mergeSequences(const std::vector<std::pair<std::string, std::string>>& flat,
const std::unordered_set<std::string>& xmlNames) {
@@ -341,6 +409,15 @@ struct ParsedKey {
std::string baseName; // 去掉 G_/MAX_/MIN_ 后的 name
};
/*
* 从 jsondata key 中提取排序和展示维度。
*
* 输入Value_V_A_MAX_V1、Value_I_T_G_THD 等。
* 输出:
* phase A/B/C/T
* stat 由指标名前缀转换G_=95值MAX_=最大值MIN_=最小值,默认平均值;
* baseName 去掉统计前缀后的基础指标名,用于匹配 XML Value/@name。
*/
static ParsedKey parseForSort(const std::string& key) {
std::string base = key;
auto bpos = base.find('[');
@@ -495,6 +572,7 @@ static bool metaFilterAllowed(const ValueMeta& vm, const MetaFilter& filter) {
return true;
}
// 多个 ValueMeta 都可能同名时用相位、统计类型、Offset、Tot 等特征打分选最像的那一个。
static int scoreMetaForKey(const ValueMeta& vm, const ParsedKey& pk) {
int score = 0;
if (daMatchesPhase(vm.daPath, pk.phase)) score += 20;
@@ -581,6 +659,7 @@ static const ValueMeta* findMetaForKey(
std::string preferred = preferredMetaName(rawMetric, pk);
std::string secondary = secondaryMetaName(rawMetric, pk);
// 第一优先级key 已经是 [start-end] 数组时,匹配 XML 的 %start,end% 序列模板。
int keyStart = 0, keyEnd = 0;
if (parseKeyRange(key, keyStart, keyEnd)) {
std::string seqPreferred = rawMetric.empty() ? preferred : rawMetric;
@@ -590,6 +669,7 @@ static const ValueMeta* findMetaForKey(
if (!seqCandidates.empty()) return bestMeta(seqCandidates, pk);
}
// 第二优先级按指标名精确匹配。G_ 前缀会优先转成 baseName因为 95 值常复用同一 XML 描述。
std::vector<const ValueMeta*> exactCandidates;
collectByName(metas, preferred, wiring, filter, exactCandidates);
if (exactCandidates.empty()) collectByName(metas, secondary, wiring, filter, exactCandidates);
@@ -693,6 +773,13 @@ struct OutItem {
int offset = 0;
};
/*
* 一张输出工作表的筛选条件。
*
* topic/dataType/wiring 决定从 XML ValueMeta 中取哪些项;
* showStatBlocks 决定统计类 Sheet 是否按 95值/平均值/最大值/最小值分段显示;
* frontType 用于 GUI 选择统计(cfg_stat_data)或实时(cfg_3s_data)时裁剪 Sheet。
*/
struct SheetSpec {
std::string name;
std::string topic;
@@ -727,7 +814,7 @@ struct OriginData {
std::vector<OriginRecord> records;
std::vector<OriginHeader> headers;
std::unordered_map<std::string, std::vector<size_t>> pathToRecords;
std::unordered_set<size_t> used;
std::unordered_set<size_t> used; // matchOrigin 命中后标记,最后可输出“已匹配/未匹配”清单。
};
struct OriginMatch {
@@ -754,8 +841,8 @@ struct JsonDataBucket {
struct WorkbookContext {
fs::path xmlPath;
std::unordered_map<std::string, std::vector<ValueMeta>> valueMetas;
std::unordered_set<std::string> xmlNames;
std::unordered_map<std::string, JsonDataBucket> dataByType;
std::unordered_set<std::string> xmlNames; // XML 中存在的原始 name用于避免序列误合并。
std::unordered_map<std::string, JsonDataBucket> dataByType; // DATA_TYPE -> jsondata 桶。
OriginData originData;
bool hasOrigin = false;
std::string frontType;
@@ -831,6 +918,7 @@ static std::vector<json> parseJsonRoots(const std::string& text) {
throw std::runtime_error("origin file contains unexpected content outside JSON object");
}
// origin 文件可能是多个 JSON 对象直接拼接;这里用括号深度扫描出每个完整根对象。
bool inString = false;
bool escape = false;
int depth = 0;
@@ -867,6 +955,7 @@ static void appendOriginRoot(OriginData& data, const fs::path& originPath, int s
std::string reportName = reportNameFromRoot(root, originPath, sourceOrder);
appendOriginHeader(data, originPath, sourceOrder, root, reportName);
// 常见 origin 结构是 { "mms_json": { "path": value } };如果没有 mms_json则直接把根对象当路径表。
const json* mms = &root;
if (root.is_object() && root.contains("mms_json") && root["mms_json"].is_object()) {
mms = &root["mms_json"];
@@ -932,6 +1021,12 @@ static std::string statInstanceSuffix(const std::string& stat) {
return "";
}
/*
* 把统计类型转换到 DO 实例号。
*
* 规则来自原始路径约定:平均/最大/最小/95值分别用 1/2/3/4 替换 DO 头部末尾的 0。
* 例如 xxx0$MX$... + 最大值 -> xxx2$MX$...
*/
static std::string applyStatInstance(const std::string& doPath, const std::string& stat) {
std::string suffix = statInstanceSuffix(stat);
if (suffix.empty()) return doPath;
@@ -947,6 +1042,7 @@ static std::string applyStatInstance(const std::string& doPath, const std::strin
}
static std::string phaseTokenForDa(const std::string& phase, const std::string& wiring) {
// 角形接线的 A/B/C 在 DA 中对应线电压 AB/BC/CA星形对应普通相 A/B/C。
if (wiring == "delta") {
if (phase == "A") return "phsAB";
if (phase == "B") return "phsBC";
@@ -975,6 +1071,7 @@ static std::string makeOriginPath(const std::string& doPath,
const std::string& phase,
const std::string& wiring) {
if (doPath.empty() || daPath.empty()) return "";
// origin 的 MMS 路径由 DO 与具体相位后的 DA 拼接stat 预留给 DO 实例号转换。
return doPath + "$" + concreteDaPath(daPath, phase, wiring);
}
@@ -1039,6 +1136,13 @@ static OriginMatch matchOrigin(OriginData* origin,
OriginMatch match;
if (!origin) return match;
/*
* 匹配步骤:
* 1. 根据 DO/DA/stat/phase/wiring 生成候选 origin 路径;
* 2. 若候选路径是 [start-end] 序列,按 sourceOrder 分组,选覆盖目标区间最多的报告;
* 3. 将选中报告中的连续索引拼成逗号值,并记录 valueIndexes 用于最终值对齐;
* 4. 普通路径则取第一个未使用记录,避免同一路径在多个输出行重复占用。
*/
for (const std::string& path : makeOriginPathCandidates(doPath, daPath, stat, phase, wiring)) {
if (path.empty()) continue;
@@ -1220,6 +1324,7 @@ static bool makeOutItemForWiring(const std::unordered_map<std::string, std::vect
const ValueMeta* vm = findMetaForKey(valueMetas, key, pk, wiring, filter);
if (requireMeta && !vm) return false;
// label 用“去掉统计后缀的 desc + stat”组成例如“电压最大值”。
std::string desc = vm ? vm->desc : "";
std::string descClean = cleanDescSuffix(desc);
std::string attrSort = !descClean.empty() ? descClean : pk.baseName;
@@ -1230,6 +1335,7 @@ static bool makeOutItemForWiring(const std::unordered_map<std::string, std::vect
int keyStart = 0, keyEnd = 0;
if (vm && vm->isSequence && parseKeyRange(key, keyStart, keyEnd)) {
// XML 中 DA 写的是 [%offset] 占位符,表格展示时改成最终可对齐的 [start-end] 范围。
daDisp = rewriteDaRangeForDisplay(daDisp, *vm, keyStart, keyEnd);
}
@@ -1352,6 +1458,7 @@ static OutItem makeOutItemFromMeta(const ValueMeta& vm,
const std::string& sheetWiring,
const std::unordered_map<std::string, std::string>& valueByKey) {
std::string dataKey = dataKeyForMeta(vm, phase);
// lookupDataKeyForMeta 会把 XML Offset 应用到取数 key 上,使最终值和 XML 定义对齐。
auto it = valueByKey.find(lookupDataKeyForMeta(vm, phase));
std::string val = it == valueByKey.end() ? "" : it->second;
@@ -1423,6 +1530,7 @@ static std::vector<OutItem> buildItemsForSheet(const std::unordered_map<std::str
const std::unordered_map<std::string, std::string>& valueByKey) {
std::vector<OutItem> out;
std::unordered_set<std::string> seen;
// 以 XML 定义为主生成行,即使 jsondata 缺值也保留表格行,后续用红色标出缺失。
for (const auto& kv : valueMetas) {
for (const ValueMeta& vm : kv.second) {
if (vm.topic != spec.topic || !specAllowsDataType(spec, vm.dataType)) continue;
@@ -1515,6 +1623,7 @@ static std::vector<std::string> roundOriginValuesForDisplay(const std::vector<st
static std::string compareValueKey(const std::string& value) {
std::string trimmed = trimValue(value);
long double number = 0;
// 数字统一为 6 位小数比较,避免 "1" 与 "1.000000" 被误判不同;非数字则小写比较。
if (parseNumberValue(trimmed, number)) return fixedSixNumber(number);
return lowerValue(trimmed);
}
@@ -1587,6 +1696,7 @@ static ComparePlan comparePlanForItem(const std::string& finalKey, const OutItem
int baseEnd = 0;
if (!parseArrayBaseRange(item.arrayBase, baseStart, baseEnd)) return plan;
int valueOffset = parseOffsetText(item.valueOffset, 0);
// originPickStart/End 表示原始值中真正要拿来和最终值第 1 列比较的数组下标区间。
plan.originPickStart = baseStart + valueOffset;
plan.originPickEnd = baseEnd + valueOffset;
return plan;
@@ -1715,6 +1825,16 @@ static void writeDataWorksheet(std::ofstream& ofs,
std::vector<OriginHeader> visibleOriginHeaders = visibleOriginHeadersForSheet(origin, items, wiring);
const int totalCols = std::max(9 + maxValues, 3 + static_cast<int>(visibleOriginHeaders.size()));
/*
* 每个数据项写两行:
* 第一行“最终值”:来自 jsondata按逗号拆到 值1..值64
* 第二行“原始值”:来自 origin按同一比较区间对齐展示。
* 行颜色:
* MatchGreen 最终值与原始值一致;
* MatchOrange 找到原始值但内容不一致;
* MatchRed 最终值为 null 或没有匹配到原始值;
* OriginPickBlue 标出序列比较时真正对齐起点。
*/
ofs << "<Worksheet ss:Name=\"" << htmlEscape(sheetName) << "\"><Table>\n";
ofs << "<Column ss:Width=\"210\"/><Column ss:Width=\"75\"/><Column ss:Width=\"85\"/><Column ss:Width=\"85\"/>";
ofs << "<Column ss:Width=\"310\"/><Column ss:Width=\"120\"/><Column ss:Width=\"280\"/>";
@@ -2014,6 +2134,7 @@ static void writeExcelWorkbook(const fs::path& outPath,
ofs << "<Style ss:ID=\"OriginPickBlue\" ss:Parent=\"Default\"><Interior ss:Color=\"#CFE2F3\" ss:Pattern=\"Solid\"/></Style>\n";
ofs << "</Styles>\n";
// SheetSpec 是报表目录统计数据和实时数据各自按接线方式、DATA_TYPE 分 Sheet。
std::vector<SheetSpec> sheets = {
{"星形接线", "HISDATA", "01", "star", true, "cfg_stat_data"},
{"角型接线", "HISDATA", "01", "delta", true, "cfg_stat_data"},
@@ -2060,6 +2181,7 @@ static const std::vector<std::string>& statBlocks() {
}
static void writeWorkbookStage(WorkbookContext& ctx, const fs::path& outPath) {
// 每次重写工作簿前清空 used让匹配过程从头计算避免多次刷新后状态累积。
if (ctx.hasOrigin) ctx.originData.used.clear();
OriginData* origin = ctx.hasOrigin ? &ctx.originData : nullptr;
writeExcelWorkbook(outPath, statBlocks(), ctx.valueMetas, ctx.dataByType, origin, ctx.frontType);
@@ -2071,6 +2193,7 @@ static WorkbookContext createWorkbookTemplateFromXml(const fs::path& xmlPath, co
ctx.frontType = frontType;
ctx.valueMetas = loadValueMetas(xmlPath);
ctx.xmlNames.reserve(ctx.valueMetas.size() * 2);
// xmlNames 只保存 XML 原名,用于 mergeSequences 防止 V1/G_V1 被误合并。
for (const auto& kv : ctx.valueMetas) ctx.xmlNames.insert(kv.first);
writeWorkbookStage(ctx, outPath);
return ctx;
@@ -2117,6 +2240,7 @@ static void applyJsonDataToWorkbook(WorkbookContext& ctx, const json& data, cons
auto merged = mergeSequences(flat, ctx.xmlNames);
std::string dataType = dataTypeFromMerged(merged);
JsonDataBucket& bucket = ctx.dataByType[dataType];
// 同一个 DATA_TYPE 多次出现时保留第一份,避免 MQ 重复消息覆盖已生成结果。
if (bucket.hasData) continue;
bucket.headers.clear();
bucket.valueByKey.clear();
@@ -2200,6 +2324,7 @@ bool pqRunParser(const std::string& xmlPath,
fs::path xmlFile = fs::u8path(xmlPath);
fs::path output = outPath.empty() ? fs::path("final_sorted.xls") : fs::u8path(outPath);
// 解析顺序固定:先 XML 建模板,再填 jsondata最后追加 origin 并重写带对比结果的工作簿。
WorkbookContext ctx = createWorkbookTemplateFromXml(xmlFile, output, frontType);
for (const auto& jsonPath : jsonPaths) {
if (!jsonPath.empty()) applyJsonDataToWorkbook(ctx, readJsonDataFile(fs::u8path(jsonPath)), output);
@@ -2218,6 +2343,7 @@ bool pqRunParser(const std::string& xmlPath,
int main(int argc, char** argv) {
try {
if (argc == 1) {
// 无参数启动 GUI带参数则走下面的旧命令行解析模式。
return runPqGuiApp();
}