update at 2026-02-14 11:20:38
This commit is contained in:
@@ -863,19 +863,16 @@ Page({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一读取并解析文件。
|
* 统一读取并解析文件。
|
||||||
* - CSV 按 utf8 文本读取
|
* - 统一按二进制读取,交由解析器根据内容与后缀判断
|
||||||
* - XLS/XLSX 按二进制读取
|
* - 避免上传文件名缺失/后缀错误时误按 CSV 读取(出现 PK... 头)
|
||||||
*/
|
*/
|
||||||
readAndApplyFile(filePath, fileName, onReadFailPrefix) {
|
readAndApplyFile(filePath, fileName, onReadFailPrefix) {
|
||||||
const that = this;
|
const that = this;
|
||||||
const extension = getFileExtension(fileName) || getFileExtension(getBaseNameFromPath(filePath));
|
|
||||||
const isCsvFile = extension === 'csv';
|
|
||||||
const readOptions = {
|
const readOptions = {
|
||||||
filePath,
|
filePath,
|
||||||
success(readRes) {
|
success(readRes) {
|
||||||
try {
|
try {
|
||||||
const filePayload = isCsvFile ? String(readRes.data || '') : readRes.data;
|
const table = parseTableByFileName(fileName, readRes.data);
|
||||||
const table = parseTableByFileName(fileName, filePayload);
|
|
||||||
that.applyParsedTable(table, fileName);
|
that.applyParsedTable(table, fileName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
that.setData({
|
that.setData({
|
||||||
@@ -891,9 +888,6 @@ Page({
|
|||||||
that.refreshInfoLogs();
|
that.refreshInfoLogs();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (isCsvFile) {
|
|
||||||
readOptions.encoding = 'utf8';
|
|
||||||
}
|
|
||||||
wx.getFileSystemManager().readFile(readOptions);
|
wx.getFileSystemManager().readFile(readOptions);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -270,6 +270,13 @@ function parseXlsxBuffer(buffer) {
|
|||||||
return toRawTable(rows);
|
return toRawTable(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断 payload 是否为 ArrayBuffer 形态。
|
||||||
|
*/
|
||||||
|
function isArrayBufferLike(payload) {
|
||||||
|
return !!payload && typeof payload === 'object' && typeof payload.byteLength === 'number';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断二进制是否为 Zip 容器(xlsx)魔数:50 4B。
|
* 判断二进制是否为 Zip 容器(xlsx)魔数:50 4B。
|
||||||
*/
|
*/
|
||||||
@@ -298,25 +305,60 @@ function isOleMagic(bufferLike) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将二进制内容按 UTF-8 解码为文本,供 CSV 解析使用。
|
||||||
|
*/
|
||||||
|
function decodeUtf8Text(payload) {
|
||||||
|
if (typeof payload === 'string') {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
if (!isArrayBufferLike(payload)) {
|
||||||
|
return String(payload || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(payload);
|
||||||
|
if (typeof TextDecoder === 'function') {
|
||||||
|
try {
|
||||||
|
return new TextDecoder('utf-8').decode(bytes);
|
||||||
|
} catch (error) {
|
||||||
|
// 继续走下方兼容解码分支
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let binary = '';
|
||||||
|
const chunkSize = 0x8000;
|
||||||
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||||
|
const chunk = bytes.subarray(i, i + chunkSize);
|
||||||
|
binary += String.fromCharCode.apply(null, chunk);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(escape(binary));
|
||||||
|
} catch (error) {
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按文件名后缀自动分流解析器。
|
* 按文件名后缀自动分流解析器。
|
||||||
*/
|
*/
|
||||||
function parseTableByFileName(fileName, payload) {
|
function parseTableByFileName(fileName, payload) {
|
||||||
const lowerName = String(fileName || '').toLowerCase();
|
const lowerName = String(fileName || '').toLowerCase();
|
||||||
if (lowerName.endsWith('.csv')) {
|
const isBinaryPayload = isArrayBufferLike(payload);
|
||||||
return parseCsvText(String(payload || ''));
|
|
||||||
}
|
// 优先按文件魔数识别 Excel,避免后缀错误导致误判 CSV。
|
||||||
if (lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls')) {
|
if (isBinaryPayload && (isZipMagic(payload) || isOleMagic(payload))) {
|
||||||
return parseXlsxBuffer(payload);
|
return parseXlsxBuffer(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底:后缀不可用时,通过文件魔数自动识别 Excel。
|
if (lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls')) {
|
||||||
if (payload && typeof payload === 'object' && typeof payload.byteLength === 'number') {
|
|
||||||
if (isZipMagic(payload) || isOleMagic(payload)) {
|
|
||||||
return parseXlsxBuffer(payload);
|
return parseXlsxBuffer(payload);
|
||||||
}
|
}
|
||||||
|
if (lowerName.endsWith('.csv')) {
|
||||||
|
return parseCsvText(decodeUtf8Text(payload));
|
||||||
}
|
}
|
||||||
throw new Error('仅支持 .csv / .xlsx / .xls 文件');
|
|
||||||
|
// 后缀缺失时,默认按 CSV 尝试解析(文本/二进制都支持)。
|
||||||
|
return parseCsvText(decodeUtf8Text(payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -103,4 +103,33 @@ describe('miniapp utils sankey', () => {
|
|||||||
expect(table.headers).toEqual(['source', 'target', 'value']);
|
expect(table.headers).toEqual(['source', 'target', 'value']);
|
||||||
expect(table.rows).toEqual([['A', 'B', '1']]);
|
expect(table.rows).toEqual([['A', 'B', '1']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('文件名伪装为 csv 时,仍可根据二进制魔数识别 xlsx', () => {
|
||||||
|
const sheet = XLSX.utils.aoa_to_sheet([
|
||||||
|
['source', 'target', 'value'],
|
||||||
|
['A', 'B', 1]
|
||||||
|
]);
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, sheet, 'S1');
|
||||||
|
const buffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' });
|
||||||
|
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
||||||
|
|
||||||
|
const table = parseTableByFileName('wrong.csv', arrayBuffer);
|
||||||
|
|
||||||
|
expect(table.headers).toEqual(['source', 'target', 'value']);
|
||||||
|
expect(table.rows).toEqual([['A', 'B', '1']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('csv 二进制内容应按 utf8 正常解析', () => {
|
||||||
|
const csvBuffer = Buffer.from('source,target,value\n甲,乙,1', 'utf8');
|
||||||
|
const arrayBuffer = csvBuffer.buffer.slice(
|
||||||
|
csvBuffer.byteOffset,
|
||||||
|
csvBuffer.byteOffset + csvBuffer.byteLength
|
||||||
|
);
|
||||||
|
|
||||||
|
const table = parseTableByFileName('demo.csv', arrayBuffer);
|
||||||
|
|
||||||
|
expect(table.headers).toEqual(['source', 'target', 'value']);
|
||||||
|
expect(table.rows).toEqual([['甲', '乙', '1']]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user