#include "CsvTable.hpp" #include "Utf8Util.hpp" #include #include #include #include #include namespace jdi { namespace { /** @brief 单个字段在原始缓冲中的位置区间。 */ struct FieldSpan { const char* begin = nullptr; /**< 字段起始指针。 */ const char* end = nullptr; /**< 字段结束指针(不包含)。 */ }; const double kMissingValue = std::numeric_limits::quiet_NaN(); /**< 缺失值占位。 */ /** * @brief 一次性将整个文件读入内存。 * @param [IN] filePath 文件路径。 * @param [OUT] buffer 文件内容输出。 * @param [OUT] error 失败原因描述。 * @return 读取成功返回 true。 */ bool ReadWholeFile(const std::wstring& filePath, std::string& buffer, std::wstring& error) { FILE* file = _wfopen(filePath.c_str(), L"rb"); if (file == nullptr) { error = L"无法打开文件,请确认路径与访问权限。"; return false; } if (std::fseek(file, 0, SEEK_END) != 0) { std::fclose(file); error = L"无法定位文件末尾。"; return false; } const long fileSize = std::ftell(file); if (fileSize < 0) { std::fclose(file); error = L"无法获取文件长度。"; return false; } if (fileSize == 0) { std::fclose(file); error = L"文件内容为空。"; return false; } if (fileSize > (512L * 1024L * 1024L)) { std::fclose(file); error = L"文件体积超过 512 MB,暂不支持。"; return false; } std::fseek(file, 0, SEEK_SET); buffer.resize(static_cast(fileSize)); const size_t readCount = std::fread(&buffer[0], 1, buffer.size(), file); std::fclose(file); buffer.resize(readCount); return true; } /** * @brief 统计一行中某个候选分隔符出现的次数。 * @param [IN] begin 行起始指针。 * @param [IN] end 行结束指针(不包含)。 * @param [IN] candidate 候选分隔符。 * @return 出现次数。 */ int CountDelimiter(const char* begin, const char* end, char candidate) { int count = 0; bool inQuote = false; for (const char* p = begin; p < end; ++p) { if (*p == '"') { inQuote = !inQuote; } else if (*p == candidate && !inQuote) { ++count; } } return count; } /** * @brief 依据表头行推断分隔符。 * @param [IN] begin 表头行起始指针。 * @param [IN] end 表头行结束指针(不包含)。 * @return 推断出的分隔符。 */ char DetectDelimiter(const char* begin, const char* end) { static const char kCandidates[] = {',', ';', '\t', '|'}; char best = ','; int bestCount = 0; for (const char candidate : kCandidates) { const int count = CountDelimiter(begin, end, candidate); if (count > bestCount) { bestCount = count; best = candidate; } } return best; } /** * @brief 将一行文本按分隔符拆分为多个字段(支持双引号包裹与转义)。 * @param [IN] begin 行起始指针。 * @param [IN] end 行结束指针(不包含)。 * @param [IN] delimiter 分隔符。 * @param [OUT] fields 字段区间输出。 */ void SplitFields(const char* begin, const char* end, char delimiter, std::vector& fields) { fields.clear(); const char* fieldBegin = begin; bool inQuote = false; for (const char* p = begin; p < end; ++p) { const char c = *p; if (c == '"') { if (inQuote && (p + 1) < end && p[1] == '"') { ++p; continue; } inQuote = !inQuote; } else if (c == delimiter && !inQuote) { FieldSpan span; span.begin = fieldBegin; span.end = p; fields.push_back(span); fieldBegin = p + 1; } } FieldSpan span; span.begin = fieldBegin; span.end = end; fields.push_back(span); } /** * @brief 将字段区间转换为字符串并去除引号与空白。 * @param [IN] span 字段区间。 * @return 规范化后的字段文本。 */ std::wstring FieldToText(const FieldSpan& span) { std::wstring text = Utf8ToWide(span.begin, static_cast(span.end - span.begin)); return StripQuotes(text); } /** * @brief 判断字段是否应当被整行跳过(空行或注释行)。 * @param [IN] span 首字段区间。 * @param [IN] lineIsEmpty 该行是否为纯空白行。 * @return 需要跳过返回 true。 */ bool ShouldSkipLine(const FieldSpan& span, bool lineIsEmpty) { if (lineIsEmpty) { return true; } if (span.begin == nullptr || span.begin >= span.end) { return false; } const char first = span.begin[0]; return first == '#' || first == '%'; } /** * @brief 在一行内查找首个非空行的终止位置。 * @param [IN] begin 文本起始指针。 * @param [IN] end 文本结束指针(不包含)。 * @return 该行结束指针(不含换行符)。 */ const char* FindLineEnd(const char* begin, const char* end) { const char* p = begin; while (p < end && *p != '\n') { ++p; } const char* lineEnd = p; while (lineEnd > begin && (lineEnd[-1] == '\r' || lineEnd[-1] == '\n')) { --lineEnd; } return lineEnd; } } bool LoadCsvTable(const std::wstring& filePath, CsvTable& table, std::wstring& error) { table = CsvTable(); error.clear(); std::string buffer; if (!ReadWholeFile(filePath, buffer, error)) { return false; } const char* data = buffer.data(); size_t length = buffer.size(); // 跳过 UTF-8 BOM,避免首列名被污染。 if (length >= 3 && static_cast(data[0]) == 0xEF && static_cast(data[1]) == 0xBB && static_cast(data[2]) == 0xBF) { data += 3; length -= 3; } const char* cursor = data; const char* limit = data + length; std::vector fields; bool headerReady = false; while (cursor < limit) { const char* rawEnd = FindLineEnd(cursor, limit); const char* lineEnd = rawEnd; // 跳过纯空白行,但不产生数据行。 const char* scan = cursor; while (scan < lineEnd && (*scan == ' ' || *scan == '\t' || *scan == '\r')) { ++scan; } const bool lineIsEmpty = (scan >= lineEnd); if (!headerReady && lineIsEmpty) { cursor = (rawEnd < limit) ? (rawEnd + 1) : limit; continue; } if (!headerReady) { table.delimiter = DetectDelimiter(cursor, lineEnd); SplitFields(cursor, lineEnd, static_cast(table.delimiter), fields); table.headers.clear(); table.headers.reserve(fields.size()); for (const FieldSpan& span : fields) { std::wstring text = FieldToText(span); if (text.empty()) { text = L"Column" + std::to_wstring(table.headers.size() + 1); } table.headers.push_back(text); } if (table.headers.empty()) { error = L"表头为空,无法解析列名。"; return false; } headerReady = true; cursor = (rawEnd < limit) ? (rawEnd + 1) : limit; continue; } SplitFields(cursor, lineEnd, static_cast(table.delimiter), fields); // 空行与注释行(以 # 或 % 开头)不参与数据统计。 if (ShouldSkipLine(fields.empty() ? FieldSpan() : fields.front(), lineIsEmpty)) { cursor = (rawEnd < limit) ? (rawEnd + 1) : limit; continue; } if (fields.size() != table.headers.size()) { ++table.skippedRowCount; } std::vector row; row.reserve(table.headers.size()); bool rowHasValue = false; for (size_t column = 0; column < table.headers.size(); ++column) { if (column >= fields.size()) { row.push_back(kMissingValue); continue; } double value = kMissingValue; if (TryParseDouble(fields[column].begin, fields[column].end, value)) { rowHasValue = true; } row.push_back(value); } if (!rowHasValue) { cursor = (rawEnd < limit) ? (rawEnd + 1) : limit; continue; } table.rows.push_back(std::move(row)); ++table.sourceRowCount; cursor = (rawEnd < limit) ? (rawEnd + 1) : limit; } if (!table.HasData()) { error = L"未从文件中解析出有效数据行。"; return false; } return true; } } // namespace jdi