如何使用Node-CSV在Node.js中读取和写入CSV文件
作者选择 女工程师协会 作为 Write for DOnations 计划的一部分接受捐赠。
介绍
CSV 是一种用于存储表格数据的纯文本文件格式。 CSV 文件使用逗号分隔符来分隔表格单元格中的值,并用新行划定行的开始和结束位置。 大多数电子表格程序和数据库都可以导出和导入 CSV 文件。 因为 CSV 是纯文本文件,所以任何编程语言都可以解析和写入 CSV 文件。 Node.js 有许多可以处理 CSV 文件的模块,例如 node-csv、fast-csv 和 papaparse。
在本教程中,您将使用 node-csv
模块通过 Node.js 流读取 CSV 文件,这使您可以在不消耗大量内存的情况下读取大型数据集。 您将修改程序以将从 CSV 文件解析的数据移动到 SQLite 数据库中。 您还将从数据库中检索数据,使用 node-csv
对其进行解析,并使用 Node.js 流将其分块写入 CSV 文件。
先决条件
要遵循本教程,您将需要:
- Node.js 安装在您的本地或服务器环境中。 按照【X7X】如何安装Node.js并创建本地开发环境【X76X】安装Node.js。
- SQLite 安装在本地或服务器环境中,您可以按照 如何在 Ubuntu 20.04 上安装和使用 SQLite 中的步骤 1 进行安装。 有关如何使用 SQLite 的知识很有帮助,可以在安装指南的步骤 2-7 中学习。
- 熟悉编写 Node.js 程序。 请参阅 如何在 Node.js 中编写和运行您的第一个程序。
- 熟悉 Node.js 流。 请参阅 如何使用 Node.js 中的流处理文件。
第 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-csv
和 node-sqlite3
。
输入以下命令安装node-csv
:
npm install csv
node-csv
模块是一组模块,允许您解析数据并将其写入 CSV 文件。 该命令会安装 node-csv
包中的所有四个模块:csv-generate
、csv-parse
、csv-stringify
和 stream-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
文件中,通过添加以下行来导入 fs
和 csv-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
文件中,添加以下行以导入 fs
和 node-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
,则记录一条成功消息,确认已建立连接。
目前,if
和 else
块建立连接对象。 在调用 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 一起插入。
最后,从文件中删除 end
和 error
事件。 由于 node-sqlite3
方法的异步特性,end
和 error
事件在数据插入数据库之前执行,因此不再需要它们。
保存并退出您的文件。
使用 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
导入 fs
和 csv-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
选项设置为true
,columns
采用一个数组,其中包含将写入 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-csv
和 node-sqlite3
模块将其数据插入到数据库中。 然后,您从数据库中检索数据并将其写入另一个 CSV 文件。
您现在可以读取和写入 CSV 文件。 下一步,您现在可以使用与内存高效流相同的实现来处理大型 CSV 数据集,或者您可以查看像 event-stream 这样的包,它可以更轻松地处理流。
要了解有关 node-csv
的更多信息,请访问他们的文档 CSV 项目 - Node.js CSV 包。 要了解有关 node-sqlite3
的更多信息,请访问他们的 Github 文档。 要继续提高您的 Node.js 技能,请参阅 如何在 Node.js 系列中编码。