如何使用Node.js在Node.js中讀取和寫入CSV文件

作者選擇女性工程師協會作為為捐贈寫作計畫的受益方。

介紹

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文件。

使用數字海洋應用平台從GitHub部署您的Node應用程序。讓數字海洋專注於擴展您的應用程序。

先決條件

要遵循本教程,您將需要:

第1步 – 設置項目目錄

在這一部分中,您將創建項目目錄並從應用程序中下載包。 您還將從Stats NZ下載一個包含新西蘭國際移民數據的CSV數據集。

要開始,請創建一個名為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 套件的所有四個模組:csv-generatecsv-parsecsv-stringifystream-transform。你將使用 csv-parse 模組來解析 CSV 文件,以及使用 csv-stringify 模組將數據寫入 CSV 文件。

接下來,安裝 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
...

第一行包含列名,所有後續行都包含與每列對應的數據。逗號分隔每個數據片段。這個字符被稱為分隔符,因為它界定了字段。你不僅限於使用逗號。其他流行的分隔符包括冒號(:)、分號(;)和制表符(\td)。你需要知道文件中使用的分隔符,因為大多數模組需要它來解析文件。

經過檔案檢閱並識別定界符後,請使用 CTRL+X 退出您的 migration_data.csv 檔案。

您現在已安裝了專案所需的必要依賴項。在下一節中,您將閱讀一個 CSV 檔案。

步驟 2 — 讀取 CSV 檔案

在本節中,您將使用 node-csv 讀取 CSV 檔案並在控制台中記錄其內容。您將使用 fs 模組的 createReadStream() 方法從 CSV 檔案中讀取數據並創建可讀取的串流。然後,您將串流導向使用 csv-parse 模組初始化的另一個串流來解析數據的片段。一旦數據片段被解析,您就可以在控制台中記錄它們。

在您的首選編輯器中創建並打開一個名為 readCSV.js 的檔案:

  1. nano readCSV.js

在您的 readCSV.js 檔案中,通過添加以下行來導入 fscsv-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。然后,它创建一个可读,将大文件分成较小的块。可读流允许您仅从中读取数据,而不是向其写入。

创建可读流后,Node的pipe()方法将数据块从可读流转发到另一个流。当在pipe()方法中调用csv-parse模块的parse()方法时,将创建第二个流。csv-parse模块实现了一个转换流(可读和可写流),接受数据块并将其转换为另一种形式。例如,当它收到像2001-01,2020-09,长期移民,到达,女性,0-4岁,344这样的数据块时,parse()方法将其转换为数组。

parse()方法接受一个接受属性的对象。然后,该对象配置并提供有关方法将解析的数据的更多信息。对象接受以下属性:

  • 分隔符 定義了在每行中分隔每個字段的字符。值為 , 告訴解析器逗號標示了字段。

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

保存并退出您的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文件中的所有行都已使用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";
...

在第三行,您定義了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,則記錄一個成功消息,確認連接已建立。

目前,ifelse塊確立了連接對象。在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語句創建一個名為migration的表,具有7個列。列名與migration_data.csv文件中的標題匹配。

最後,調用connectToDatabase()函數並導出該函數返回的連接對象,以便在其他文件中重複使用。

保存並退出您的db.js文件。

數據庫連接建立後,您現在將複製並修改readCSV.js文件,以將csv-parse模塊解析的行插入數據庫。

使用以下命令複製並重命名文件為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}`);
        }
      );
    });
  });

在第三行,從db.js文件中導入連接對象並將其存儲在變量db中。

在附加到fs模塊流的data事件回調中,調用連接對象上的serialize()方法。該方法確保在另一個開始執行之前完成執行SQL語句,這可以幫助防止數據庫競爭條件,即系統同時運行競爭操作。

serialize()方法接受一個回調。在回調中,調用db連接對象上的run方法。該方法接受三個參數:

  • 第一個參數是一個SQL語句,將被傳遞並在SQLite數據庫中執行。run()方法僅接受不返回結果的SQL語句。 INSERT INTO migration VALUES (?, ..., ?語句向表migration插入一行,?是稍後在run()方法的第二個參數中用值替換的佔位符。

  • 第二個參數是一個陣列[row[0], ... row[5], row[6]]。在前一節中,parse()方法從可讀流中接收到一塊數據並將其轉換為陣列。由於數據是作為一個陣列接收的,要獲取每個字段的值,您必須使用陣列索引來訪問它們,如[row[1], ..., row[6]]等。

  • 第三個參數是一個回調函數,當數據被插入或出現錯誤時運行。回調函數檢查是否發生錯誤並記錄錯誤消息。如果沒有錯誤,函數將使用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文件中,添加以下行以導入fscsv-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 文件名稱中。

在第四行,您定義了一個 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,則生成標題。
  • columns 接受包含將在 CSV 文件的第一行中寫入的列名的數組,如果 header 選項設置為 true

接下來,您從 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 文件:

  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文件。

結論

在本文中,您讀取了一個CSV文件,並使用node-csvnode-sqlite3模塊將其數據插入到數據庫中。然後,您從數據庫中檢索數據並將其寫入另一個CSV文件。

您現在可以讀取和寫入CSV文件了。作為下一步,您現在可以使用具有內存效率的流來處理大型CSV數據集,或者您可以查看一個像event-stream這樣的包,它可以使與流的工作變得更加簡單。

要了解更多關於node-csv的信息,請訪問它們的文檔CSV項目 – Node.js CSV包。要了解有關node-sqlite3的更多信息,請訪問它們的Github文檔。要繼續提升您的Node.js技能,請參閱如何在Node.js中編碼系列

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