如何使用 Node-CSV 在 Node.js 中读写 CSV 文件

作者选择了 女性工程师协会作为 写给捐赠计划的一部分获得捐赠。

介绍

CSV 是用于存储表格数据的简单文本文件格式. CSV 文件使用符号划分器来分离表单元格中的值,而新的行则描述了行开始和结束的地方. 大多数电子表格程序和数据库可以导出和导入 CSV 文件. 由于 CSV 是一个简单文本文件,任何编程语言都可以对 CSV 文件进行解析和写入。 Node.js有许多模块可以与 CSV 文件一起工作,例如 node-csv, fast-csv和 [papaparse(LINK3])。

在本教程中,您将使用node-csv模块读取 CSV 文件使用 Node.js 流,允许您读取大数据集而不消耗大量内存。您将修改程序,将从 CSV 文件中解析的数据移动到 SQLite 数据库中。

前提条件

要遵循本教程,您将需要:

步骤 1 – 设置项目目录

在本节中,您将创建项目目录并下载您的应用程序的包,您还将从 Stats NZ下载一个CSV数据集,其中包含新西兰的国际迁移数据。

要开始,创建一个名为csv_demo的目录,并导航到目录:

1mkdir csv_demo
2cd csv_demo

接下来,使用npm init命令将目录初始化为 npm 项目:

1npm init -y

-y 选项通知 npm init 对所有提示说 此命令创建一个 package.json 与默认值,您可以随时更改。

使用初始化为 npm 项目的目录,您现在可以安装所需的依赖性: node-csvnode-sqlite3

输入以下命令来安装 node-csv:

1npm install csv

node-csv模块是一个模块集合,允许您对 CSV 文件进行解析和编写数据。该命令安装了所有四个组件,这些模块是node-csv包的一部分:csv-generate,csv-parse,csv-stringifystream-transform

接下来,安装node-sqlite3模块:

1npm install sqlite3

node-sqlite3模块允许您的应用程序与 SQLite 数据库进行交互。

在您项目中安装包后,请下载新西兰迁移CSV文件,使用wget命令:

1wget https://www.stats.govt.nz/assets/Uploads/International-migration/International-migration-September-2021-Infoshare-tables/Download-data/international-migration-September-2021-estimated-migration-by-age-and-sex-csv.csv

您下载的 CSV 文件有一个长的名称. 为了方便您工作,请使用 mv 命令将文件名改为更短的名称:

1mv international-migration-September-2021-estimated-migration-by-age-and-sex-csv.csv migration_data.csv

新的 CSV 文件名 migration_data.csv 更短,更容易使用。

使用nano或您最喜欢的文本编辑器,打开文件:

1nano migration_data.csv

一旦打开,你会看到类似于此的内容:

1[label demo_csv/migration_data.csv]
2year_month,month_of_release,passenger_type,direction,sex,age,estimate,standard_error,status
32001-01,2020-09,Long-term migrant,Arrivals,Female,0-4 years,344,0,Final
42001-01,2020-09,Long-term migrant,Arrivals,Male,0-4 years,341,0,Final
5...

第一行包含列名称,所有后续行都有相应的数据,每个列都有一个字符串分开每个数据片段,这个字符被称为分界符,因为它划界了字段,您不受限于使用字符串。 其他流行的分界符包括列(:),半色列(;)和卡(\td)。 您需要知道文件中使用的分界符是哪个,因为大多数模块需要它来解析文件。

檢查檔案並識別分界器後,使用「CTRL + X」退出您的「migration_data.csv」檔案。

您现在已经安装了您项目所需的依赖性,下一节,您将阅读 CSV 文件。

步骤 2 – 阅读 CSV 文件

在本节中,您将使用node-csv读取 CSV 文件并在控制台中登录其内容。您将使用fs模块的createReadStream()方法从 CSV 文件中读取数据并创建可读的流程。

在您喜爱的编辑器中创建并打开readCSV.js文件:

1nano readCSV.js

readCSV.js文件中,通过添加以下行来导入fscsv-parse模块:

1[label demo_csv/readCSV.js]
2const fs = require("fs");
3const { parse } = require("csv-parse");

在第一行中,您定义fs变量,并分配给它fs对象,Node.jsrequire()方法在导入模块时返回。

在第二行中,您将parse方法从require()方法返回的对象中提取到parse变量中,使用destructuring syntax(https://andsky.com/tech/tutorials/understanding-destructuring-rest-parameters-and-spread-syntax-in-javascript)。

添加以下行来阅读 CSV 文件:

1[label demo_csv/readCSV.js]
2...
3fs.createReadStream("./migration_data.csv")
4  .pipe(parse({ delimiter: ",", from_line: 2 }))
5  .on("data", function (row) {
6    console.log(row);
7  })

fs模块中使用的createReadStream()方法接受您要阅读的文件名参数,即这里的migration_data.csv。然后,它创建了一个可读的 stream,它将一个大文件取出并将其分成较小的片段。

在创建可读流之后,Node 的pipe()方法将可读流的数据转移到另一个流中。第二条流是创建的,当csv-parse模块的parse()方法在pipe()方法中被召唤时。csv-parse模块实现了转换流(可读和可写流),将数据片段转换成另一种形式。例如,当它收到一个片段,如2001-01,2020-09,长期迁移,到来,女性,0-4年,344,parse()方法将它转化为一个数组。

「parse()」方法采用一个接受属性的对象,然后对象配置并提供有关该方法要解析的数据的更多信息。

  • delimiter 定义了在行中分离每个字符的字符. 值 , 告诉解析器符号划分字段。
  • from_line 定义了解析器应该开始解析字段的行。 使用值 2,解析器会跳过第 1 行并从第 2 行开始。

接下来,您使用 Node.js on() 方法添加流媒体事件。 流媒体事件允许该方法在发送特定事件时消耗一小块数据。 当从 parse() 方法转换的数据准备好被消耗时, data 事件会被触发。 为了访问数据,您将调用转换到 on() 方法,该方法采用名为 row 的参数。 row 参数是将数据变成一个数组的数据片段。 在调用中,您使用 `console.log() 方法在控制台中登录数据。

在运行文件之前,您将添加更多的流事件. 这些流事件处理错误,并在 CSV 文件中的所有数据消耗后向控制台写成功消息。

仍然在你的readCSV.js文件中,添加突出的代码:

 1[label demo_csv/readCSV.js]
 2...
 3fs.createReadStream("./migration_data.csv")
 4  .pipe(parse({ delimiter: ",", from_line: 2 }))
 5  .on("data", function (row) {
 6    console.log(row);
 7  })
 8  .on("end", function () {
 9    console.log("finished");
10  })
11  .on("error", function (error) {
12    console.log(error.message);
13  });

当 CSV 文件中的所有数据都读完后,发出终结事件,当发生这种情况时,召回回调用并记录一个消息,称它已经完成。

如果在阅读和解析 CSV 数据时发生错误,会发出错误事件,该事件会召回回调用并在控制台中记录错误消息。

您的完整文件现在应该看起来如下:

 1[label demo_csv/readCSV.js]
 2const fs = require("fs");
 3const { parse } = require("csv-parse");
 4
 5fs.createReadStream("./migration_data.csv")
 6  .pipe(parse({ delimiter: ",", from_line: 2 }))
 7  .on("data", function (row) {
 8    console.log(row);
 9  })
10  .on("end", function () {
11    console.log("finished");
12  })
13  .on("error", function (error) {
14    console.log(error.message);
15  });

保存并退出您的readCSV.js文件,使用CTRL+X

接下来,使用node命令运行文件:

1node readCSV.js

输出将看起来类似于此(编辑以简化):

 1[secondary_label Output]
 2[
 3  '2001-01',
 4  '2020-09',
 5  'Long-term migrant',
 6  'Arrivals',
 7  'Female',
 8  '0-4 years',
 9  '344',
10  '0',
11  'Final'
12]
13...
14[
15  '2021-09',
16  ...
17  '70',
18  'Provisional'
19]
20finished

CSV 文件中的所有行都通过csv-parse转换流被转换为数组,因为每当从流中接收一小部分时都会发生日志,所以数据似乎正在下载,而不是同时显示。

在此步骤中,您会读取 CSV 文件中的数据并将其转换为数组,然后将 CSV 文件中的数据插入到数据库中。

步骤 3 – 将数据插入数据库

使用 Node.js 将 CSV 文件中的数据插入数据库,可以访问广泛的模块库,您可以在将数据插入数据库之前使用它们来处理、清理或增强数据。

在本节中,您将使用node-sqlite3模块与 SQLite 数据库建立连接,然后在数据库中创建表,复制readCSV.js文件,并修改它以将从 CSV 文件中读取的所有数据插入到数据库中。

在您的编辑器中创建并打开db.js文件:

1nano db.js

db.js文件中,添加以下行来导入fsnode-sqlite3模块:

1[label demo_csv/db.js]
2const fs = require("fs");
3const sqlite3 = require("sqlite3").verbose();
4const filepath = "./population.db";
5...

在第三行中,您定义 SQLite 数据库的路径,并将其存储在变量 filepath 中. 数据库文件尚不存在,但需要 node-sqlite3 才能与数据库建立连接。

在相同的文件中,添加以下行以将 Node.js 连接到 SQLite 数据库:

 1[label demo_csv/db.js]
 2...
 3function connectToDatabase() {
 4  if (fs.existsSync(filepath)) {
 5    return new sqlite3.Database(filepath);
 6  } else {
 7    const db = new sqlite3.Database(filepath, (error) => {
 8      if (error) {
 9        return console.error(error.message);
10      }
11      console.log("Connected to the database successfully");
12    });
13    return db;
14  }
15}

在这里,您定义了一个名为connectToDatabase()的函数,以建立与数据库的连接。在该函数中,您在一个if陈述中调用了fs模块的existsSync()方法,该陈述检查了数据库文件是否存在于项目目录中。如果if条件评估为true,则您将SQLite的Database()node-sqlite3模块与数据库文件路径进行实例化。

但是,如果如果语句评估为 (如果数据库文件不存在),执行将跳过到else块。

第一个参数是 SQLite 数据库文件的路径,即 ./population.db. 第二个参数是召回,当与数据库的连接成功建立或出现错误时会自动召回。召回将一个 error 对象作为参数,如果连接成功时就是 null。在召回中, if 声明会检查是否设置了 error 对象。如果评估为 true,则召回会记录一个错误消息并返回。

目前,如果else块建立了连接对象. 您在else块中召唤数据库类时传递回调,以在数据库中创建表,但仅在数据库文件不存在的情况下。

如果数据库文件不存在,则要创建表,请添加突出的代码:

 1[label demo_csv/db.js]
 2const fs = require("fs");
 3const sqlite3 = require("sqlite3").verbose();
 4const filepath = "./population.db";
 5
 6function connectToDatabase() {
 7  if (fs.existsSync(filepath)) {
 8    return new sqlite3.Database(filepath);
 9  } else {
10    const db = new sqlite3.Database(filepath, (error) => {
11      if (error) {
12        return console.error(error.message);
13      }
14      createTable(db);
15      console.log("Connected to the database successfully");
16    });
17    return db;
18  }
19}
20
21function createTable(db) {
22  db.exec(`
23  CREATE TABLE migration
24  (
25    year_month VARCHAR(10),
26    month_of_release VARCHAR(10),
27    passenger_type VARCHAR(50),
28    direction VARCHAR(20),
29    sex VARCHAR(10),
30    age VARCHAR(50),
31    estimate INT
32  )
33`);
34}
35
36module.exports = connectToDatabase();

现在connectToDatabase()召唤了createTable()函数,该函数将存储在db变量中的连接对象作为参数。

connectToDatabase()函数之外,您定义了createTable()函数,该函数将连接对象db作为参数。 在db连接对象上,您调用了exec()方法,该方法将 SQL 语句作为参数。

最后,您调用connectToDatabase()函数并导出该函数返回的连接对象,以便在其他文件中重复使用。

保存并退出您的db.js文件。

有了建立的数据库连接,您现在将复制和修改readCSV.js文件,将csv-parse模块对数据库进行解析的行插入。

复制并重命名文件为insertData.js使用以下命令:

1cp readCSV.js insertData.js

在您的编辑器中打开insertData.js文件:

1nano insertData.js

添加突出的代码:

 1[label demo_csv/insertData.js]
 2const fs = require("fs");
 3const { parse } = require("csv-parse");
 4const db = require("./db");
 5
 6fs.createReadStream("./migration_data.csv")
 7  .pipe(parse({ delimiter: ",", from_line: 2 }))
 8  .on("data", function (row) {
 9    db.serialize(function () {
10      db.run(
11        `INSERT INTO migration VALUES (?, ?, ? , ?, ?, ?, ?)`,
12        [row[0], row[1], row[2], row[3], row[4], row[5], row[6]],
13        function (error) {
14          if (error) {
15            return console.log(error.message);
16          }
17          console.log(`Inserted a row with the id: ${this.lastID}`);
18        }
19      );
20    });
21  });

在第三行中,您从db.js文件中导入连接对象,并将其存储在变量db中。

在附带到fs模块流的数据事件调用中,您在连接对象上调用了serialize()方法,该方法确保 SQL 语句在另一个语句开始执行之前完成执行,这有助于防止数据库竞赛条件,系统同时运行竞争操作。

「serialize()」方法會採取回應。在回應中,您會在「db」連接對象上召喚「 Run」方法。

第一种参数是 SQL 语句,将通过并在 SQLite 数据库中执行。运行()方法只接受不会返回结果的 SQL 语句。INSERT INTO migration VALUES(?,...,?语句将一行插入表迁移中,而?是随后被运行()方法第二个参数中的值所取代的位置持有人。 *第二种参数是数组指数,如[row[0],... row[5], row[6]等。在上一节中,parse() 方法从可读流中接收一部分数据,并将其转化为一个序列。由于数据被接收为数组,以获取每个字段值,您必须使用数组信息指数访问它们,如[

最后,从文件中删除终结错误事件.由于node-sqlite3方法的非同步性质,在数据被插入数据库之前执行终结错误事件,因此不再需要它们。

保存和退出您的文件。

使用node运行insertData.js文件:

1node insertData.js

取决于您的系统,可能需要一段时间,但节点应该返回下面的输出:

1[secondary_label Output]
2Connected to the database successfully
3Inserted a row with the id: 1
4Inserted a row with the id: 2
5...
6Inserted a row with the id: 44308
7Inserted a row with the id: 44309
8Inserted a row with the id: 44310

该消息,特别是ID,证明 CSV 文件的行已被保存到数据库中。

您现在可以阅读 CSV 文件并将其内容插入数据库中,接下来,您将写一个 CSV 文件。

步骤 4 – 写 CSV 文件

在本节中,您将从数据库中获取数据并使用流将其写入 CSV 文件中。

在您的编辑器中创建并打开writeCSV.js:

1nano writeCSV.js

WriteCSV.js文件中,添加以下行来导入fscsv-stringify模块以及来自db.js的数据库连接对象:

1[label demo_csv/writeCSV.js]
2const fs = require("fs");
3const { stringify } = require("csv-stringify");
4const db = require("./db");

csv-stringify模块将对象或数组中的数据转换为CSV文本格式。

接下来,添加以下行来定义含有您要写数据的 CSV 文件名和您要写数据的可写流的变量:

 1[label demo_csv/writeCSV.js]
 2...
 3const filename = "saved_from_db.csv";
 4const writableStream = fs.createWriteStream(filename);
 5
 6const columns = [
 7  "year_month",
 8  "month_of_release",
 9  "passenger_type",
10  "direction",
11  "sex",
12  "age",
13  "estimate",
14];

createWriteStream方法取决于您要编写数据流的文件名,即存储在文件名变量中的saved_from_db.csv文件名。

在第四行中,您定义了一个变量,该变量存储包含 CSV 数据的标题名称的数组,这些标题将在 CSV 文件的第一行中写入,当您开始将数据写入文件时。

仍然在你的 writeCSV.js 文件中,添加以下行来从数据库中获取数据,并在 CSV 文件中写下每个行:

 1[label demo_csv/writeCSV.js]
 2...
 3const stringifier = stringify({ header: true, columns: columns });
 4db.each(`select * from migration`, (error, row) => {
 5  if (error) {
 6    return console.log(error.message);
 7  }
 8  stringifier.write(row);
 9});
10stringifier.pipe(writableStream);
11console.log("Finished writing data");

首先,您将用一个对象作为参数调用stringify方法,从而创建一个转换流. 转换流将数据从一个对象转换为CSV文本. 通过stringify()方法传输的对象有两个属性:

  • header 接受 boolean 值,如果 boolean 值设置为 true,则生成一个标题。
  • columns 采用包含列的名称的数组,如果 header 选项设置为 true,则将被写入 CSV 文件的第一行。

接下来,您将从db连接对象中调用each()方法,使用两种参数。第一个参数是 SQL 语句从迁移中选择 *来检索数据库中的每一个行。第二个参数是每次从数据库中检索一个行时调用的调用回复。调用回复需要两个参数:一个错误对象和一个包含数据从数据库中的单一行中检索到的数据的序列对象。在调用回复中,您会检查错误对象是否设置在if语句中。如果条件评估为true,则使用console.()日志方法在控制台中登录错误消息。如果没有错误,则在stringifier上调用write()方法

each()方法完成迭代时,pipe()方法在stringifier流中开始将数据发送成片段,并将其写入writableStream

完整的文件现在将看起来如下:

 1[label demo_csv/writeCSV.js]
 2const fs = require("fs");
 3const { stringify } = require("csv-stringify");
 4const db = require("./db");
 5const filename = "saved_from_db.csv";
 6const writableStream = fs.createWriteStream(filename);
 7
 8const columns = [
 9  "year_month",
10  "month_of_release",
11  "passenger_type",
12  "direction",
13  "sex",
14  "age",
15  "estimate",
16];
17
18const stringifier = stringify({ header: true, columns: columns });
19db.each(`select * from migration`, (error, row) => {
20  if (error) {
21    return console.log(error.message);
22  }
23  stringifier.write(row);
24});
25stringifier.pipe(writableStream);
26console.log("Finished writing data");

保存并关闭您的文件,然后在终端中运行writeCSV.js文件:

1node writeCSV.js

您将获得以下输出:

1[secondary_label Output]
2Finished writing data

要确认数据已被写入,请使用cat命令检查文件中的内容:

1cat saved_from_db.csv

'cat' 會返回檔案中所寫的所有行(以簡潔性編輯):

1[secondary_label Output]
2year_month,month_of_release,passenger_type,direction,sex,age,estimate
32001-01,2020-09,Long-term migrant,Arrivals,Female,0-4 years,344
42001-01,2020-09,Long-term migrant,Arrivals,Male,0-4 years,341
52001-01,2020-09,Long-term migrant,Arrivals,Female,10-14 years,
6...

现在,您可以从数据库中获取数据,并使用流将每个行写入 CSV 文件中。

结论

在本文中,您读过 CSV 文件并使用node-csvnode-sqlite3模块将其数据插入数据库中,然后从数据库中获取数据并将其写入另一个 CSV 文件中。

作为下一个步骤,您现在可以使用相同的实现来处理大型 CSV 数据集,使用具有记忆效率的流,或者您可以寻找一个类似 event-stream的包,使与流的工作变得更容易。

要了解更多关于node-csv的信息,请访问他们的文档 CSV 项目 - Node.js CSV 包。 要了解更多关于node-sqlite3的信息,请访问他们的 Github 文档。 要继续发展您的 Node.js 技能,请参阅 如何在 Node.js 系列中编码.

Published At
Categories with 技术
comments powered by Disqus