sironekotoroの日記

Perl で楽をしたい

久々の @xtetsuji さんからの出題をやってみる

意外と面倒だった・・・もっと、ロジックだけに集中すれば早そうだけど、ロジックを追いかける頭がないのだった(完

gist.github.com

Google スプレッドシートの情報を GAS で WebAPI にしてXML で取得できるようにして、それを Excel の PowerQuery で取得する

悩み

経理ネタです。Perl 出てきません(往年の YAPC みたいになってきた)

  • 請求書ベースの振込先の管理に Excel シートを利用している
  • 自社サービスの一部の振込先の管理に Google スプレッドシートを使っている

これをどっちかにまとめたい。というか、マスタ的なものが複数あるのは面倒。仕事(というか作業)に IF 文は少ないに限る。

振込のデータ作成と、振込先のデータは分けておきたい。そして、振込先のデータは編集へのハードルが高い状態にしておきたい。

一意になっていないレコードがあったりして、こういうのを何とかしていきたい(企業の名前はよく変わる。口座番号そのままに・・・)

考えた

作った

  • 手始めに WebAPI として JSON で吐き出してみる
    • これはok
  • XML での出力も何とかなった、というかちゃんと関数あった

    • developers.google.com
    • しかし、エレメント名(タグ名とでもいうか)に半角カタカナを使えないことを知らずに2時間くらいハマってた
      • 半角カタカナ現役の世界があるんすよ・・・
    • この辺りは PerlRSS をいじったりなんだりした経験が生きた
      • 主に欲望方面での RSS
  • ExcelWEBSERVICE 関数からのアクセス・・・エラー「ログインしてね」

    • あー、Web ブラウザから URL を直接叩けば、Cookie なり SAML なり SSO で権限の問題は解消されるけど、Excel が URL にアクセスしにいく時はそうじゃないもんなー
    • WebAPI を全公開にしたらちゃんと反映出来た
      • 現行よりセキュリティ要件甘くなってるやん
      • WebAPI の URL に BASIC 認証をかけるとかどうよ
        • なさそう
      • WebAPI のリクエストパラメータに認証情報を設けて、そこで区別するというのはどうか?
        • うーん、出来なくはないけどー、せっかく社で使ってるのに、それとは別の認証を設けるのはちょっとなんか
  • 項目(カラム)1個ごとに1回、WebAPI のアクセスが発生しちゃう!非効率!

    • 今のところ呼び出す項目の順番は固定だから、配列にしちゃうかー
      • ところで、 XML って配列サポートしてるの・・・?
    • いやちょっと待って、1件1件データとってくる必要ある?
      • ない
  • 全件取得して、そのシートを参照すれば良いのでは

    • Excel の PowerQuery で XML を取り込むことでうまくいった
      • これ、結局手動では?シート開くたびに更新とかできるのかな?(未確認

この辺りで全てがめんどくさくなってくる

  • そもそも、なぜこんな苦労を・・・普通にDB立てたら?

    • 担当者がいなくなった後、誰がメンテするんだ
  • Microsoft 製品同士 Accessを使ってみては?

    • 担当者がいなくなった後、誰がメンテするんだ(2回目
    • 15年くらい前に Accessの資格とった気がするが、全然覚えてない
  • ExcelGoogle スプレッドシートがぱっと見分かりやすすぎるんだよな・・・

    • 利用者が Excel 側だけ更新して、正のデータである GAS を更新しないということはあり得そう
    • だって、それが現行の使い方で、1ステップで終わるけど、GAS 更新=> Excel で取り込み って2ステップを強いてもうまく行かないよな
      • 得られる結果は同じで、手数は増える
      • 野良マスタが増えるわけよなー
      • データの一元化、正規化のメリットは大きいんだけどなー

ということで結論的な

  • まぁ、うちだけでも使えるようにすればいいか
  • 欲しかったものをすぐ使えないのは残念だけど、まぁ、それはそれで
  • 経験積めたしー
  • とはいえ悔しい

2020年7月のオリンピック開催にあわせて国が用意した4連休ですが、コロナ自粛で遠出もせず、こういうものを作ってました

  • 個人的には失敗なのだけど、こういうのを作ってました、って残しておこう

Googleスプレッドシート

docs.google.com

XML取得のURL

Googleスプレッドシートの1行目の列名をXMLのタグ名とする

なお、列名に半角カタカナ入れるとコケて面白い(面白くない https://script.googleusercontent.com/a/macros/sironekotoro.com/echo?user_content_key=dax6BTzkq7IeQQ3h-NknDD2IBTMIbIL_6PslC5LmBLAEa4hlkdRClhKqoKUnM3q8_8qO4thL0iVD3vHg_hOR4pFpYRdKinrym5_BxDlH2jW0nuo2oDemN9CCS2h10ox_nRPgeZU6HP93n8xsgl2lv7M3QUtYsNJpyc5IGh1q_XKB3dFIzDdUtB5Qdgb8Zy7FHv7ZCHVgFop9_MCr1SAs-DMeGy7bcuKtdIQwAH1Q462S69gbKJ2_-9z9Jw9Md8uu&lib=Mi5FVeXc-rscpK5HLaNixMyOoJz7hZlKj

Excel の Power Query で取り込む(Windows版のみ)

  1. メニューから 「データ」 を選ぶ
  2. [データの取得] をクリックし、「その他のデータソースから」->「Webから」を選択
  3. ラジオボタンは [基本] のままで、URLに上記のURLを入力し [OK] ボタンをクリックする
  4. 「Webコンテンツへのアクセス」の画面が出るので、そのまま(匿名のまま)、右下の [接続] ボタンをクリックする
  5. ナビゲーター の画面が出るので、左側メニューの row を選択して、右側のプレビューが出ることを確認する
  6. 確認したら、右下の [データの変換] ボタンをクリックする
  7. Power Query エディターの画面が出るので、右下の 「適用したステップ」のところで「変更された型」をクリックし、❌マークが赤くなったら❌マーククリックして削除する
  8. 左上の [閉じて読み込む] をクリックして、「閉じて読み込む」 を選択

f:id:sironekotoro:20200726111614p:plain

GAS

手元にコピペして試す時は、メニューの [公開] から 「ウェブアプリケーションとして導入」しておくのを忘れずに

"use strict"

const SHEETNAME = 'シート1';

function getValues(){
  const sourceSheet     = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEETNAME);
  const sourceRange     = sourceSheet.getDataRange();
  const sourceValues    = sourceRange.getValues();

  return sourceValues;
}


//function searchId(id = 1) {
//  const sourceValues = getValues();
//  const row = sourceValues.filter( row => id === row[0] );
//
////  console.log(row);
//
//  return row;
//}


function searchAll(){
  const sourceValues = getValues();

  sourceValues.shift();

//  console.log(sourceValues);

  return sourceValues;
}

function getColNames(){
  const sourceSheet     = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEETNAME);
  
  const lastColumn = sourceSheet.getLastColumn();

  const colNames = sourceSheet.getRange(1,1,1,lastColumn).getValues();
  
  return colNames[0];
}


function makeXml(rows = searchId(3) ){

  const colNames = getColNames();
  
  const root = XmlService.createElement('rows');  

  rows.forEach( row => {
    const child = XmlService.createElement('row');

    for (let i =0;i<row.length;i++){
      child.addContent(XmlService.createElement(colNames[i]).setText(row[i]));
    }
    root.addContent(child);
  });

  const document = XmlService.createDocument(root);
  const xml = XmlService.getPrettyFormat().format(document);

//  console.log(xml);

  return xml;

}

function doGet(e) {

  //  const id = e.parameter.id;
  const rows = searchAll();
  const xml = makeXml(rows);
  
  return ContentService
      .createTextOutput(xml)
      .setMimeType(ContentService.MimeType.XML);

}

function doPost(e) {
  doGet(e);
}

nodejs を macOS Catalina にインストールした

前は何かで nodejs を入れたのだけど、macOS Catalina をクリーンインストールした時に消えたままだった。

昨日土曜日に挑戦して、2時間かかってインストールできず、本日再チャレンジしたらすんなりインストールできた。

メモっておけばよかったけど、パスが通らなかったのと、ディレクトリがないってエラーだった。

備忘録的にコマンド履歴を残しておく。

nodejs を macOS Catalina にインストール

環境

sironekotoro 20-07-12 10:58:24 ~
$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.5
BuildVersion:   19F101

homebrew のバージョン

$ brew -v
Homebrew 2.4.5
Homebrew/homebrew-core (git revision d2eb63; last commit 2020-07-11)
Homebrew/homebrew-cask (git revision e6f3da; last commit 2020-07-11)

nodebrew のインストール

  • 一旦インストールできたのを削除して記録とっているので、Already downloaded: ってなっている
$ brew install nodebrew
Updating Homebrew...
==> Downloading https://github.com/hokaccha/nodebrew/archive/v1.0.1.tar.gz
Already downloaded: /Users/sironekotoro/Library/Caches/Homebrew/downloads/9895acc38dc859a4a1a841cf2e8cd78b519163d3499cdfcd4a08a068ce0babcd--nodebrew-1.0.1.tar.gz
==> Caveats
You need to manually run setup_dirs to create directories required by nodebrew:
  /usr/local/opt/nodebrew/bin/nodebrew setup_dirs

Add path:
  export PATH=$HOME/.nodebrew/current/bin:$PATH

To use Homebrew's directories rather than ~/.nodebrew add to your profile:
  export NODEBREW_ROOT=/usr/local/var/nodebrew

Bash completion has been installed to:
  /usr/local/etc/bash_completion.d

zsh completions have been installed to:
  /usr/local/share/zsh/site-functions
==> Summary
🍺  /usr/local/Cellar/nodebrew/1.0.1: 8 files, 38.6KB, built in 3 seconds

インストール先のフォルダを作る

  • 上の nodebrew 入れた後のメッセージにもある通り、コマンドを打つ必要がある
You need to manually run setup_dirs to create directories required by nodebrew:
  /usr/local/opt/nodebrew/bin/nodebrew setup_dirs
  • このコマンドにより、 ~/.nodebrew フォルダが作成される
  • mkdir で作っている方法を紹介してるエントリもあったけど、その後のインストールが進まなかった
sironekotoro 20-07-12 10:59:24 ~
$ ls -la ~/.nodebrew
ls: /Users/sironekotoro/.nodebrew: No such file or directory

sironekotoro 20-07-12 11:00:29 ~
$ /usr/local/opt/nodebrew/bin/nodebrew setup_dirs

sironekotoro 20-07-12 11:00:33 ~
$ ls -la ~/.nodebrew
total 0
drwxr-xr-x   6 sironekotoro  staff   192  7 12 11:00 .
drwxr-xr-x@ 52 sironekotoro  staff  1664  7 12 11:00 ..
drwxr-xr-x   3 sironekotoro  staff    96  7 12 11:00 default
drwxr-xr-x   2 sironekotoro  staff    64  7 12 11:00 iojs
drwxr-xr-x   2 sironekotoro  staff    64  7 12 11:00 node
drwxr-xr-x   2 sironekotoro  staff    64  7 12 11:00 src

nodebrew のバージョンを確認

$ nodebrew -v
nodebrew 1.0.1

nodebrew へのパスを追加する

  • パスを追加することで、 $HOME/.nodebrew/current/bin 以外の場所にいても nodebrew コマンドを利用することができるようになる
sironekotoro 20-07-12 11:01:13 ~
$ export PATH=$HOME/.nodebrew/current/bin:$PATH

nodebrew で現時点での安定版をインストールする

  • 2020年07月12日時点では v12.18.2 が最新の安定版となっている

f:id:sironekotoro:20200712112934p:plain

sironekotoro 20-07-12 11:01:28 ~
$ nodebrew install v12.18.2
Fetching: https://nodejs.org/dist/v12.18.2/node-v12.18.2-darwin-x64.tar.gz
######################################################################### 100.0%
Installed successfully

nodebrew で、nodejs がインストールできたかを確認

sironekotoro 20-07-12 11:02:56 ~
$ nodebrew ls
v12.18.2

current: none

nodebrew で、nodejs のバージョンを指定する

  • 今回は1つのバージョンしか入れないけど、いずれ複数のバージョンを入れる。その時にもインストールとバージョンの指定が必要
  • current が v12.18.2 になる
sironekotoro 20-07-12 11:03:22 ~
$ nodebrew use v12.18.2
use v12.18.2

sironekotoro 20-07-12 11:03:39 ~
$ nodebrew ls
v12.18.2

current: v12.18.2

nodejs のバージョンを確認する

sironekotoro 20-07-12 11:03:52 ~
$ node -v
v12.18.2

npm(Node package maneger)も入っているか確認する

  • npm は nodejs での開発に必須なパッケージマネージャ
  • これも一緒にインストールされる
sironekotoro 20-07-12 12:14:01 ~
$ npm -v
6.14.5

~/.zshrc にパスを追加しておく

  • 現時点のままだと、ターミナルを終了すると通したパスの設定がなくなってしまう
  • ターミナル起動ごとにパスが通るように、~/.zshrc に設定を追加する
$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zshrc
  • これで、~/.zshrc の最終行にパスを追加するコマンドが追加される

なぜ nodejs インストールしたのか?

JavaScript との接点

  • 最近は Google Apps Script で経理を(やっている自分が)楽をするためのスクリプトを作っている
  • が、「動けばいい」みたいなレベルなので、ちゃんとした学習をしたくなった
    • まぁ、動いて目的を果たせるならそれはそれで素晴らしいとは思うんだけど
    • 強欲である

今回の教材

今回の勉強法

  • いつも初心者本を買うと、すぐ写経を始めるのだけど、「このコードは Perl ではどう書くんだろう?」って欲求が抑えられない
    • 抑えられない結果、無限に脱線していく
  • このため、この本はまず一通りを通勤途中に読んでいる
    • 経理のお仕事は出社前提で組まれていて、コロナ禍の中でも出社せざるを得ないのですー
    • ただまぁ、通勤そんなに嫌いではない
  • で、一通り読み終わりそうなので nodejs をインストールしたというわけ
    • 多分今週には読み終わりそう
  • おかげで、脱線せずに最後まで読み切ることができそう
  • JavaScript は今までも何回も入門してる
  • とっとと先( TypeScript, Angular, React, Vue )に行きたいけど、自分の中に基礎ができてないのが気持ち悪い
  • ということで、何度でも入門する
  • 2周目はもちろん写経しながら読む
  • ちなみに、技術書読むときは iPad Pro 12.9 インチモデルがとてもいいです。とても重くて嵩張るけど。 f:id:sironekotoro:20200712120047j:plain

ある月の最後の平日を求める

ってことでこんにちは。休日になると仕事のスクリプト作成が捗りますね。

なぜか仕事のある平日はそうでもないのですが・・・

その月の最後の平日を求めたい理由

経理の世界では・・・と主語を大きく言いたいところですが、経理は各会社それぞれの色が強く出るところなので「今の業務では」と言っておきます。

まぁ、矛盾のない貸借対照表損益計算書が出てくれば、その過程は問われないというというのはあります。もちろん説明責任はあります。

今の業務では、ある月に発生した未払金(翌月払い等)は経理上、末日付で未払金計上します。クレカとか、AWSの利用料とか。

6月のAWS使用料金が20,000円だった場合の仕訳例です。未払金は翌月以降に支払うお金なので、末日が平日であろうが休日であろうがかまいません。

日付 借方 貸方 摘要
2020/06/30 支払手数料 20,000 未払金 20,000 AWS 6月分使用料

これを7月末に支払いすると、このような仕訳になります。この支払日は銀行営業日、つまり平日である必要があります。

銀行の通帳に記録される増減と仕訳上の記録を合わせておく必要があるためです。合わせないと照合作業が面倒すぎて死ねますね。

日付 借方 貸方 摘要
2020/07/31 未払金 20,000 普通預金 20,000 AWS 6月分使用料

この「その月の最後の平日」、支払の仕訳の「2020/07/31」を楽に求めたい!

特に経理ソフト上でコツコツ入力せず、csv作ってまとめて一発登録!みたいなのやっている自分だと特に!!

その月の最後の平日を求める

実際には Google Apps Script 、つまりJavaScript で書いてたんですが Perl のコードで書いてみます。

まず、日時を扱う Time::Piece で日時のオブジェクトを作成します。

オブジェクトは、「データと、そのデータを扱う関数が一緒になったもの」という説明をしておきます。

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;

# 2020年7月でTime::Pieceオブジェクトを作る
my $t = Time::Piece->strptime("2020-07", '%Y-%m');

# 7月の最終日を求める
my $day = $t->month_last_day();
print "last day of month: ", $day . "\n";# 31

# 2020年7月最終日のTime::Pieceオブジェクトを作る
my $last_date = Time::Piece->strptime("2020-07-$day", '%Y-%m-%d');

# 最終日の曜日を求める
# 0: 日曜 〜 6: 土曜
print "day of week: ", $last_date->day_of_week . "\n";# 5

2020年7月31日は金曜日なので $last_date->day_of_week の結果は 5 が返ります。

ふむふむ、つまり、最終日が日曜日( $last_date->day_of_week0)か土曜日( $last_date->day_of_week6)だったら前日を設定し、もう一回判定して・・・とやればいいな?と目星をつけます。

前日を設定するのは、 Time::Seconds モジュールの ONE_DAY 使いますかね。楽だし。

$yesterday = $today - ONE_DAY; こんな感じ。

なお、日本の休日&会社の休日は考慮しません。面倒なので・・・

で、最初に作ったのがこれです。while ループのところに安全策で count によるループ抜けを仕掛けておきます。

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;
use Time::Seconds qw/ONE_DAY/;

# 2020年7月でTime::Pieceオブジェクトを作る
my $t = Time::Piece->strptime("2020-07", '%Y-%m');

# 7月の最終日を求める
my $day = $t->month_last_day();
print "last day of month: ". $day . "\n";# 31

# 2020年7月最終日のTime::Pieceオブジェクトを作る
my $last_date = Time::Piece->strptime("2020-07-$day", '%Y-%m-%d');

# 最終日の曜日を求める
# 0: 日曜 〜 6: 土曜
print "day of week: ". $last_date->day_of_week . "\n";# 5

my $count = 0;
while ($last_date->day_of_week == 0 || $last_date->day_of_week == 6){

    $last_date = $last_date - ONE_DAY;  # Time::Pieceオブジェクトを1日前にする

    if ($count > 7){
        last;
    }
    $count++;
}

print 'last normal day: '. $last_date->ymd . "\n";  # last normal day: 2020-07-31

7月は一見うまくいったように見えます・・・というか、7月末日は平日なので while ループ通りません。

2020年10月の末日は31日で土曜日です。30日が帰ってくれば大丈夫です。これでやってみましょう。

ついでに、年と月も変数にしておきます。

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;
use Time::Seconds qw/ONE_DAY/;

my $year = 2020;
my $month = 10;

my $t = Time::Piece->strptime("$year-$month", '%Y-%m');

# 最終日を求める
my $day = $t->month_last_day();
print "last day of month: ", $day . "\n";# 31

# 最終日のTime::Pieceオブジェクトを作る
my $last_date = Time::Piece->strptime("$year-$month-$day", '%Y-%m-%d');

# 最終日の曜日を求める
# 0: 日曜 〜 6: 土曜
print "day of week: ", $last_date->day_of_week . "\n";# 5

my $count = 0;
while ($last_date->day_of_week == 0 || $last_date->day_of_week == 6){

    $last_date = $last_date - ONE_DAY;  # Time::Pieceオブジェクトを1日前にする

    if ($count > 7){
        last;
    }
    $count++;
}

print 'last normal day: '. $last_date->ymd . "\n";  # last normal day: 2020-10-30

大丈夫っすね。

これでいいかなー、というところですが、日曜日のパターンもやっておきましょう。

2021年1月は末日が31日で日曜日です。1月29日が帰ってくれば正解です。さて・・・?

あまり代わり映えしないので折りたたみ

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;
use Time::Seconds qw/ONE_DAY/;

my $year = 2021;
my $month = 1;

my $t = Time::Piece->strptime("$year-$month", '%Y-%m');

# 最終日を求める
my $day = $t->month_last_day();
print "last day of month: ", $day . "\n";# 31

# 最終日のTime::Pieceオブジェクトを作る
my $last_date = Time::Piece->strptime("$year-$month-$day", '%Y-%m-%d');

# 最終日の曜日を求める
# 0: 日曜 〜 6: 土曜
print "day of week: ", $last_date->day_of_week . "\n";# 5

my $count = 0;
while ($last_date->day_of_week == 0 || $last_date->day_of_week == 6){

    $last_date = $last_date - ONE_DAY;  # Time::Pieceオブジェクトを1日前にする

    if ($count > 7){
        last;
    }
    $count++;
}

print 'last normal day: '. $last_date->ymd . "\n";  # last normal day: 2021-12-31

大丈夫そうです。

それでも祝日とか会社の休みとかを考慮したい!

Perl のモジュールでも同様の動機で作られたモジュールがあります。そういうの使うのもいいと思います。

せっかくなので、先のコードを変えてみましょう。

対象は2020年の年末としますかねー。

12月の末日は31日ですが、会社は28日から休みって事にしておきますか。ホワイト会社だ!

この場合の最後の平日は12月25日になります。最高ですね。あ、でも経理の月末の仕事溜まって大変になりそうでダメだ(経理脳)

従来のコードは while ループに入る条件が、土曜日か日曜日というものだったので、これを変更するところから。

while を無限ループにして、条件によってループを抜ける、と変更します。大連休も考慮して、安全策のループ抜けは30日にしておきます。

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;
use Time::Seconds qw/ONE_DAY/;

my $year  = 2020;
my $month = 12;

my $t = Time::Piece->strptime( "$year-$month", '%Y-%m' );

# 最終日を求める
my $day = $t->month_last_day();
print "last day of month: ", $day . "\n";    # 31

# 最終日のTime::Pieceオブジェクトを作る
my $last_date = Time::Piece->strptime( "$year-$month-$day", '%Y-%m-%d' );

# 最終日の曜日を求める
# 0: 日曜 〜 6: 土曜
print "day of week: ", $last_date->day_of_week . "\n";    # 5

my $count = 0;
while (1) {

    if ( $last_date->day_of_week == 0 || $last_date->day_of_week == 6 ) {

        $last_date = $last_date
            - ONE_DAY;    # Time::Pieceオブジェクトを1日前にする
    }

    if ( $count > 30 ) {
        last;
    }
    $count++;
}

print 'last normal day: '
    . $last_date->ymd
    . "\n";               # last normal day: 2021-01-29

表示結果だけ見るとうまくいってるように見えますが、安全策のループがなかったら無限ループしています。

というわけで、ちゃんとループの終了判定入れて、スクリプト内に書いた祝日の日付があったら平日とみなさずに飛ばすようにしてみました。

もし、もっと手を掛けたくない!ってなったら、Google Calenderに「日本の祝日」「会社の祝日」のデータを登録して、それを引っ張ってくるとかやるかもしれません・・・が今日はやりません。

気休めのつもりが2時間くらいかかってしまってるので・・・

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;
use Time::Seconds qw/ONE_DAY/;

my $year  = 2020;
my $month = 12;

# 休日を設定
my $holiday = [ '2020-12-28', '2020-12-29', '2020-12-30', '2020-12-31' ];

my $t = Time::Piece->strptime( "$year-$month", '%Y-%m' );

# 最終日を求める
my $day = $t->month_last_day();
print "last day of month: ", $day . "\n";    # 31

# 最終日のTime::Pieceオブジェクトを作る
my $last_date = Time::Piece->strptime( "$year-$month-$day", '%Y-%m-%d' );

# 最終日の曜日を求める
# 0: 日曜 〜 6: 土曜
print "day of week: ", $last_date->day_of_week . "\n";    # 5

my $count = 0;
while (1) {

    # 設定した休日と同じymdだったらnext
    if ( grep { $_ eq $last_date->ymd } @{$holiday} ) {
        $last_date = $last_date
            - ONE_DAY;    # Time::Pieceオブジェクトを1日前にする
        next;
    }
    elsif ( $last_date->day_of_week == 0 || $last_date->day_of_week == 6 ) {

        $last_date = $last_date
            - ONE_DAY;    # Time::Pieceオブジェクトを1日前にする
        next;
    }
    else {
        last;
    }

    if ( $count > 30 ) {
        last;
    }
    $count++;
}

print 'last normal day: '
    . $last_date->ymd
    . "\n";    # last normal day: 2020-12-25

定型のフォルダ構造を作る

先週はブログも書かずに何をしてたんだっけか

あぁ、部屋片付けてたのか。

あれから1週間たった今も片付いてない。なんぼかは減ったけど、ベッドの上で処分品のダンボールと一緒に寝てます。

定型のフォルダ構造を作る

最近は経理業務の割合が70%ほどになりました。

経理というのは月次、四半期、年次、と繰り返しの多いお仕事です。

その中でも定型の業務ってのがいくつかあります。

毎月コツコツ、温かみのある手運用でやるのもいいのですが、楽をして早く帰ったりtwitter見てたいですよね?

ってことで、経理業務で毎月毎に作成するフォルダ構造を作るPerlスクリプトです。

こういうフォルダ構造を作ります。

$ tree -N
.
├── 売上
├── 海外
├── 立替金
├── 支払いCSV
├── 支払データ
├── 仕訳データ作成
│   ├── 支払
│   ├── 未払
│   └── 弥生販売から弥生会計
├── 給与・社会保険
│   ├── 概算
│   ├── 確定
│   └── 人件費
└── 経費請求書スキャンデータ

作る前にちょっと考える

  • 他の人(Windowsユーザ)が使えるように、クロスプラットフォームgolang で作った方が良いのでは? -> まず自分が作ってみて、自分が楽をするところから

  • Windowsユーザが使いまわせるように、バッチファイル(.bat)でいいのでは? -> まず!自分が!!楽をする!!!

  • bashzsh)でかけるのでは? -> うちはPerlが書きたい

  • フォルダ構造をどう指定する? -> 面倒なので、スクリプトに直接書いちゃう

  • 引数で指定したフォルダを作れたら良いのでは? -> 指定先が Windows共有(samba) なので、文字コードが面倒そう。とりあえず、macOSの環境で作って持っていくことにしよう

〜が面倒!しか書いてない。

出来上がったもの

最初は File::Spec とかいつも使ってるのでやろうと思ったのですが、せっかくなので使ったことのない Path::Tiny 使ってみました。

スクリプト名は mkdir_per_month.pl としました。最近、長くても分かりやすい名前つけようって感じの命名してます。

#!/usr/bin/env perl
use strict;
use warnings;

use utf8
    ; # スクリプト内にマルチバイト文字列(全角)書く時必須
binmode STDOUT, ':utf8'
    ; # macだけでの運用を想定してるので、出力時の文字コードをutf8で決めうち

use Path::Tiny
    ; # パスの作成をよしなにやってくれるモジュール(要cpanmでインストール)

my $target_root_path = path('.')
    ;    # スクリプトと同じところにところに作成する

my $parent_dir;    # 親ディレクトリ名の保存用変数

# スクリプト下部のDATAフィールドから持ってくる
for my $line (<DATA>) {

    chomp $line;    # 改行はしっかり削除

    # 行が文字から始まっていれば親ディレクトリ
    if ( $line =~ /\A\w/ ) {
        $parent_dir = path($line);  # パスを作って
        $parent_dir->mkpath;        # パスに相当するディレクトリを作成
    }

    # 文字から始まっていなければ子ディレクトリ
    else {
        $line =~ s/\s+//;   # 行頭のスペース削除
        my $path = path( $parent_dir, $line );
        $path->mkpath;
    }
}

__DATA__
支払いCSV
海外
給与・社会保険
    概算
    確定
    人件費
経費請求書スキャンデータ
仕訳データ作成
    支払
    未払
    弥生販売から弥生会計
支払データ
売上
立替金

フォルダ構成が三階層以上になった時はどうするんや、ってなりますが、そん時はそん時ってことで(再帰とか使う事になるんかな)

その他

macOS には tree コマンドがない

macOSのパッケージマネージャ、homebrew で インストールします

$ brew install tree

tree コマンドで文字化けする

$ tree
.
├── mkdir_per_month.pl
└── �\203\233�\202��\203\233�\202�

-N つけましょう

$ tree -N
.
├── mkdir_per_month.pl
└── ホゲホゲ

Path::Tiny でお世話になったページ

Perl から Selenium を使う

色々あって

Twitter がデザイン変更して、スクレイピングが失敗するようになり、またDOM解析して修正かー・・・とか思ったら、SPA 化かなんかで全然 DOM の把握ができず、スクレイピングどころではないってなって、あー!!

ってことで、以前から気になっていた Selenium を使ってみます。

大体、Twitter が提供している API でちゃんと全部情報が取れないのが悪い。センシティブと判定された tweet とか取れないんだよなー

Perl 側の準備

ためらうことなく cpanm

$ cpanm Selenium::Remote::Driver

Perl のモジュールインストールが終わったか確認

$ perl -e -MSelenium::Remote::Driver

これは、ワンライナーという Perl の書き方。モジュールを呼び出しただけのコード。ここでエラーが出なければok

引数の説明はこんな感じ。

  • -e 引数をPerlのコードとして解釈して実行する
  • -M モジュールを呼び出す

上記のワンライナーをコードに起こすとこう。この1行。

use Selenium::Remote::Driver;

ここで use できないとエラーが出る。エラーが出ない、ってことはとりあえずインストールはできている。

ワンライナーは短くかけるので、ギュッとまとめるとこう。

$ perl -eMSelenium::Remote::Driver

この perl -eMモジュール名 ってワンライナーでインストールの成否を見たりするのはワンライナーの定番。

Mac 側の準備

Perl (とか他のプログラム言語)側からブラウザを操作するためのドライバをインストールする。

$ brew install geckodriver
$ brew cask install chromedriver

brew の formula がインストールされたか確認

まずは Chrome のドライバを実行

$ chromedriver
Starting ChromeDriver 83.0.4103.39 (ccbf011cb2d2b19b506d844400483861342c20cd-refs/branch-heads/4103@{#416}) on port 9515
Only local connections are allowed.
Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
ChromeDriver was started successfully.

こちらは大丈夫そう。CTRL-C で閉じる。

次は Firefox 用のドライバ。Gecko ってのは Firefox のHTML描画エンジンなのだけど、最近はあまり名前聞かなかったんで懐かしくなった。

$ geckodriver

応答ないので CTRL-C で閉じる・・・大丈夫かなぁ。

Google のトップページを表示してタイトル取得して終了するだけ

初めてなので、基礎からやっていく。まずは Google のトップページを出して、タイトルを取得するだけ。

#!/usr/bin/env perl
use strict;
use warnings;

# Selenium::Web::Driver と一緒にインストールされる Chrome 用のドライバを呼び出す
use Selenium::Chrome;

# ドライバのインスタンスを起動する。初期化みたいなもの。
my $driver = Selenium::Chrome->new;

# Google のトップページに移動する。Get は Perl入学式 第5回でやったアレです。メソッドってやつです。
$driver->get('https://www.google.com');

# 移動した後のページのタイトルを表示する
print $driver->get_title() . "\n";  # Google

# ドライバを終了する
$driver->shutdown_binary();

Chrome が立ち上がって、Googleのトップページを表示して自動的に終了する。コンソールにはページのタイトルが表示されている。

f:id:sironekotoro:20200606133157p:plain

次は Firefox。説明は省略。

use Selenium::Firefox;
my $driver = Selenium::Firefox->new;
$driver->get('https://www.google.com');
print $driver->get_title() . "\n";   # Google
$driver->shutdown_binary();

ドライバが何も表示しなかったんで不安だったけど、ちゃんと動いた。よかった。

なお、Firefox の場合は終了時にポートの情報などが表示される。

Killing Driver PID 46947 listening on port 49580...

f:id:sironekotoro:20200606133531p:plain

Yahoo.co.jp からニュースを持ってくる

sironekotoro.hateblo.jp

これの Selenium 版をやってみます

#!/usr/bin/env perl
use strict;
use warnings;

use Selenium::Chrome;

# utf8 で出力する
binmode STDOUT, ":utf8";

my $driver = Selenium::Chrome->new;

# Yahooのトップページに移動する
$driver->get('https://www.yahoo.co.jp/');

# ニュース一覧のDOMを特定する
my $news
    = $driver->find_element_by_xpath(
    '/html/body/div/div/main/div[2]/div[1]/article/div/section/div/div[1]/ul'
    );

# ニュース一覧のDOMから、個別のニュースの要素を取得する
my $articles = $news->children( './li//h1/span', 'xpath' );

# 1記事ずつタイトルを取得する
for my $article ( @{$articles} ) {
    print $article->get_text() . "\n";
}


# ニュース一覧のDOMから、個別のニュースのURLを取得する
my $aTags = $news->children( './li//a[@href]', 'xpath' );

# 1記事ずつURLを取得する
for my $aTag ( @{$aTags} ) {
    print $aTag->get_attribute('href') . "\n";
}


sleep 3;

# WebDriver を終了する
$driver->shutdown_binary();
給付金委託 元電通社員に委任
激しい雨 東京や埼玉竜巻注意
習主席の国賓来日 年内見送り
緊急宣言 再指定に消極的な訳
加首相 デモ参加し片膝をつく
夜は高架下 困窮の留学生帰国
ヤクルト スアレスの陰性発表
鬼滅に続く?若手集うジャンプ
https://news.yahoo.co.jp/pickup/6361779
https://news.yahoo.co.jp/pickup/6361787
https://news.yahoo.co.jp/pickup/6361782
https://news.yahoo.co.jp/pickup/6361778
https://news.yahoo.co.jp/pickup/6361780
https://news.yahoo.co.jp/pickup/6361781
https://news.yahoo.co.jp/pickup/6361783
https://news.yahoo.co.jp/pickup/6361762

うまく取れたんだけど、結構時間がかかる・・・20 〜 30 秒くらい。

あと、タイトルとURLはもう少しスマートな取り方があると思う・・・

Twitter にログインしてみる

さて、本命。

$username_or_email$password を変更して実行

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;

use Selenium::Chrome;

my $username_or_email = 'hogehoge';
my $password          = 'fugafuga';

my $driver = Selenium::Chrome->new;

# Twitter のログイン画面に移動する
$driver->get('https://www.twitter.com/login');

# name 属性でユーザー名の入力欄の要素を特定する
my $username = $driver->find_element_by_name('session[username_or_email]');
# 特定した入力欄にユーザー名を入力する
$username->send_keys($username_or_email);

sleep 1;  # 1秒待つ

# パスワードも、ユーザー名と同様に処理
my $psw = $driver->find_element_by_name('session[password]');
$psw->send_keys($password);

sleep 1;  # 1秒待つ

# ログインボタン には name 属性がないので、div タグと role 属性で要素を特定する
my $button = $driver->find_element('//div[@role="button"]');

# ボタンをクリックする
$button->click();

sleep 5;  # 5秒待つ


# WebDriver を終了する。ブラウザも閉じられる。
$driver->shutdown_binary();

うちの環境ではうまくいきました。

これで、あとはソースを解析することができればー、なのですが本日はここまで。

補足

Twitter側にはこのような通知が出ます。

f:id:sironekotoro:20200607112315p:plain

あまりやりすぎると「不正アクセスされてる!」って判断されちゃうかな。

参考にしたサイト

www.selenium.dev

qiita.com

aiacademy.jp

www.seleniumqref.com

Perl の情報は少ないんですが、他の言語のリファレンスが大変参考になりました。

Mojolicious::Lite でヘルパー関数を使う & サブルーチンリファレンス

近況

こんな感じの会社生活やってます。

なんで経理のお手伝いやってるかっていうと、経理の人員が足りないってところ簿記2級所持の自分がいたから、という巡り合わせです。

会計ソフトに記帳していくだけなら経理未経験でも行けるだろうと。

(とはいえ、取得から7年くらいは経ってるのでかなり忘れてる)

3月の最終週からバックオフィスのメンバーに助けてもらいつつお仕事しております。

なお、明日月曜日からは6月の月初なので経理が大変な週です。皆さんの職場の経理マンをそっと見守りつつ、労わりましょう。

テンプレートで使える便利関数

で、3割のサーバの中で何かするってやつなのですが、テンプレートの出力を変更したり、ということをよくやっています。

そのテンプレートにはマクロ機能というのがあり、これが便利だったんですね。View専用の関数とでもいうか。

似たような機能が Mojolicious::Lite にないかなぁ、と思ったら helper という機能名で実装されていました。

helper 、聞いたことはあったのですが、その時は理解できませんでした。今なら行けそう。

Mojolicious::Lite でヘルパー関数を使う

業務に近い例を考えていたのですが、汎用性ないなぁってことで、いつもの fizzbuzz にしてみました。

#!/usr/bin/env perl
use Mojolicious::Lite;

get '/' => sub {
    my $c = shift;
    $c->stash( num => [ 1 .. 100 ] );
    $c->render( template => 'index' );
};

# ヘルパー関数の宣言
helper fizzbuzz => sub {
    my ( $self, $num ) = @_;

    if ( $num % 15 == 0 ) {
        return 'fizzbuzz';
    }
    elsif ( $num % 3 == 0 ) {
        return 'fizz';
    }
    elsif ( $num % 5 == 0 ) {
        return 'buzz';
    }
    else {
        return $num;
    }
};

app->start;
__DATA__

@@ index.html.ep
% layout 'default';
% title 'Welcome';
<h1>Welcome to the Mojolicious real-time web framework!</h1>

<% for my $n ( @{$num}) { %>
    <%= fizzbuzz($n) %><br>
<% } %>

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head><title><%= title %></title></head>
  <body><%= content %></body>
</html>

以上です。

Perl入学式の講義ではテンプレートの中でIFの条件分岐を書いていましたが、ヘルパー関数にまとめることでスッキリしました。

サブルーチンリファレンス

ヘルパー関数の宣言は以下のコードです。

helper fizzbuzz => sub {
    my ( $self, $num ) = @_;
# ... 中略
}

これはサブルーチンリファレンスを使っています。

例えば、サブルーチンの例としてこんなサブルーチン作ってみます。

#!/usr/bin/env perl
use strict;
use warnings;

sub greet {
    my $name = shift;
    return "Hello, $name !\n";
}

print greet('sironekotoro');    # Hello, sironekotoro !

サブルーチンをリファレンスにすることで、スカラー変数に入れてしまうことができます。

以下はデリファレンスして引数を設定した例ですが、これはちょっと書いていて辛いですね・・・

#!/usr/bin/env perl
use strict;
use warnings;

sub greet {
    my $name = shift;
    return "Hello, $name !\n";
}

my $greet_ver = \&greet
    ; # サブルーチンをリファレンスにしてスカラー変数に格納
print &{$greet_ver}('sironekotoro');    # デリファレンスしてる

もちろん、配列リファレンスやハッシュリファレンスと同様、無名サブルーチンリファレンス を作ることができます。これはよくコードリファレンスまたはコードレフ(coderef)と呼ばれてます。

すっきり書けますね。

#!/usr/bin/env perl
use strict;
use warnings;

my $greet = sub {
    my $name = shift;
    return "Hello, $name !\n";
};

print $greet->('sironekotoro');

アロー記法で引数を設定できるとか、配列リファレンスやハッシュリファレンスと同じですね。

比較しながら考えたり書いてみると分かりやすいと思います。

この最後の書き方はほぼ、Mojolicious::Lite の helper 関数の書き方や、 view での呼び出し方に近いです。

ということで view がごちゃごちゃしてきた時にはぜひ helper 関数にチャレンジしてみてください!