Node-CSVを使用してNode.jsでCSVファイルを読み書きする方法
著者は、 Society of Women Engineers を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
CSVは、表形式のデータを保存するためのプレーンテキストファイル形式です。 CSVファイルは、コンマ区切り文字を使用してテーブルセルの値を区切り、新しい行で行の開始位置と終了位置を示します。 ほとんどのスプレッドシートプログラムとデータベースは、CSVファイルをエクスポートおよびインポートできます。 CSVはプレーンテキストファイルであるため、どのプログラミング言語でもCSVファイルを解析して書き込むことができます。 Node.js には、 node-csv 、 fast-csv 、 papaparse など、CSVファイルを処理できる多くのモジュールがあります。
このチュートリアルでは、node-csv
モジュールを使用して、Node.jsストリームを使用してCSVファイルを読み取ります。これにより、大量のメモリを消費することなく、大きなデータセットを読み取ることができます。 プログラムを変更して、解析されたデータをCSVファイルからSQLiteデータベースに移動します。 また、データベースからデータを取得し、node-csv
で解析し、Node.jsストリームを使用してCSVファイルにチャンクで書き込みます。
前提条件
このチュートリアルに従うには、次のものが必要です。
-
ローカル環境またはサーバー環境にインストールされたNode.js。 Node.jsのインストール方法とローカル開発環境の作成に従って、Node.jsをインストールします。
-
ローカル環境またはサーバー環境にインストールされたSQLite。Ubuntu20.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
の4つのモジュールすべてをインストールします。 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
開くと、次のようなコンテンツが表示されます。
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
モジュールをインポートします。
const fs = require("fs");
const { parse } = require("csv-parse");
最初の行では、fs
変数を定義し、Node.jsrequire()
メソッドがモジュールをインポートするときに返すfs
オブジェクトを割り当てます。
2行目では、破壊構文を使用して、require()
メソッドによって返されたオブジェクトから[X11X]変数にparse
メソッドを抽出します。
次の行を追加して、CSVファイルを読み取ります。
...
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()
メソッドは、読み取り可能なストリームから別のストリームにデータのチャンクを転送します。 2番目のストリームは、csv-parse
モジュールのparse()
メソッドがpipe()
メソッド内で呼び出されたときに作成されます。 csv-parse
モジュールは、変換ストリーム(読み取りおよび書き込み可能なストリーム)を実装し、データチャンクを取得して別の形式に変換します。 たとえば、2001-01,2020-09,Long-term migrant,Arrivals,Female,0-4 years,344
のようなチャンクを受信すると、parse()
メソッドはそれを配列に変換します。
parse()
メソッドは、プロパティを受け入れるオブジェクトを受け取ります。 次に、オブジェクトは、メソッドが解析するデータに関する詳細情報を構成および提供します。 オブジェクトは次のプロパティを取ります。
-
delimiter
は、行の各フィールドを区切る文字を定義します。 値,
は、コンマでフィールドを区切ることをパーサーに通知します。 -
from_line
は、パーサーが行の解析を開始する行を定義します。 値が2
の場合、パーサーは1行目をスキップし、2行目から開始します。 後でデータベースにデータを挿入するため、このプロパティは、データベースの最初の行に列名を挿入しないようにするのに役立ちます。
次に、Node.json()
メソッドを使用してストリーミングイベントをアタッチします。 ストリーミングイベントを使用すると、特定のイベントが発生した場合にメソッドがデータのチャンクを消費できます。 data
イベントは、parse()
メソッドから変換されたデータを使用する準備ができたときにトリガーされます。 データにアクセスするには、on()
メソッドにコールバックを渡します。このメソッドは、row
という名前のパラメーターを取ります。 row
パラメーターは、配列に変換されたデータチャンクです。 コールバック内で、console.log()
メソッドを使用してコンソールにデータを記録します。
ファイルを実行する前に、ストリームイベントをさらに追加します。 これらのストリームイベントはエラーを処理し、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
イベントが発行され、コールバックが呼び出され、コンソールにエラーメッセージが記録されます。
これで、完全なファイルは次のようになります。
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
モジュールをインポートします。
const fs = require("fs");
const sqlite3 = require("sqlite3").verbose();
const filepath = "./population.db";
...
3行目では、SQLiteデータベースのパスを定義し、それを変数filepath
に格納します。 データベースファイルはまだ存在していませんが、node-sqlite3
がデータベースとの接続を確立するために必要になります。
同じファイルに、次の行を追加してNode.jsをSQLiteデータベースに接続します。
...
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()
という名前の関数を定義して、データベースへの接続を確立します。 関数内で、fs
モジュールのexistsSync()
メソッドをif
ステートメントで呼び出します。このメソッドは、データベースファイルがプロジェクトディレクトリに存在するかどうかを確認します。 if
条件がtrue
と評価された場合、データベースファイルパスを使用してnode-sqlite3
モジュールのSQLiteのDatabase()
クラスをインスタンス化します。 接続が確立されると、関数は接続オブジェクトを返し、終了します。
ただし、if
ステートメントがfalse
と評価された場合(データベースファイルが存在しない場合)、実行はelse
ブロックにスキップされます。 else
ブロックで、データベースファイルパスとコールバックの2つの引数を使用してDatabase()
クラスをインスタンス化します。
最初の引数は、SQLiteデータベースファイルのパスである./population.db
です。 2番目の引数は、データベースとの接続が正常に確立されたとき、またはエラーが発生したときに自動的に呼び出されるコールバックです。 コールバックは、error
オブジェクトをパラメーターとして受け取ります。これは、接続が成功した場合はnull
です。 コールバック内で、if
ステートメントは、error
オブジェクトが設定されているかどうかを確認します。 true
と評価された場合、コールバックはエラーメッセージをログに記録して戻ります。 false
と評価された場合は、接続が確立されたことを確認する成功メッセージをログに記録します。
現在、if
およびelse
ブロックは接続オブジェクトを確立します。 else
ブロックのDatabase
クラスを呼び出してデータベースにテーブルを作成するときにコールバックを渡しますが、データベースファイルが存在しない場合に限ります。 データベースファイルが既に存在する場合、関数はif
ブロックを実行し、データベースに接続して、接続オブジェクトを返します。
データベースファイルが存在しない場合にテーブルを作成するには、強調表示されたコードを追加します。
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()
関数の外で、接続オブジェクトdb
をパラメーターとして受け入れるcreateTable()
関数を定義します。 SQLステートメントを引数として取るdb
接続オブジェクトでexec()
メソッドを呼び出します。 SQLステートメントは、7列のmigration
という名前のテーブルを作成します。 列名は、migration_data.csv
ファイルの見出しと一致します。
最後に、connectToDatabase()
関数を呼び出し、関数によって返された接続オブジェクトをエクスポートして、他のファイルで再利用できるようにします。
db.js
ファイルを保存して終了します。
データベース接続が確立されたら、readCSV.js
ファイルをコピーして変更し、csv-parse
モジュールが解析した行をデータベースに挿入します。
次のコマンドを使用して、ファイルをコピーしてinsertData.js
に名前を変更します。
- cp readCSV.js insertData.js
エディタでinsertData.js
ファイルを開きます。
- nano 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}`);
}
);
});
});
3行目では、接続オブジェクトをdb.js
ファイルからインポートし、変数db
に格納します。
fs
モジュールストリームにアタッチされたdata
イベントコールバック内で、接続オブジェクトのserialize()
メソッドを呼び出します。 このメソッドは、SQLステートメントが実行を終了してから別のステートメントが実行を開始することを保証します。これにより、システムが競合する操作を同時に実行するデータベースの競合状態を防ぐことができます。
serialize()
メソッドはコールバックを受け取ります。 コールバック内で、db
接続オブジェクトのrun
メソッドを呼び出します。 このメソッドは、次の3つの引数を受け入れます。
-
最初の引数は、SQLiteデータベースで渡されて実行されるSQLステートメントです。
run()
メソッドは、結果を返さないSQLステートメントのみを受け入れます。INSERT INTO migration VALUES (?, ..., ?
ステートメントは、テーブルmigration
に行を挿入し、?
はプレースホルダーであり、後でrun()
メソッドの2番目の引数の値に置き換えられます。 -
2番目の引数は配列
[row[0], ... row[5], row[6]]
です。 前のセクションで、parse()
メソッドは、読み取り可能なストリームからデータのチャンクを受け取り、それを配列に変換します。 データは配列として受信されるため、各フィールド値を取得するには、[row[1], ..., row[6]]
などのように配列インデックスを使用してそれらにアクセスする必要があります。 -
3番目の引数は、データが挿入されたとき、またはエラーが発生したときに実行されるコールバックです。 コールバックはエラーが発生したかどうかをチェックし、エラーメッセージをログに記録します。 エラーがない場合、関数は
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
ファイルに次の行を追加して、fs
およびcsv-stringify
モジュールとデータベース接続オブジェクトをdb.js
からインポートします。
const fs = require("fs");
const { stringify } = require("csv-stringify");
const db = require("./db");
csv-stringify
モジュールは、オブジェクトまたは配列からのデータをCSVテキスト形式に変換します。
次に、次の行を追加して、データを書き込むCSVファイルの名前とデータを書き込む書き込み可能なストリームを含む変数を定義します。
...
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
ファイル名です。
4行目では、columns
変数を定義します。この変数は、CSVデータのヘッダーの名前を含む配列を格納します。 これらのヘッダーは、ファイルへのデータの書き込みを開始すると、CSVファイルの最初の行に書き込まれます。
writeCSV.js
ファイルに、次の行を追加してデータベースからデータを取得し、CSVファイルに各行を書き込みます。
...
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()
メソッドに渡されるオブジェクトには、次の2つのプロパティがあります。
header
はブール値を受け入れ、ブール値がtrue
に設定されている場合、ヘッダーを生成します。columns
は、header
オプションがtrue
に設定されている場合に、CSVファイルの最初の行に書き込まれる列の名前を含む配列を取ります。
次に、2つの引数を使用してdb
接続オブジェクトからeach()
メソッドを呼び出します。 最初の引数は、データベース内の行を1つずつ取得するSQLステートメントselect * from migration
です。 2番目の引数は、データベースから行が取得されるたびに呼び出されるコールバックです。 コールバックは2つのパラメーターを取ります。データベースの単一の行から取得されたデータを含むerror
オブジェクトとrow
オブジェクトです。 コールバック内で、error
オブジェクトがif
ステートメントで設定されているかどうかを確認します。 状態がtrue
と評価された場合、console.log()
メソッドを使用してエラーメッセージがコンソールに記録されます。 エラーがない場合は、stringifier
でwrite()
メソッドを呼び出します。これにより、データがstringifier
変換ストリームに書き込まれます。
each()
メソッドの反復が終了すると、stringifier
ストリームのpipe()
メソッドは、データをチャンクで送信し、writableStream
に書き込み始めます。 書き込み可能なストリームは、データの各チャンクをsaved_from_db.csv
ファイルに保存します。 すべてのデータがファイルに書き込まれると、console.log()
は成功メッセージをログに記録します。
完全なファイルは次のようになります。
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 Project-Node.js CSVpackageにアクセスしてください。 node-sqlite3
の詳細については、Githubドキュメントにアクセスしてください。 Node.jsスキルを継続的に向上させるには、Node.jsシリーズのコーディング方法を参照してください。