Node.jsを使用してCSVファイルを読み書きする方法(Node-CSVを使用)

著者は、女性技術者協会(Society of Women Engineers)を、寄付のための執筆プログラムの一環として選択しました。

導入

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モジュールを使用して、Node.jsストリームを介してCSVファイルを読み取ります。これにより、大規模なデータセットをメモリを大量に消費することなく読み取ることができます。プログラムを変更して、CSVファイルから解析されたデータをSQLiteデータベースに移動します。また、データベースからデータを取得し、node-csvで解析し、Node.jsストリームを使用してデータをチャンクごとにCSVファイルに書き込みます。

DigitalOcean App Platformを使用して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 — プロジェクトディレクトリの設定

このセクションでは、プロジェクトディレクトリを作成し、アプリケーション用のパッケージをダウンロードします。また、ニュージーランドの国際移民データを含む CSV データセットを Stats NZ からダウンロードします。

まず、csv_demo というディレクトリを作成し、そのディレクトリに移動します:

  1. mkdir csv_demo
  2. cd csv_demo

次に、npm init コマンドを使用して、ディレクトリを npm プロジェクトとして初期化します:

  1. npm init -y

-y オプションは、すべてのプロンプトに「はい」と答えるように npm init に通知します。このコマンドは、いつでも変更できるデフォルト値を持つ package.json を作成します。

npm プロジェクトとしてディレクトリが初期化されたら、必要な依存関係をインストールできます: node-csvnode-sqlite3

node-csv をインストールするには、次のコマンドを入力します:

  1. npm install csv

node-csvモジュールは、CSVファイルのデータを解析および書き込みするためのモジュールのコレクションです。以下のコマンドは、node-csvパッケージの一部である4つのモジュールをすべてインストールします:csv-generatecsv-parsecsv-stringify、およびstream-transform。CSVファイルを解析するためにcsv-parseモジュールを使用し、データをCSVファイルに書き込むためにcsv-stringifyモジュールを使用します。

次に、node-sqlite3モジュールをインストールしてください:

  1. npm install sqlite3

node-sqlite3モジュールを使用すると、アプリがSQLiteデータベースとやり取りできます。

プロジェクトにパッケージをインストールした後、wgetコマンドを使用してニュージーランドの移住CSVファイルをダウンロードしてください:

  1. 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コマンドを使用してファイル名を短く変更してください:

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

新しいCSVファイル名migration_data.csvは、短くて作業しやすくなりました。

nanoまたはお気に入りのテキストエディタを使用してファイルを開いてください:

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

最初の行には列名が含まれており、すべての後続の行には各列に対応するデータが含まれています。データの各部分はコンマで区切られています。この文字をデリミタと呼びます。ファイルを解析するために大部分のモジュールがデリミタを必要とするので、ファイルでどのデリミタが使用されているかを知る必要があります。制限されることはありません。他の一般的なデリミタには、コロン(:)、セミコロン(;)、およびタブ(\t)が含まれます。

ファイルを確認し、区切り文字を識別した後、migration_data.csv ファイルを終了するには、CTRL+X を使用します。

必要な依存関係をインストールしました。次のセクションでは、CSV ファイルを読み込みます。

ステップ 2 — CSV ファイルの読み込み

このセクションでは、node-csv を使用して CSV ファイルを読み込み、その内容をコンソールにログに出力します。 fs モジュールの createReadStream() メソッドを使用して CSV ファイルからデータを読み取り、読み取り可能なストリームを作成します。 次に、データのチャンクを解析する csv-parse モジュールで初期化された別のストリームにストリームをパイプします。 データのチャンクが解析されたら、それらをコンソールにログに出力できます。

好みのエディタで readCSV.js ファイルを作成し、開きます:

  1. 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メソッドをdestructuring syntaxを使用して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()メソッドは、読み取り可能なストリームからデータのチャンクを別のストリームに転送します。2番目のストリームは、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()メソッドを使用してストリーミングイベントをアタッチします。ストリーミングイベントは、特定のイベントが発生した場合にメソッドがデータのチャンクを消費できるようにします。dataイベントは、parse()メソッドから変換されたデータが消費の準備ができたときにトリガーされます。データにアクセスするには、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);
  });

endイベントは、CSVファイルのすべてのデータが読み取られたときに発生します。そのときに、コールバックが呼び出され、「終了した」というメッセージをログに記録します。

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);
  });

readCSV.jsファイルからCTRL+Xを使用して保存して終了します。

次に、nodeコマンドを使用してファイルを実行します:

  1. 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-parse変換ストリームを使用して配列に変換されました。ストリームからチャンクが受信されるたびにログが記録されるため、データは一度にすべて表示されるのではなく、ダウンロードされているかのように表示されます。

このステップでは、CSVファイル内のデータを読み取り、配列に変換しました。次に、データベースにCSVファイルからデータを挿入します。

ステップ3 — データをデータベースに挿入する

Node.jsを使用してCSVファイルからデータをデータベースに挿入すると、データを挿入する前に処理、クリーニング、またはデータの強化に使用できる豊富なモジュールライブラリにアクセスできます。

このセクションでは、node-sqlite3モジュールを使用してSQLiteデータベースとの接続を確立します。次に、データベース内のテーブルを作成し、readCSV.jsファイルをコピーして、CSVファイルから読み取ったすべてのデータをデータベースに挿入するように修正します。

エディターでdb.jsファイルを作成し、開きます:

  1. nano db.js

db.jsファイルに、次の行を追加してfsnode-sqlite3モジュールをインポートします:

demo_csv/db.js
const fs = require("fs");
const sqlite3 = require("sqlite3").verbose();
const filepath = "./population.db";
...

3行目では、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に評価される場合は、SQLite の Database() クラスをnode-sqlite3 モジュールでデータベースファイルパスとともにインスタンス化します。接続が確立されると、関数は接続オブジェクトを返して終了します。

ただし、if 文がfalseに評価される場合(データベースファイルが存在しない場合)、実行はelse ブロックに移ります。else ブロックでは、2 つの引数を指定して Database() クラスをインスタンス化します。これらの引数は、SQLite データベースファイルのパスとコールバックです。

最初の引数は SQLite データベースファイルのパスであり、./population.db です。2 番目の引数は、データベースとの接続が正常に確立された場合、またはエラーが発生した場合に自動的に呼び出されるコールバックです。コールバックは 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をパラメーターとして受け取ります。 db接続オブジェクトでexec()メソッドを呼び出し、SQLステートメントを引数として渡します。 SQLステートメントは、7つの列を持つmigrationという名前のテーブルを作成します。 列名はmigration_data.csvファイルの見出しと一致します。

最後に、connectToDatabase()関数を呼び出し、関数によって返された接続オブジェクトをエクスポートして、他のファイルで再利用できるようにします。

db.jsファイルを保存して終了します。

データベース接続が確立されたので、csv-parseモジュールによって解析された行をデータベースに挿入するためにreadCSV.jsファイルをコピーして変更します。

次のコマンドでファイルをコピーして名前を変更します:insertData.js

  1. cp readCSV.js insertData.js

エディターでinsertData.jsファイルを開きます:

  1. 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}`);
        }
      );
    });
  });

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が示されます。

最後に、ファイルからenderrorイベントを削除します。 node-sqlite3メソッドの非同期性のため、enderrorイベントはデータがデータベースに挿入される前に実行されるため、これらはもはや必要ありません。

ファイルを保存して終了します。

nodeを使用してinsertData.jsファイルを実行します:

  1. node insertData.js

システムによっては、時間がかかる場合がありますが、nodeは以下の出力を返すはずです:

Output
Connected 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を作成して開きます:

  1. nano writeCSV.js

writeCSV.jsファイルで、以下の行を追加してfsモジュールとcsv-stringifyモジュール、およびdb.jsからのデータベース接続オブジェクトをインポートします:

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 ファイル名です。

4行目では、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() メソッドに渡されるオブジェクトには2つのプロパティがあります:

  • header はブール値を受け取り、ブール値が true に設定されている場合はヘッダーを生成します。
  • columns は、header オプションが true に設定されている場合に、CSVファイルの最初の行に書き込まれる列名を含む配列を取ります。

次に、db接続オブジェクトからeach()メソッドを2つの引数で呼び出します。最初の引数はSQLステートメントselect * from migrationであり、データベースから1行ずつ行を取得します。2番目の引数は、データベースから行が取得されるたびに呼び出されるコールバックです。コールバックは2つのパラメータを取ります:エラーオブジェクトとデータベースの単一の行から取得された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ファイルを実行します:

  1. node writeCSV.js

次の出力が表示されます:

Output
Finished writing data

データが書き込まれたことを確認するには、catコマンドを使用してファイルの内容を検査してください。

  1. cat saved_from_db.csv

catは、ファイルに書かれたすべての行を返します(簡潔に編集されました):

Output
year_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ファイルに書き込むことができます。

結論

この記事では、node-csvnode-sqlite3モジュールを使用してCSVファイルを読み取り、そのデータをデータベースに挿入しました。その後、データベースからデータを取得して別のCSVファイルに書き込みました。

CSVファイルの読み書きができるようになりました。次のステップとしては、メモリ効率の良いストリームを使用して大規模なCSVデータセットを扱うか、またはevent-streamのようなパッケージを検討して、ストリームの操作を簡素化することができます。

node-csvについてさらに詳しく知りたい場合は、彼らのドキュメントCSVプロジェクト – Node.js CSVパッケージをご覧ください。また、node-sqlite3について詳しく知りたい場合は、Githubのドキュメントをご覧ください。Node.jsのスキルをさらに向上させるには、How To Code in Node.jsシリーズを参照してください。

Source:
https://www.digitalocean.com/community/tutorials/how-to-read-and-write-csv-files-in-node-js-using-node-csv