Help us understand the problem. What is going on with this article?

MFクラウドの勤怠管理をコマンドラインから操作(puppeter)

More than 1 year has passed since last update.

こんばんは。スマブラ発売間近で胸がワクワクmorifujiです。

最近、MFクラウドの勤怠管理を利用することになりまして、出勤時退勤時には専用webサイトにログインして、ボタンを押さないといけなくなりました。。

めんどくさいので、ヘッドレスブラウザでサクッとつくったので知見を共有します。puppeter久しぶりすぐる

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種類のスクリプトを叩きます

スクリプト

スクリプト本体作成

main.js
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();
})();

ログイン情報を設定

config.js
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載せてるやん。。。 :astonished:

以下、無駄ですがどうぞ

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.

無理でした。

diggy-mo
クソ雑魚エンジニア
https://blog.morifuji-is.ninja/
atma_inc
Change the common sense with algorithm を達成するためのスタートアップ
https://atma.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした