用JS解析CSV数据和导出

12,864 阅读3分钟

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",23

即:

[[
  '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/YYYYYYYY-MM-DD之类。

也就是说它把日期认成了月份,而30超出了月份范围,造成数据无法识别。那就需要来个日期转格式了。

除了日期外经常遇到的可能还有不同国家的数字格式,像1,000这种带分隔符的软件就经常无法识别出来,会作为一般字符处理。