289 lines
10 KiB
C++
289 lines
10 KiB
C++
|
|
#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>
|
|||
|
|
|
|||
|
|
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) {
|
|||
|
|
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) {
|
|||
|
|
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);
|
|||
|
|
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;
|
|||
|
|
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;
|
|||
|
|
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);
|
|||
|
|
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) {
|
|||
|
|
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 {
|
|||
|
|
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);
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
}
|