Quantcast
Viewing all articles
Browse latest Browse all 577

Node.js 爬蟲開發新手技巧﹍Google Apps Script 替代品

Image may be NSFW.
Clik here to view.
過去一直使用「Google Apps Script 製作網頁爬蟲程式」,並配合「Google 試算表做為資料庫」,基本上不但免費、可應付大多數的需求,同時 Google 試算表還很強大,操作起來不但方便,只要能上網的地方就可使用,省下雲端同步的麻煩,比 Excel 方便太多。 Google Apps Script(以下簡稱 GAS)免費使用的主要限制為,每次執行的時間上限為 6 分鐘,一天最多可執行約 90 分鐘(非手動執行的情況下),如果是簡單的爬蟲不會有什麼問題。不過最近需要製作執行時間較長的爬蟲,GAS 就算是付費版(Google Workspace),雖然一天可執行總時間長達 6 小時,但每次執行時間上限仍然為 6 分鐘,所以只好另尋其他方案。 找到最簡單、方便的方案為 Node.js,跟 GAS 一樣都是使用 Javascript 語言,在 Windows / Mac 作業系統可直接執行,可說是前端工程師的福音,只要學會一種語言就可通吃「前端+後端+資料庫」,本篇將整理使用 Node.js 製作爬蟲需要具備的基本知識、技巧、開發環境等等內容。 (圖片出處: pexels.com)

一、Node.js 開發環境

1. 介紹、安裝 Node.js 跟前端 Javascript(以下簡稱 JS) 不太一樣,是後端環境使用的 JS,所以沒有前端的 window、DOM 等等,詳細的介紹與安裝方式,可參考: 2. 使用 Sublime Text 3 開發我慣用的 JS 開發、編輯工具為 Sublime Text 3(以下簡稱 ST3),這個工具同樣可以做為 Node.js 開發之用。在作業系統安裝完 Node.js 後,可參考以下文章進行: 完成以上流程後,在 ST3 編輯 .js檔並儲存,按下 Ctrl+B就能執行 Node.js 程式碼。 3. 安裝模組 npm yarn Node.js 有數十萬個模組可以取用,如何選擇合適的模組需要一些智慧與判斷方式,例如查看近期下載數量、更新版本、更新頻率等等。而管理這些模組的工具,過去知名的是 npm,近期受好評的是 yarn,如何抉擇與安裝,可參考以下:

二、Node.js 上手資源

開發環境處理完畢後,需要了解如何開始寫 Node.js,以下提供新手可以隨時查閱的上手資訊: 接下來會介紹跟爬蟲相關,需要了解的知識。

三、全域變數

執行爬蟲之前可能需要從檔案讀取資料,完成之後可能需要儲存檔案,那麼有兩個 Node.js 全域變數需要了解:
  • __dirname:執行 js 檔的目錄
  • __filename:該 js 檔的檔案路徑
以下為範例: console.log(__dirname); // D:\WFU console.log(__filename); // D:\WFU\node_test.js

四、原生模組

1. 引用原生模組所有 Node.js 原生模組可在「官方文件」找到,執行模組之前要用 require 來載入,以下為載入 os 模組,取得系統資訊的範例: var os = require("os"); console.log("CPU 資訊:" + JSON.stringify(os.cpus()));以上程式碼可讀取本機的 CPU 資訊,os 的用法可參考「os 操作系統」。 2. 重要原生模組以下為製作爬蟲可能用到的原生模組:

五、外部模組

1. 第三方模組
  • node.js 除了官方模組,還有許多自行開發且好用的模組,可到「NPM 官網」搜尋需要的模組
  • 只有原生模組才能直接 require 載入,安裝非原生模組請先參考前面的說明,使用 npm 或 yarn。
  • 進入專案資料夾路徑,執行指令 npm inityarn init進行初始化(加入參數 -y 可省略回答)
  • 然後執行指令安裝第三方模組 npm install 模組名稱yarn add 模組名稱
  • 完成以上就可以 require 載入第三方模組
2. 自建模組除了第三方模組,也可建立模組自用,方法為:
  • 在 js 檔中建立一個物件
  • 將該物件存入 module.exports
  • 記住此 js 檔的檔名、存放路徑
範例如下: function wfu_module(){ console.log("成功載入模組 !"); } module.exports = wfu_module;引用此模組的範例程式碼如下,假設檔名為 wfu.js: var wfu_module = require("./wfu"); wfu_module(); // 成功載入模組 !
  • require 參數需要加上正確的檔案路徑, "./"代表「wfu.js」放在同樣的路徑
  • 副檔名 ".js"可省略
  • 在 ST3 按下 Ctrl+B執行即可看到效果

六、載入外部函數

GAS 用習慣後,Node.js 會稍微不適應,因為 GAS 可以很輕鬆地引用所有外部函數,就跟前端的 JS 操作起來一模一樣。 舉例來說,前端載入一段 script 後,這段 script 的所有全域變數、所有函數等等,在整個網頁的其他地方都可引用。而 Node.js 不一樣,載入了一個外部模組,只能使用那個模組的變數,這代表想要執行 10 個外部模組,得執行載入 10 次的動作,沒有比較簡化的作法,很花時間。 於是研究了一下,能否一次打包多個函數,只要載入一次就好,就能執行多個外部函數,我找到了這個討論串: 該討論串提出了不少實用的作法,列出兩個實用的方法做為記錄。使用這兩個技巧後,就可將多個函數打包入一個檔案,只需載入一次即可。 1. 使用 eval假設外部 wfu.js 內容如下: function wfu_module(){ console.log("成功載入模組 !"); }引用 wfu_module 函數的範例程式碼如下: var fs = require("fs"); // 操作存取檔案的原生模組 eval(fs.readFileSync("./wfu.js") + ""); // 讀取 wfu.js 內容並轉為字串, 用 eval 執行 wfu_module(); // 呼叫外部函數2. 使用 this假設外部 wfu.js 內容如下: module.exports = function() { this.wfu_module = function() { console.log("成功載入模組 !"); }; }引用 wfu_module 函數的範例程式碼如下: require("./wfu.js")(); // 執行 wfu.js 內容, 讓 wfu_module 成為全域變數 wfu_module(); // 呼叫外部函數

七、同步 / 非同步

1. 同步與非同步的差異 JS「同步/非同步」造成的問題可參考「前端 JS 如何避免 callback 地獄?」,這方面不得不令人懷念起 GAS 的單純環境,所有程式碼一律是「同步」執行緒,一件事執行完才會接續下個動作,程式碼非常好寫。 Node.js 以同時間可大量平行運算、執行高效而聞名,可參考「Node.js 單執行緒、非同步、非阻塞 I/O」。那麼在後端執行爬蟲任務時,不可避免會遇上大量「非同步」程式碼,若程式架構寫不好就會形成「callback 地獄」,同時後續也難以閱讀、維護、管理,以下大致說明 Node.js 這部分需要注意、學習的內容。 2. async / await從 JS ES7 這一版開始加入了 async / await 函數,用來讓「非同步」執行緒可以「同步」執行,只要 Node.js 安裝的是 v7.6 以後的版本就可使用 async / await,其原理及操作方式可參考這篇「Async / Await 深度介紹」。 3. 非同步執行範例下面以常用的檔案存取功能 fs 做為範例,說明非同步執行緒會有什麼狀況。 假設要讀取的文字檔 wfu.txt 內容如下: Blogger 調校資料庫執行程式碼如下: var fs = require("fs"); console.log("程式執行開始!"); fs.readFile("./wfu.txt", "utf8", function(err, data) { if (err) { console.log("無法讀取檔案!"); } else { console.log(data); } }); console.log("程式執行結束!");執行結果如下,可以看的出 wfu.txt 檔案的內容無法依序顯示出來(最後一行才顯示): 程式執行開始! 程式執行結束! Blogger 調校資料庫4. 同步執行範例以下利用 async / await 標準寫法,來修改執行程式碼,: var fs = require("fs"); function readFile() { return new Promise((resolve, reject) => { fs.readFile("./wfu.txt", "utf8", function(err, data) { if (err) { console.log("無法讀取檔案!"); reject(err); } else { console.log(data); resolve(data); } }); }); } async function wfu_sync() { console.log("程式執行開始!"); await readFile(); console.log("程式執行結束!"); } wfu_sync();執行結果如下,可以看出 wfu.txt 檔案內容已經依序顯示出來: 程式執行開始! Blogger 調校資料庫 程式執行結束!

八、Node.js 同步技巧

前面介紹的是 JS「非同步改同步」標準作法,Node.js 在這方面提供一些實用工作可以簡化程式碼。 1. 改用 fs 同步方法除了「非同步」版本,Node.js 為所有的 fs 檔案存取方法都提供了「同步」版本,可參考官方文件「fs 文件系統」→「同步的API」,這裡列出了全部方法。 舉例來說,前面用到的 fs.readFile 方法,同步版本就是 fs.readFileSync,字尾一律多出 "Sync"字串,範例程式碼如下: var fs = require("fs"); console.log("程式執行開始!"); var content = fs.readFileSync("./wfu.txt", "utf8"); console.log(content); console.log("程式執行結束!");簡直是太舒暢了,不用搞什麼 Promise / async / await 這些有的沒的對吧?一行搞定所有工作! 但其實這是最不建議的作法,因為這樣的同步方法,無法使用 callback,萬一讀取檔案失敗,系統會立刻報錯,之後的所有程式碼都會中斷無法繼續執行。 所以若要使用 Node.js 內建的「同步」方法,得每個地方都另外寫 try catch 才行,例如這樣: try { fs.readFileSync("./wfu.txt", "utf8"); } catch (error) { console.log(error); }2. 改用 fs.promises比較推薦的作法會是 fs.promises,可以將 fs 模組的所有方法轉換為 Promise 物件,例如 fs.promises.readFile。經此處理後的程式碼比較簡潔,範例如下: var fs = require("fs"); ~async function() { console.log("程式執行開始!"); await fs.promises.readFile("./wfu.txt", "utf8") .then(data => { console.log(data); }).catch(err => { console.log(err); }); console.log("程式執行結束!"); }();3. 改用 util.promisify Node.js 主要為 fs 模組提供了同步方法,不代表其他模組都有同步方法。還好官方提供了一個超級實用的工具 util.promisify,可以將其他原生非同步模組轉換為 Promise 物件,來執行「同步」效果,以下為範例程式碼: var fs = require("fs"), util = require("util"), readFile = util.promisify(fs.readFile); ~async function() { console.log("程式執行開始!"); await readFile("./wfu.txt", "utf8") .then(data => { console.log(data); }).catch(err => { console.log(err); }); console.log("程式執行結束!"); }();

九、延遲執行

1. 爬網頁不可頻繁最後補充爬蟲程式常會用到的功能。爬網頁時為了不造成對伺服器的困擾與負擔,間隔頻率不可太頻繁,否則就成了「DDOS 攻擊」。而且伺服器對頻繁的爬取也會進行封鎖,所以需要重複爬取同網站時,最好間隔一段時間。 可參考「降低網頁爬蟲被偵測封鎖的實用方法」→「五、設定隨機的延遲時間」,這裡提出的概念:
  • 間隔時間都相同時很容易被偵測到
  • 根據程式碼,延遲的秒數最好都在 5 秒以上
2. Node.js 實現延遲時間一般後端語言都會內建延遲時間(sleep)的相關功能,但 Node.js 並沒有相關的內建模組。找到這篇「nodejs中實現sleep休眠函數」提供了多種作法,由於篇幅的關係不解釋原理了,多數的作法要嘛需耗用大量 CPU 運算,要嘛需要安裝及載入第三方模組,最推薦的作法是使用 JS 原生函數 Atomics.wait,以下提供範例程式碼: // 延遲執行 n 毫秒; function sleep(n) { var sharedArrayBuffer = new SharedArrayBuffer(4), sharedArray = new Int32Array(sharedArrayBuffer); Atomics.wait(sharedArray, 0, 0, n); } console.log("程式執行開始!"); sleep(2000); // 暫停 2 秒 console.log("程式執行結束!");在 ST3 儲存後,按下 Ctrl+B執行可發現,2 秒後才會印出 "程式執行結束!",完美實現延遲時間的功能。 了解本篇基礎知識後,下一篇會分享完整的爬蟲配套方案如何建構。
更多「爬蟲」相關技巧:

Viewing all articles
Browse latest Browse all 577

Trending Articles