上次做了一些基礎的講解後,這次就來利用 Puppeteer 實作一個自動化機器人吧!
這次要做的應用是在 LINE 官方帳號的貼文串自動排程及發文,由於 LINE 官方帳號的貼文串功能並無提供 API,剛好能用 Puppeteer 來實作。
先附上 GitHub 連結:https://github.com/AlanSyue/line-timeline-bot
簡單說明一下實作的過程,主要分成 6 個步驟:
- 登入 ( 輸入帳號、密碼 )
- 跳轉至建立貼文頁面
- 輸入預約貼文的指定日期時間
- 上傳照片
- 撰寫貼文
- 按下預約按鈕
大部分的步驟上篇的基礎應用都有說過,這次主要講解兩個較特殊的步驟:
輸入預約貼文的指定日期時間
這裡我是用比較特殊的方法,將日期跟時間拆成一個一個存在 Array,然後用 Puppeteer 模擬鍵盤輸入一個一個打上去。我有試過用 evaluate
這個 API 讓他直接賦值到 date picker,不過不確定為什麼失敗,有可能是那時給的日期是過去的時間,date picker 會抓不到。後來試了直接一個一個輸入就成功了。
上傳照片
這裡我是用 fileChooser 這個 api,他的使用方式是必須先幫他點開到出現檔案選擇器( 下圖 ),然後提供檔案路徑 ( 放在 Array 裡面,如果有多個就會一次上傳多個 ),這邊主要踩到的一個雷是 Chromium 的版本,造成無法上傳,有興趣可以參考 Puppeteer 使用 FileChooser 無法上傳檔案 | 開發經驗分享 。
總結
這邊附上完整的程式碼,有興趣可以拿回去使用看看,就可以做出第一隻自動化的機器人,節省重複的工作,那我們下篇再見囉,謝謝!
const puppeteer = require('puppeteer');
(async () => {
const user = {
"email" : `${line email}`, // LINE 登入帳號
"password" : `${line password}` // LINE 登入密碼
"lineId" : `${line ID}` // LINE 官方帳號 id
}
const postData = {
"imagePath" : `${imgPathArray}`, // 圖檔路徑 ( 需下載下來才能上傳 ) 格式為 Array
"date" : `2020/10/20`, // 預約發文日期
"time" : `10:20`, // 預約發文時間
"content" : "testesteewewefwfwfewfewf" // 文字內容
}
// 開啟 browser
const browser = await puppeteer.launch({
headless: false
});
// 新增分頁
const page = await browser.newPage();
// 監聽 dialog 事件
page.on('dialog', async dialog => {
await dialog.accept();
});
await page.goto(`https://manager.line.biz/account/${user.lineId}`);
await page.click('input[type="submit"]');
await page.waitFor(2000); // stop 2s prevent too fast to redirect
// login
await page.waitForSelector('input[name="tid"]')
await page.type("input[name='tid']", user.email)
await page.waitFor(4);
await page.waitForSelector('input[name="tpasswd"]')
await page.type('input[name="tpasswd"]', user.password)
await page.waitFor(2000);
await page.click('button[type="submit"]')
await page.waitFor(5000);
await page.goto(`https://manager.line.biz/account/${user.lineId}/timeline/create`);
await page.waitForSelector("i[class='las la-image fa-3x']", {timeout:8000});
// 輸入日期與時間
await page.waitForSelector('input[name="datepicker"]')
await page.click("input[name='datepicker']");
await keyInDate(page, postData.date);
await page.click("input[name='timepicker']");
await keyInTime(page, postData.time)
// 上傳照片
await page.click("i[class='las la-image fa-3x']");
await page.waitForSelector('label[class="custom-file-label text-left cursor-pointer"]')
await uploadImage(postData.imagePath, page);
// 建立文字
await page.waitForSelector("div[class='form-control editor']");
await page.type("div[class='form-control editor']", postData.content);
console.log("type success");
// 預約貼文
await page.waitFor(2000);
await page.click("button[class='btn btn-lg btn-primary px-5 mx-1']"); // 點擊貼文按鈕
await page.waitForSelector("button[class='btn btn-lg rounded-0 flex-1 btn-primary']");
await page.click("button[class='btn btn-lg rounded-0 flex-1 btn-primary']"); // 點擊預約按鈕
browser.close();
process.exit();
})();
// use fileChooser to upload images
const uploadImage = async (images, page) => {
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.click('label[class="custom-file-label text-left cursor-pointer"]'),
]);
await fileChooser.accept(images);
}
// key in post date in datepicker
const keyInDate = async (page, date) => {
date.split("").forEach(async (number, index) => {
await page.keyboard.press(number);
await page.keyboard.up(number);
await page.waitFor(5);
})
}
// key in post time in timepicker
const keyInTime = async (page, time) => {
time.split("").forEach(async (number, index) => {
await page.keyboard.press(number);
await page.keyboard.up(number);
await page.waitFor(5);
})
}