在开发过程中,可能会遇到这样的需求,我们需要从本地的 Excel 或 CSV 等文件中解析出信息,这些信息可能是考勤打卡记录,可能是日历信息,也可能是近期账单流水。但是它们共同的特点是数据多且繁杂,人工录入的工作量庞大容易出错,需要花费大量时间。那有没有什么方法能自动解析文件并获取有用信息呢?
当这个文件数据量也不是很多的时候,有很多前端工具可供选择。例如 SheetJS,就提供了从 Excel、CSV 中解析出用信息的很多方法,十分方便。
当数据量只是几千条的程度的,选择的余地很多,但是一旦数据量级增加,处理就变得复杂。如果 XLSX/CSV 数据量达到了 100w+ 条,Office、WPS 想打开看一下,都会需要很长的时间。
那又该如何从这样大体积的 Excel/CSV/TXT 中解析出数据呢?
背景
下面我们通过一个假设的需求,来讲述理解整个过程。假设我们需求是从本地 Excel、CSV、TXT(或者其他格式的)文件中解析出数据,并经过清洗后存入本地数据库文件中。但是这些文件体积可能是 5M、50M、500M 甚至更大。那么在浏览器环境下如何上传?Node 环境下应该如何解析?
首先,我们需要了解的是浏览器 Web 页面如何上传大体积文件?
Web 页面如何上传大体积文件?
Web 页面一般也是可以上传大文件的,但是会面临一个问题。如果要上传的数据比较大,那么整个上传过程会比较漫长,再加上上传过程的不确定因素,一旦失败,那整个上传就要从头再来,耗时很长。
面对这个问题,我们可以通过将大文件分成多份小文件,每一次只上传一份的方法来解决。这样即使某个请求失败了,也无需从头开始,只要重新上传失败的那一份就好了。
如果想要使用这个方法,我们需要满足以下几项需求:
- 大体积文件支持切片上传
- 可以断点续传
- 可以得知上传进度
首先看一下如何进行大文件切割。Web 页面基本都是通过 <input type=’file’ /> 来获取本地文件的。 而通过 input 的 event.target.files 获取到的 file,其实是一个 File 类的实例,是 Blob 类的子类。
Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。 简单理解合一将 Blob 看做二进制容器,表示存放着一个大的二进制文件。Blob 对象有一个很重要的方法:slice(),这里需要注意的是 Blob 对象是不可变的,slice 方法返回的是一个新的 Blob,表示所需要切割的二进制文件。
slice() 方法接受三个参数,起始偏移量,结束偏移量,还有可选的 mime 类型。如果 mime 类型,没有设置,那么新的 Blob 对象的 mime 类型和父级一样。而 File 接口基于 Blob,File 对象也包含了slice方法,其结果包含有源 Blob 对象中指定范围的数据。
看完了切割的方法,我们就可以对二进制文件进行拆分了。拆分示例如下:
function sliceInPiece(file, piece = 1024 * 1024 * 5) { let totalSize = file.size; // 文件总大小 let start = 0; // 每次上传的开始字节 let end = start + piece; // 每次上传的结尾字节 let chunks = [] while (start < totalSize) { // 根据长度截取每次需要上传的数据 // File对象继承自Blob对象,因此包含slice方法 let blob = file.slice(start, end); chunks.push(blob) start = end; end = start + piece; } return chunks }
获得文件切割后的数组后,就可以挨个调用接口上传至服务端。
let file = document.querySelector("[name=file]").files[0]; const LENGTH = 1024 * 1024 * 0.1; let chunks = sliceInPiece(file, LENGTH); // 首先拆分切片 chunks.forEach(chunk=>{ let fd = new FormData(); fd.append("file", chunk); post('/upload', fd) })
完成上传后再至服务端将切片文件拼接成完整文件,让 FileReader 对象从 Blob 中读取数据。
当然这里会遇到两个问题,其一是面对上传完成的一堆切片文件,服务端要如知道它们的正确顺序?其二是如果有多个大体积文件同时上传,服务端该如何判断哪个切片属于哪个文件呢?
前后顺序的问题,我们可以通过构造切片的 FormData 时增加参数的方式来处理。比如用参数 ChunkIndex 表示当前切片的顺序。
而第二个问题可以通过增加参数比如 sourceFile 等(值可以是当前大体积文件的完整路径或者更严谨用文件的 hash 值)来标记原始文件来源。这样服务端在获取到数据时,就可以知道哪些切片来自哪个文件以及切片之间的前后顺序。
如果暂时不方便自行构架,也可以考虑使用云服务,比如