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

来自菜鸟教程
跳转至:导航、​搜索

作者选择 女工程师协会 作为 Write for DOnations 计划的一部分接受捐赠。

介绍

CSV 是一种用于存储表格数据的纯文本文件格式。 CSV 文件使用逗号分隔符来分隔表格单元格中的值,并用新行划定行的开始和结束位置。 大多数电子表格程序和数据库都可以导出和导入 CSV 文件。 因为 CSV 是纯文本文件,所以任何编程语言都可以解析和写入 CSV 文件。 Node.js 有许多可以处理 CSV 文件的模块,例如 node-csvfast-csvpapaparse

在本教程中,您将使用 node-csv 模块通过 Node.js 流读取 CSV 文件,这使您可以在不消耗大量内存的情况下读取大型数据集。 您将修改程序以将从 CSV 文件解析的数据移动到 SQLite 数据库中。 您还将从数据库中检索数据,使用 node-csv 对其进行解析,并使用 Node.js 流将其分块写入 CSV 文件。

先决条件

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

第 1 步 — 设置项目目录

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

首先,创建一个名为 csv_demo 的目录并导航到该目录:

mkdir csv_demo
cd csv_demo

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

npm init -y

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

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

输入以下命令安装node-csv

npm install csv

node-csv 模块是一组模块,允许您解析数据并将其写入 CSV 文件。 该命令会安装 node-csv 包中的所有四个模块:csv-generatecsv-parsecsv-stringifystream-transform。 您将使用 csv-parse 模块解析 CSV 文件并使用 csv-stringify 模块将数据写入 CSV 文件。

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

npm install sqlite3

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

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

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 命令将文件名重命名为更短的名称:

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

新的 CSV 文件名 migration_data.csv 更短且更易于使用。

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

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
...

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

查看文件并确定分隔符后,使用 CTRL+X 退出 migration_data.csv 文件。

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

第 2 步 — 读取 CSV 文件

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

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

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。 然后,它创建一个可读的 stream,它接收一个大文件并将其分成更小的块。 可读流允许您仅从中读取数据而不向其写入数据。

创建可读流后,Node 的 pipe() 方法将数据块从可读流转发到另一个流。 当在 pipe() 方法中调用 csv-parse 模块的 parse() 方法时,会创建第二个流。 csv-parse 模块实现了一个转换流(可读可写流),获取一个数据块并将其转换为另一种形式。 例如,当它接收到像 2001-01,2020-09,Long-term migrant,Arrivals,Female,0-4 years,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);
  });

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

如果在读取和解析 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);
  });

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

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

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 文件中的所有行都已使用 csv-parse 转换流转换为数组。 因为每次从流中接收到块时都会发生日志记录,因此数据看起来好像正在下载,而不是一次全部显示。

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

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

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

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

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

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 作为参数。 您在将 SQL 语句作为参数的 db 连接对象上调用 exec() 方法。 SQL 语句创建一个名为 migration 的表,其中包含 7 列。 列名与 migration_data.csv 文件中的标题匹配。

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

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

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

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

cp readCSV.js insertData.js

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

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 语句在另一个 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 文件:

node insertData.js

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

OutputConnected 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 文件。

第 4 步 — 编写 CSV 文件

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

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

nano writeCSV.js

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

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 时生成标头。
  • 如果 header 选项设置为 truecolumns 采用一个数组,其中包含将写入 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 文件:

node writeCSV.js

您将收到以下输出:

OutputFinished writing data

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

cat saved_from_db.csv

cat 将返回文件中写入的所有行(为简洁而编辑):

Outputyear_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 系列中编码。