【Day20】Google Sheets - 寫入爬蟲資料,跟 Copy & Paste 的日子說掰掰
這篇文章寫於 2020 年,文中的工具與版本可能已經更新,實作時請以官方文件為準。
本文目錄
- 今日目標
- 1. 改寫crawlerIG、crawler這兩隻函式,讓他們依照我們想要格式回傳爬蟲資料
- 2. 以主程式當橋樑,將爬蟲資料傳遞到updateGoogleSheets
- 3. 將 FB、IG粉專爬蟲資料寫入各自的 Sheet
- 1. 改寫crawlerIG、crawler這兩隻函式,讓他們依照我們想要格式回傳爬蟲資料
- 1.1 分析 Google Sheets 上要呈現的資料、寫入時可能遇到的問題
- 1.2 組合回傳的資訊:以 crawlerFB 為範例
- 2. 以主程式當橋樑,將爬蟲資料傳遞到updateGoogleSheets
- 2.1 用主程式傳遞爬蟲資料
- 2.2 改寫 updateGoogleSheets 函式來接收並處理爬蟲資料
- 3. 將FB、IG粉專爬蟲資料寫入各自的Sheet
- 3.1 分析寫入 Google Sheets 的步驟:writeSheet
- 3.2 在官方文件尋找 寫入Sheet 的範例
- 3.3 第一欄寫入 title(粉專名稱):writeTitle
- 3.4 取得 Sheet 最後一欄的空白欄位:getLastCol
- 3.5 將 trace(追蹤人數)資訊寫入最後一欄:writeTrace
- 執行程式
- ℹ️ 專案原始碼
爬蟲是一個技術,他將網頁的數據收集下來
Google Sheets 是一個容器,他可以儲存資料並將資料以不同面向做展示
爬蟲 X Google Sheets = 你要學習的技術整合
今日目標
1. 改寫crawlerIG、crawler這兩隻函式,讓他們依照我們想要格式回傳爬蟲資料
1.1 分析 Google Sheets 上要呈現的資料、寫入時可能遇到的問題
1.2 組合回傳的資訊:以 crawlerFB 為範例
2. 以主程式當橋樑,將爬蟲資料傳遞到updateGoogleSheets
2.1 用主程式傳遞爬蟲資料
2.2 改寫 updateGoogleSheets 函式來接收並處理爬蟲資料
3. 將 FB、IG粉專爬蟲資料寫入各自的 Sheet
3.1 分析寫入 Google Sheets 的步驟:writeSheet
3.2 在官方文件尋找 寫入Sheet 的範例
3.3 第一欄寫入 title(粉專名稱):writeTitle
3.4 取得 Sheet 最後一欄的空白欄位:getLastCol
3.5 將 trace(追蹤人數)資訊寫入最後一欄:writeTrace
1. 改寫crawlerIG、crawler這兩隻函式,讓他們依照我們想要格式回傳爬蟲資料
過去我們只將 FB粉專、IG粉專的資訊用 console.log 輸出,現在我們要把這些資料有結構的存入 json 物件方便後續使用
1.1 分析 Google Sheets 上要呈現的資料、寫入時可能遇到的問題
- Google Sheets 呈現的資料:
- 粉專名稱
- 追蹤人數
- 追蹤日期
- 可能會遇到的問題:
- 重複名稱的粉專
- 解決方案:粉專名稱有可能重名,但是粉專網址是唯一值,所以使用
粉專名稱+粉專網址作為key就能解決這個問題,同時也方便使用者可以點擊連結直接前往粉專
- 解決方案:粉專名稱有可能重名,但是粉專網址是唯一值,所以使用
- 重複名稱的粉專
- 總結:
- 因為追蹤的 FB、IG粉專數量很多,所以 crawlerFB、crawlerIG 這兩個函式回傳的最外層用陣列(array)包起來
- 根據 Google Sheets 所需的資料,每個粉專的物件(object)需要有以下內容:
- 粉專名稱(title)
- 粉專網址(url)
- 追蹤人數(trace)
1.2 組合回傳的資訊:以 crawlerFB 為範例
-
在 try-catch 的後面加上 finally,在 finally 階段才把粉專物件存入 result_array 是因為無論這個粉專爬蟲成功或是失敗我們都要記錄他的數據
finally 代表在 try-catch 結束後會執行的任務,忘記的可以回到Day14 try-catch-finally 基礎語法說明複習喔
-
在 FB粉專爬蟲完成後,回傳儲存所有粉專資訊的 result_array 讓主程式做資料串接
async function crawlerFB (driver) {
const isLogin = await loginFacebook(driver)
if (isLogin) {
console.log(`FB開始爬蟲`)
let result_array = []
for (fanpage of fanpage_array) {
let trace = null
try {
const isGoFansPage = await goFansPage(driver, fanpage.url)
if (isGoFansPage) {
await driver.sleep((Math.floor(Math.random() * 4) + 3) * 1000)
trace = await getTrace(driver, By, until)
}
if (trace === null) {
console.log(`${fanpage.title}無法抓取追蹤人數`)
} else {
console.log(`${fanpage.title}追蹤人數:${trace}`)
}
} catch (e) {
console.error(e);
continue;
} finally {// 將粉專的資訊塞入物件
result_array.push({
url: fanpage.url,
title: fanpage.title,
trace: trace
})
}
}
// 回傳FB粉專爬蟲資料
return result_array
}
}
2. 以主程式當橋樑,將爬蟲資料傳遞到updateGoogleSheets
2.1 用主程式傳遞爬蟲資料
主程式將 crawlerIG、crawlerFB 回傳的爬蟲資訊提供給 updateGoogleSheets 當參數
async function crawler () {
const driver = await initDrive();
if (!driver) {
return
}
//取得回傳的爬蟲資料
const ig_result_array = await crawlerIG(driver)
const fb_result_array = await crawlerFB(driver)
driver.quit();
//將回傳的爬蟲資料傳遞給Google Sheets處理
await updateGoogleSheets(ig_result_array, fb_result_array)
}
2.2 改寫 updateGoogleSheets 函式來接收並處理爬蟲資料
- 接受爬蟲回傳的資料:ig_result_array, fb_result_array
- 新增函式
writeSheet將收到的爬蟲資料寫入對應的 Sheet - 最後印出 Google Sheets 的網址方便查看
async function updateGoogleSheets (ig_result_array, fb_result_array) {
try {
const auth = await getAuth()
let sheets = await getFBIGSheet(auth)
console.log('FB、IG Sheet資訊:')
console.log(sheets)
// 將爬蟲資料寫入各自的Sheet
await writeSheet('FB粉專', fb_result_array, auth)
await writeSheet('IG帳號', ig_result_array, auth)
console.log(`成功更新Google Sheets:https://docs.google.com/spreadsheets/d/${process.env.SPREADSHEET_ID}`);
} catch (err) {
console.error('更新Google Sheets失敗');
console.error(err);
}
}
3. 將FB、IG粉專爬蟲資料寫入各自的Sheet
3.1 分析寫入 Google Sheets 的步驟:writeSheet
目標:希望寫入的 Google Sheets 長得像下圖:

我們先將想要完成的功能羅列出來,再一個個去實現:
- 第一欄為
粉專名稱+粉專網址組合的 HYPERLINK,每次執行爬蟲時我們會用writeTitle這個函式來更新第一欄的資料 - 因為希望新的爬蟲紀錄會寫入到下一欄,所以我們寫一個函式
getLastCol來取得 Sheet 最後一欄的空白欄位 - 取得目標寫入欄位後,我們用函式
writeTrace把爬蟲下來的追蹤人數寫入就完成嚕
實作:將上述要完成的功能各自撰寫成函式後,由 writeSheet 這個函式來管理寫入 Google Sheets 的步驟
async function writeSheet (title, result_array, auth) {
// title可以直接超連結到粉專
let title_array = result_array.map(fanpage => [`=HYPERLINK("${fanpage.url}","${fanpage.title}")`]);
// 開頭填入填上FB粉專/IG帳號
title_array.unshift([title])//unshift是指插入陣列開頭
// 在第一欄寫入title(粉專名稱)
await writeTitle(title, title_array, auth)
// 取得目前最後一欄
let lastCol = await getLastCol(title, auth)
let trace_array = result_array.map(fanpage => [fanpage.trace]);
// 抓取當天日期
const datetime = new Date()
trace_array.unshift([dateFormat(datetime, "GMT:yyyy/mm/dd")])
// 再寫入trace(追蹤人數)
await writeTrace(title, trace_array, lastCol, auth)
}
3.2 在官方文件尋找 寫入Sheet 的範例
-
有了昨天的經驗我們可以很容易找到今天所需的資源,在範例首頁的大標題Basic Writing就符合今天的需求

-
在閱讀 Basic Writing 描述時有兩個標題吸引我的注意
-
Append Values:在閱讀標題時我原本覺得是最好的選擇,但發現他只能插入列(row),與我們插入欄(col)的需求不符
-
Write a single range:在開頭便提供了一個連結,並說這個方法能寫入指定 Sheet 範圍內的資料

-
-
接著你可以用
Try this API來確認 Method: spreadsheets.values.update 是否能正確更新 Sheet 的內容,相關操作方式昨天的文章有示範,這裡就不再贅述了,下面我把官方範例翻譯了一下,讓大家更清楚使用的方法let title = '你的sheet title' //Google Sheets能吃的array格式範例 let array = [['test1'],['test2'],['test3'],['test4']] async function writeSheet (title, array, auth) {//auth為憑證通過後取得 const sheets = google.sheets({ version: 'v4', auth }); const request = { spreadsheetId: process.env.SPREADSHEET_ID, valueInputOption: "USER_ENTERED",//寫入格式的分類有:INPUT_VALUE_OPTION_UNSPECIFIED|RAW|USER_ENTERED range: [ `'${title}'!A:A`//title是sheet的標題,A:A是能寫入的範圍 ], resource: { values: array } } try { await sheets.spreadsheets.values.update(request);//執行後即完成Google Sheets更新 console.log(`updated ${title} title`); } catch (err) { console.error(err); } }
3.3 第一欄寫入 title(粉專名稱):writeTitle
async function writeTitle (title, title_array, auth) {
const sheets = google.sheets({ version: 'v4', auth });
const request = {
spreadsheetId: process.env.SPREADSHEET_ID,
valueInputOption: "USER_ENTERED",
range: [
`'${title}'!A:A` // 這是寫入第一欄的意思
],
resource: {
values: title_array
}
}
try {
await sheets.spreadsheets.values.update(request);
console.log(`updated ${title} title`);
} catch (err) {
console.error(err);
}
}
3.4 取得 Sheet 最後一欄的空白欄位:getLastCol
- 在這個函式中我們取得 Sheet 的第一列來做分析,詳細的文件請參考官方說明
- 同時我們也需要用函式
toColumnName把取得的欄位名稱轉換為英文,這樣 Google Sheets 才知道要寫入的欄位
async function getLastCol (title, auth) {
const sheets = google.sheets({ version: 'v4', auth });
const request = {
spreadsheetId: process.env.SPREADSHEET_ID,
ranges: [
`'${title}'!A1:ZZ1`
],
majorDimension: "COLUMNS",
}
try {
let values = (await sheets.spreadsheets.values.batchGet(request)).data.valueRanges[0].values;
// console.log(title + " StartCol: " + toColumnName(values.length + 1))
return toColumnName(values.length + 1)
} catch (err) {
console.error(err);
}
}
function toColumnName (num) {//Google Sheets無法辨認數字欄位,需轉為英文才能使用
for (var ret = '', a = 1, b = 26; (num -= a) >= 0; a = b, b *= 26) {
ret = String.fromCharCode(parseInt((num % b) / a) + 65) + ret;
}
return ret;
}
3.5 將 trace(追蹤人數)資訊寫入最後一欄:writeTrace
async function writeTrace (title, trace_array, lastCol, auth) {
const sheets = google.sheets({ version: 'v4', auth });
const request = {
spreadsheetId: process.env.SPREADSHEET_ID,
valueInputOption: "USER_ENTERED",
range: [
`'${title}'!${lastCol}:${lastCol}`//將追蹤人數填入最後一欄
],
resource: {
values: trace_array
}
}
try {
await sheets.spreadsheets.values.update(request);
console.log(`updated ${title} trace`);
} catch (err) {
console.error(err);
}
}
執行程式
-
在專案資料夾的終端機(Terminal)執行指令
yarn start -
確認執行過程中爬蟲資料抓取正常且更新 Google sheets 時沒有錯誤

-
點進去連結看看線上的 Google Sheets 是不是也被成功寫入惹~

目前為止將爬蟲寫入 Google Sheets 的動作已經完成了,大家可以思考一下還有什麼東西是我們忽略的呢?有什麼狀況會造成錯誤?希望大家在下方提供自己的想法喔~
ℹ️ 專案原始碼
- 今天的完整程式碼可以在這裡找到喔
- 我也貼心地把昨天的把昨天的程式碼打包成壓縮檔,你可以用裡面乾淨的環境來實作今天Google Sheets的爬蟲資料寫入喔
- 請記得在終端機下指令 yarn 才會把之前的套件安裝
- windows需下載與你chrome版本相同的chrome driver放在專案根目錄
- 要在tools/google_sheets資料夾放上自己的憑證,申請流程請參考Day17
- 調整fanspages資料夾內目標爬蟲的粉專網址
- 調整.env檔
- 填上FB登入資訊
- 填上FB版本(classic/new)
- 填上IG登入資訊
- 填上SPREADSHEET_ID
在許多人的幫助下,本系列文章已出版成書,並添加了新的篇章與細節補充:
- 加入更多實務經驗,用完整的開發流程讓讀者了解專案每個階段要注意的事項
- 將爬蟲的步驟與技巧做更詳細的說明,讓讀者可以輕鬆入門
- 調整專案架構
- 優化爬蟲程式,以更廣的視角來擷取網頁資訊
- 增加資料驗證、錯誤通知等功能,讓爬蟲執行遇到問題時可以第一時間通知使用者
- 排程部分改用 node-schedule & pm2 的組合,讓讀者可以輕鬆管理專案程序並獲得更精確的 log 資訊
有興趣的朋友可以到天瓏書局選購,感謝大家的支持。

留言載入中…