久々の @xtetsuji さんからの出題をやってみる
毎週火曜日のペアプロ講習用に作成した問題を昨晩の #Perl入学式 オンラインミーティングで紹介したので Twitter でも紹介。情勢が収束したらまた懇親会でピザを食べつつお題に興じたい。 pic.twitter.com/n68luVPNEg
— OGATA Tetsuji (@xtetsuji) 2020年7月26日
意外と面倒だった・・・もっと、ロジックだけに集中すれば早そうだけど、ロジックを追いかける頭がないのだった(完
Google スプレッドシートの情報を GAS で WebAPI にしてXML で取得できるようにして、それを Excel の PowerQuery で取得する
悩み
経理ネタです。Perl 出てきません(往年の YAPC みたいになってきた)
- 請求書ベースの振込先の管理に Excel シートを利用している
- 自社サービスの一部の振込先の管理に Google スプレッドシートを使っている
これをどっちかにまとめたい。というか、マスタ的なものが複数あるのは面倒。仕事(というか作業)に IF 文は少ないに限る。
振込のデータ作成と、振込先のデータは分けておきたい。そして、振込先のデータは編集へのハードルが高い状態にしておきたい。
一意になっていないレコードがあったりして、こういうのを何とかしていきたい(企業の名前はよく変わる。口座番号そのままに・・・)
考えた
- ローカル(社内共有サーバ)においてある Excel ファイルの情報をインターネット上の Google スプレッドシートに反映させるのは手間
- 手動?
- 絶対やだ
- 手動?
逆に、Google スプレッドシートの情報を正として、Excel に反映させるのはどうか
Google Fusion Tables とかいうのがあって、JSON とかで吐き出せるらしい
- 提供終了していた
- Google、お前はいつもそうだ
- gsuiteupdates-ja.googleblog.com
- 提供終了していた
Google スプレッドシートは Google Apps Script 使って WebAPI にできるから、それを利用できないか?
- API を呼び出す Excel 側は Excel2013 以降(かつWindows版)で利用できる WEBSERVICE 関数を使おう
- で、とってきた情報は FILTERXML でパースする・・・XML?JSONは? FILTERJSON関数は?
- そんな関数ない
- API作って、出力の時にXMLにすれば良い
作った
- 手始めに WebAPI として JSON で吐き出してみる
- これはok
XML での出力も何とかなった、というかちゃんと関数あった
- developers.google.com
- しかし、エレメント名(タグ名とでもいうか)に半角カタカナを使えないことを知らずに2時間くらいハマってた
- 半角カタカナ現役の世界があるんすよ・・・
- この辺りは Perl で RSS をいじったりなんだりした経験が生きた
- 主に欲望方面での RSS
Excel の WEBSERVICE 関数からのアクセス・・・エラー「ログインしてね」
項目(カラム)1個ごとに1回、WebAPI のアクセスが発生しちゃう!非効率!
- 今のところ呼び出す項目の順番は固定だから、配列にしちゃうかー
- ところで、 XML って配列サポートしてるの・・・?
- いやちょっと待って、1件1件データとってくる必要ある?
- ない
- 今のところ呼び出す項目の順番は固定だから、配列にしちゃうかー
全件取得して、そのシートを参照すれば良いのでは
この辺りで全てがめんどくさくなってくる
そもそも、なぜこんな苦労を・・・普通にDB立てたら?
- 担当者がいなくなった後、誰がメンテするんだ
-
- 担当者がいなくなった後、誰がメンテするんだ(2回目
- 15年くらい前に Accessの資格とった気がするが、全然覚えてない
Excel と Google スプレッドシートがぱっと見分かりやすすぎるんだよな・・・
ということで結論的な
- まぁ、うちだけでも使えるようにすればいいか
- 欲しかったものをすぐ使えないのは残念だけど、まぁ、それはそれで
- 経験積めたしー
- とはいえ悔しい
2020年7月のオリンピック開催にあわせて国が用意した4連休ですが、コロナ自粛で遠出もせず、こういうものを作ってました
- 個人的には失敗なのだけど、こういうのを作ってました、って残しておこう
Googleスプレッドシート
XML取得のURL
Googleスプレッドシートの1行目の列名をXMLのタグ名とする
Excel の Power Query で取り込む(Windows版のみ)
- メニューから 「データ」 を選ぶ
- [データの取得] をクリックし、「その他のデータソースから」->「Webから」を選択
- ラジオボタンは [基本] のままで、URLに上記のURLを入力し [OK] ボタンをクリックする
- 「Webコンテンツへのアクセス」の画面が出るので、そのまま(匿名のまま)、右下の [接続] ボタンをクリックする
- ナビゲーター の画面が出るので、左側メニューの row を選択して、右側のプレビューが出ることを確認する
- 確認したら、右下の [データの変換] ボタンをクリックする
- Power Query エディターの画面が出るので、右下の 「適用したステップ」のところで「変更された型」をクリックし、❌マークが赤くなったら❌マーククリックして削除する
- これを行わないと、金融機関コードと支店コードで左側が0埋めされない
- 左上の [閉じて読み込む] をクリックして、「閉じて読み込む」 を選択
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 が最新の安定版となっている
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 で経理を(やっている自分が)楽をするためのスクリプトを作っている
- が、「動けばいい」みたいなレベルなので、ちゃんとした学習をしたくなった
- まぁ、動いて目的を果たせるならそれはそれで素晴らしいとは思うんだけど
- 強欲である
今回の教材
- 今回はこの本を選んだ。「JavaScript Primer 迷わないための入門書」
- tatsu-zine.com
- Amazon にもあるけど、当然 kindle でしか読めない
- 達人出版会であれば、PDFで他の電子書籍ビューアで見ることができるので、達人出版回から購入した
今回の勉強法
- いつも初心者本を買うと、すぐ写経を始めるのだけど、「このコードは Perl ではどう書くんだろう?」って欲求が抑えられない
- 抑えられない結果、無限に脱線していく
- このため、この本はまず一通りを通勤途中に読んでいる
- 経理のお仕事は出社前提で組まれていて、コロナ禍の中でも出社せざるを得ないのですー
- ただまぁ、通勤そんなに嫌いではない
- で、一通り読み終わりそうなので nodejs をインストールしたというわけ
- 多分今週には読み終わりそう
- おかげで、脱線せずに最後まで読み切ることができそう
- JavaScript は今までも何回も入門してる
- とっとと先( TypeScript, Angular, React, Vue )に行きたいけど、自分の中に基礎ができてないのが気持ち悪い
- ということで、何度でも入門する
- 2周目はもちろん写経しながら読む
- ちなみに、技術書読むときは iPad Pro 12.9 インチモデルがとてもいいです。とても重くて嵩張るけど。
ある月の最後の平日を求める
「その君の勘から発した、 君の怒りと苛立ちは理由になる!」というカミーユの言葉に背を押されて、経理のスクリプト作ってる。
— sironekotoro (@sironekotoro) 2020年7月4日
ってことでこんにちは。休日になると仕事のスクリプト作成が捗りますね。
なぜか仕事のある平日はそうでもないのですが・・・
その月の最後の平日を求めたい理由
経理の世界では・・・と主語を大きく言いたいところですが、経理は各会社それぞれの色が強く出るところなので「今の業務では」と言っておきます。
まぁ、矛盾のない貸借対照表と損益計算書が出てくれば、その過程は問われないというというのはあります。もちろん説明責任はあります。
今の業務では、ある月に発生した未払金(翌月払い等)は経理上、末日付で未払金計上します。クレカとか、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_week
が 0
)か土曜日( $last_date->day_of_week
が 6
)だったら前日を設定し、もう一回判定して・・・とやればいいな?と目星をつけます。
前日を設定するのは、 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
定型のフォルダ構造を作る
先週はブログも書かずに何をしてたんだっけか
こんな感じで繋がっちゃったわけ。
— sironekotoro (@sironekotoro) 2020年6月12日
家で物理本読むの久しぶりだわ。だいたい通勤電車の中で電子書籍で読んでるからなぁ。 pic.twitter.com/VjLkdHgURN
あぁ、部屋片付けてたのか。
あれから1週間たった今も片付いてない。なんぼかは減ったけど、ベッドの上で処分品のダンボールと一緒に寝てます。
定型のフォルダ構造を作る
最近は経理業務の割合が70%ほどになりました。
経理というのは月次、四半期、年次、と繰り返しの多いお仕事です。
その中でも定型の業務ってのがいくつかあります。
毎月コツコツ、温かみのある手運用でやるのもいいのですが、楽をして早く帰ったりtwitter見てたいですよね?
ってことで、経理業務で毎月毎に作成するフォルダ構造を作るPerlスクリプトです。
こういうフォルダ構造を作ります。
$ tree -N . ├── 売上 ├── 海外 ├── 立替金 ├── 支払いCSV ├── 支払データ ├── 仕訳データ作成 │ ├── 支払 │ ├── 未払 │ └── 弥生販売から弥生会計 ├── 給与・社会保険 │ ├── 概算 │ ├── 確定 │ └── 人件費 └── 経費請求書スキャンデータ
作る前にちょっと考える
他の人(Windowsユーザ)が使えるように、クロスプラットフォームな golang で作った方が良いのでは? -> まず自分が作ってみて、自分が楽をするところから
Windowsユーザが使いまわせるように、バッチファイル(.bat)でいいのでは? -> まず!自分が!!楽をする!!!
フォルダ構造をどう指定する? -> 面倒なので、スクリプトに直接書いちゃう
引数で指定したフォルダを作れたら良いのでは? -> 指定先が 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のトップページを表示して自動的に終了する。コンソールにはページのタイトルが表示されている。
次は 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...
Yahoo.co.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側にはこのような通知が出ます。
あまりやりすぎると「不正アクセスされてる!」って判断されちゃうかな。
参考にしたサイト
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
関数にチャレンジしてみてください!