背景
在中后台场景经常会使用 CSV 文件,本文面向的 CSV 文件的预览和截断场景。即数据来源可能是一个很大的 CSV,但我们只需要一小部分数据进行预览/操作,如果采用传统的方式,将数据全部下载然后加载到内存中可能会导致内存溢出和浪费带宽的情况。
为此很多时候需要对 CSV 的读取进行限制,如限制读取前 1W 行数据。
本文使用的工具包已开源 https://github.com/onechunlin/csv-row-limit
前置知识
本文例子基于 Node 版本 v16.13.0
在正式讲如何进行 CSV 行数限制之前,你需要对 Node 的 Buffer
、Stream
和 readline
模块有一定的了解。
Buffer
Buffer 是一种类似于数组的数据结构,用于处理二进制数据。可以简单的将 Buffer 视为整数数组,每个整数代表一个数据字节(Unicode 码)
const buf = Buffer.from('Hey!')
console.log(buf[0]) //72
console.log(buf[1]) //101
console.log(buf[2]) //121
这些数字是 Unicode 码,用于标识 buffer 位置中的字符(H => 72、e => 101、y => 121)。
Stream(流)
流是为 Node.js 应用程序提供动力的基本概念之一。它是一种以高效的方式处理读/写文件、网络通信、或任何类型的端到端的信息交换。
在传统的方式中,当告诉程序读取文件时,这会将文件从头到尾读入内存,然后进行处理。
使用流,则可以逐个片段地读取并处理,而无需全部保存在内存中。
使用示例
一个典型的例子是从磁盘读取文件。使用 Node.js 的 fs 模块,可以读取文件,并在与 HTTP 服务器建立新连接时通过 HTTP 提供文件:
const http = require('http')
const fs = require('fs')
const server = http.createServer(function(req, res) {
fs.readFile(__dirname + '/data.txt', (err, data) => {
res.end(data)
})
})
server.listen(3000)
readFile()
读取文件的全部内容,并在完成时调用回调函数。回调中的 res.end(data)
会返回文件的内容给 HTTP 客户端。
如果文件很大,则该操作会花费较多的时间。 以下是使用流编写的相同内容:
const http = require('http')
const fs = require('fs')
const server = http.createServer((req, res) => {
const stream = fs.createReadStream(__dirname + '/data.txt')
stream.pipe(res)
})
server.listen(3000)
当要发送的数据块已获得时就立即开始将其流式传输到 HTTP 客户端,而不是等待直到文件被完全读取。
上面的示例使用了 stream.pipe(res)
这行代码:在文件流上调用 pipe()
方法。它获取来源流,并将其通过管道传输到目标流。在 HTTP 请求中,req
为可读流,res
为可写流,所以这里实现了边读文件边向 HTTP 响应里写数据,大大提高了大文件读取时效率。
readline 模块
从版本 7 开始,Node.js 提供了 readline 模块来执行以下操作:每次一行地从可读流(例如 process.stdin 流,在程序执行期间该流就是终端输入)获取输入,并输出到可写流(例如 process.stdout 流,在程序执行期间该流就是终端输出)。
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
})
readline.question(`你叫什么名字?`, name => {
console.log(`你好 ${
name}!`)
readline.close()
})
这段代码会询问用户名,当输入了文本并且用户按下回车键时,则会发送问候语。
每当 input 流接收到行尾输入(\n、\r 或 \r\n)时,则会触发 line
事件。 这通常发生在用户按下 回车 或 返回 时。
如果从流中读取了新数据并且该流在没有最终行尾标记的情况下结束,也会触发 line
事件。大白话讲就是如果最后一行不是空行,也会触发 line
事件。
readline.on('line', (row) => {
console.log(`Received: ${
row}`);
});
了解了 Buffer
、Stream
和 readline
模块之后我们就可以实现我们限制读取 CSV 行数的需求了。
代码实现
数据 Mock
进行 CSV 代码读取实现之前,我们先来造一个 100W 行数据的 CSV 文件,这里为了简单只造一列数据:0 - 10 亿之间的随机数。
const csvStr = new Array(1000000).fill(0).reduce((prev) => {
/**
* ~~ 为双取反位运算,作用为给数值取整,作用和 Math.floor 类似,因为
* 是位运算速度比 Math.floor 快,但是日常还是建议使用 Math.floor
*/
prev += `${
~~(Math.random() * 1000000000)}\n`;
return prev;
}, "");
fs.writeFile(