Files
datatrace/source/icd_mapper.cpp

320 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "pq_app.h"
#include "tinyxml2.h"
#include <windows.h>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
/*
* icd_mapper.cpp
*
* 功能:把外部 ICD/XML 中 LN/DOI 的 desc 描述补写到工作簿
* “未匹配的原始数据”工作表的“icd中代表的字段”列。
*
* 输入:
* icdPath 外部 ICD/XML 文件,可能是 UTF-8也可能声明/实际使用 GBK/GB2312。
* workbookPath main.cpp 生成的 Excel 2003 XML 格式 .xls。
*
* 输出:
* 直接修改 workbookPath。message 返回解析结果、匹配数量或错误原因。
*
* 核心匹配规则:
* 1. 从 ICD 的每个 LN 节点读取 lnClass + inst并遍历其 DOI 子节点;
* 2. 组成 DO keylnClass + inst + "$MX$" + DOI/@name
* 3. 工作簿未匹配页的“原始路径”如果等于该 key或以 key + "$" 开头,
* 就认为该原始 MMS 路径属于这个 DOI把 DOI/@desc 写入目标列。
*/
namespace fs = std::filesystem;
namespace {
std::string readAllBinary(const fs::path& path) {
std::ifstream ifs(path, std::ios::binary);
if (!ifs) throw std::runtime_error("Failed to open file: " + path.string());
std::ostringstream ss;
ss << ifs.rdbuf();
return ss.str();
}
void writeAllBinary(const fs::path& path, const std::string& text) {
std::ofstream ofs(path, std::ios::binary);
if (!ofs) throw std::runtime_error("Failed to write file: " + path.string());
ofs << text;
}
std::string lowerAscii(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return s;
}
bool declaresGbEncoding(const std::string& text) {
std::string head = lowerAscii(text.substr(0, std::min<size_t>(text.size(), 256)));
return head.find("gb2312") != std::string::npos ||
head.find("gbk") != std::string::npos ||
head.find("gb18030") != std::string::npos;
}
bool isLikelyUtf8(const std::string& text) {
// 只做 UTF-8 字节合法性判断,不判断语义;合法就优先按 UTF-8 给 tinyxml2。
int remaining = 0;
for (unsigned char c : text) {
if (remaining == 0) {
if ((c & 0x80) == 0) continue;
if ((c & 0xE0) == 0xC0) remaining = 1;
else if ((c & 0xF0) == 0xE0) remaining = 2;
else if ((c & 0xF8) == 0xF0) remaining = 3;
else return false;
} else {
if ((c & 0xC0) != 0x80) return false;
--remaining;
}
}
return remaining == 0;
}
std::string ansiToUtf8(const std::string& text) {
// 936 是 Windows 简体中文代码页,兼容常见 GBK/GB2312 ICD 文件。
if (text.empty()) return "";
int wideLen = MultiByteToWideChar(936, 0, text.data(), (int)text.size(), nullptr, 0);
if (wideLen <= 0) return text;
std::wstring wide(wideLen, L'\0');
MultiByteToWideChar(936, 0, text.data(), (int)text.size(), wide.data(), wideLen);
int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wide.data(), wideLen, nullptr, 0, nullptr, nullptr);
std::string out(utf8Len, '\0');
WideCharToMultiByte(CP_UTF8, 0, wide.data(), wideLen, out.data(), utf8Len, nullptr, nullptr);
return out;
}
std::string loadXmlText(const fs::path& path) {
std::string raw = readAllBinary(path);
// ICD 声明 GB 编码或字节本身不像 UTF-8 时,先转成 UTF-8 再交给 tinyxml2。
if (declaresGbEncoding(raw) || !isLikelyUtf8(raw)) return ansiToUtf8(raw);
return raw;
}
std::string localName(const char* name) {
if (!name) return "";
std::string s(name);
size_t pos = s.find(':');
return pos == std::string::npos ? s : s.substr(pos + 1);
}
std::string attr(const tinyxml2::XMLElement* e, const char* name) {
const char* v = e ? e->Attribute(name) : nullptr;
return v ? v : "";
}
void collectIcdDescriptions(const tinyxml2::XMLElement* element,
std::unordered_map<std::string, std::string>& descByDo) {
if (!element) return;
if (localName(element->Name()) == "LN") {
std::string lnClass = attr(element, "lnClass");
std::string inst = attr(element, "inst");
if (!lnClass.empty()) {
for (const tinyxml2::XMLElement* child = element->FirstChildElement();
child;
child = child->NextSiblingElement()) {
if (localName(child->Name()) != "DOI") continue;
std::string doiName = attr(child, "name");
std::string desc = attr(child, "desc");
if (doiName.empty() || desc.empty()) continue;
// 这里构造的是 origin 原始路径前缀。后面路径可继续带 $mag$f 等 DA 后缀。
std::string key = lnClass + inst + "$MX$" + doiName;
descByDo[key] = desc;
}
}
}
for (const tinyxml2::XMLElement* child = element->FirstChildElement();
child;
child = child->NextSiblingElement()) {
collectIcdDescriptions(child, descByDo);
}
}
std::unordered_map<std::string, std::string> parseIcdDescriptions(const fs::path& icdPath) {
tinyxml2::XMLDocument doc;
std::string xml = loadXmlText(icdPath);
tinyxml2::XMLError rc = doc.Parse(xml.c_str(), xml.size());
if (rc != tinyxml2::XML_SUCCESS) {
throw std::runtime_error("Failed to parse ICD XML: " + std::to_string((int)rc));
}
std::unordered_map<std::string, std::string> descByDo;
collectIcdDescriptions(doc.RootElement(), descByDo);
return descByDo;
}
std::string cellText(const tinyxml2::XMLElement* cell) {
const tinyxml2::XMLElement* data = cell ? cell->FirstChildElement("Data") : nullptr;
const char* text = data ? data->GetText() : nullptr;
return text ? text : "";
}
int cellIndex(const tinyxml2::XMLElement* cell, int fallback) {
const char* idx = cell ? cell->Attribute("ss:Index") : nullptr;
return idx && *idx ? std::max(1, std::atoi(idx)) : fallback;
}
tinyxml2::XMLElement* cellAt(tinyxml2::XMLElement* row, int col) {
int current = 1;
for (tinyxml2::XMLElement* cell = row ? row->FirstChildElement("Cell") : nullptr;
cell;
cell = cell->NextSiblingElement("Cell")) {
current = cellIndex(cell, current);
if (current == col) return cell;
++current;
}
return nullptr;
}
tinyxml2::XMLElement* ensureCell(tinyxml2::XMLDocument& doc, tinyxml2::XMLElement* row, int col) {
if (tinyxml2::XMLElement* existing = cellAt(row, col)) return existing;
// Excel XML 允许通过 ss:Index 跳列;补列时直接设置 ss:Index避免改动前面已有单元格。
tinyxml2::XMLElement* cell = doc.NewElement("Cell");
cell->SetAttribute("ss:Index", col);
row->InsertEndChild(cell);
return cell;
}
void setCellText(tinyxml2::XMLDocument& doc, tinyxml2::XMLElement* cell, const std::string& text) {
tinyxml2::XMLElement* data = cell->FirstChildElement("Data");
if (!data) {
data = doc.NewElement("Data");
data->SetAttribute("ss:Type", "String");
cell->InsertEndChild(data);
}
data->SetText(text.c_str());
}
std::vector<std::string> sortedKeysByLength(const std::unordered_map<std::string, std::string>& values) {
std::vector<std::string> keys;
keys.reserve(values.size());
for (const auto& kv : values) keys.push_back(kv.first);
// 长路径优先,避免短 key 先命中长 key 的前缀。
std::sort(keys.begin(), keys.end(), [](const std::string& a, const std::string& b) {
if (a.size() != b.size()) return a.size() > b.size();
return a < b;
});
return keys;
}
bool pathMatchesDo(const std::string& rawPath, const std::string& doKey) {
// rawPath 可以正好是 DO也可以是 DO$DA...;必须在 "$" 边界匹配,避免误命中相似前缀。
return rawPath == doKey ||
(rawPath.size() > doKey.size() &&
rawPath.compare(0, doKey.size(), doKey) == 0 &&
rawPath[doKey.size()] == '$');
}
std::string descForPath(const std::string& rawPath,
const std::unordered_map<std::string, std::string>& descByDo,
const std::vector<std::string>& orderedKeys) {
for (const auto& key : orderedKeys) {
if (pathMatchesDo(rawPath, key)) {
auto it = descByDo.find(key);
return it == descByDo.end() ? "" : it->second;
}
}
return "";
}
tinyxml2::XMLElement* findWorksheet(tinyxml2::XMLDocument& doc, const char* sheetName) {
tinyxml2::XMLElement* workbook = doc.FirstChildElement("Workbook");
for (tinyxml2::XMLElement* ws = workbook ? workbook->FirstChildElement("Worksheet") : nullptr;
ws;
ws = ws->NextSiblingElement("Worksheet")) {
const char* name = ws->Attribute("ss:Name");
if (name && std::string(name) == sheetName) return ws;
}
return nullptr;
}
int headerColumn(tinyxml2::XMLElement* headerRow, const std::string& title) {
int current = 1;
for (tinyxml2::XMLElement* cell = headerRow ? headerRow->FirstChildElement("Cell") : nullptr;
cell;
cell = cell->NextSiblingElement("Cell")) {
current = cellIndex(cell, current);
if (cellText(cell) == title) return current;
++current;
}
return 0;
}
} // namespace
bool pqApplyIcdDescriptionsToWorkbook(const std::string& icdPath,
const std::string& workbookPath,
std::string* message) {
try {
// 第一步:解析 ICD得到 “DO 路径前缀 -> 中文描述”。
auto descByDo = parseIcdDescriptions(fs::u8path(icdPath));
if (descByDo.empty()) {
if (message) *message = "ICD 中没有解析到 LN/DOI desc";
return false;
}
std::vector<std::string> orderedKeys = sortedKeysByLength(descByDo);
// 第二步:读取 main.cpp 输出的工作簿。该 .xls 实际是 XML可直接用 tinyxml2 修改。
tinyxml2::XMLDocument doc;
std::string workbookXml = readAllBinary(fs::u8path(workbookPath));
tinyxml2::XMLError rc = doc.Parse(workbookXml.c_str(), workbookXml.size());
if (rc != tinyxml2::XML_SUCCESS) {
if (message) *message = "表格文件解析失败: " + std::to_string((int)rc);
return false;
}
// 第三步:只处理“未匹配的原始数据”页,因为已匹配页已经有明确映射关系。
tinyxml2::XMLElement* sheet = findWorksheet(doc, "未匹配的原始数据");
if (!sheet) {
if (message) *message = "没有找到 sheet: 未匹配的原始数据";
return false;
}
tinyxml2::XMLElement* table = sheet->FirstChildElement("Table");
tinyxml2::XMLElement* header = table ? table->FirstChildElement("Row") : nullptr;
int pathCol = headerColumn(header, "原始路径");
int icdCol = headerColumn(header, "icd中代表的字段");
if (pathCol <= 0 || icdCol <= 0) {
if (message) *message = "未匹配 sheet 缺少 原始路径 或 icd中代表的字段 列";
return false;
}
// 第四步:逐行读取原始路径,按 DO 前缀回填 DOI 描述。
int matched = 0;
for (tinyxml2::XMLElement* row = header ? header->NextSiblingElement("Row") : nullptr;
row;
row = row->NextSiblingElement("Row")) {
tinyxml2::XMLElement* pathCell = cellAt(row, pathCol);
std::string rawPath = cellText(pathCell);
if (rawPath.empty()) continue;
std::string desc = descForPath(rawPath, descByDo, orderedKeys);
if (desc.empty()) continue;
tinyxml2::XMLElement* icdCell = ensureCell(doc, row, icdCol);
setCellText(doc, icdCell, desc);
++matched;
}
tinyxml2::XMLPrinter printer;
doc.Print(&printer);
writeAllBinary(fs::u8path(workbookPath), printer.CStr());
if (message) {
*message = "ICD 导入完成DOI=" + std::to_string(descByDo.size()) +
",填入未匹配原始数据=" + std::to_string(matched);
}
return true;
} catch (const std::exception& e) {
if (message) *message = e.what();
return false;
}
}