過去一直使用「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.14
二、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 模組名稱 -g (install 可用簡寫 i) 或yarn global 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 儲存後,按下 更多「爬蟲」相關技巧:
沒有留言:
張貼留言注意事項:
◎ 勾選「通知我」可收到後續回覆的mail!
◎ 請在相關文章留言,與文章無關的主題可至「Blogger 社團」提問。
◎ 請避免使用 Safari 瀏覽器,否則無法登入 Google 帳號留言(只能匿名留言)!
◎ 提問若無法提供足夠的資訊供判斷,可能會被無視。建議先參考這篇「Blogger 提問技巧及注意事項」。
◎ CSS 相關問題非免費諮詢,建議使用「Chrome 開發人員工具」尋找答案。
◎ 手機版相關問題請參考「Blogger 行動版範本的特質」→「三、行動版範本不一定能執行網頁版工具」;或參考「Blogger 行動版範本修改技巧 」,或本站 Blogger 行動版標籤相關文章。
◎ 非官方範本問題、或貴站為商業網站,請參考「Blogger 免費諮詢 + 付費諮詢」
◎ 若是使用官方 RWD 範本,請參考「Blogger 推出全新自適應 RWD 官方範本及佈景主題」→ 不建議對範本進行修改!
◎ 若留言要輸入語法,"<"、">"這兩個符號請用其他符號代替,否則語法會消失!
◎ 為了過濾垃圾留言,所有留言不會即時發佈,請稍待片刻。
◎ 本站「已關閉自刪留言功能」。