介绍
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 关注您的应用程序的扩展。
先决条件
要遵循本教程,您需要:
-
在您的本地或服务器环境中安装 Node.js。请按照如何安装 Node.js 并创建本地开发环境的说明安装 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
的目录并进入该目录:
然后,使用npm init
命令将该目录初始化为npm项目:
使用-y
选项通知npm init
以对所有提示回答“是”。此命令将创建一个具有默认值的package.json
,您随时可以更改它。
将目录初始化为npm项目后,现在可以安装必要的依赖项:node-csv
和node-sqlite3
。
输入以下命令安装node-csv
:
node-csv
模块是一个模块集合,允许您解析和写入数据到 CSV 文件。该命令安装 node-csv
包的四个模块:csv-generate
、csv-parse
、csv-stringify
和 stream-transform
。您将使用 csv-parse
模块来解析 CSV 文件,使用 csv-stringify
模块将数据写入 CSV 文件。
接下来,安装 node-sqlite3
模块:
node-sqlite3
模块允许您的应用与 SQLite 数据库进行交互。
在项目中安装完包之后,使用 wget
命令下载新西兰迁移 CSV 文件:
您下载的 CSV 文件名称很长。为了更方便处理,使用 mv
命令将文件名改为较短的名称:
新的 CSV 文件名 migration_data.csv
更短,更容易处理。
使用 nano
,或您喜欢的文本编辑器,打开文件:
一旦打开,您将看到类似于以下内容:
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
文件:
在您的readCSV.js
文件中,通过添加以下行来导入fs
和csv-parse
模块:
在第一行中,您定义了fs
变量,并将其赋值为Node.js require()
方法导入模块时返回的fs
对象。
在第二行,您使用解构语法将从require()
方法返回的对象中提取parse
方法,并将其赋值给parse
变量。
添加以下行以读取CSV文件:
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
文件中,添加如下突出显示的代码:
end
事件在CSV文件中的所有数据被读取时被触发。当发生这种情况时,将调用回调函数并记录一条消息,说明已经完成。
如果在读取和解析CSV数据的任何地方发生错误,则会发出error
事件,该事件会调用回调函数并在控制台中记录错误消息。
现在您的完整文件应该如下所示:
保存并退出readCSV.js
文件,使用CTRL+X
。
接下来,使用node
命令运行文件:
输出将类似于以下内容(已编辑以节省空间):
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
文件:
在您的 db.js
文件中,添加以下行以导入 fs
和 node-sqlite3
模块:
在第三行中,您定义了 SQLite 数据库的路径,并将其存储在变量 filepath
中。数据库文件尚不存在,但是 node-sqlite3
需要它来与数据库建立连接。
在同一文件中,添加以下行以将 Node.js 连接到 SQLite 数据库:
在这里,你定义了一个名为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
块,连接到数据库,并返回连接对象。
要在数据库文件不存在时创建表,请添加以下突出显示的代码:
现在,connectToDatabase()
调用 createTable()
函数,该函数将存储在 db
变量中的连接对象作为参数。
在 connectToDatabase()
函数之外,定义了 createTable()
函数,该函数将连接对象 db
作为参数。在 db
连接对象上调用了 exec()
方法,该方法接受一个 SQL 语句作为参数。SQL 语句创建了一个名为 migration
的表,其中包含 7 列。列名与 migration_data.csv
文件中的标题相匹配。
最后,调用 connectToDatabase()
函数,并导出该函数返回的连接对象,以便在其他文件中重用。
保存并退出您的 db.js
文件。
数据库连接已建立,现在您将复制并修改 readCSV.js
文件,以将 csv-parse
模块解析的行插入到数据库中。
使用以下命令复制并重命名文件为 insertData.js
:
打开你的编辑器中的insertData.js
文件:
在突出显示的代码中添加:
在第三行,你从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。
最后,从文件中删除end
和error
事件。由于node-sqlite3
方法的异步特性,end
和error
事件会在数据插入到数据库之前执行,因此它们不再需要。
保存并退出您的文件。
使用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文件。
第四步 —— 编写CSV文件
在本节中,您将使用流从数据库中检索数据并将其写入CSV文件。
在您的编辑器中创建并打开writeCSV.js
:
在您的writeCSV.js
文件中,添加以下行以导入fs
和csv-stringify
模块以及db.js
中的数据库连接对象:
csv-stringify
模块将对象或数组中的数据转换为CSV文本格式。
接下来,添加以下行以定义一个变量,其中包含要写入数据的CSV文件的名称以及一个可写流,您将向其写入数据:
createWriteStream
方法接受一个参数,该参数是您要将数据流写入的文件名,即存储在 filename
变量中的 saved_from_db.csv
文件名。
在第四行,您定义了一个 columns
变量,该变量存储包含CSV数据标题的名称的数组。当您开始将数据写入文件时,这些标题将写入CSV文件的第一行。
仍然在您的 writeCSV.js
文件中,添加以下行以从数据库检索数据并将每一行写入CSV文件:
首先,使用对象作为参数调用 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()
将记录成功消息。
现在完整的文件看起来像下面这样:
保存并关闭您的文件,然后在终端中运行writeCSV.js
文件:
您将收到以下输出:
OutputFinished writing data
要确认数据已被写入,请使用cat
命令检查文件中的内容:
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中编码系列。