Quantcast
Channel: WFU BLOG
Viewing all articles
Browse latest Browse all 571

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

$
0
0
過去一直使用「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 571

Trending Articles