excel-csv-handler 1.0.8 → 1.0.11
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 +6 -1
- package/src/excel-csv-handler.d.ts +36 -1
- package/src/index.js +117 -11
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "excel-csv-handler",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "A Node.js utility to read/write Excel and CSV files with GBK encoding support",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/excel-csv-handler.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"src/",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
8
12
|
"keywords": [
|
|
9
13
|
"excel",
|
|
10
14
|
"csv",
|
|
@@ -16,6 +20,7 @@
|
|
|
16
20
|
"author": "Chao_bei",
|
|
17
21
|
"license": "MIT",
|
|
18
22
|
"dependencies": {
|
|
23
|
+
"chardet": "^2.1.1",
|
|
19
24
|
"fast-csv": "^5.0.5",
|
|
20
25
|
"iconv-lite": "^0.7.0",
|
|
21
26
|
"xlsx": "^0.18.5"
|
|
@@ -1,12 +1,47 @@
|
|
|
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;
|
|
9
|
+
/**
|
|
10
|
+
* 读取 Excel 或 CSV 文件(支持 GBK 编码)
|
|
11
|
+
* @param filePath - 文件路径
|
|
12
|
+
* @param headerRow - 标题行索引(从 0 开始)
|
|
13
|
+
* @returns Promise<Array<Object>>
|
|
14
|
+
*/
|
|
3
15
|
read(
|
|
4
16
|
filePath: string,
|
|
5
17
|
headerRow?: number
|
|
6
18
|
): Promise<Array<Record<string, string>>>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 写入 Excel 或 CSV 文件(以 GBK 编码保存 CSV,Excel 保持默认)
|
|
22
|
+
* @param filePath - 文件路径
|
|
23
|
+
* @param data - 要写入的数据
|
|
24
|
+
* @param headers - 可选列顺序
|
|
25
|
+
* @param sheetName - Excel sheet 名称(默认为 'Sheet1')
|
|
26
|
+
*/
|
|
7
27
|
write(
|
|
8
28
|
filePath: string,
|
|
9
29
|
data: Array<Record<string, any>>,
|
|
10
|
-
headers?: string[] | null
|
|
30
|
+
headers?: string[] | null,
|
|
31
|
+
sheetName?: string
|
|
32
|
+
): Promise<void>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 追加数据到 CSV 文件末尾(以 GBK 编码)
|
|
36
|
+
* @param filePath - 文件路径
|
|
37
|
+
* @param data - 要追加的数据
|
|
38
|
+
* @param headers - 可选列顺序(如果文件不存在会自动创建并写入标题行)
|
|
39
|
+
* @param encoding - 编码格式,默认 'gbk',可选 'utf8'
|
|
40
|
+
*/
|
|
41
|
+
appendCsv(
|
|
42
|
+
filePath: string,
|
|
43
|
+
data: Array<Record<string, any>>,
|
|
44
|
+
headers?: string[] | null,
|
|
45
|
+
encoding?: string
|
|
11
46
|
): Promise<void>;
|
|
12
47
|
}
|
package/src/index.js
CHANGED
|
@@ -6,9 +6,48 @@ import * as path from 'path';
|
|
|
6
6
|
import iconv from 'iconv-lite';
|
|
7
7
|
import * as csv from 'fast-csv';
|
|
8
8
|
import { writeToString } from 'fast-csv';
|
|
9
|
+
import chardet from 'chardet';
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
10
11
|
const XLSX = require('xlsx');
|
|
11
12
|
class ExcelCsvHandler {
|
|
13
|
+
/**
|
|
14
|
+
* 检测文件编码(用于 CSV 文件)
|
|
15
|
+
* @param {string} filePath - 文件路径
|
|
16
|
+
* @returns {string} 检测到的编码格式 (utf8 或 gbk)
|
|
17
|
+
*/
|
|
18
|
+
detectEncoding(filePath) {
|
|
19
|
+
// 读取文件的前几个字节来检测编码
|
|
20
|
+
const buffer = fs.readFileSync(filePath);
|
|
21
|
+
|
|
22
|
+
// 检查 UTF-8 BOM
|
|
23
|
+
if (buffer.length >= 3 &&
|
|
24
|
+
buffer[0] === 0xEF &&
|
|
25
|
+
buffer[1] === 0xBB &&
|
|
26
|
+
buffer[2] === 0xBF) {
|
|
27
|
+
return 'utf8';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 使用 chardet 库检测编码
|
|
31
|
+
const detected = chardet.detect(buffer);
|
|
32
|
+
|
|
33
|
+
// 将检测结果映射到 iconv-lite 支持的编码名称
|
|
34
|
+
if (detected) {
|
|
35
|
+
const encoding = detected.toLowerCase();
|
|
36
|
+
|
|
37
|
+
// GB2312, GBK, GB18030 都映射为 gbk
|
|
38
|
+
if (encoding.includes('gb') || encoding.includes('gbk')) {
|
|
39
|
+
return 'gbk';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// UTF-8 相关
|
|
43
|
+
if (encoding.includes('utf-8') || encoding.includes('utf8')) {
|
|
44
|
+
return 'utf8';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 默认返回 gbk(考虑到中国用户常用 GBK)
|
|
49
|
+
return 'gbk';
|
|
50
|
+
}
|
|
12
51
|
/**
|
|
13
52
|
* 读取 Excel 或 CSV 文件(支持 GBK 编码)
|
|
14
53
|
* @param {string} filePath - 文件路径
|
|
@@ -19,9 +58,9 @@ class ExcelCsvHandler {
|
|
|
19
58
|
const ext = path.extname(filePath).toLowerCase();
|
|
20
59
|
|
|
21
60
|
if (ext === '.xlsx' || ext === '.xls') {
|
|
22
|
-
return this
|
|
61
|
+
return this.#readExcelFile(filePath, headerRow);
|
|
23
62
|
} else if (ext === '.csv') {
|
|
24
|
-
return this
|
|
63
|
+
return this.#readCsvFile(filePath, headerRow);
|
|
25
64
|
} else {
|
|
26
65
|
throw new Error(`不支持的文件格式: ${ext}`);
|
|
27
66
|
}
|
|
@@ -38,9 +77,9 @@ class ExcelCsvHandler {
|
|
|
38
77
|
const ext = path.extname(filePath).toLowerCase();
|
|
39
78
|
|
|
40
79
|
if (ext === '.xlsx' || ext === '.xls') {
|
|
41
|
-
this
|
|
80
|
+
this.#writeExcelFile(filePath, data, headers, sheetName);
|
|
42
81
|
} else if (ext === '.csv') {
|
|
43
|
-
await this
|
|
82
|
+
await this.#writeCsvFile(filePath, data, headers);
|
|
44
83
|
} else {
|
|
45
84
|
throw new Error(`不支持的文件格式: ${ext}`);
|
|
46
85
|
}
|
|
@@ -48,7 +87,7 @@ class ExcelCsvHandler {
|
|
|
48
87
|
|
|
49
88
|
// ========= Excel 读写 =========
|
|
50
89
|
|
|
51
|
-
readExcelFile(filePath, headerRow) {
|
|
90
|
+
#readExcelFile(filePath, headerRow) {
|
|
52
91
|
const workbook = XLSX.readFile(filePath);
|
|
53
92
|
const sheetName = workbook.SheetNames[0];
|
|
54
93
|
const worksheet = workbook.Sheets[sheetName];
|
|
@@ -80,7 +119,7 @@ class ExcelCsvHandler {
|
|
|
80
119
|
});
|
|
81
120
|
}
|
|
82
121
|
|
|
83
|
-
writeExcelFile(filePath, data, headers = null, sheetName = 'Sheet1') {
|
|
122
|
+
#writeExcelFile(filePath, data, headers = null, sheetName = 'Sheet1') {
|
|
84
123
|
if (!headers && data.length > 0) {
|
|
85
124
|
headers = Object.keys(data[0]);
|
|
86
125
|
} else if (!headers) {
|
|
@@ -96,11 +135,14 @@ class ExcelCsvHandler {
|
|
|
96
135
|
|
|
97
136
|
// ========= CSV 读写 =========
|
|
98
137
|
|
|
99
|
-
async readCsvFile(filePath, headerRow) {
|
|
138
|
+
async #readCsvFile(filePath, headerRow) {
|
|
139
|
+
// 自动检测编码
|
|
140
|
+
const encoding = this.detectEncoding(filePath);
|
|
141
|
+
|
|
100
142
|
return new Promise((resolve, reject) => {
|
|
101
143
|
const allRows = [];
|
|
102
144
|
const stream = fs.createReadStream(filePath)
|
|
103
|
-
.pipe(iconv.decodeStream(
|
|
145
|
+
.pipe(iconv.decodeStream(encoding))
|
|
104
146
|
.pipe(csv.parse({ headers: false, skipLines: headerRow }));
|
|
105
147
|
|
|
106
148
|
stream.on('data', row => {
|
|
@@ -132,7 +174,14 @@ class ExcelCsvHandler {
|
|
|
132
174
|
});
|
|
133
175
|
}
|
|
134
176
|
|
|
135
|
-
|
|
177
|
+
/**
|
|
178
|
+
* 写入 CSV 文件(以 GBK 编码保存)
|
|
179
|
+
* @param {string} filePath - 文件路径
|
|
180
|
+
* @param {Array<Object>} data - 要写入的数据
|
|
181
|
+
* @param {Array<string>} headers - 可选列顺序
|
|
182
|
+
* @param {string} encoding - 编码格式,默认 'gbk',可选 'utf8'
|
|
183
|
+
*/
|
|
184
|
+
async #writeCsvFile(filePath, data, headers = null, encoding = 'gbk') {
|
|
136
185
|
if (!headers && data.length > 0) {
|
|
137
186
|
headers = Object.keys(data[0]);
|
|
138
187
|
} else if (!headers) {
|
|
@@ -145,8 +194,65 @@ class ExcelCsvHandler {
|
|
|
145
194
|
rowDelimiter: '\r\n',
|
|
146
195
|
});
|
|
147
196
|
|
|
148
|
-
const
|
|
149
|
-
|
|
197
|
+
const buffer = iconv.encode(csvString, encoding);
|
|
198
|
+
|
|
199
|
+
// 如果是 UTF-8,添加 BOM 标记以确保 Excel 正确识别
|
|
200
|
+
if (encoding === 'utf8') {
|
|
201
|
+
const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
|
|
202
|
+
fs.writeFileSync(filePath, Buffer.concat([bom, buffer]));
|
|
203
|
+
} else {
|
|
204
|
+
fs.writeFileSync(filePath, buffer);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 追加数据到 CSV 文件末尾(以 GBK 编码)
|
|
210
|
+
* @param {string} filePath - 文件路径
|
|
211
|
+
* @param {Array<Object>} data - 要追加的数据
|
|
212
|
+
* @param {Array<string>} headers - 可选列顺序(如果文件不存在会自动创建并写入标题行)
|
|
213
|
+
* @param {string} encoding - 编码格式,默认 'gbk',可选 'utf8'
|
|
214
|
+
*/
|
|
215
|
+
async appendCsv(filePath, data, headers = null, encoding = 'gbk') {
|
|
216
|
+
if (!headers && data.length > 0) {
|
|
217
|
+
headers = Object.keys(data[0]);
|
|
218
|
+
} else if (!headers) {
|
|
219
|
+
headers = [];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 检查文件是否存在
|
|
223
|
+
const fileExists = fs.existsSync(filePath);
|
|
224
|
+
|
|
225
|
+
if (!fileExists) {
|
|
226
|
+
// 文件不存在,自动创建并写入标题行和数据
|
|
227
|
+
await this.#writeCsvFile(filePath, data, headers, encoding);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 文件存在时,检测现有文件的编码
|
|
232
|
+
const detectedEncoding = this.detectEncoding(filePath);
|
|
233
|
+
|
|
234
|
+
// 文件存在,追加数据(不包含标题行)
|
|
235
|
+
const csvString = await writeToString(data, {
|
|
236
|
+
headers: false,
|
|
237
|
+
includeEndRowDelimiter: true,
|
|
238
|
+
rowDelimiter: '\r\n',
|
|
239
|
+
writeHeaders: false,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// 将数据按照指定的列顺序格式化
|
|
243
|
+
const rows = data.map(row =>
|
|
244
|
+
headers.map(h => {
|
|
245
|
+
const value = row[h] ?? '';
|
|
246
|
+
// 如果值包含逗号、引号或换行符,需要用引号包裹并转义
|
|
247
|
+
if (value.toString().includes(',') || value.toString().includes('"') || value.toString().includes('\n')) {
|
|
248
|
+
return '"' + value.toString().replace(/"/g, '""') + '"';
|
|
249
|
+
}
|
|
250
|
+
return value;
|
|
251
|
+
}).join(',')
|
|
252
|
+
).join('\r\n') + '\r\n';
|
|
253
|
+
|
|
254
|
+
const buffer = iconv.encode(rows, detectedEncoding);
|
|
255
|
+
fs.appendFileSync(filePath, buffer);
|
|
150
256
|
}
|
|
151
257
|
}
|
|
152
258
|
|