[譯] 使用 Node.js 讀取超大的文件(第一部分)

使用 Node.js 讀取超大的文件(第一部分)

這篇博文有一個很是有趣的啓發點。上週,某我的在個人 Slack 頻道上發佈了一個編碼挑戰,這個挑戰是他在申請一家保險技術公司的開發崗位時收到的。html

這個挑戰激起了個人興趣,這個挑戰要求讀取聯邦選舉委員會的大量數據文件,而且展現這些文件中的某些特定數據。因爲我沒有作過什麼和原始數據相關的工做,而且我老是樂於接受新的挑戰,因此我決定用 Node.js 來解決這個問題,看看我是否可以完成這個挑戰,而且從中找到樂趣。前端

下面是提出的四個問題,以及這個程序須要解析的數據集的連接。node

  • 實現一個能夠打印出文件總行數的程序。
  • 注意,第八列包含了人的名字。編寫一個程序來加載這些數據,而且建立一個數組,將全部的名字字符串保存進去。打印出第 432 個以及第 43243 個名字。
  • 注意,第五列包含了格式化的時間。計算每月的捐贈數,而且打印出結果。
  • 注意,第八列包含了人的名字。建立一個數組來保存每一個 first name。標記出數據中最常使用的 first name,以及其出現的次數。

數據的連接:www.fec.gov/files/bulk-…android

當你解壓完這個文件夾,你能夠看到一個大小爲 2.55 GB 的 .txt 主文件,以及一個包含了主文件部分數據的文件夾(這個是我在跑主文件以前,用來測試個人解決方案的)。ios

不是很是可怕,對吧?彷佛是可行的。因此讓咱們看看我是怎麼實現的。git

我想出來的兩個原生 Node.js 解決方案

處理大型文件對於 JavaScript 來講並非什麼新鮮事了,實際上,在 Node.js 的核心功能當中,有不少標準的解決方案能夠進行文件的讀寫。github

其中,最直接的就是 fs.readFile(),這個方法會將整個文件讀入到內存當中,而後在 Node 讀取完成後當即執行操做,第二個選擇是 fs.createReadStream(),這個方法以數據流的形式處理數據的輸入輸出,相似於 Python 或者是 Java。spring

我使用的解決方案以及我爲何要使用它

因爲個人解決方案涉及到計算行的總數以及解析每一行的數據來獲取捐贈名和日期,因此我選擇第二個方法:fs.createReadStream()。而後在遍歷文件的時候,我可使用 rl.on('line',...) 函數來從文件的每一行中獲取必要的數據。docker

對我來講,這比將整個文件讀入到內存中,而後再逐行讀取更加簡單。npm

Node.js CreateReadStream() 和 ReadFile() 代碼實現

下面是我用 Node.js 的 fs.createReadStream() 函數實現的代碼。我會在下面將其分解。

我所要作的第一件事就是從 Node.js 中導入須要的函數:fs(文件系統),readline,以及 stream。導入這些內容後,我就能夠建立一個 instreamoutstream 而後調用 readLine.createInterface(),它們讓我能夠逐行讀取流,而且從中打印出數據。

我還添加了一些變量(和註釋)來保存各類數據:一個 lineCountnames 數組、donation 數組和對象,以及 firstNames 數組和 dupeNames 對象。你能夠稍後看到它們發揮做用。

rl.on('line',...)函數裏面,我能夠完成數據的逐行分析。在這裏,我爲數據流的每一行都進行了 lineCount 的遞增。我用 JavaScript 的 split() 方法來解析每個名字,而且將其添加到 names 數組當中。我會進一步將每一個名字都縮減爲 first name,同時在 JavaScript 的 trim()includes() 以及 split() 方法的幫助下,計算 middle name 的首字母,以及名字出現的次數等信息。而後我將時間列的年份和時間進行分割,將其格式化爲更加易讀的 YYYY-MM 格式,而且添加到 dateDonationCount 數組當中。

rl.on('close',...) 函數中,我對我收集到數組中的數據進行了轉換,而且在 console.log 的幫助下將個人全部數據展現給用戶。

找到第 432 個以及第 43243 個下標處的 lineCountnames 不須要進一步的操做了。而找到最常出現的名字和每月的捐款數量比較棘手。

對於最多見的名字,我首先須要建立一個鍵值對對象用於存儲每一個名字(做爲 key)和這個名字出現的次數(做爲 value),而後我用 ES6 的函數 Object.entries() 來將其轉換爲數組。以後再對這個數組進行排序而且打印出最大值,就是一件很是簡單的事情了。

獲取捐贈數量也須要一個相似的鍵值對對象,咱們建立一個 logDateElements() 函數,咱們可使用 ES6 的字符串插值來展現每月捐贈數量的鍵值。而後,建立一個 new Map()dateDonations 對象轉換爲嵌套數組,而且對於每一個數組元素調用 logDateElements() 函數。呼!並不像我開始想的那麼簡單。

至少對於我測試用的 400 MB 大小的文件是奏效的……

在用 fs.createReadStream() 方法完成後,我回過頭來嘗試使用 fs.readFile() 來實現個人解決方案,看看有什麼不一樣。下面是這個方法的代碼,可是我不會在這裏詳細介紹全部細節。這段代碼和第一個代碼片十分類似,只是看起來更加同步(除非你使用 fs.readFileSync() 方法,可是不用擔憂,JavaScript 會和運行其餘異步代碼同樣執行這段代碼)。

若是你想要看個人代碼的完整版,能夠在這裏找到。

Node.js 的初始結果

使用個人解決方案,我將傳入到 readFileStream.js 的文件路徑替換成了那個 2.55 GB 的怪物文件,而且看着個人 Node 服務器由於 JavaScript heap out of memory 錯誤而崩潰。

Fail. Whomp whomp…

事實證實,雖然 Node.js 採用流來進行文件的讀寫,可是其仍然會嘗試將整個文件內容保存在內存中,而這對於這個文件的大小來講是沒法作到的。Node 能夠一次容納最大 1.5 GB 的內容,可是不可以再大了。

所以,我目前的解決方案都不可以完成這整個挑戰。

我須要一個新的解決方案。一個基於 Node 的,可以處理更大的數據集的解決方案。

新的數據流解決方案

EventStream 是一個目前很流行的 NPM 模塊,它每週有超過 200 萬的下載量,號稱可以「讓流的建立和使用更加簡單」。

在 EventStream 文檔的幫助下,我再次弄清楚瞭如何逐行讀取代碼,而且以更加 CPU 友好的方式來實現。

EventStream 代碼實現

這個是我使用 EventStream NPM 模塊實現的新代碼。

最大的變化是以文件開頭的管道命令 —— 全部這些語法,都是 EventStream 文檔所建議的方法,經過 .txt 文件每一行末尾的 \n 字符來進行流的分解。

我惟一改變的內容是修改了 names 的結果。我不得不實話實說,由於我嘗試將 1300 萬個名字放到數組裏面,結果仍是發生了內存不足的問題。我繞過了這個問題,只收集了第 432 個和第 43243 個名字,而且將它們加入到了它們本身的數組當中。並非由於其餘什麼緣由,我只是想有點本身的創意。

Node.js 和 EventStream 的實現成果:第二回合

好了,新的解決方案實現好了,又一次,我使用 2.55 GB 的文件啓動了 Node.js,同時雙手合十起到此次可以成功。來讓咱們看看結果。

Woo hoo!

成功了!

結論

最後,Node.js 的純文件和大數據處理功能與我須要的能力還有些差距,可是隻要使用一個額外的 NPM 模塊,好比 EventStream,我就可以解析巨大的數據而不會形成 Node 服務器的崩潰。

請繼續關注本系列的第二部分,我對在 Node.js 中讀取數據的三種方式的性能進行了測試和比較,看看哪種方式的性能可以優於其餘方式。結果變得很是矚目 —— 特別是隨着數據量的變大……

感謝你的閱讀,我但願本文可以幫助你瞭解如何使用 Node.js 來處理大量數據。感謝你的點贊和關注!

若是您喜歡閱讀本文,你可能還會喜歡個人其餘一些博客:


引用和繼續閱讀資源:

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索