Puppeteer,由 Google 出品的 Node Library,可以用來控制瀏覽器和模擬使用者行為,不管用來寫爬蟲或測試程式都是不可多得的利器;而在這次學習案例中,則是以 Twitch 為研究目標,來開發個模擬使用者看台的輔助程式。
Puppeteer
學習心得就是簡單好用,尤其是結合 Chrome 的 Headless 功能,簡直讓我驚為天人。
那到底 Puppeteer 要做什麼事呢?首先把要模擬的行為先簡單列出來,仔細一看原來也就一項而已。
User | Puppeteer |
---|
開啟某個實況主頻道 | 開啟某個 Url |
觀看 | ❌ |
Twitch-watcher
而這樣的一支程式已經有人寫好了,專案名稱就叫 Twitch-watcher,這個專案讓我學習到很多東西,實在是非常感謝該作者。
專案目標
我這邊要做的,就是以現有的 Twitch-watcher 加入自己的需求進行客製化,以下列出這次的目標:
- 指令控制
- 列出追隨頻道
- 觀看多個頻道
- 關台偵測
- 領取特殊額外獎勵
- 計算忠誠點數
實作-指令控制
使用 readline 重新設計使用者操作流程,並加入 help(幫助)、list(列出追隨頻道)、watch(開始觀看指定頻道)、state(查詢觀看狀態)、leave(離開指定頻道)、exit(離開程式)。
1 2 3 4 5 6 7 8
| const readline = require("readline");
process.stdin.setEncoding("utf-8"); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "> ", });
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| rl.on("line", async (input) => { const source = input.trim().split(" "); const command = source[0]; const args = source[1]; rl.pause(); switch (command) { default: break; case "help": await output.help(); break; ... ... } rl.resume(); rl.prompt(); });
|
實作-列出追隨頻道
因為原專案是列出 Valorant 的相關頻道,所以只要修改取得頻道的網址和 Query 就 OK 了。
1 2 3
| const streamersUrl = process.env.streamersUrl || "https://www.twitch.tv/directory/following/live"; const channelsQuery = 'a[data-a-target="preview-card-image-link"]';
|
實作-觀看多個頻道
配合前面的 watch 指令重新設計了一個方法(watch)來接收頻道資料,每個觀看任務都有獨立的 Browser 和 Page,初始化完成後開始觀看任務(loopWatch)。
1 2 3 4 5 6 7 8 9 10 11 12 13
| switch (command) { ... ... case "watch": if (args && streamers[args] != null && !streamers[args].isRun) { await watch(streamers[args]); } else { await output.help(); } break; ... ... }
|
1 2 3 4 5 6 7 8
| async function watch(streamer) { let browser = await spawnBrowser(); let page = await spawnPage(browser); ... ... await initialWatchPage(streamer); loopWatch(streamer); }
|
實作-關台偵測
每小時重整一次頁面,當偵測到頻道離線時,停止任務並關閉瀏覽器(killStream)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| async function loopWatch(streamer) { while (streamer.isRun) { if (dayjs(streamer.reload).isBefore(dayjs())) { await streamer.page.reload({ waitUntil: ["networkidle0"] }); ... ... let channelOfflineDiv = await queryOnWebsite(page, streamOfflineQuery); if (channelOfflineDiv.length > 0) { await killStream(streamer); return; } } await claimPoint(streamer); } }
|
實作-領取特殊額外獎勵
偵測領取按鈕,每秒執行一次,直到任務結束;每成功領取一次則計數器(claim)++。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| async function claimPoint(streamer) { let reload = dayjs(streamer.reload); while (!reload.isBefore(dayjs())) { if (streamer.isRun === false) break; let claimBtn = await queryOnWebsite(streamer.page, claimPointQuery); if (claimBtn.length > 0) { await eval.eval( streamer.page, `() => document.querySelector('${claimPointQuery}').click()` ); streamer.claim++; } await streamer.page.waitFor(1000); } }
|
實作-計算忠誠點數
任務開始時紀錄初始數值,每次查詢(state)時計算相差多少,簡直不要太簡單。
1 2 3 4 5 6 7 8 9
| async function state(token, streamer) { ... ... let point = await gqlapi.channelPointsContext(token, streamer.login); let balance = point.data.community.channel.self.communityPoints.balance; console.log( `✨ [${streamer.login}] Point: ${streamer.point} ~ ${balance} (claim ${streamer.claim} times)` ); }
|
Demo
這次額外學習到的新工具 asciinema,可以非常輕量的錄製 terminal 執行過程並發布成影片,但在 Windows 上只能用 PowerSession 就是了(我自己測試有亂碼問題)。