CSV(comma separated values)是常用的数据文档格式之一。它是个格式简单的纯文本文件:每一行数据为行,每一列数据由分隔符隔开,像是个简化版表格。因此,一般的表格处理软件如微软的Excel都可以打开CSV,只是不包含任何样式。
分析读取CSV文件时,最好先将其转为二维数组或一组对象以便继续处理。处理完导出CSV时,则将数组转为纯文本格式。
听上去很简单吧!不过,由于CSV并未标准化,不同软件系统默认使用的行分隔符和列分隔符不尽相同,这为分析和转化增加了一些复杂度。
读取CSV
首先我们FileReader类创建一个reader实例,并用该实例的.readAsText()方法将CSV文件内容读为文本。
const csv = await new Promise<string>(async (resolve, reject) => {
try {
const reader = new FileReader();
reader.addEventListener('loadend', () => {
resolve(reader.result?.toString() || '');
});
reader.readAsText(file);
} catch (e) {
reject(e);
}
});
将CSV转化为二维数组
读取到文本后,我们就可以通过一系列逻辑将CSV转换为数组了。讲解代码之前,让我们先看一下CSV的规则:
行分隔符
CSV的行分隔符有:\n, \r, \r\n三种
操作软件、系统的默认设置不同会导致CSV文件行分割符不同。三种不同的分隔符我们都得考虑进去。
列分隔符
最常见列分隔符是英文半角逗号“,”。此外,还有使用半角分号“;”,空格,tab等等。不过这些情况比较少见,我们先假设只处理逗号。
如果一个单元的内容中,出现了分隔符,则需将该单元的内容用双引号包起来避免软件无法识别。如下:
上述文件用Excel等软件打开会显示为:
那如果单元里还有双引号怎么办?那就用2个双引号,第一个双引号作为转义符,如
"1"",",2,3
转换为
| 第一列 | 第二列 | 第三列 |
|---|---|---|
| 1", | 2 | 3 |
即:
[[
'1",',
'2',
'3'
]]
转换
代码:
// 假设只处理逗号列分隔符
const COLUMN_DELIMITER = ',';
export function csvToArray(csv: string): string[][] {
const table = [] as string[][];
let row = [];
let cell = '';
let openQuote = false;
let i = 0;
const pushCell = () => {
row.push(cell);
cell = '';
};
const pushRow = () => {
pushCell();
table.push(row);
row = [];
}
// 处理行分隔符和列分隔符
const handleSeparator = (i: number) => {
const c = csv.charAt(i);
if (c === COLUMN_DELIMITER) {
pushCell();
} else if (c === '\r') {
if (csv.charAt(i + 1) === '\n') {
i++;
}
pushRow();
} else if (c === '\n') {
pushRow();
} else {
return false;
}
return true;
}
while (i < csv.length) {
const c = csv.charAt(i);
const next = csv.charAt(i + 1);
if (!openQuote && !cell && c === '"') {
// 遇到单元第一个字符为双引号时假设整个单元都是被双引号括起来
openQuote = true;
} else if (openQuote) {
// 双引号还未成对的时候
if (c !== '"') {
// 如非双引号,直接添加进单元内容
cell += c;
} else if (next === '"') {
// 处理双引号转义
cell += c;
i++;
} else {
// 确认单元结束
openQuote = false
if (!handleSeparator(++i)){
throw new Error('Wrong CSV format!');
}
}
} else if (!handleSeparator(i)) {
// 没有双引号包起来时,如非行列分隔符,一律直接加入单元内容
cell += c;
}
i++;
}
if (cell) {
pushRow();
}
return table;
}
以上并未用正则表达式,而是用字符遍历的方式一遍完成转换。
除了必要的行列和索引号变量之外,还声明了一个openQuote状态表示目前单元是否在在双引号里,并且这个双引号还成对。此外,还有下一个邻接字符用于帮助判断转义、以及单元分隔是否符合要求。
导出CSV文件
比起解析,导出简单很多。由于软件基本都能识别逗号列分隔符和\n行分隔符,我们只需要考虑这两个分隔符就好。
另外,我们需要注意处理可能在内容中出现的分隔符和双引号:
const columnDelimiter = ',';
const rowDelimiter = '\n';
export function arrayToCsv(data) {
return data.map((row) =>
row.map((col) => /,/.test(col) ? `"${col.replace(/"/g, '""')}` : col)
.join(',')
).join(rowDelimiter);
}
再将文本作为CSV文件让浏览器自动下载:
const BOM = '\uFEFF';
function exportCsv(inputData, filename = 'export.csv') {
const csv = arrayToCsv(inputData);
if (navigator.msSaveOrOpenBlob) {
let blob = new Blob([BOM + csv], { type: 'text/csv;charset=utf-8;' });
navigator.msSaveOrOpenBlob(blob, filename);
} else {
let uri = encodeURI(`data:text/csv;charset=utf-8,${BOM}${csv}`);
let downloadLink = document.createElement('a');
downloadLink.href = uri;
downloadLink.download = filename;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
}
此处注意浏览器兼容,浏览器对<a>的download属性支持如下:
IE可以利用.msSaveOrOpenBlob()来打开一个选择保存路径的窗口,而其他浏览器要做到这点需要废一些功夫,这边我就简单地直接利用download属性自动下载。
另外,需要在文件头部加入一个不影响整个文件内容显示的BOM字符(如\uFEFF,是个unicode零宽空格符)才可以让Excel等软件正确识别utf-8编码。
其他问题:数据格式
下图中有一些数据的显示不正常。一般来说能被软件识别的数据格式都会向右对齐。
比较麻烦的像有些国家的日期格式为DD/MM/YYYY,在Excel等软件中是无法识别的,它们只识别英美格式MM/DD/YYYY,YYYY-MM-DD之类。
也就是说它把日期认成了月份,而30超出了月份范围,造成数据无法识别。那就需要来个日期转格式了。
除了日期外经常遇到的可能还有不同国家的数字格式,像1,000这种带分隔符的软件就经常无法识别出来,会作为一般字符处理。