こんばんは。スマブラ発売間近で胸がワクワクmorimorikochanです。
最近、MFクラウドの勤怠管理を利用することになりまして、出勤時退勤時には専用webサイトにログインして、ボタンを押さないといけなくなりました。。
めんどくさいので、ヘッドレスブラウザでサクッとつくったので知見を共有します。puppeter久しぶりすぐる
[:contents]
git
https://gitlab.com/morifuji/mf-auto-attendance
筆者環境
環境 | バージョン |
---|---|
PC | MacBook Pro (13-inch, 2016, Four Thunderbolt 3 Ports) |
仮想環境/ローカル | ローカル環境 |
nodejs | v10.7.0 |
yarn | 0.27.5 |
puppeter | 1.11.0 |
ゴール
- 以下の手順をヘッドレスブラウザで実行すること
- 1.ログインページからログイン
- 2.出勤ボタンまたは退勤ボタンをクリック
- 3.確認のダイアログに対して入力
- 出勤・退勤の2つのスクリプトを作成
- どちらも1コマンドで
- ID/PASSは外部ファイルに
実装
準備
yarn init mf
yarn add puppeter
package.jsonを編集
{
"name": "mf",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"puppeteer": "^1.11.0"
},
"scripts": {
"mf-in": "node ./main.js in",
"mf-out": "node ./main.js out"
}
}
scripts
で出勤・退勤の2種類のスクリプトを叩きます
スクリプト
スクリプト本体作成
const puppeteer = require('puppeteer');
const config = require('./config.js')
console.log("action: " + process.argv[2])
let isInAttendance = null
switch(process.argv[2]) {
case "in":
isInAttendance = true
break;
case "out":
isInAttendance = false
break;
}
if (null === isInAttendance) {
console.error("引数に `in` または `out` を設定してください")
return;
}
if (!config.id || !config.pass) {
console.error("config.jsでID/パスワードを設定してください")
return;
}
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://payroll.moneyforward.com/session/new', {waitUntil: 'networkidle2'});
await page.type('input[id=sign_in_session_service_email]', config.id);
await page.type('input[id=sign_in_session_service_password]', config.pass);
const inputElement = await page.$('input[name=commit]');
await inputElement.click();
await page.waitFor(2000);
page.on("dialog", (dialog) => {
dialog.accept();
});
let attendanceButton = null
if (isInAttendance) {
attendanceButton = await page.$('.btn-attendance');
} else {
attendanceButton = await page.$('.btn-leaving');
}
await attendanceButton.click();
// 3秒待つ
await page.waitFor(3000);
await browser.close();
})();
ログイン情報を設定
module.exports = {
id: "",
pass: ""
}
idとpassに、自身のログイン情報を設定
実行
# 出勤
yarn mf-in
# 退勤
yarn mf-out
もうちょい楽に
毎日実行することを考えるともう少し短くしたいので、エイリアスを設定
echo -e "alias mf-in='cd ~/mf/ && yarn mf-in'\nalias mf-out='cd ~/mf/ && yarn mf-out'" >> ~/.bashrc
:warning: cd ~/mf/
の部分は、自分のプロジェクトディレクトリに書き直してください
エイリアス実行
# 出勤
mf-in
# 退勤
mf-out
本体スクリプトざっくり説明
もろもろimport
const puppeteer = require('puppeteer');
const config = require('./config.js')
...
if (!config.id || !config.pass) {
console.error("config.jsでID/パスワードを設定してください")
return;
}
ライブラリ(puppeter)をrequireして 設定ファイルから設定値を取得。取得できなかった/false評価ならエラー
引数から、出勤/退勤を判定
console.log("action: " + process.argv[2])
let isInAttendance = null
switch(process.argv[2]) {
case "in":
isInAttendance = true
break;
case "out":
isInAttendance = false
break;
}
if (null === isInAttendance) {
console.error("引数に `in` または `out` を設定してください")
return;
}
process.argv[2]には、 in
またはout
が入っているので、それをもとに出勤か退勤か判定。
puppeterおまじない
(async () => {
...
})();
async/awaitを使いたいので、asyncつけて即時関数にしてる
ページ表示
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://payroll.moneyforward.com/session/new', {waitUntil: 'networkidle2'});
puppeterを起動して、ページを開いてます。
puppeteer.launch()
の第3引数には、様々な設定ができます。例えば、{headless: false}
とすると、ブラウザが表示された上で操作されます。デバッグに便利ですねー。
page.goto
の第三引数のwaitUntilは、puppeterがどの時点でページ描画完了とするかの設定値です。
ほかにも色々な設定ができます
form入力・submit
await page.type('input[id=sign_in_session_service_email]', config.id);
await page.type('input[id=sign_in_session_service_password]', config.pass);
const inputElement = await page.$('input[name=commit]');
await inputElement.click();
await page.waitFor(2000);
クエリセレクタを書いて、そこに第二引数の文字を入力しています。 クリックは少しめんどくさい
waitForでページの描画を待機してます。
出勤ボタンクリック・ダイアログaccept
page.on("dialog", (dialog) => {
dialog.accept();
});
let attendanceButton = null
if (isInAttendance) {
attendanceButton = await page.$('.btn-attendance');
} else {
attendanceButton = await page.$('.btn-leaving');
}
await attendanceButton.click();
出勤ボタン/退勤ボタンのクリックは先ほどと同じ流れです。
この勤怠システムでは、出勤ボタン/退勤ボタンを押すと、確認のダイアログが表示されます。puppeterがそのダイアログを選択する必要があります。
今回は、ダイアログが表示されるとdialog
イベントが発火するので、page.on('{イベント}', {発火する関数})
でイベントリスなを設定してます。他にも、いろんなイベントをみることができるみたいです。
page.on('close')v1.3.0
page.on('console')v0.9.0
page.on('dialog')v0.9.0
page.on('domcontentloaded')v1.1.0
page.on('error')v0.9.0
page.on('frameattached')v0.9.0
page.on('framedetached')v0.9.0
page.on('framenavigated')v0.9.0
page.on('load')v0.9.0
page.on('metrics')v0.12.0
page.on('pageerror')v0.9.0
page.on('request')v0.9.0
page.on('requestfailed')v0.9.0
page.on('requestfinished')v0.9.0
page.on('response')v0.9.0
page.on('workercreated')v1.5.0
page.on('workerdestroyed')v1.5.0
https://pptr.dev/#?product=Puppeteer&version=v1.11.0&show=api-class-page
終了
// 3秒待つ
await page.waitFor(3000);
await browser.close();
出勤・退勤ボタンのクリックから三秒ほど待機。 そのあとヘッドレスブラウザを閉じます
Docker化
nodejsのイメージ使えばyarnがデフォで入っているのでamazonlinux2を使ってyarnのインストールしてるのは無駄でした。
というか、公式でDockerfile載せてるやん。。。 😭
以下、無駄ですがどうぞ
FROM amazonlinux:2
RUN yum update -y
RUN yum upgrade -y
RUN curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | tee /etc/yum.repos.d/yarn.repo
# nodeのバージョンに注意
RUN curl --silent --location https://rpm.nodesource.com/setup_8.x | bash -
RUN yum install -y nodejs
RUN yum install -y yarn
RUN mkdir /root/mf
WORKDIR /root/mf
ADD . /root/mf
# 環境依存なのでdockerないでyarnさせる
RUN rm -rf node_modules
RUN yarn
# 起動設定
CMD /bin/bash -c "yarn mf-in"
実行
❯ docker build -t mf .
...
❯ docker run mf
yarn run v1.12.3
$ node ./main.js in
action: in
(node:27) UnhandledPromiseRejectionWarning: Error: Failed to launch chrome!
/root/mf/node_modules/puppeteer/.local-chromium/linux-609904/chrome-linux/chrome: error while loading shared libraries: libX11.so.6: cannot open shared object file: No such file or directory
TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md
at onClose (/root/mf/node_modules/puppeteer/lib/Launcher.js:342:14)
at Interface.helper.addEventListener (/root/mf/node_modules/puppeteer/lib/Launcher.js:331:50)
at emitNone (events.js:111:20)
at Interface.emit (events.js:208:7)
at Interface.close (readline.js:368:8)
at Socket.onend (readline.js:147:10)
at emitNone (events.js:111:20)
at Socket.emit (events.js:208:7)
at endReadableNT (_stream_readable.js:1064:12)
at _combinedTickCallback (internal/process/next_tick.js:139:11)
(node:27) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:27) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Done in 0.57s.
無理でした。