如何在 Node.js 中使用流处理文件

作者选择了 Girls Who Code以作为 Write for Donations计划的一部分获得捐款。

介绍

计算中的流的概念通常描述了数据的交付在一个稳定的,连续的流中. 您可以使用流来从源不断地读取或写入源,从而消除需要同时将所有数据放在内存中。

使用流程提供了两大优点:一是您可以高效地使用内存,因为在您可以开始处理之前不必将所有数据加载到内存中。 另一个优点是使用流程是节约时间的。 您可以立即开始处理数据,而不是等待整个负载。 这些优点使流程成为 I/O 操作中大数据传输的合适工具。 文件是包含一些数据的字节的集合。 由于文件是 Node.js 中的常见数据源, 流程可以提供 Node.js 中的文件工作的有效途径。

Node.js 在模块中提供流程 API,这是一种核心 Node.js 模块,用于工作流程. 所有 Node.js 流程都是EventEmitter类的实例(有关更多信息,请参阅 使用 Node.js 中的事件发射器)。

在 Node.js 中有四种不同类型的流,它们是:

*可读流:可以读取数据的流 *可写流:可以写数据的流 *双重流:可以读取和写入(通常同时)的流 *转换流:输出(或可写流)取决于输入(或可读流)的修改的双重流。

文件系统模块(fs)是一个原始的 Node.js 模块,用于操作文件和通用导航本地文件系统. 它提供了几种方法来做到这一点。 其中两种方法实现流媒体 API。

在本文中,您将使用fs.createReadStreamfs.createWriteStream函数读取和写入一个文件,您还将使用一个流的输出作为另一个流的输入,并实施自定义转换蒸汽。通过执行这些操作,您将学习如何使用流在Node.js中使用文件。

前提条件

要完成本教程,您将需要:

步骤 1 - 设置一个文件处理命令行程序

在此步骤中,您将编写一个命令行程序,使用基本的命令. 这个命令行程序将展示您将在教程中稍后学习的概念,在那里您将使用这些命令与您创建的函数来处理文件。

首先,创建一个包含此程序所有文件的文件夹. 在您的终端中,创建一个名为node-file-streams的文件夹:

1mkdir node-file-streams

使用cd命令,将工作目录更改为新文件夹:

1cd node-file-streams

接下来,在你最喜欢的文本编辑器中创建并打开名为mycliprogram的文件。本教程使用GNUnano,一个终端文本编辑器。

1nano mycliprogram

在文本编辑器中,添加以下代码来指定 shebang,从 Node.js 流程中存储命令行参数,并存储应用程序应有的命令列表。

1[label node-file-streams/mycliprogram]
2#!/usr/bin/env node
3
4const args = process.argv;
5const commands = ['read', 'write', 'copy', 'reverse'];

第一行包含一个 shebang,这是一个通向程序解释器的路径. 添加此行告诉程序加载器使用 Node.js 对该程序进行解析。

当您在命令行上运行 Node.js 脚本时,在 Node.js 流程运行时会传递多个命令行参数. 您可以使用argv属性或Node.js流程访问这些参数。

接下来,创建一个getHelpText函数以显示该程序的使用手册. 将下面的代码添加到您的mycliprogram文件:

 1[label node-file-streams/mycliprogram]
 2...
 3const getHelpText = function() {
 4    const helpText = `
 5    simplecli is a simple cli program to demonstrate how to handle files using streams.
 6    usage:
 7        mycliprogram <command> <path_to_file>
 8
 9        <command> can be:
10        read: Print a file's contents to the terminal
11        write: Write a message from the terminal to a file
12        copy: Create a copy of a file in the current directory
13        reverse: Reverse the content of a file and save its output to another file.
14
15        <path_to_file> is the path to the file you want to work with.
16    `;
17    console.log(helpText);
18}

getHelpText函数打印了您创建的多行字符串作为程序的帮助文本。

接下来,您将添加控制逻辑来检查``的长度,并提供适当的答案:

 1[label node-file-streams/mycliprogram]
 2...
 3let command = '';
 4
 5if(args.length < 3) {
 6    getHelpText();
 7    return;
 8}
 9else if(args.length > 4) {
10    console.log('More arguments provided than expected');
11    getHelpText();
12    return;
13}
14else {
15    command = args[2]
16    if(!args[3]) {
17        console.log('This tool requires at least one path to a file');
18        getHelpText();
19        return;
20    }
21}

在上面的代码片段中,你创建了一个空的字符串命令,以存储从终端接收的命令。第一个如果块检查数组的长度是否小于3;如果小于3,则意味着在运行程序时没有其他额外的参数。

else if块检查了args数组的长度是否大于4;如果是,那么程序已经收到比它需要的更多参数。

最后,在else块中,您将第三个元素或元素存储在command变量中的args数组的第二个索引中,该代码还会检查args数组中是否有第四个元素或指数为3的元素。

保存檔案,然後執行應用程式:

1./mycliprogram

您可能会收到类似于下面的输出的允许被拒绝错误:

1[secondary_label Output]
2-bash: ./mycliprogram: Permission denied

要修复此错误,您需要为文件提供执行权限,您可以使用以下命令:

1chmod +x mycliprogram

重新运行文件. 输出将看起来像这样:

1[secondary_label Output]
2simplecli is a simple cli program to demonstrate how to handle files using streams.
3usage:
4    mycliprogram <command> <path_to_file>
5
6    read: Print a file's contents to the terminal
7    write: Write a message from the terminal to a file
8    copy: Create a copy of a file in the current directory
9    reverse: Reverse the content of a file and save it output to another file.

最后,您将部分执行您之前创建的命令阵列中的命令,打开mycliprogram文件并添加以下代码:

 1[label node-file-streams/mycliprogram]
 2...
 3switch(commands.indexOf(command)) {
 4    case 0:
 5        console.log('command is read');
 6        break;
 7    case 1:
 8        console.log('command is write');
 9        break;
10    case 2:
11        console.log('command is copy');
12        break;
13    case 3:
14        console.log('command is reverse');
15        break;
16    default:
17        console.log('You entered a wrong command. See help text below for supported functions');
18        getHelpText();
19        return;
20}

每次你输入在交换声明中发现的命令时,该程序会运行该命令的相应案例块. 对于此部分实现,你会将命令的名称打印到终端。 如果字符串不在您上面创建的命令列表中,该程序将用帮助文本打印到此目的的消息。

保存文件,然后使用阅读命令和任何文件名重新运行程序:

1./mycliprogram read test.txt

结果将看起来像这样:

1[secondary_label Output]
2command is read

您现在已经成功创建了一个命令行程序. 在下一节中,您将使用createReadStream()在应用程序中复制cat功能作为read命令。

步骤 2 — 阅读一个文件与 createReadStream()

命令行应用程序中的命令将从文件系统中读取文件,并将其打印到终端中,类似于基于Linux的终端中的cat命令。

CreateReadStream函数创建一个可读的流,它发出可以听到的事件,因为它继承了EventsEmitter类别。数据事件是这些事件之一。每当可读的流读一块数据时,它发出数据事件,释放一块数据。当与回调函数一起使用时,它呼吁与该数据片或chunk一起回调,并且您可以在该回调函数内处理该数据。

首先,您可以将文本文件添加到您的工作目录,以便轻松访问。在本节和随后的部分中,您将使用名为lorem-ipsum.txt的文件。 这是一个文本文件,包含使用 Lorem Ipsum Generator生成的 ~ 1200 行 lorem ipsum 文本,并托管在 GitHub上。 在您的终端中,输入以下命令来下载该文件到您的工作目录:

1wget https://raw.githubusercontent.com/do-community/node-file-streams/999e66a11cd04bc59843a9c129da759c1c515faf/lorem-ipsum.txt

要复制你的命令行应用程序中的cat功能,你需要导入fs模块,因为它包含你需要的createReadStream函数。

1[label node-file-streams/mycliprogram]
2#!/usr/bin/env node
3
4const fs = require('fs');

接下来,您将创建一个名为read()交换声明下方的函数,其中有一个参数:您想要阅读的文件的文件路径。

 1[label node-file-streams/mycliprogram]
 2...
 3function read(filePath) {
 4    const readableStream = fs.createReadStream(filePath);
 5
 6    readableStream.on('error', function (error) {
 7        console.log(`error: ${error.message}`);
 8    })
 9
10    readableStream.on('data', (chunk) => {
11        console.log(chunk);
12    })
13}

代码还通过听取错误事件来检查错误,当错误发生时,错误消息会打印到终端。

最后,您应该在第一个案例块中的case 0中用read()函数代替console.log(),如下列代码块所示:

1[label node-file-streams/mycliprogram]
2...
3switch (command){
4    case 0:
5        read(args[3]);
6        break;
7    ...
8}

保存文件以保持新的更改并运行程序:

1./mycliprogram read lorem-ipsum.txt

结果将看起来像这样:

1[secondary_label Output]
2<Buffer 0a 0a 4c 6f 72 65 6d 20 69 70 73 75 6d 20 64 6f 6c 6f 72 20 73 69 74 20 61 6d 65 74 2c 20 63 6f 6e 73 65 63 74 65 74 75 72 20 61 64 69 70 69 73 63 69 ... >
3...
4<Buffer 76 69 74 61 65 20 61 6e 74 65 20 66 61 63 69 6c 69 73 69 73 20 6d 61 78 69 6d 75 73 20 75 74 20 69 64 20 73 61 70 69 65 6e 2e 20 50 65 6c 6c 65 6e 74 ... >

基于上面的输出,你可以看到数据被读成块或块,这些块的数据是缓冲器类型。为了简化,上面的终端输出只显示两个块,而圆形表明这里显示的块之间有几个缓冲器。

若要返回可人读格式的数据,您将通过将您想要的编码类型的字符串值作为第二个参数传递给 createReadStream() 函数来设置数据的编码类型. 在 createReadStream() 函数的第二个参数中,添加以下突出的代码来将编码类型设置为 utf8

1[label node-file-streams/mycliprogram]
2
3...
4const readableStream = fs.createReadStream(filePath, 'utf8')
5...

重新运行该程序将显示文件的内容在终端. 该程序打印了从lorem-ipsum.txt文件一行一行的 lorem ipsum 文本,因为它出现在文件中。

1[secondary_label Output]
2Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean est tortor, eleifend et enim vitae, mattis condimentum elit. In dictum ex turpis, ac rutrum libero tempus sed...
3
4...
5
6...Quisque nisi diam, viverra vel aliquam nec, aliquet ut nisi. Nullam convallis dictum nisi quis hendrerit. Maecenas venenatis lorem id faucibus venenatis. Suspendisse sodales, tortor ut condimentum fringilla, turpis erat venenatis justo, lobortis egestas massa massa sed magna. Phasellus in enim vel ante viverra ultricies.

上面的输出显示了在终端上打印的文件内容的一小部分。当你将终端输出与‘lorem-ipsum.txt’文件进行比较时,你会看到内容与文件相同,并且具有相同的格式化,就像在‘cat’命令中一样。

在本节中,您在命令行程序中实现了cat功能,以便读取文件的内容,并使用createReadStream函数将其打印到终端。

步骤 3 — 写到一个文件与 createWriteStream()

在本节中,您将使用createWriteStream()来编写从终端的输入到文件中。createWriteStream函数返回可编写的文件流,您可以编写数据到。 与上一步中可读的流一样,这个可编写的流会发出一组事件,如error,finishpipe。 此外,它还提供write函数来编写数据到流中的块或比特。 write函数将chunk带入,这可能是一个字符串,一个Buffer,Uint8Array或任何其他JavaScript值。 它还允许您指定编码类型,如果字符串是字符串。

要将输入从终端写入到文件中,您将在命令行程序中创建一个名为写入的函数. 在此函数中,您将创建一个提示,从终端接收输入(直到用户终止)并将数据写入文件。

首先,您需要导入在mycliprogram文件的顶部的readline模块。readline模块是一个原生Node.js模块,您可以使用它来从可读的流中接收数据,例如标准输入(stdin)或您的终端一次一行。

1[label node-file-streams/mycliprogram]
2#!/usr/bin/env node
3
4const fs = require('fs');
5const readline = require('readline');

然后,在read()函数下方添加以下代码。

1[label node-file-streams/mycliprogram]
2...
3function write(filePath) {
4    const writableStream = fs.createWriteStream(filePath);
5
6    writableStream.on('error',  (error) => {
7        console.log(`An error occured while writing to the file. Error: ${error.message}`);
8    });
9}

在这里,您正在使用filePath参数创建可编写的流程. 此文件路径将是字之后的命令行参数. 如果有任何错误,您还正在听取错误事件(例如,如果您提供不存在的filePath).

接下来,您将使用您先前导入的readline模块编写接收终端消息的提示,并将其写入指定的filePath。 为了创建一个 readline 界面,一个提示,并听取line事件,请更新write函数,如图所示:

 1[label node-file-streams/mycliprogram]
 2...
 3function write(filePath) {
 4    const writableStream = fs.createWriteStream(filePath);
 5
 6    writableStream.on('error',  (error) => {
 7        console.log(`An error occured while writing to the file. Error: ${error.message}`);
 8    });
 9
10    const rl = readline.createInterface({
11        input: process.stdin,
12        output: process.stdout,
13        prompt: 'Enter a sentence: '
14    });
15
16    rl.prompt();
17
18    rl.on('line', (line) => {
19        switch (line.trim()) {
20            case 'exit':
21                rl.close();
22                break;
23            default:
24                sentence = line + '\n'
25                writableStream.write(sentence);
26                rl.prompt();
27                break;
28        }
29    }).on('close', () => {
30        writableStream.end();
31        writableStream.on('finish', () => {
32            console.log(`All your sentences have been written to ${filePath}`);
33        })
34        setTimeout(() => {
35            process.exit(0);
36        }, 100);
37    });
38}

您创建了一种 readline 接口(rl),该接口允许程序从您的终端读取标准输入(stdin)并将指定的 prompt 字符串写入标准输出(stdout)。

然后,你在rl界面上连接了两个事件倾听者。第一个倾听每当输入流收到一个端线输入时发出的线事件。 此输入可能是线输入字符(\n),车厢返回字符(\r),或两个字符一起(\r\n),并且通常发生在您在计算机上按下ENTER或返回``键时。

您剪切了行并检查了是否是出口字符,否则程序会将新行字符添加到线中,并使用.write()函数写入filePath中的句子。然后您调用了提示函数,要求用户输入另一行文本。

此函数将我们带到您在rl实例中听到的第二个事件:关闭事件. 此事件在您调用rl.close()时发出。 将数据写入流程后,您必须调用流程上的终止函数,以告知您的程序不再将数据写入可编写流程。

为了向用户提供反馈,即该程序已成功地将所有文本从终端写入到指定的filePath,您在writableStream上收听了完成事件。在回复函数中,您已登录到终端的消息,以便在写完了时通知用户。

最后,要在mycliprogram中调用此函数,请将switch语句中的console.log语句替换为新的write函数,如下所示:

 1[label node-file-streams/mycliprogram]
 2...
 3switch (command){
 4    ...
 5
 6    case 1:
 7        write(args[3]);
 8        break;
 9
10    ...
11}

保存包含新更改的文件,然后使用命令在终端运行命令行应用程序。

1./mycliprogram write output.txt

输入一个句子提示下,添加您想要的任何输入。

输出将看起来类似于此(您的输入显示而不是突出的行):

1[secondary_label Output]
2Enter a sentence: Twinkle, twinkle, little star
3Enter a sentence: How I wonder what you are
4Enter a sentence: Up above the hills so high
5Enter a sentence: Like a diamond in the sky
6Enter a sentence: exit
7All your sentences have been written to output.txt

点击output.txt查看使用您之前创建的阅读命令的文件内容。

1./mycliprogram read output.txt

终端输出应该包含您在命令中输入的所有文本,除了exit

1[secondary_label Output]
2Twinkle, twinkle, little star
3How I wonder what you are
4Up above the hills so high
5Like a diamond in the sky

在此步骤中,您使用流写到一个文件,接下来,您将执行在命令行程序中复制文件的函数。

步骤 4 — 使用 pipe() 复制文件

在此步骤中,您将使用函数创建使用流的文件副本. 虽然有其他方法可以使用流来复制文件,但使用是优先的,因为您不需要管理数据流。

例如,使用流来复制文件的一种方法是为该文件创建可读的流,在数据事件中聆听流,并从流事件中写出每一个chunk到文件副本的可写流。

 1[label example.js]
 2const fs = require('fs');
 3const readableStream = fs.createReadStream('lorem-ipsum.txt', 'utf8');
 4const writableStream = fs.createWriteStream('lorem-ipsum-copy.txt');
 5
 6readableStream.on('data', () => {
 7    writableStream.write(chunk);
 8});
 9
10writableStream.end();

这种方法的缺点是,您需要在可读和可写流上管理事件。

使用流来复制文件的首选方法是使用管道。水管将水从源头(如水罐(输出)传递到水管或输入(输入)中。同样,你使用管道将数据从输出流向输入流。

Node.js 中的 Piping 提供了从源头读取数据并在其他地方写入数据的能力,而不像使用第一个方法一样管理数据流。

mycliprogram文件中,您将添加一个新函数,当用户使用复制命令行参数运行该程序时,它将被召唤。 复制方法将使用pipe()来从输入文件复制到目的地复制该文件。

 1[label node-file-streams/mycliprogram]
 2...
 3function copy(filePath) {
 4    const inputStream = fs.createReadStream(filePath)
 5    const fileCopyPath = filePath.split('.')[0] + '-copy.' + filePath.split('.')[1]
 6    const outputStream = fs.createWriteStream(fileCopyPath)
 7
 8    inputStream.pipe(outputStream)
 9
10    outputStream.on('finish', () => {
11        console.log(`You have successfully created a ${filePath} copy. The new file name is ${fileCopyPath}.`);
12    })
13}

在复制函数中,您使用fs.createReadStream()创建了输入或可读流,您还为目的地生成了新名称,输出了文件的副本,并使用fs.createWriteStream()创建了输出或可写流。

请记住,要关闭可写流,你必须在流中调用end()函数。当管道流时,在可写流(outputStream)上调用end()函数,当可读流(inputStream)发出end事件时。

若要看到此函数在运作中,请打开mycliprogram文件并如下所示更新switch陈述的案例2块:

 1[label node-file-streams/mycliprogram]
 2...
 3switch (command){
 4    ...
 5
 6    case 2:
 7        copy(args[3]);
 8        break;
 9
10    ...
11}

调用复制函数在案例2块中的交换语句,确保当你运行mycliprogram程序与复制命令和所需的文件路径时,复制函数被执行。

点击点击MYCLIPROGRAM:

1./mycliprogram copy lorem-ipsum.txt

结果将看起来像这样:

1[secondary_label Output]
2You have successfully created a lorem-ipsum-copy.txt copy. The new file name is lorem-ipsum-copy.txt.

node-file-streams文件夹中,你会看到一个新添加的文件名为lorem-ipsum-copy.txt

您已成功地使用pipe将复制函数添加到命令行程序中,在下一步中,您将使用流来修改文件的内容。

步骤 5 – 使用转换( )来逆转文件的内容

在本教程的前三个步骤中,您已经使用了使用fs模块的流程。在本节中,您将使用原生stream模块的Transform()类来修改文件流程,该模块提供了转换流程。您可以使用转换流程来读取数据,操纵数据,并作为输出提供新数据。因此,输出是输入数据的转换。使用转换流程的Node.js模块包括加密的crypto模块和zlib模块,用于压缩和脱压文件。

您将使用Transform()抽象类实现自定义转换流,而您创建的转换流将每行转换一个文件的内容,这将展示如何使用转换流来随意修改文件的内容。

mycliprogram文件中,您将添加一个反向函数,该程序将在用户通过反向命令行参数时调用。

首先,您需要在其他导入下方的文件顶部导入 Transform() 类,如下所示:

1[label mycliprogram]
2#!/usr/bin/env node
3...
4const stream = require('stream');
5const Transform = stream.Transform || require('readable-stream').Transform;

v0.10之前的 Node.js 版本中,缺少Transform抽象类,因此,上面的代码块包括可读流的多元文件,以便该程序可以与早期版本的 Node.js 工作。

<$>[注] 注: 如果您正在使用 Node.js 版本 < 0.10,您将需要运行 npm init -y 来创建 package.json 文件,并将使用 npm install readable-stream 安装到您的工作目录中,以便将其应用。

在该函数中,您将使用filePath参数创建可读流,生成被逆转文件的名称,并使用该名称创建可写流,然后创建transform()类的实例reverseStream

复制函数下方,添加下面的代码块来添加反向函数。

 1[label node-file-streams/mycliprogram]
 2...
 3function reverse(filePath) {
 4    const readStream = fs.createReadStream(filePath);
 5    const reversedDataFilePath = filePath.split('.')[0] + '-reversed.'+ filePath.split('.')[1];
 6    const writeStream = fs.createWriteStream(reversedDataFilePath);
 7
 8    const reverseStream = new Transform({
 9        transform (data, encoding, callback) {
10            const reversedData = data.toString().split("").reverse().join("");
11            this.push(reversedData);
12            callback();
13        }
14    });
15
16    readStream.pipe(reverseStream).pipe(writeStream).on('finish', () => {
17        console.log(`Finished reversing the contents of ${filePath} and saving the output to ${reversedDataFilePath}.`);
18    });
19}

转换函数接收三个参数:数据编码类型和回调函数. 在此函数中,您将数据转换为字符串,将字符串划分,将结果的数组内容逆转,并将它们重新合并在一起。

接下来,您将readStream连接到reverseStream,并使用两个pipe()函数连接到writeStream

您会注意到上面的代码使用另一个语法来听完成事件,而不是在新行上听写流完成事件,您将打开函数链接到第二个函数。

要将事物包裹起来,请将switch陈述的案例 3块中的console.log陈述替换为reverse()

 1[label node-file-streams/mycliprogram]
 2...
 3switch (command){
 4    ...
 5
 6    case 3:
 7        reverse(args[3]);
 8        break;
 9
10    ...
11}

要测试此功能,您将使用含有国家名称在字母顺序的另一个文件(countries.csv)。

1wget https://raw.githubusercontent.com/do-community/node-file-streams/999e66a11cd04bc59843a9c129da759c1c515faf/countries.csv

然后你可以运行mycliprogram

1./mycliprogram reverse countries.csv

结果将看起来像这样:

1[secondary_label Output]
2Finished reversing the contents of countries.csv and saving the output to countries-reversed.csv.

country-reversed.csv的内容与country.csv的内容进行比较,以便看到转换。现在每个名字都被写回头,名字的顺序也被扭转了(Afghanistan被写为natsinahgfA并出现在最后,而Zimbabwe被写为ewbabmiZ并出现在第一个)。

您已成功创建了自定义转换流,您还创建了一个命令行程序,具有用于文件处理的流功能。

结论

在 Node.js 原生模块中,以及各种yarnnpm包中,流程可以执行输入/输出操作,因为它们提供了处理数据的高效方法。在本文中,您使用了各种基于流程的函数来与 Node.js 中的文件一起工作。您用read,write,copyreverse命令构建了一个命令行程序,然后将每个命令应用于相应命名的函数中。 为了执行这些函数,您使用了createReadStream,createWriteStream,pipefs模块中,从readline模块中创建Interface函数,最后将Transform()类抽象化。 最后,您将这些函数组成一个小型命

作为下一步,您可以扩展您创建的命令行程序,以包括其他文件系统功能,您可能希望本地使用。

您所编写的命令行程序处理命令行参数本身,并使用一个简单的提示,以获取用户输入. 您可以了解更多关于构建更强大和可维护的命令行应用程序的信息,如下 如何在 Node.js 脚本中处理命令行参数如何使用 Inquirer.js 创建交互式命令行参数

此外,Node.js 提供了有关您可能需要的各种 Node.js 流模块(LINK0)类、方法和事件的广泛文档。

Published At
Categories with 技术
comments powered by Disqus