excel-csv-handler 1.0.10 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +30 -25
- package/src/excel-csv-handler.d.ts +24 -1
- package/src/index.js +89 -8
- package/debug.js +0 -121
- package/test-output/multi-append.csv +0 -4
- package/test-output/new-file.csv +0 -4
- package/test-output/special.csv +0 -4
- package/test-output/test.csv +0 -6
- package/test-output/test.xlsx +0 -0
package/package.json
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
2
|
+
"name": "excel-csv-handler",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A Node.js utility to read/write Excel and CSV files with GBK encoding support",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/excel-csv-handler.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"src/",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"excel",
|
|
17
|
+
"csv",
|
|
18
|
+
"xlsx",
|
|
19
|
+
"gbk",
|
|
20
|
+
"node",
|
|
21
|
+
"file"
|
|
22
|
+
],
|
|
23
|
+
"author": "Chao_bei",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"chardet": "^2.1.1",
|
|
27
|
+
"fast-csv": "^5.0.5",
|
|
28
|
+
"iconv-lite": "^0.7.0",
|
|
29
|
+
"xlsx": "^0.18.5"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// src/excel-csv-handler.d.ts
|
|
2
2
|
export default class ExcelCsvHandler {
|
|
3
|
+
/**
|
|
4
|
+
* 检测文件编码(用于 CSV 文件)
|
|
5
|
+
* @param filePath - 文件路径
|
|
6
|
+
* @returns 检测到的编码格式 (utf8 或 gbk)
|
|
7
|
+
*/
|
|
8
|
+
detectEncoding(filePath: string): string;
|
|
3
9
|
/**
|
|
4
10
|
* 读取 Excel 或 CSV 文件(支持 GBK 编码)
|
|
5
11
|
* @param filePath - 文件路径
|
|
@@ -30,10 +36,27 @@ export default class ExcelCsvHandler {
|
|
|
30
36
|
* @param filePath - 文件路径
|
|
31
37
|
* @param data - 要追加的数据
|
|
32
38
|
* @param headers - 可选列顺序(如果文件不存在会自动创建并写入标题行)
|
|
39
|
+
* @param encoding - 编码格式,默认 'gbk',可选 'utf8'
|
|
33
40
|
*/
|
|
34
41
|
appendCsv(
|
|
35
42
|
filePath: string,
|
|
36
43
|
data: Array<Record<string, any>>,
|
|
37
|
-
headers?: string[] | null
|
|
44
|
+
headers?: string[] | null,
|
|
45
|
+
encoding?: string
|
|
38
46
|
): Promise<void>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 从调用文件的相对路径获取绝对路径
|
|
50
|
+
* @param importMetaUrl - 调用文件的 import.meta.url
|
|
51
|
+
* @param relativePath - 相对路径(如 './dataset/file.docx' 或 'dataset/file.docx')
|
|
52
|
+
* @returns 绝对路径
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import ExcelCsvHandler from 'excel-csv-handler';
|
|
57
|
+
* const docxPath = ExcelCsvHandler.getAbsolutePath(import.meta.url, './dataset/sci-high-question.docx');
|
|
58
|
+
* const csvPath = ExcelCsvHandler.getAbsolutePath(import.meta.url, 'data/output.csv');
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
static getAbsolutePath(importMetaUrl: string, relativePath: string): string;
|
|
39
62
|
}
|
package/src/index.js
CHANGED
|
@@ -1,14 +1,54 @@
|
|
|
1
1
|
// excel-csv-handler.js
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
3
4
|
|
|
4
5
|
import * as fs from 'fs';
|
|
5
6
|
import * as path from 'path';
|
|
6
7
|
import iconv from 'iconv-lite';
|
|
7
8
|
import * as csv from 'fast-csv';
|
|
8
9
|
import { writeToString } from 'fast-csv';
|
|
10
|
+
import chardet from 'chardet';
|
|
9
11
|
const require = createRequire(import.meta.url);
|
|
10
12
|
const XLSX = require('xlsx');
|
|
11
13
|
class ExcelCsvHandler {
|
|
14
|
+
/**
|
|
15
|
+
* 检测文件编码(用于 CSV 文件)
|
|
16
|
+
* @param {string} filePath - 文件路径
|
|
17
|
+
* @returns {string} 检测到的编码格式 (utf8 或 gbk)
|
|
18
|
+
*/
|
|
19
|
+
detectEncoding(filePath) {
|
|
20
|
+
// 读取文件的前几个字节来检测编码
|
|
21
|
+
const buffer = fs.readFileSync(filePath);
|
|
22
|
+
|
|
23
|
+
// 检查 UTF-8 BOM
|
|
24
|
+
if (buffer.length >= 3 &&
|
|
25
|
+
buffer[0] === 0xEF &&
|
|
26
|
+
buffer[1] === 0xBB &&
|
|
27
|
+
buffer[2] === 0xBF) {
|
|
28
|
+
return 'utf8';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 使用 chardet 库检测编码
|
|
32
|
+
const detected = chardet.detect(buffer);
|
|
33
|
+
|
|
34
|
+
// 将检测结果映射到 iconv-lite 支持的编码名称
|
|
35
|
+
if (detected) {
|
|
36
|
+
const encoding = detected.toLowerCase();
|
|
37
|
+
|
|
38
|
+
// GB2312, GBK, GB18030 都映射为 gbk
|
|
39
|
+
if (encoding.includes('gb') || encoding.includes('gbk')) {
|
|
40
|
+
return 'gbk';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// UTF-8 相关
|
|
44
|
+
if (encoding.includes('utf-8') || encoding.includes('utf8')) {
|
|
45
|
+
return 'utf8';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 默认返回 gbk(考虑到中国用户常用 GBK)
|
|
50
|
+
return 'gbk';
|
|
51
|
+
}
|
|
12
52
|
/**
|
|
13
53
|
* 读取 Excel 或 CSV 文件(支持 GBK 编码)
|
|
14
54
|
* @param {string} filePath - 文件路径
|
|
@@ -97,10 +137,13 @@ class ExcelCsvHandler {
|
|
|
97
137
|
// ========= CSV 读写 =========
|
|
98
138
|
|
|
99
139
|
async #readCsvFile(filePath, headerRow) {
|
|
140
|
+
// 自动检测编码
|
|
141
|
+
const encoding = this.detectEncoding(filePath);
|
|
142
|
+
|
|
100
143
|
return new Promise((resolve, reject) => {
|
|
101
144
|
const allRows = [];
|
|
102
145
|
const stream = fs.createReadStream(filePath)
|
|
103
|
-
.pipe(iconv.decodeStream(
|
|
146
|
+
.pipe(iconv.decodeStream(encoding))
|
|
104
147
|
.pipe(csv.parse({ headers: false, skipLines: headerRow }));
|
|
105
148
|
|
|
106
149
|
stream.on('data', row => {
|
|
@@ -132,7 +175,14 @@ class ExcelCsvHandler {
|
|
|
132
175
|
});
|
|
133
176
|
}
|
|
134
177
|
|
|
135
|
-
|
|
178
|
+
/**
|
|
179
|
+
* 写入 CSV 文件(以 GBK 编码保存)
|
|
180
|
+
* @param {string} filePath - 文件路径
|
|
181
|
+
* @param {Array<Object>} data - 要写入的数据
|
|
182
|
+
* @param {Array<string>} headers - 可选列顺序
|
|
183
|
+
* @param {string} encoding - 编码格式,默认 'gbk',可选 'utf8'
|
|
184
|
+
*/
|
|
185
|
+
async #writeCsvFile(filePath, data, headers = null, encoding = 'gbk') {
|
|
136
186
|
if (!headers && data.length > 0) {
|
|
137
187
|
headers = Object.keys(data[0]);
|
|
138
188
|
} else if (!headers) {
|
|
@@ -145,8 +195,15 @@ class ExcelCsvHandler {
|
|
|
145
195
|
rowDelimiter: '\r\n',
|
|
146
196
|
});
|
|
147
197
|
|
|
148
|
-
const
|
|
149
|
-
|
|
198
|
+
const buffer = iconv.encode(csvString, encoding);
|
|
199
|
+
|
|
200
|
+
// 如果是 UTF-8,添加 BOM 标记以确保 Excel 正确识别
|
|
201
|
+
if (encoding === 'utf8') {
|
|
202
|
+
const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
|
|
203
|
+
fs.writeFileSync(filePath, Buffer.concat([bom, buffer]));
|
|
204
|
+
} else {
|
|
205
|
+
fs.writeFileSync(filePath, buffer);
|
|
206
|
+
}
|
|
150
207
|
}
|
|
151
208
|
|
|
152
209
|
/**
|
|
@@ -154,8 +211,9 @@ class ExcelCsvHandler {
|
|
|
154
211
|
* @param {string} filePath - 文件路径
|
|
155
212
|
* @param {Array<Object>} data - 要追加的数据
|
|
156
213
|
* @param {Array<string>} headers - 可选列顺序(如果文件不存在会自动创建并写入标题行)
|
|
214
|
+
* @param {string} encoding - 编码格式,默认 'gbk',可选 'utf8'
|
|
157
215
|
*/
|
|
158
|
-
async appendCsv(filePath, data, headers = null) {
|
|
216
|
+
async appendCsv(filePath, data, headers = null, encoding = 'gbk') {
|
|
159
217
|
if (!headers && data.length > 0) {
|
|
160
218
|
headers = Object.keys(data[0]);
|
|
161
219
|
} else if (!headers) {
|
|
@@ -167,10 +225,13 @@ class ExcelCsvHandler {
|
|
|
167
225
|
|
|
168
226
|
if (!fileExists) {
|
|
169
227
|
// 文件不存在,自动创建并写入标题行和数据
|
|
170
|
-
await this.#writeCsvFile(filePath, data, headers);
|
|
228
|
+
await this.#writeCsvFile(filePath, data, headers, encoding);
|
|
171
229
|
return;
|
|
172
230
|
}
|
|
173
231
|
|
|
232
|
+
// 文件存在时,检测现有文件的编码
|
|
233
|
+
const detectedEncoding = this.detectEncoding(filePath);
|
|
234
|
+
|
|
174
235
|
// 文件存在,追加数据(不包含标题行)
|
|
175
236
|
const csvString = await writeToString(data, {
|
|
176
237
|
headers: false,
|
|
@@ -191,11 +252,31 @@ class ExcelCsvHandler {
|
|
|
191
252
|
}).join(',')
|
|
192
253
|
).join('\r\n') + '\r\n';
|
|
193
254
|
|
|
194
|
-
const
|
|
195
|
-
fs.appendFileSync(filePath,
|
|
255
|
+
const buffer = iconv.encode(rows, detectedEncoding);
|
|
256
|
+
fs.appendFileSync(filePath, buffer);
|
|
196
257
|
}
|
|
197
258
|
}
|
|
198
259
|
|
|
260
|
+
/**
|
|
261
|
+
* 从调用文件的相对路径获取绝对路径
|
|
262
|
+
* @param {string} importMetaUrl - 调用文件的 import.meta.url
|
|
263
|
+
* @param {string} relativePath - 相对路径(如 './dataset/file.docx' 或 'dataset/file.docx')
|
|
264
|
+
* @returns {string} 绝对路径
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* // 在你的文件中使用
|
|
268
|
+
* import ExcelCsvHandler from 'excel-csv-handler';
|
|
269
|
+
* const docxPath = ExcelCsvHandler.getAbsolutePath(import.meta.url, './dataset/sci-high-question.docx');
|
|
270
|
+
* const csvPath = ExcelCsvHandler.getAbsolutePath(import.meta.url, 'data/output.csv');
|
|
271
|
+
*/
|
|
272
|
+
ExcelCsvHandler.getAbsolutePath = function (importMetaUrl, relativePath) {
|
|
273
|
+
const __filename = fileURLToPath(importMetaUrl);
|
|
274
|
+
const __dirname = path.dirname(__filename);
|
|
275
|
+
// 去除开头的 './' 如果存在
|
|
276
|
+
const cleanPath = relativePath.startsWith('./') ? relativePath.slice(2) : relativePath;
|
|
277
|
+
return path.join(__dirname, cleanPath);
|
|
278
|
+
};
|
|
279
|
+
|
|
199
280
|
export default ExcelCsvHandler;
|
|
200
281
|
|
|
201
282
|
// 示例用法(取消注释可测试)
|
package/debug.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
// debug.js - 测试 ExcelCsvHandler
|
|
2
|
-
import ExcelCsvHandler from './src/index.js';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
|
|
6
|
-
const handler = new ExcelCsvHandler();
|
|
7
|
-
|
|
8
|
-
// 创建测试目录
|
|
9
|
-
const testDir = './test-output';
|
|
10
|
-
if (!fs.existsSync(testDir)) {
|
|
11
|
-
fs.mkdirSync(testDir, { recursive: true });
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
console.log('🧪 开始测试 ExcelCsvHandler...\n');
|
|
15
|
-
|
|
16
|
-
// 测试数据
|
|
17
|
-
const testData = [
|
|
18
|
-
{ 姓名: '张三', 年龄: '25', 城市: '北京' },
|
|
19
|
-
{ 姓名: '李四', 年龄: '30', 城市: '上海' },
|
|
20
|
-
{ 姓名: '王五', 年龄: '28', 城市: '广州' }
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
const appendData = [
|
|
24
|
-
{ 姓名: '赵六', 年龄: '32', 城市: '深圳' },
|
|
25
|
-
{ 姓名: '孙七', 年龄: '27', 城市: '杭州' }
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
(async () => {
|
|
29
|
-
try {
|
|
30
|
-
// ==================== 测试 1: CSV 写入 ====================
|
|
31
|
-
console.log('📝 测试 1: CSV 写入');
|
|
32
|
-
const csvPath = path.join(testDir, 'test.csv');
|
|
33
|
-
await handler.write(csvPath, testData, ['姓名', '年龄', '城市']);
|
|
34
|
-
console.log('✅ CSV 写入成功:', csvPath);
|
|
35
|
-
|
|
36
|
-
// ==================== 测试 2: CSV 读取 ====================
|
|
37
|
-
console.log('\n📖 测试 2: CSV 读取');
|
|
38
|
-
const csvData = await handler.read(csvPath, 0);
|
|
39
|
-
console.log('✅ CSV 读取成功,数据:');
|
|
40
|
-
console.table(csvData);
|
|
41
|
-
|
|
42
|
-
// ==================== 测试 3: appendCsv - 追加到已存在的文件 ====================
|
|
43
|
-
console.log('\n➕ 测试 3: appendCsv - 追加数据到已存在文件');
|
|
44
|
-
await handler.appendCsv(csvPath, appendData, ['姓名', '年龄', '城市']);
|
|
45
|
-
console.log('✅ 追加成功');
|
|
46
|
-
|
|
47
|
-
// 读取追加后的数据
|
|
48
|
-
const appendedData = await handler.read(csvPath, 0);
|
|
49
|
-
console.log('📊 追加后的完整数据:');
|
|
50
|
-
console.table(appendedData);
|
|
51
|
-
|
|
52
|
-
// ==================== 测试 4: appendCsv - 自动创建新文件 ====================
|
|
53
|
-
console.log('\n🆕 测试 4: appendCsv - 自动创建新文件');
|
|
54
|
-
const newCsvPath = path.join(testDir, 'new-file.csv');
|
|
55
|
-
// 确保文件不存在
|
|
56
|
-
if (fs.existsSync(newCsvPath)) {
|
|
57
|
-
fs.unlinkSync(newCsvPath);
|
|
58
|
-
}
|
|
59
|
-
await handler.appendCsv(newCsvPath, testData, ['姓名', '年龄', '城市']);
|
|
60
|
-
console.log('✅ 新文件创建成功:', newCsvPath);
|
|
61
|
-
|
|
62
|
-
const newFileData = await handler.read(newCsvPath, 0);
|
|
63
|
-
console.log('📊 新文件数据:');
|
|
64
|
-
console.table(newFileData);
|
|
65
|
-
|
|
66
|
-
// ==================== 测试 5: Excel 写入和读取 ====================
|
|
67
|
-
console.log('\n📊 测试 5: Excel 写入和读取');
|
|
68
|
-
const xlsxPath = path.join(testDir, 'test.xlsx');
|
|
69
|
-
await handler.write(xlsxPath, testData, ['姓名', '年龄', '城市'], 'Sheet1');
|
|
70
|
-
console.log('✅ Excel 写入成功:', xlsxPath);
|
|
71
|
-
|
|
72
|
-
const xlsxData = await handler.read(xlsxPath, 0);
|
|
73
|
-
console.log('✅ Excel 读取成功,数据:');
|
|
74
|
-
console.table(xlsxData);
|
|
75
|
-
|
|
76
|
-
// ==================== 测试 6: 包含特殊字符的数据 ====================
|
|
77
|
-
console.log('\n🔤 测试 6: 包含特殊字符的数据');
|
|
78
|
-
const specialData = [
|
|
79
|
-
{ 姓名: '测试,逗号', 年龄: '25', 备注: '包含"引号"的内容' },
|
|
80
|
-
{ 姓名: '测试换行', 年龄: '30', 备注: '第一行\n第二行' }
|
|
81
|
-
];
|
|
82
|
-
const specialPath = path.join(testDir, 'special.csv');
|
|
83
|
-
await handler.write(specialPath, specialData, ['姓名', '年龄', '备注']);
|
|
84
|
-
console.log('✅ 特殊字符数据写入成功');
|
|
85
|
-
|
|
86
|
-
const specialReadData = await handler.read(specialPath, 0);
|
|
87
|
-
console.log('✅ 特殊字符数据读取成功:');
|
|
88
|
-
console.table(specialReadData);
|
|
89
|
-
|
|
90
|
-
// ==================== 测试 7: 连续追加多次 ====================
|
|
91
|
-
console.log('\n🔄 测试 7: 连续追加多次');
|
|
92
|
-
const multiAppendPath = path.join(testDir, 'multi-append.csv');
|
|
93
|
-
if (fs.existsSync(multiAppendPath)) {
|
|
94
|
-
fs.unlinkSync(multiAppendPath);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 第一次追加(创建文件)
|
|
98
|
-
await handler.appendCsv(multiAppendPath, [testData[0]], ['姓名', '年龄', '城市']);
|
|
99
|
-
console.log(' ✓ 第 1 次追加完成');
|
|
100
|
-
|
|
101
|
-
// 第二次追加
|
|
102
|
-
await handler.appendCsv(multiAppendPath, [testData[1]], ['姓名', '年龄', '城市']);
|
|
103
|
-
console.log(' ✓ 第 2 次追加完成');
|
|
104
|
-
|
|
105
|
-
// 第三次追加
|
|
106
|
-
await handler.appendCsv(multiAppendPath, [testData[2]], ['姓名', '年龄', '城市']);
|
|
107
|
-
console.log(' ✓ 第 3 次追加完成');
|
|
108
|
-
|
|
109
|
-
const multiAppendData = await handler.read(multiAppendPath, 0);
|
|
110
|
-
console.log('✅ 连续追加测试成功,最终数据:');
|
|
111
|
-
console.table(multiAppendData);
|
|
112
|
-
|
|
113
|
-
console.log('\n✨ 所有测试通过!');
|
|
114
|
-
console.log(`\n📁 测试文件已保存到: ${path.resolve(testDir)}`);
|
|
115
|
-
|
|
116
|
-
} catch (error) {
|
|
117
|
-
console.error('\n❌ 测试失败:', error.message);
|
|
118
|
-
console.error(error.stack);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
})();
|
package/test-output/new-file.csv
DELETED
package/test-output/special.csv
DELETED
package/test-output/test.csv
DELETED
package/test-output/test.xlsx
DELETED
|
Binary file
|