介紹
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應用程序。讓數字海洋專注於擴展您的應用程序。
先決條件
要遵循本教程,您將需要:
-
在本地或服務器環境中安裝了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
...
第一行包含列名,所有後續行都包含與每列對應的數據。逗號分隔每個數據片段。這個字符被稱為分隔符,因為它界定了字段。你不僅限於使用逗號。其他流行的分隔符包括冒號(:
)、分號(;
)和制表符(\td
)。你需要知道文件中使用的分隔符,因為大多數模組需要它來解析文件。
經過檔案檢閱並識別定界符後,請使用 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()
方法接受一个接受属性的对象。然后,该对象配置并提供有关方法将解析的数据的更多信息。对象接受以下属性:
-
分隔符
定義了在每行中分隔每個字段的字符。值為,
告訴解析器逗號標示了字段。 -
from_line
定義了解析器應該從哪一行開始解析行。使用值2
,解析器將跳過第 1 行,從第 2 行開始。因為稍後你將把數據插入數據庫,這個屬性可以幫助你避免將列名插入數據庫的第一行。
接下來,使用 Node.js 的 on()
方法附加一個流事件。流事件允許該方法在某個事件被觸發時消耗一部分數據。當從 parse()
方法轉換的數據準備好被消耗時,會觸發 data
事件。為了訪問數據,你將一個回調傳遞給 on()
方法,該回調接受一個名為 row
的參數。row
參數是轉換為數組的數據塊。在回調中,你使用 console.log()
方法將數據記錄到控制台中。
在运行文件之前,您将添加更多的流事件。这些流事件处理错误,并在CSV文件中的所有数据被消耗完毕时,在控制台中编写成功消息。
仍然在您的readCSV.js
文件中,添加以下突出显示的代码:
当CSV文件中的所有数据都已读取时,将触发end
事件。当发生这种情况时,将调用回调函数并记录一条消息,说明已经完成。
如果在读取和解析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文件中的所有行都已使用csv-parse
转换流转换为数组。因为每次从流接收到一块数据时都会记录日志,所以数据看起来好像是在下载而不是一次性显示。
在此步骤中,您读取了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
方法。該方法接受三個參數:
-
第一個參數是一個SQL語句,將被傳遞並在SQLite數據庫中執行。
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文件。
步驟4 —— 編寫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
接受包含將在 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()
將記錄成功消息。
完整的文件現在看起來如下:
保存並關閉您的文件,然後在終端中運行 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中編碼系列。