ジョブカンの工数入力を自動で行うスクリプト

会社員たるもの何人たりとも勤怠管理から逃れることができません.. 私が所属している会社では勤怠管理システムとしてジョブカンというものを利用していますが

毎日の工数入力がめんどくさい、苦痛

それ以外の何物でもありません。

f:id:sanshonoki:20210304220703p:plain (ジョブカンのホームページをキャプチャ。CMを見たことがある人も多いのでは?)

そこで、その苦痛を少しでも軽減するための入力サポートツールを作りました。 久しぶりに作りたいと思って作ったソフトウェアです。

github.com

仕組み

やっていることは 工数CSVファイルから読み出し、Selenium を使ってブラウザを操作して自動登録する というだけです。

ただ、これを実際に使ってみると非常にストレスが減り、また工数入力が自動で行われるのを見る様は楽しくさえもあります。(ちょっと言い過ぎかもしれないけど。。ただ、第三者に入力してもらっている感が心地良い。)

使い方

githubのReadmeに書いてますが ChromeDriver をダウンロードして、ジョブカンのログインに必要な情報を環境変数にセットして 工数CSVファイルに記入してスクリプトを走らせるだけです。

# Example (2021_03.csv)
date,プロジェクト1_タスクA,プロジェクト2_タスクB,プロジェクト2_タスクC
2021/03/01,1:00,6:00,1:00
2021/03/02,1:00,,7:00
2021/03/03,-1,1:00,1:00

のようなCSVファイルを用意し、

$ ./jobcan_auto_input.sh 2021_03.csv

だけ

工夫した点

いくつか工夫した点があってそれは以下になります。

  1. プロジェクトとプロジェクトに紐づくタスクは自動で取得してCSVの雛形を作ってくれる

      $ python generate_projects_and_tasks.py
    

    とすると、 f:id:sanshonoki:20210305224430p:plain
    この画面に自動遷移し、ドロップダウンリストの内容をチェックしてプロジェクトとタスクの一覧を取得します

  2. 毎月の工数入力を記録しておくCSVファイルは年と月を入力すると自動的に作成する

      $ python generate_monthly_csv.py 2021 3
    

    とすると、2021_03.csv を作成します

  3. 端数時間を自動的に計算して予め割り当てたタスクに割り振ってくれる

     2021/03/03,-1,1:00,1:00
    

    このように工数 -1 としておくと、例えば総労働時間が8:00の場合、余りの6:00を-1に割り当てて自動登録します

  4. 前月の入力をしたあとでも毎回、自動で前の月に戻ってくれる

4番目に関しては現状のジョブカンのシステム挙動では過去の月の入力を行うと毎回、最新の月に戻ってしまう謎仕様であり、その苦痛たるや筆舌に尽くしがたくその仕様を阻止するためのChrome拡張(GitHub - mob-sakai/JobcanExtensionForChrome)も存在しているぐらいです。

全国6万社のジョブカンユーザー(注:ホームページにそう書いてある)の皆さんの苦痛を少しでも軽減できたらと思います。

(使用される場合は、自己責任でお使いください)

オンラインPOGドラフト用のツール

今年度の仲間うちでのPOGドラフト会議は3密回避のためリモート開催となりました。

ローカルルール

  • 1~3位は入札方式。競合した場合はあみだくじで抽選
  • 4位以降はウェイバー方式で順繰りに指名

入札フェーズ: GoogleFormsの利用

GoogleFormsを利用しました。 以下のようなアンケートフォームを順位ごとに予め作成しておき、URLを参加者に共有し入力してもらいました。

f:id:sanshonoki:20200616052534p:plain

競合が出たときは、LINEのあみだくじ機能を使って決定しました。 グループのトークルーム上で幹事があみだくじを作って抽選します。

あみだくじ機能の使い方はコチラ guide.line.me

ウェイバーフェーズ: 独自開発アプリの利用

Vue.js + Firebaseの練習用として指名馬入力ツールを作りました。

参考にしたもの

グループの作成(オーナーの登録)

f:id:sanshonoki:20200618055434p:plain

馬名の選択

f:id:sanshonoki:20200618055509p:plain

部分一致で検索するので入力の手間はだいぶ楽でした。 すでに指名された馬は候補で出てこないように重複チェックも入れてます。

馬のマスターデータベースはnetkeiba.comからスクレイピングして(競走馬検索から年齢条件を2歳~2歳にして検索)、jsonファイルとしてもっておきそれを読み込みます。

const horse_catalogue = require("../assets/horse_catalogue.json")

指名馬のオーナーへの割当て

f:id:sanshonoki:20200618055817p:plain

割当て後 f:id:sanshonoki:20200618060040p:plain

登録結果のダウンロード

f:id:sanshonoki:20200618060551p:plain CSVでリストをダウンロードできます

downloadCSV: function() {
  let csv = '\ufeff' + 'order_no,owner_name,name,sire,mare,id\n'

  const sorted_horses = this.selected_horses.sort(function(a, b) {
    ...
  })

  sorted_horses.forEach(el => {
    const line = `${el['po_order_no']},${el['po_name']},${el['name']},${el['sire']},${el['mare']},${el['id']}\n`
    csv += line
  })
  const blob = new Blob([csv], { type: 'text/csv' })
  let link = document.createElement('a')
  link.href = window.URL.createObjectURL(blob)
  link.download = 'pog_result.csv'
  link.click()
},

ドラフトで使ってみた結果

今年度に関してはプロトタイプということでスマホ対応も含めてデザインがアレすぎますが機能面に関しては今のところ、1点を除いて問題なく使えました。

見つかった問題点(デザイン以外の改善点)
  • スマホ環境で事前にテストしていたとき、通信環境の問題からか Firebaseに同じ馬がなぜか重複して登録されることがあった..

来年度は Vuetify とか使ってデザインを改善しようと思います。 Vue.jsがオワコンになってなければよいけど。。

ちなみに、アプリは コチラ からアクセスできます。

自由に試せるのでもし興味ある方がいれば使ってみてください。 そのうちクローズしますがしばらくはopenしていると思います

ruby1.9.3でseleniumを動かす

超レガシー環境のruby1.9.3で動かしていたスクレイピングseleniumを使ったものに対応させる必要性があり、その時のメモです。

Dockerfile

FROM ruby:1.9.3

RUN apt-get update
RUN apt-get install -y ttf-freefont chromium unzip libgconf-2-4

ADD Gemfile .
ADD Gemfile.lock .
RUN bundle install

RUN wget https://chromedriver.storage.googleapis.com/2.28/chromedriver_linux64.zip && unzip chromedriver_linux64.zip
RUN rm -f /usr/local/bundle/bin/chromedriver && mv chromedriver /usr/local/bin/ && chmod 755 /usr/local/bin/chromedriver

RUN mkdir /noto
ADD https://noto-website.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip /noto
WORKDIR /noto
RUN unzip NotoSansCJKjp-hinted.zip && \
    mkdir -p /usr/share/fonts/noto && \
    cp *.otf /usr/share/fonts/noto && \
    chmod 644 -R /usr/share/fonts/noto/ && \
    fc-cache -fv
RUN rm -rf /noto

docker-compose.yml

version: '3'
services:
  ruby_selenium:
    build: .
    image: ruby_selenium
    container_name: ruby_selenium
    working_dir: /root/src
    volumes:
      - ./src/:/root/src/
    tty: true

src/scrape.rb

# coding: utf-8

require 'selenium-webdriver'

driver = Selenium::WebDriver.for :chrome, switches: ["--headless", "--no-sandbox"]

driver.navigate.to 'https://www.yahoo.co.jp/'
driver.save_screenshot './screenshot.png'

動かし方

コンテナを起動し、コンテナ内のシェルに入ります。

$ docker-compose build
$ docker-compose up -d
$ docker-compose exec ruby_selenium sh

シェルの中でサンプルスクリプトを動かすと

# ruby scrape.rb

次のように yahoo.co.jp のスクリーンショットが撮れます。

f:id:sanshonoki:20200213220552p:plain

手こずった点

ruby1.9.3だと selenium-webdriver の バージョンを指定する必要がある

selenium-webdriver のバージョン 2.53.4 じゃないとruby1.9 にはインストールできません

selenium-webdriver 2.53.4 だと Selenium::WebDriver::Chrome::Options がない

rubyで headless chrome を動かすコードをWebで調べると以下のようなサンプルコードが簡単に見つかるのですが、

options = Selenium::WebDriver::Chrome::Options.new
options.add_argument('--headless')

driver = Selenium::WebDriver.for :chrome, options: options

selenium-webdriver 2.53.4 ではこのコードは動きません..

How to use Chrome Options in Ruby Selenium? - Stack Overflow

に助けられました

chromiumのバージョンが古い

apt-get でインストールした chromiumブラウザ はかなりバージョンが古いです。

# /usr/bin/chromium --version
Chromium 57.0.2987.98 Built on 8.7, running on Debian 8.0

このバージョンに適合する chromedriver を探してくるのに苦労しました。

chromedriverのダウンロードページ だと ChromeDriver 2.41、Supports Chrome v67-69 までの情報しかのってなくv57 は見つかりません..

いろいろググっていたところ以下の記事が見つかり、助けられました。

javascript - Which ChromeDriver version is compatible with which Chrome Browser version? - Stack Overflow

この表から v57 に適合するのは 2.28 と分かり、対応するchromedriveをインストールできました

# /usr/local/bin/chromedriver --version
ChromeDriver 2.28.455506 (18f6627e265f442aeec9b6661a49fe819aeeea1f)

今回のコード

今回の記事のコードはこちらにあります github.com

参考にした記事

Heroku Schedulerの怪しい挙動

WebアプリでHeroku Schedulerでバッチ処理をしていたのですがたまに実行されないことが幾度かありました。

あれ?と思いつつも自分しか使わないサービスだし、たまにだったので手動再実行で対応していました。。

が、やはり気持ち悪いので重い腰をあげて今回調べてみることにしました

エラーは出てないので怪しいと思ったのは同じ時刻に2つのタスクが登録されていたこと

ということで、実験で再現するか調べます

実験

(1) 同じ時間に3タスク f:id:sanshonoki:20190929215716p:plain

(2) 30分ずつずらす f:id:sanshonoki:20190929215735p:plain

タスクの内容はslackに文字列を送るというタスクです (tsk1であれば "tsk1"というメッセージを送る)

交互に7日間ずつ実行しました

結果

(1)のときは 以下のように 2タスク分しか実行されないときが 2日(/7日)ありました

f:id:sanshonoki:20190929223207p:plain

結論

やはり同時刻に複数タスクが登録されていると実行されない場合があるようです

letter_opener_webをRails3.2+ruby1.9.3の環境で使う

送信メール確認用にdevelopment環境ではletter_openerを使っていたのですが dockerへ移行したので動かなくなりました。。

そこで、Webインターフェースで送信メールを確認できるletter_opener_webに乗り換えました。 ただ、Rails3.2+ruby1.9.3という古い環境では簡単にはいかなかったのでメモを残します。

結論

以下の環境で無事動きました

Gemfile

group :development do
  gem 'public_suffix', '~> 1.4.6' 
  gem "letter_opener", '1.4.1' 
  gem 'letter_opener_web', '1.2.2' 
end

docker-compose.yml

services:
  web:
    # 他は省略
    environment:
      - BROWSER=echo

動くようになるまでの道筋

おまけです。

public_suffixのインストールに失敗

依存gemの1つ、public_suffixのインストールで失敗しました。

public_suffix requires Ruby version >= 2.0.

そこで、ruby1.9.3をサポートしているバージョン '1.4.6' にダウングレードします。

参考にしたページ

Ruby 1.9.3 build broken due to public_suffix gem not compatible + Selenium Error · Issue #6885 · travis-ci/travis-ci · GitHub

/letter_openerを開くとエラー

/usr/local/bundle/gems/letter_opener_web-1.3.4/app/controllers/letter_opener_web/letters_controller.rb:8: unknown type of %string
    before_action :load_letter, only: %i[show attachment destroy]
  • %i の記法は ruby2.0からのサポート
  • before_action は Rails4からの機能

このため、letter_opener_web を '1.2.2' にダウングレードしました

Launchy::CommandNotFoundError のエラーが出る

このエラーは letter_opener_web の バージョン1.3.3 から rescue節でハンドリングすることで出ないようになっているのですが 1.2.2 にダウングレードしているためエラーが出てしまいます..

該当のコミット

https://github.com/fgrehm/letter_opener_web/commit/661ef92c9c83afa47e913b5ddad264b39f22ad09#diff-b83fce5786125deb6a48e47051caa2fe

環境変数 BROWSER をセットすることで回避します。 ブラウザでなくともコマンドにURLを渡すことさえできればエラーは出ないので 環境変数 BROWSER=echo をセットします。

メール送信時に undefined method `html_part'

letter_opener がエラーを出しています。 以下のページによると バージョン1.4.xではエラーが出ないようなので 1.4.1 にダウングレードします。

undefined method `html_part' for #<String:0x00007f5b16161520> · Issue #147 · ryanb/letter_opener · GitHub

Heroku上のRails3アプリの延命対策

プライベートユースでHeroku上で運用しているWebアプリ(Rails3.2 + ruby1.9.3)があるのですが

Cedar-14 stack end-of-life window begins 1 May 2019 | Heroku Dev Center

のアナウンスがあり、来年5月からビルドができなくなってしまうとのこと

ならば、アップグレードしておくかと

アップグレードガイド Upgrading to the Latest Stack | Heroku Dev Center に従って

$ heroku stack:set heroku-18
$ git commit --allow-empty -m "Upgrading to heroku-18"
$ git push heroku master

とheroku-18 stackベースでデプロイしてみたところ、buildエラーに...。

remote:  !     An error occurred while installing ruby-1.9.3-p551
remote:  !
remote:  !     Heroku recommends you use the latest supported Ruby version listed here:
remote:  !     https://devcenter.heroku.com/articles/ruby-support#supported-runtimes
remote:  !
remote:  !     For more information on syntax for declaring a Ruby version see:
remote:  !     https://devcenter.heroku.com/articles/ruby-versions

なんと..

こりぁアカン..

どうやらruby-1.9.3は新しいstackでサポートされてないようです..

ということでcontainerでデプロイするように作戦変更しました。

Rails/rubyの段階的アップグレードはとうの昔に放棄)

containerベースへの移行方法

Dockerfileを作ってローカル環境にてdocker上で動くようにできたら、

あとは

$ heroku container:login
$ heroku container:push web   # コンテナイメージのpush
$ heroku container:release web   # pushしたイメージのリリース

です。

既存のCedar-14 stackでのアプリケーションが稼働中であっても問題なく上書き?でデプロイできました。

コードの修正点は

config/database.yml

production:
  <<: *default
  url: <%= ENV['DATABASE_URL'] %>

それ以外としては、

.dockerignore

tmp/pids/server.pid

を追加しておくとよいと思います。

トラブルシューティング

やはりというかいくつか問題が発生したのでその対策です。 もっとスマートな方法はあるはず。。

トラブル1: Boot timeoutエラー

Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch

どうやらアプリケーションがPORTにアクセスできてないようです

Qiitaの記事 DockerでRailsの環境構築してHerokuへデプロイする - Qiita では

CMD ["rails", "server", "-b", "0.0.0.0"]

でサーバー起動していますが同様にしたら上記のエラーになってしまいました

どうやら自分の環境ではWebrickを使っているからのようです。。 (Rails5だとデフォルトサーバーのpumaの設定ファイル内で環境変数PORTにアクセスしているから問題ない)

本来ならばpumaを使うようにするべきだと思いますが、とりあえず次のように修正して解決しました。

CMD bundle exec rails server -b 0.0.0.0 -p $PORT

ただ、

配列形式の

CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0", "-p", "$PORT"]

にすると

2019-07-17T13:43:33.652404+00:00 heroku[web.1]: Starting process with command `bundle exec rails s -p \8224 -b 0.0.0.0

というエラーになりNGでした...

謎。。

トラブル2: assets precompileされていなくアプリケーションエラー

An ActionView::Template::Error occurred in page#index:

  application.css isn't precompiled

が出ます

assets precompileが必要です。(今まではHerokuデプロイ時に自動でやってくれていた)

Dockerfile に以下の行を追加して解決しました

RUN bundle exec rake assets:precompile

Rakefile を ADD/COPY する前だと No Rakefile found (looking for: rakefile, Rakefile, rakefile.rb, Rakefile.rb) が出るので要注意です

トラブル3: コンパイルしたassetsにアクセスできない

ActionController::RoutingError (No route matches [GET] "/assets/application-bf61d1c6485bd814e710bb58a3622bea.css"):

コンパイルされたCSSにアクセスできず、レイアウトが崩れてしまいました

Rails 4+ Asset Pipeline on Heroku | Heroku Dev Center を参考に

Gemfile に

gem 'rails_12factor', group: :production

を追加して解決しました

トラブル4: development環境用にインストールしたgemが悪さをする

development環境用のgemがインストールされていてproduction環境で悪さをしているということがありました

production環境用のgemのみインストールするのは

# RUN bundle install  # を
RUN bundle install --without test development #  に変更

でよいのですがこれだとローカルの開発環境でdockerビルドしたときに開発用のgemがインストールされません

ARGを使って使い分けるようにしました

Dockerfile

ARG bundle_install_opt="--without test development"
RUN bundle install ${bundle_install_opt}

docker-compose.yml

version: '2'

services:
  web:
    build:
      context: .
      args:
        - bundle_install_opt

というようにすると、ローカル環境では docker-compose build とすれば開発用のgemがインストールされたイメージをビルドできます


とりあえず、引き続きメンテナンスできそうで一安心です

Azureデータサイエンス仮想マシンでハマったこと

東大松尾研のDL4USのLesson7 Section4は学習時間が相当かかるのでGoogle Colab上での実行が困難です。 (実際に40時間以上かかりました。。)

このため、このSectionだけは Azureにインスタンスを作って動かしました。

そのときのメモです。

一言でいうと、インスタンスの作成時の認証は SSH公開鍵キー」 ではなく 「パスワード」 にしたほうが良いかも? という話です。

この画面です

f:id:sanshonoki:20190704214732p:plain

Azureデータサイエンス仮想マシンのイメージには機械学習に必要なツール、ライブラリが一通りインストールされていて環境構築の手間が省けてとても助かります。

作成方法は 公式ページ の通りですが、イメージ選択で「Data Science Virtual Machine for Linux (Ubuntu)」を選択します。

なぜパスワード認証がよい?

この仮想マシンイメージには JupyterHub が最初からインストールされていて自動起動します。

なので、https://<仮想マシンのIPアドレス>:8000 で JupyterHub にアクセスできるのですが SSH公開鍵認証の場合は、そのパスフレーズで JupyterHubにログインできないのです... (UNIXパスワードをもつユーザーでないとログインできない)

f:id:sanshonoki:20190704212742p:plain:w400

パスワード認証でインスタンス作成するとUNIXパスワードをもつユーザーが作成されるのでそのパスワードでJupyterHubにログインできます。

なお、パスワード認証でインスタンス作成してSSH認証したいときはインスタンス作成後、~/.ssh/authorized_keys に公開鍵を置けばオーケーです。

ローカルPCからだと次の1行で登録できます。

$ cat 公開鍵のパス | ssh ユーザー名@IPアドレス 'cat >> .ssh/authorized_keys'

ユーザー名はパスワード認証にしたときのユーザー名です

SSH公開鍵認証でインスタンスを作ってしまった場合

ご安心あれ。Jupyter notebookの設定ファイルを作って Jupyter notebookを起動すれば大丈夫です。

通常通りの手順ですね。

  1. Jupyter Notebookの設定ファイルを作る

    $ jupyter notebook --generate-config
    
  2. .jupyter/jupyter_notebook_config.py を編集する

    1. パスワードのハッシュを作成する

      $ python -c 'from notebook.auth import passwd; print(passwd())'
      
    2. パスワードを設定する

      c.NotebookApp.password = 'sha1:XXX'  # 上で生成したハッシュ
      

あとは jupyter notebook で起動し、ポート8888にアクセスすればオーケー

ただし、このままだと暗号化されていないhttp通信になるので sshポートフォワーディングでログインしてからjupyter起動するのがよいです。

$ ssh -i 秘密鍵へのパス -L ローカルのポート番号:localhost:8888 ユーザー名@インスタンスのIPアドレス

そして http://localhost:ポート番号 にアクセスすればjupyterにアクセスできます。

リスクを承知でポート転送しないときは追加で

  • 設定ファイルで c.NotebookApp.ip = '0.0.0.0' を設定する
  • インスタンスの 受信ポートの規則 で 8888 の TCP を追加する

が必要です