過去一直使用「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) .js 檔並儲存,按下 Ctrl+B 就能執行 Node.js 程式碼。 3. 安裝模組 npm yarn Node.js 有數十萬個模組可以取用,如何選擇合適的模組需要一些智慧與判斷方式,例如查看近期下載數量、更新版本、更新頻率等等。而管理這些模組的工具,過去知名的是 npm,近期受好評的是 yarn,如何抉擇與安裝,可參考以下: Ctrl+B 執行可發現,2 秒後才會印出 "程式執行結束!",完美實現延遲時間的功能。 了解本篇基礎知識後,下一篇會分享完整的爬蟲配套方案如何建構。
一、Node.js 開發環境
1. 介紹、安裝 Node.js 跟前端 Javascript(以下簡稱 JS) 不太一樣,是後端環境使用的 JS,所以沒有前端的 window、DOM 等等,詳細的介紹與安裝方式,可參考:- Node.js 入門
- Node.js 官站下載
- 支援 Win7 的最後一個版本為 Node.js v13.6
二、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. 重要原生模組以下為製作爬蟲可能用到的原生模組: - fs:用來操作檔案的讀寫,十分重要,這篇是簡單的教學「Node.js 檔案系統」
- path:用於檔案路徑的字串轉換,也很常用到,這篇是簡單的教學「Node.js中路徑處理模組path詳解」
- url:用來處理與解析網址相關字串
- querystring:如果會用到 url 的話,querystring 可以處理網址編碼的轉換
五、外部模組
1. 第三方模組- node.js 除了官方模組,還有許多自行開發且好用的模組,可到「NPM 官網」搜尋需要的模組
- 只有原生模組才能直接 require 載入,安裝非原生模組請先參考前面的說明,使用 npm 或 yarn。
- 進入專案資料夾路徑,執行指令
npm init 或yarn init 進行初始化(加入參數 -y 可省略回答) - 然後執行指令安裝第三方模組
npm install 模組名稱 或yarn add 模組名稱 - 完成以上就可以 require 載入第三方模組
- 在 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 秒以上
// 延遲執行 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 儲存後,按下 更多「爬蟲」相關技巧: