CSV是comma separated values的缩写,现已成为常用的数据文档格式之一。虽说它可以用Excel等表格处理软件打开修改,但它本质上是个纯文本文件,并且格式相当简单:每一行数据为行,每一列数据由分隔符隔开。因此,在分析CSV文件的时候,我会选择将其转为二维数组或一组Object便于处理。而在导出时,则相反地,转为纯文本。
说起来很简单,但由于CSV并未标准化,不同软件系统使用的行分隔符和列分隔符不一定相同,这为分析和转化增加了一些难度。再加上数据内容的格式(比如日期、金额等)会由不同的国家习惯和个人习惯造成不同,处理起来会有一些麻烦。
CSV => 二维数组
行分隔符
行分隔符有:\n | \r | \r\n
根据上图内容,猜测造成输入源csv文件行分割符不同的原因可能是因为操作的软件、系统的默认设置不同。
列分隔符
最常用的列分隔符是英文半角逗号“,”。除逗号外还有使用半角分号“;”,空格,tab等等。如果一列内容中,出现了分隔符,则将这列内容用双引号包起来。如下:
上述文件用Excel等软件打开会显示为:
在此,我为了避免奇奇怪怪的分隔符处理,将它作为一个参数传入,而不是直接去处理各种可能性(因为这些出现的几率很低)。为了鉴别匹配的分隔符是不是真的列分割,需要对双引号做特殊处理:
function csvToArray(csv, columnDelimiter = ',') {
const table = csv.trim().replace(/\r\n?/g, '\n');
let quoteCounter = 0;
let lastDelimiterIndex = 0;
let arrTable = [[]];
let anchorRow = arrTable[arrTable.length - 1];
for (let i = 0; i < table.length; i++) {
const char = table[i];
if (char === '"' && table[i - 1] !== '\\') {
quoteCounter = quoteCounter ? 0 : 1;
if (quoteCounter) {
lastDelimiterIndex = i + 1;
}
} else if (
!quoteCounter &&
(char === columnDelimiter || char === '\n' || i === table.length - 1)
) {
const startPos = lastDelimiterIndex;
let col = startPos >= i ? '' : table.slice(startPos, i).trim();
if (col[col.length - 1] === '"') {
col = col.slice(0, col.length - 1);
}
anchorRow.push(col);
lastDelimiterIndex = i + 1;
if (char === '\n') {
anchorRow = arrTable[arrTable.push([]) - 1];
}
}
}
return arrTable;
}
格式问题
如上图所示,你可以看到有一些数据的显示并不正常,能被识别数据格式的都会右对齐。比如说日期,我使用的是新加坡惯用格式DD/MM/YYYY。而通常Excel也好,google sheets也好,都只默认识别英美格式,比如MM/DD/YYYY,YYYY-MM-DD之类。所以将我的数据换成另外一种格式就可以看到问题出在哪里:
它把我的日期认成了月份,30超出了月份范围,造成数据无法识别。造成我如果要用软件处理会很难纠正。
再说数字,大数字的整数部分通常会在每3位数由逗号分割。然而你可能不知道的是,印度并不是每三位分割,印尼习惯用点“.”作为分隔符,而逗号“,”作为小数点。风中凌乱……幸好大部分情况下都不需要处理这个。
由于上述这些格式问题并不属于本次话题范围内,我就不在这深入讨论了。
导出CSV文件
相比分析来说,导出就简单很多。因为控制权在我们自己手上。
先将二维数组转成纯文本,重点是要处理可能在内容中出现的分隔符和双引号:
export function arrayToCsv(data, args = {}) {
let columnDelimiter = args.columnDelimiter || ',';
let lineDelimiter = args.lineDelimiter || '\n';
return data.reduce((csv, row) => {
const rowContent = Array.isArray(row)
? row.reduce((rowTemp, col) => {
let ret = rowTemp ? rowTemp + columnDelimiter : rowTemp;
if (col) {
let formatedCol = col.toString().replace(new RegExp(lineDelimiter, 'g'), ' ');
ret += /,/.test(formatedCol) ? `"${formatedCol}"` : formatedCol;
}
return ret;
}, '')
: row;
return (csv ? csv + lineDelimiter : '') + rowContent;
}, '');
}
再将文本导出成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属性自动下载。
需要注意的是,除了将文本设置为utf-8编码外,还需要做另外一层处理才能让Excel等软件正确识别中文。经实验最有用的方法就是在文件头部加入一个不影响整个文件内容显示的BOM字符,这边我选择了使用\uFEFF
,是个unicode零宽空格符,不会对显示造成任何影响。
题外话
刚开始接触用JS处理CSV文件这块,完全是被逼的T-T。做电商总要为用户提供一些数据的处理的服务啥的,然后我们的后端童鞋甩锅给我。实际上这由前端来做的话,对于有分页的数据很难操作。
另外,CSV没有漂亮的格式,不能像xlxs这些文件一样,有颜色之类。而前端处理的库比如js-xlsx都非常大,为了一个小功能加几百kb的一个库对互联网产品来说是很难接受的事情(虽然现在网速已经很快了)。所以建议能在服务器端处理还是由服务器端处理比较好。
除了业务需求外,这个技能给我带来的最直接的好处是,帮我处理账务。作为一个身在国外同时拥有两个国家多个账号的人来说,账务已经复杂到无法直接手动处理分析了。所以我除了利用挖财来帮我记录和生成统计报表以外,我利用我的JS代码来将多个银行下载下来的csv格式流水分析、自动填写分类,并整合、转换成挖财可以导入的格式,帮我大大节省了时间。