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

作者选择了女性工程师协会作为写作捐赠计划的捐赠对象。

介绍

A CSV is a plain text file format for storing tabular data. The CSV file uses a comma delimiter to separate values in table cells, and a new line delineates where rows begin and end. Most spreadsheet programs and databases can export and import CSV files. Because CSV is a plain-text file, any programming language can parse and write to a CSV file. Node.js has many modules that can work with CSV files, such as node-csv, fast-csv, and papaparse.

在本教程中,您将使用node-csv模块来读取一个 CSV 文件,使用 Node.js 流,这使您可以读取大型数据集而不会消耗大量内存。您将修改程序以将从 CSV 文件中解析的数据移入 SQLite 数据库。您还将从数据库检索数据,用node-csv解析它,并使用 Node.js 流将其以块的形式写入 CSV 文件。

使用DigitalOcean 应用平台从 GitHub 部署您的 Node 应用程序。让 DigitalOcean 关注您的应用程序的扩展。

先决条件

要遵循本教程,您需要:

步骤1 — 设置项目目录

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

要开始,请创建一个名为csv_demo的目录并进入该目录:

  1. mkdir csv_demo
  2. cd csv_demo

然后,使用npm init命令将该目录初始化为npm项目:

  1. npm init -y

使用-y选项通知npm init以对所有提示回答“是”。此命令将创建一个具有默认值的package.json,您随时可以更改它。

将目录初始化为npm项目后,现在可以安装必要的依赖项:node-csvnode-sqlite3

输入以下命令安装node-csv

  1. npm install csv

node-csv 模块是一个模块集合,允许您解析和写入数据到 CSV 文件。该命令安装 node-csv 包的四个模块:csv-generatecsv-parsecsv-stringifystream-transform。您将使用 csv-parse 模块来解析 CSV 文件,使用 csv-stringify 模块将数据写入 CSV 文件。

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

  1. npm install sqlite3

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

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

  1. wget 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 命令将文件名改为较短的名称:

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

新的 CSV 文件名 migration_data.csv 更短,更容易处理。

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

  1. nano migration_data.csv

一旦打开,您将看到类似于以下内容:

demo_csv/migration_data.csv
year_month,month_of_release,passenger_type,direction,sex,age,estimate,standard_error,status
2001-01,2020-09,Long-term migrant,Arrivals,Female,0-4 years,344,0,Final
2001-01,2020-09,Long-term migrant,Arrivals,Male,0-4 years,341,0,Final
...

第一行包含列名称,所有后续行都有对应于每列的数据。逗号分隔每个数据片段。此字符称为分隔符,因为它界定了字段。您不限于使用逗号。其他常用分隔符包括冒号(:)、分号(;)和制表符(\t)。您需要知道文件中使用的分隔符,因为大多数模块需要它来解析文件。

检查文件并识别分隔符后,使用CTRL+X退出您的migration_data.csv文件。

现在您已经安装了项目所需的必要依赖项。在下一节中,您将读取一个CSV文件。

步骤2 — 读取CSV文件

在本节中,您将使用node-csv来读取一个CSV文件,并在控制台中记录其内容。您将使用fs模块的createReadStream()方法从CSV文件中读取数据并创建一个可读流。然后,您将流导向另一个使用csv-parse模块初始化的流,以解析数据块。一旦数据块被解析,您就可以在控制台中将它们记录下来。

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

  1. nano readCSV.js

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

demo_csv/readCSV.js
const fs = require("fs");
const { parse } = require("csv-parse");

在第一行中,您定义了fs变量,并将其赋值为Node.js require()方法导入模块时返回的fs对象。

在第二行,您使用解构语法将从require()方法返回的对象中提取parse方法,并将其赋值给parse变量。

添加以下行以读取CSV文件:

demo_csv/readCSV.js
...
fs.createReadStream("./migration_data.csv")
  .pipe(parse({ delimiter: ",", from_line: 2 }))
  .on("data", function (row) {
    console.log(row);
  })

fs模块的createReadStream()方法接受要读取的文件名作为参数,在这里是migration_data.csv。然后,它创建一个可读,将大文件分解为较小的块。可读流允许您仅从中读取数据而不写入数据。

创建可读流后,Node的pipe()方法将数据块从可读流转发到另一个流。当在pipe()方法中调用csv-parse模块的parse()方法时,将创建第二个流。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文件中,添加如下突出显示的代码:

demo_csv/readCSV.js
...
fs.createReadStream("./migration_data.csv")
  .pipe(parse({ delimiter: ",", from_line: 2 }))
  .on("data", function (row) {
    console.log(row);
  })
  .on("end", function () {
    console.log("finished");
  })
  .on("error", function (error) {
    console.log(error.message);
  });

end事件在CSV文件中的所有数据被读取时被触发。当发生这种情况时,将调用回调函数并记录一条消息,说明已经完成。

如果在读取和解析CSV数据的任何地方发生错误,则会发出error事件,该事件会调用回调函数并在控制台中记录错误消息。

现在您的完整文件应该如下所示:

demo_csv/readCSV.js
const fs = require("fs");
const { parse } = require("csv-parse");

fs.createReadStream("./migration_data.csv")
  .pipe(parse({ delimiter: ",", from_line: 2 }))
  .on("data", function (row) {
    console.log(row);
  })
  .on("end", function () {
    console.log("finished");
  })
  .on("error", function (error) {
    console.log(error.message);
  });

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

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

  1. node readCSV.js

输出将类似于以下内容(已编辑以节省空间):

Output
[ '2001-01', '2020-09', 'Long-term migrant', 'Arrivals', 'Female', '0-4 years', '344', '0', 'Final' ] ... [ '2021-09', ... '70', 'Provisional' ] finished

使用csv-parse转换流,将CSV文件中的所有行转换为数组。因为每次从流中接收到一个块时都会进行日志记录,所以数据看起来好像是在下载而不是一次性显示。

在此步骤中,您读取了CSV文件中的数据并将其转换为数组。接下来,您将向数据库插入来自CSV文件的数据。

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

使用 Node.js 将 CSV 文件中的数据插入数据库,使您能够访问大量模块库,您可以在将数据插入数据库之前使用这些库来处理、清理或增强数据。

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

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

  1. nano db.js

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

demo_csv/db.js
const fs = require("fs");
const sqlite3 = require("sqlite3").verbose();
const filepath = "./population.db";
...

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

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

demo_csv/db.js
...
function connectToDatabase() {
  if (fs.existsSync(filepath)) {
    return new sqlite3.Database(filepath);
  } else {
    const db = new sqlite3.Database(filepath, (error) => {
      if (error) {
        return console.error(error.message);
      }
      console.log("Connected to the database successfully");
    });
    return db;
  }
}

在这里,你定义了一个名为connectToDatabase()的函数,用于建立与数据库的连接。在函数内部,你在一个if语句中调用了fs模块的existsSync()方法,该方法用于检查项目目录中是否存在数据库文件。如果if条件评估为true,则会使用数据库文件路径实例化node-sqlite3模块的SQLite的Database()类。一旦连接建立成功,函数就会返回连接对象并退出。

然而,如果if语句评估为false(即数据库文件不存在),执行将跳转到else块。在else块中,你使用两个参数实例化了Database()类:数据库文件路径和一个回调函数。

第一个参数是SQLite数据库文件的路径,即./population.db。第二个参数是一个回调函数,在与数据库成功建立连接或发生错误时会自动调用。回调函数接受一个error对象作为参数,如果连接成功则为null。在回调函数内部,if语句检查error对象是否已设置。如果评估为true,则回调函数会记录错误消息并返回。如果评估为false,则会记录连接已建立的成功消息。

当前,ifelse 块建立了连接对象。在 else 块中调用 Database 类时传递了一个回调函数,用于在数据库中创建表,但仅在数据库文件不存在时才这样做。如果数据库文件已经存在,该函数将执行 if 块,连接到数据库,并返回连接对象。

要在数据库文件不存在时创建表,请添加以下突出显示的代码:

demo_csv/db.js
const fs = require("fs");
const sqlite3 = require("sqlite3").verbose();
const filepath = "./population.db";

function connectToDatabase() {
  if (fs.existsSync(filepath)) {
    return new sqlite3.Database(filepath);
  } else {
    const db = new sqlite3.Database(filepath, (error) => {
      if (error) {
        return console.error(error.message);
      }
      createTable(db);
      console.log("Connected to the database successfully");
    });
    return db;
  }
}

function createTable(db) {
  db.exec(`
  CREATE TABLE migration
  (
    year_month       VARCHAR(10),
    month_of_release VARCHAR(10),
    passenger_type   VARCHAR(50),
    direction        VARCHAR(20),
    sex              VARCHAR(10),
    age              VARCHAR(50),
    estimate         INT
  )
`);
}

module.exports = connectToDatabase();

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

connectToDatabase() 函数之外,定义了 createTable() 函数,该函数将连接对象 db 作为参数。在 db 连接对象上调用了 exec() 方法,该方法接受一个 SQL 语句作为参数。SQL 语句创建了一个名为 migration 的表,其中包含 7 列。列名与 migration_data.csv 文件中的标题相匹配。

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

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

数据库连接已建立,现在您将复制并修改 readCSV.js 文件,以将 csv-parse 模块解析的行插入到数据库中。

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

  1. cp readCSV.js insertData.js

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

  1. nano insertData.js

在突出显示的代码中添加:

demo_csv/insertData.js
const fs = require("fs");
const { parse } = require("csv-parse");
const db = require("./db");

fs.createReadStream("./migration_data.csv")
  .pipe(parse({ delimiter: ",", from_line: 2 }))
  .on("data", function (row) {
    db.serialize(function () {
      db.run(
        `INSERT INTO migration VALUES (?, ?, ? , ?, ?, ?, ?)`,
        [row[0], row[1], row[2], row[3], row[4], row[5], row[6]],
        function (error) {
          if (error) {
            return console.log(error.message);
          }
          console.log(`Inserted a row with the id: ${this.lastID}`);
        }
      );
    });
  });

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

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

serialize()方法接受一个回调。在回调中,你调用db连接对象上的run方法。该方法接受三个参数:

  • 第一个参数是将传递并在SQLite数据库中执行的SQL语句。run()方法仅接受不返回结果的SQL语句。INSERT INTO migration VALUES (?, ..., ?语句在表migration中插入一行,并且?是稍后在run()方法的第二个参数中替换为值的占位符。

  • 第二个参数是一个数组 [row[0], ... row[5], row[6]]。在前面的部分中,parse() 方法从可读流中接收一块数据并将其转换为数组。由于数据以数组形式接收,要获取每个字段的值,必须使用数组索引来访问它们,如 [row[1], ..., row[6]] 等。

  • 第三个参数是一个回调函数,当数据已插入或发生错误时运行。该回调检查是否发生错误并记录错误消息。如果没有错误,函数将使用 console.log() 方法在控制台中记录成功消息,让您知道已插入一行数据以及其 ID。

最后,从文件中删除enderror事件。由于node-sqlite3方法的异步特性,enderror事件会在数据插入到数据库之前执行,因此它们不再需要。

保存并退出您的文件。

使用node运行insertData.js文件:

  1. node insertData.js

根据您的系统,可能需要一些时间,但是node应该返回以下输出:

Output
Connected to the database successfully Inserted a row with the id: 1 Inserted a row with the id: 2 ... Inserted a row with the id: 44308 Inserted a row with the id: 44309 Inserted a row with the id: 44310

这条消息,尤其是ID,证明了CSV文件中的行已保存到数据库中。

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

第四步 —— 编写CSV文件

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

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

  1. nano writeCSV.js

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

demo_csv/writeCSV.js
const fs = require("fs");
const { stringify } = require("csv-stringify");
const db = require("./db");

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

接下来,添加以下行以定义一个变量,其中包含要写入数据的CSV文件的名称以及一个可写流,您将向其写入数据:

demo_csv/writeCSV.js
...
const filename = "saved_from_db.csv";
const writableStream = fs.createWriteStream(filename);

const columns = [
  "year_month",
  "month_of_release",
  "passenger_type",
  "direction",
  "sex",
  "age",
  "estimate",
];

createWriteStream 方法接受一个参数,该参数是您要将数据流写入的文件名,即存储在 filename 变量中的 saved_from_db.csv 文件名。

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

仍然在您的 writeCSV.js 文件中,添加以下行以从数据库检索数据并将每一行写入CSV文件:

demo_csv/writeCSV.js
...
const stringifier = stringify({ header: true, columns: columns });
db.each(`select * from migration`, (error, row) => {
  if (error) {
    return console.log(error.message);
  }
  stringifier.write(row);
});
stringifier.pipe(writableStream);
console.log("Finished writing data");

首先,使用对象作为参数调用 stringify 方法,该方法创建一个转换流。转换流将数据从对象转换为CSV文本。传递给 stringify() 方法的对象具有两个属性:

  • header 接受布尔值,并且如果布尔值设置为 true,则生成标题。
  • columns 接受包含列名称的数组,如果 header 选项设置为 true,则这些列的名称将写入CSV文件的第一行。

接下来,您调用db连接对象的each()方法,并传入两个参数。第一个参数是SQL语句select * from migration,用于逐行检索数据库中的行。第二个参数是每次从数据库检索到一行时调用的回调函数。回调函数接受两个参数:一个error对象和一个包含从数据库中单行检索到的数据的row对象。在回调函数中,您检查error对象是否在if语句中设置。如果条件求值为true,则使用console.log()方法在控制台中记录错误消息。如果没有错误,则在stringifier上调用write()方法,将数据写入stringifier转换流。

each()方法完成迭代后,stringifier流上的pipe()方法开始发送数据块并将其写入writableStream。可写流将每个数据块保存在saved_from_db.csv文件中。一旦所有数据都已写入文件,console.log()将记录成功消息。

现在完整的文件看起来像下面这样:

demo_csv/writeCSV.js
const fs = require("fs");
const { stringify } = require("csv-stringify");
const db = require("./db");
const filename = "saved_from_db.csv";
const writableStream = fs.createWriteStream(filename);

const columns = [
  "year_month",
  "month_of_release",
  "passenger_type",
  "direction",
  "sex",
  "age",
  "estimate",
];

const stringifier = stringify({ header: true, columns: columns });
db.each(`select * from migration`, (error, row) => {
  if (error) {
    return console.log(error.message);
  }
  stringifier.write(row);
});
stringifier.pipe(writableStream);
console.log("Finished writing data");

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

  1. node writeCSV.js

您将收到以下输出:

Output
Finished writing data

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

  1. cat saved_from_db.csv

cat将返回文件中的所有行(已编辑以简洁为准):

Output
year_month,month_of_release,passenger_type,direction,sex,age,estimate 2001-01,2020-09,Long-term migrant,Arrivals,Female,0-4 years,344 2001-01,2020-09,Long-term migrant,Arrivals,Male,0-4 years,341 2001-01,2020-09,Long-term migrant,Arrivals,Female,10-14 years, ...

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

结论

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

您现在可以读取和写入CSV文件了。作为下一步,您可以使用内存高效的流来处理大型CSV数据集,或者您可以查看类似event-stream这样的包,使流处理变得更加容易。

要了解更多关于node-csv的信息,请访问其文档CSV项目 – Node.js CSV包。要了解有关node-sqlite3的更多信息,请访问其Github文档。要继续提升您的Node.js技能,请查看如何在Node.js中编码系列。

Source:
https://www.digitalocean.com/community/tutorials/how-to-read-and-write-csv-files-in-node-js-using-node-csv