作者选择了 女性工程师协会作为 写给捐赠计划的一部分获得捐赠。
介绍
CSV 是用于存储表格数据的简单文本文件格式. CSV 文件使用符号划分器来分离表单元格中的值,而新的行则描述了行开始和结束的地方. 大多数电子表格程序和数据库可以导出和导入 CSV 文件. 由于 CSV 是一个简单文本文件,任何编程语言都可以对 CSV 文件进行解析和写入。 Node.js有许多模块可以与 CSV 文件一起工作,例如 node-csv
, fast-csv
和 [papaparse
(LINK3])。
在本教程中,您将使用node-csv
模块读取 CSV 文件使用 Node.js 流,允许您读取大数据集而不消耗大量内存。您将修改程序,将从 CSV 文件中解析的数据移动到 SQLite 数据库中。
前提条件
要遵循本教程,您将需要:
- Node.js 安装在您的本地或服务器环境中. 按照 如何安装和使用 Node.js 并创建本地开发环境来安装 Node.js.
- SQLite 安装在您的本地或服务器环境中,您可以通过在 如何安装和使用 SQLite 在 Ubuntu 20.04中的步骤 1 进行安装。 了解如何使用 SQLite 是有帮助的,并可以在安装指南的步骤 2-7 中学习。
- 熟悉编写 Node.js 程序。 参见 如何在 Node.js 中编写和运行您的第一个程序。
- 熟悉 Node.js 流。 参见 [如何使用 Node.js 中的
步骤 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-csv
和 node-sqlite3
。
输入以下命令来安装 node-csv
:
1npm install csv
node-csv
模块是一个模块集合,允许您对 CSV 文件进行解析和编写数据。该命令安装了所有四个组件,这些模块是node-csv
包的一部分:csv-generate
,csv-parse
,csv-stringify
和stream-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
文件中,通过添加以下行来导入fs
和csv-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
文件中,添加以下行来导入fs
和node-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
文件中,添加以下行来导入fs
和csv-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-csv
和node-sqlite3
模块将其数据插入数据库中,然后从数据库中获取数据并将其写入另一个 CSV 文件中。
作为下一个步骤,您现在可以使用相同的实现来处理大型 CSV 数据集,使用具有记忆效率的流,或者您可以寻找一个类似 event-stream
的包,使与流的工作变得更容易。
要了解更多关于node-csv
的信息,请访问他们的文档 CSV 项目 - Node.js CSV 包。 要了解更多关于node-sqlite3
的信息,请访问他们的 Github 文档。 要继续发展您的 Node.js 技能,请参阅 如何在 Node.js 系列中编码.