Ubuntu再起動時にSDカードが認識されない問題の対処

背景

我が家ではMELE quieter3cというファンレスPCにUbuntuを入れて自宅サーバとして稼働させている。同PCをファイルサーバとしても使おうと、安価な1TBのMicroSDカードを挿入してそこを参照する形でSambaを構成した。
構築直後は正常に動いていたが、サーバを再起動するとSDカードが認識されず、ファイル共有が動かない。SDカードを抜いて挿せば再び認識されるが、ずっと挿しっぱなしで運用したいので解決法を調べた。

参照したサイト

結局ここの方法に従った。MELE quieter 3qという姉妹製品の事例だったから再現性高いかと思って採用したけど、ハードウェアやBIOSというよりはOSレベルの問題だったんじゃないかと思っている。 unix.stackexchange.com

全てが終わった後で見つけた記事。試してないがこれで十分だったんじゃないかという気がしている。 qiita.com

調査結果と今回の対応方法

調査結果

  • LinuxにおいてSDカードはmmcというドライバによって認識される
  • echo 1 > /sys/class/mmc_host/mmc0/device/removeによってmmc0(mmcによって認識されている0番目のデバイス)を切断し、物理的にデバイスを取り出したのと同じ状態を作ることができる
  • echo 1 > /sys/class/pci_bus/0000\:00/rescanによってPCIバス上の全てのデバイスを再スキャンし、物理的に接続されているデバイスを認識させることができる
  • MELE quieter3cには128GBのeMMCストレージ(最近の小型PCによく搭載されているマザーボード上に組み込まれたストレージ)があり、その名の通りこれもmmcによって制御される
  • eMMCとSDカードが読み込まれる順序は一定でなく、SDカードがmmc0になったりmmc1になったりする
  • mmcの読み込み順によってパーティションを指定するときのパスが変わり、/dev/mmcblk0p1になったり/dev/mmcblk1p1になったりする
  • mmcデバイスとして接続されているが中身が読み込まれていない状態は/sys/class/mmc_host/mmc#配下にmmc#:0001のようなフォルダが存在するか否かによって見分けられる

今回の対応

以下のようなシェルスクリプトを作り、OS起動後に実行するようにした。
本当はメタデータ的なものを参照して該当のMMCデバイスがSDカードであることを確認したかったが、やり方が分からなかった&自分の環境だとこれで問題なく動くのでこれで運用している。

echo '+ checking mmc devices'
mmcdev=nodevice
while read -r line; do # ヒアストリングで指定しているmmcデバイス(/sys/class/mmc_host/配下)でループを回す
  echo "  - checking $line"
  ls /sys/class/mmc_host/$line | egrep -v 'device|power|subsystem|uevent' # mmc#:0001が存在すれば$?が0になる = 該当のmmcデバイスが認識されている
  if [ $? -ne 0 ]; then
    mmcdev=$line
    break
  fi
done<<EOF # ls | while read line ...とするとループの外の変数が見えなくなるため普段使わない書き方になった
$(ls /sys/class/mmc_host)
EOF

if [ $mmcdev = 'nodevice' ]; then
  echo '  - no empty mmc devices found'
  exit 0
else
  echo "  - sdcard should be recognized at $mmcdev"
fi
mmcnum=${mmcdev:3}

echo '+ rescanning sdcard'
echo 1 > /sys/class/mmc_host/${mmcdev}/device/remove
echo 1 > /sys/class/pci_bus/0000\:00/rescan
echo '  - waiting for 10 seconds...'
sleep 10

echo '+ checking device status'
fdisk -l | grep /dev/mmcblk${mmcnum}
if [ $? -eq 0 ]; then
  echo '  - sdcard was found'
else
  echo '  - failed to recognize sdcard'
  exit 1
fi

echo '+ mounting sdcard'
mount ${devpath} /mnt/sdcard
status=$?
if [ $status -eq 0 ]; then
  echo "  - success. ${devpath} -> /mnt/sdcard"
else
  echo "  - mount failed with status $status"
  exit $status
fi

echo '+ starting smbd'
systemctl start smbd
status=$?
if [ $status -eq 0 ]; then
  echo '  - success'
else
  echo '  - failed to start smbd'
  exit $status
fi

Cytoscape.jsで表からデータフロー図を描く

この記事でやったこと

ネットワークグラフを描くJSライブラリ(Cytoscape.js)を使って、データやプロセスの繋がりが記述された表からデータフロー図を描いてみた。

背景

前々からデータフローのような意味が明確なもの(このプロセスのインプットはこれでアウトプットはこれ、このデータはこことここに流れる、etc.)をExcelで図にすることで、内部的には本来の意味が死んでしまい、図形と線と図形の繋がりでしか無くなってしまうことが嫌だった。
PlantUML(DFD無いけど...)みたいな仕組みも根本的には同じ問題を抱えており、結局のところ要素Aと要素Bが繋がっているという情報をテキストで持つのか図形の配置で持つのかというだけの違いに過ぎない。あと、たぶんExcelの方が長文コメント書けたりして表現力が高い。

仕事で大きめのシステム群のデータフローを起こすことになってExcelで作業を始めたものの、以下のようなことがあって嫌になってしまった。

  • データフローが多いので線がごちゃごちゃになって見にくい
  • 現段階では確定情報ではないものが多く、変更が発生した場合にせっかく見やすく整えた図形の配置とか線の曲がり具合とかが台無しになる可能性が高い
  • 量が多いので絶対どこかで線繋ぐ先ミスる
  • 相当頑張ったところで結局何がどこに繋がっているのかいまいち見にくい図で終わる気がした
  • より明確に定義を記述した表との二重管理になる

そこで、ゴールデンウィークの宿題として、表からデータフロー図を自動で描いてくれる仕組みを作ってみることにした。

要件

仕事での使いやすさを考え、以下のような要件を想定した。

  • データやプロセスを記述した表からデータフロー図を作れる
  • 図中の各要素について補足コメントが書ける
  • 図中の各要素に色を付けられる
  • データやプロセスのまとまりを示すことができる(例:データAとデータBがシステムAに属することが図で表現できる)
  • データベース、帳票、外的要素、プロセスを図形の違いで描き分けることができる
  • 単一のファイルとしてクライアント等に共有でき、共有先で特殊なアプリケーションをインストールすることなく内容が確認できる
    →Office系や画像を除くとたぶんHTML一択

やったこと

技術選定

候補となるライブラリについて、ここがよくまとまっていた。 qiita.com

この中から,実装のしやすさに惹かれてvis.jsを試したが機能が足りなかった。機能の豊富さと拡張性を見てCytoscape.jsを試したところ上手く行きそうだったのでこれを採用した。

要件との対応はこんな感じ

要件 vis.js Cytoscape.js
データやプロセスを記述した表からデータフロー図を作れる
図中の各要素について補足コメントが書ける △(拡張機能)
図中の各要素に色を付けられる
データやプロセスのまとまりを示すことができる(例:データAとデータBがシステムAに属することが図で表現できる) ×
データベース、帳票、外的要素、プロセスを図形の違いで描き分けることができる
単一のファイルとしてクライアント等に共有でき、共有先で特殊なアプリケーションをインストールすることなく内容が確認できる

vis.js

とりあえず表(をHTMLファイルにTSVとしてコピペしたもの)からこんな図が書けるようになったが、上記の通りデータやプロセスをグループで示すことができなかった。
ドキュメントを読んだときにグループという概念があったので誤解していたのだが、複数の要素に共通のスタイルを適用するための仕組みだった。

Cytoscape.js

今のところ良い感じに動くやつができた。仕事で想定しているような大規模なものを突っ込んだときにどうなるかは知らない。 図は以下の状態

  • 「データC連携」というプロセスをクリックした結果、該当プロセスと繋がっているフロー、データがハイライトされている
  • 「ユーザ部署」というグループにマウスカーソルを当てた結果、該当要素に設定されているメモがツールチップとして表示されている

とりあえずソースコードを置いておく。 diagram/dfd-cytoscape.html at main · miyake32/diagram · GitHub

作ったものの説明

使っているライブラリ等

  • 図の描画:Cytoscape.js
  • 図のレイアウト(要素配置の計算エンジン):klay(cytoscape.js-klayというExtensionを通じて動かしている)
  • ツールチップの表示:tippy.js

仕組み

以下の3種類の表をExcel等で作成し、HTMLファイルの所定の場所にコピペする(ペースト先ではタブ区切りテキストになる)とデータフロー図が描画される。

  • データやプロセスをまとめるグループ(グループをまとめるグループも定義可能)
  • データ要素(データストア、帳票、外的要素の3種類が定義可能)
  • プロセス(インプットとアウトプットを持つ)

表の列はグループの指定にIDが必要だったりして無機質というか使いにくそうに見えるが、実際に仕事で使う場合はExcel上で関数とか入力制御とか使って、例えばグループの選択肢(名称)を選ぶとIDがLOOKUPされるみたいな感じで使いやすくできるはず。

工夫した点

  • 複数行テキストに対応した(Excel上でセル内に改行を入れるとTSVの構造が少しややこしくなる)
  • 帳票用のアイコンを自分で作った
  • コメントの表示をツールチップで実装し、各要素にコメントが存在することを示すアイコンを付けた

課題

  • 要素の配置に対する制御が不足している気がする
    大まかな階層(例:データソースとなる外部システム群→取り込み直後のデータ→加工後のデータ→加工後のデータを集約したデータ)を定義してその順に並べることができそうだが上手く実現できていない
  • データストアがあんまりデータベースっぽくない

DAXクエリでPower AutomateからPower BIデータセットを読む

背景

Power BIで色々見える化する際に、単にレポートを公開するだけでなく、定期レポートのような形でメールなりチャットなりで現在の状況を端的に通知したいというニーズがあった。
Power BIレポートからデータを取得しようと思うと、以下のような方法があるが、前者はテキストデータを取得できない(図表を画像で取得することはできる)、後者はデータ取得のために色々不要なオブジェクトを作る必要があるなど、使い勝手がいまいちだった。

レポートではなくその源流にあるデータセットに対してクエリを実行することができることを知ったので、やり方を調べてみた。

最終的に実現したいこと

Power Automateでデータ更新間隔に応じて日次・月次などのスケジュールで動くフローを作成し、定期レポートを配信する。

DAXクエリを試す環境の準備

Power Automate上でクエリを書いてトライアンドエラーを繰り返すのは何とも効率が悪い。特に慣れない言語を使うときには高速でトライアンドエラーを回せる環境を整えるのがとても大事。今回はMSの公式ドキュメントで言及されていたDAX Studioを使ってみる。
ローカルのPower BI Desktopでpbixファイルを開いていれば、DAX Studioを使ってpbixファイルの中のデータセットに接続してクエリを試せるらしい。
というわけで以下のようにしてクエリを試す環境を整えた。

  1. 公式サイトからDAX Studioのインストーラーを入手してインストール
  2. Power BIサービスからクエリを発行したいデータセットをダウンロード
  3. ダウンロードしたpbixファイルをPower BI Desktopで開く
  4. DAX Studioを開いてデータセットに接続

DAXクエリを書く

DAXクエリの基本

DAX クエリ - DAX | Microsoft Learnに書いてあるが、要点は以下の通り。

  • DAXクエリの返り値はテーブル
  • EVALUATE <テーブルを返すDAX式>が最も基本的な構文
  • DEFINEステートメントを使って変数を定義することができる

クエリについて学習する前に、DAX の基礎を十分に理解しておくことが重要です。 まだ十分に理解していない場合は、必ず「DAX の概要」を確認してください。

とのこと。いつも雰囲気でDAX式を使っているので、この機会に勉強しなおすことにする。

DAX式の基本

DAX関数

Excel関数によく似たDAX関数を組み合わせて目的の値を得るための式を構築することが基本になる。

データ型

DAX関数の引数や返り値で使うデータの型。公式ドキュメントがうまくまとまっていないので整理してみる。
一般に型と言えば、数値や文字列などの最小のデータ単位を表現するための基本型と、複数の基本型のデータを組み合わせて構造を持たせた複合型(言語によって呼び方が違う)があるが、DAX関数のドキュメントは以下のような状況。

  • 基本データ型は見つけやすいところに記載がある
  • 複合データ型が分かりやすく説明されたページが見つからないので自力で理解するしかない…
DAX式で使える複合データ型

色々見ながら私が理解した内容を記載しています。ということなので間違っている可能性があります。

テーブル

型(基本型)を持った列によって定義された行の集まりによって構成される表構造。要は普通のRDBのテーブルをイメージすればよい。
DAXクエリでは返り値をテーブル型にする必要があるので、EVALUATEステートメントの直下に記述する関数はテーブル型を返すことになる。

式の中ではシングルクオート'で囲み、'テーブル名'のようにして参照できる。 式の中でリテラルでテーブルを書くこともでき、ここに説明があって、DAXクエリでは以下のように試せる。

DEFINE TABLE '複数列から成るテーブル' = {("row1", 1), ("row2", 2), ("row3", 3)}
EVALUATE '複数列から成るテーブル'
Value1 Value2
row1 1
row2 2
row3 3
リスト(列が1つのテーブル)

どうやら厳密に言うとリスト型とういうものがDAXにはなく、列が1つのテーブルを使う様子。書き方は上と同じページに説明がある

DEFINE TABLE '1列から成るテーブル' = {1,2,3}
EVALUATE '1列から成るテーブル' 
Value
1
2
3

例えばIN演算子を書くときに使う。
ネストされたリストも作れるらしく、例えばDATATABLE関数の引数になるが、それ以外の使い道は不明。

Tableの中の単一の列に対する参照であり、実際に処理される際には行コンテキスト(後述)によって特定される基本型の具体的な値として評価される。
'Table'[Column]のようにして[ ]で囲んで指定する。
関数の引数として列そのものを指定したり、引数として指定するDAX式の中で使う。

コンテキスト

DAXを理解するキモになるのがおそらくコンテキスト。
DAX関数に渡す引数は"Value"のようにリテラルで定数を記述する場合を除き、テーブルや列になる。
[Column1] + [Column2]あるいはSUM('Table'[Column])のような式を書いたときに、「計算に含まれるのがどの行の値なのか」を決めるのがコンテキスト。
列名による参照のスコープと言い換えても良いかもしれない。
例えば以下のような場合にコンテキストが重要になる。

  • 累計値を得るために<該当行の日時以前の日時を持つすべての行の値の合計>を計算する
  • 販売シェア(%)を得るために<自社製品の販売個数の合計値> / <全製品の販売個数の合計値>を計算する
行コンテキスト

正確な解説はここが詳しい。
テーブルの行を一行ずつ扱う処理において、列名による参照を「現在処理されている行」に束縛するのが行コンテキスト。
行コンテキストが存在する場合、[Column1] + [Column2]の計算に使われる値は、現在処理されている行のColumn1とColumn2の値となる。

では「行コンテキストが存在する場合」とはどのような場合かというと、以下の2パターンがある。

  1. あるテーブルに作られた「計算列」(例:[Column1] + [Column2])のDAX式が処理されるときには、テーブルの各行が順番に処理されていき、DAX式の中で指定された列の参照先は行コンテキストによって束縛される(つまり、現在処理されている行の該当列の値が参照される)
  2. テーブルを引数として受け取るDAX関数(SUMX、FILTERなど)は与えられたテーブルの全行を順番に処理していく。多くは「処理の内容」を2つ目以降の引数で受け付けるようになっている(例えばFILTERはフィルタ条件のDAX式を第二引数で受け取る)が、「処理の内容」の中で記載された列名の参照先は行コンテキストによって束縛される(つまり第1引数で与えられたテーブルの現在処理されている行の該当列の値が参照される)
行コンテキストのネスト

理解が難しかったのが行コンテキストのネスト。ある行を処理しているときに「複数の行を含むテーブル」を参照しながら値を作る必要がある場合に行コンテキストのネストが発生する。
もう少し具体的に言うと、上記の2のパターン(テーブルを引数として受け取るDAX関数)が行コンテキストの中で現れた時、現在の行コンテキストの中で新しく「テーブルの全行を順番に処理する」ことになり、すなわち新しい行コンテキストが入れ子状に発生する。

例えば、累計値を計算したい場合には「現在の行の日時以前の日時を持つすべての行」を含むテーブルを参照する必要がある。
そのような処理を例えばforループで記述するときにはループがネストするはずである。

for (let row_context of ) {
  const current_datetime = row_context.datetime;
  let cumulative_val = 0;
  for (let nested_row_context of table) {
    if (nested_row_context.datetime <= current_datetime) {
      cumulative_val += nested_row_context.val;
    }
  }
}

このような処理を想定したとき、最初のループの中で1つの行コンテキストを処理している最中に、新しいループができて新しいループの中の複数の行コンテキストが処理されるという流れになる。
DAXにはネストされた行コンテキストの中から外側の行コンテキストの値を参照する仕組み(上記の実装例の中のcurrent_datetimeに相当)が用意されている。EARLIER関数がそれで、ここここが詳しい。

EARLIER関数の典型的なユースケースが累計の計算であり、上記の処理と同じことをDAX関数では以下のように記述できる。

SUMX(FILTER('Table', 'Table'[datetime] <= EARLIER('Table'[datetime])), 'Table'[val])
フィルターコンテキスト

公式ドキュメントではDAXのコンテキストは行コンテキスト・クエリコンテキスト・フィルターコンテキストの3つだと書いてあるが、どうやらクエリコンテキストはフィルターコンテキストの亜種らしいのと、Power BIレポートでは重要だがDAXクエリには関係なさそうなので省略。
例によってここが詳しい。

DAX式の中でDAX関数の引数などでテーブル名を指定したとき、テーブル名による参照を「あるフィルタ条件を満たす行」のみに束縛するのがフィルターコンテキスト。
要はあるDAX式が評価されるときにはテーブルにどのようなフィルタがかかっているか というのがフィルターコンテキスト。
フィルタは列に対する条件(例:[Column1] >= 30)であり、リレーションシップが定義されたテーブル間で伝播する。

DAXではフィルターコンテキストを明示的に操作する関数が用意されており、CALCULATE, CALCULATETABLEフィルター関数がそれにあたる。
例えば自社製品の販売シェアを得るために以下のように書ける。

SUMX(FILTER('Sales', [Company] = "My Company"), [Sales Amount]) / SUMX('Sales', [Sales Amount])

DAXクエリの作成(まだ書いてない)

DAXクエリのPower Automateへの組み込み(まだ書いてない)

まとめ(まだ書いてない)

家計簿アプリの情報をBIツールに入れて家計のダッシュボードを作ってみた

概要

クレジットカードとか銀行とかの情報が自動で取り込まれるのが便利な某家計簿アプリを利用しており、アプリ上でもちょっとした分析ができるようにはなっていたものの、アプリの機能に無いグラフも含め、欲しいグラフが1画面で見れるようなダッシュボードが欲しくなった。
そこで、以下のような感じで情報を取ってきて整えてダッシュボードに表示するような仕組みを作ってみた。
(個別の記事を追加していく予定。すでに書いたものはリンクあり)

  • 家計簿アプリからの情報取得
    Puppeteerで家計簿アプリにアクセスして家計簿アプリのエクスポート機能を叩くようなスクリプトを作成してRundeckで定期的に実行するようにした。

  • 取得した情報の整形
    家計簿アプリ上の情報は、同じ店で払った金額についてのレコードでも支払い方法(クレジットカード、非接触型決済、etc.)によって店名の表記にばらつきがあり、分析に利用できない状態だったため、
    「表記が一致しないが同一の意味をもつ摘要を集約」し、「摘要や口座名等によって利用店舗や受益者等の分析用情報を付加」するツールを作って情報を整えるようにした。

    • 某家計簿アプリからエクスポートしたデータを分析用に整えるツールを作った
  • 家計簿情報をダッシュボードに表示
    これまでBIツールはPowerBI Desktopを使っていたが、個人で気軽に使うにはいちいち学習コストが高く、デスクトップアプリなので分析結果を家族に共有するにも不便だったため、Webで動く使いやすいBIツールを探したところ、Metabaseがちょうどいい感じだったので採用。
    整形後のデータをMetabaseで読み込み、欲しい情報だけが表示されるダッシュボードを作った。

    • Metabaseのダッシュボードを別のアプリに組み込むところで若干手入れが必要だったのでネタはあるが記事にするかは微妙…

構成

TODO: システム構成図を描く

作ったグラフの例

たまに便利だけどほとんど趣味の世界なのは放っておいてほしい。

最近のクレジットカードは〇〇で利用すると還元率○%!それ以外は△%ね!みたいなのばっかなので実際の還元率がみたかった。

f:id:miyataro32:20210117003147p:plain
クレジットカードの利用状況と実際の還元率

投信の積み立て用のプール金が減ってきたときに検知できるように。

f:id:miyataro32:20210117004159p:plain
ジュニアNISA購入用に子供の証券口座に入れてある預け金の残高

この分析によって特定のスーパーの利用を避ければ食費が少し減ることが分かってきた

f:id:miyataro32:20210117002744p:plain
特定期間の店ごとの支出を分析するためのバブルチャート

パスワードロックされたExcel VBAのソースコードを見る

背景

はるか昔、人々はExcel VBAを使って今では考えられないほど大規模なツールを構築していました。
現代のとある企業で、そのようなレガシーツールを現代的なWebシステムとして再構築することになり、まずは現行ツールが何をしているか紐解く必要がありました。
そのxlsファイルは今でもたくさんの人が使っているものでしたが、VBAプロジェクトが保護されており、ソースコードを見るためにはパスワードが必要でした。
しかし、パスワードの伝承はすでに途絶えて久しく、保護を解除することができる人はいませんでした。
そこで人々はパスワードを破るすべを探し求め、ついにはソースコードを見ることに成功しました。
※この記事はパスワードを破ることを推奨するものではありません。この記事に記載されている方法はそれ以外に方法がない場合にExcelツールの権利保有者の許可を得た上で試してください。

調査

とりあえずググってみたところ、VBAプロジェクトのロックを一時的に解除するVBAスクリプトが広く知られているようでした。

puu-0328.hatenablog.com

しかし、試してみたところロック解除できた旨のメッセージが表示されるのにVBAプロジェクトを選択したらExcelが落ちる。。
恐らくこの方法はExcel2007以降のファイル(xlsx)用なのだろうと推測し、他の方法を探しました。

すると出てきたのがこれ。 stackoverflow.com

なんか一番人気の回答にxlsでもスクリプトで解除できる的なことが書いてあるけど、一度試してできなかったので無視。
この回答を参考にしました。

Is there a way to crack the password on an Excel VBA Project? - Stack Overflow

結局どうすればいいの?

A: バイナリエディタでパスワードを書き換えます。

詳しくは上記のサイトを見れば分かりますが、ざっと以下のような手順です。

  1. 新しくxls形式のExcelファイルを作成し、VBAをパスワード保護する
  2. バイナリエディタで1で作成したファイルを開き、CMG=... DPB=... GC=...と書いてある箇所を探してコピー
  3. バイナリエディタVBAのパスワードが分からないExcelファイルを開き、2でコピーした値で対応する箇所を上書き

1Passwordに保管した認証情報を使ってPuppeteerでスクレイピングする

概要

ユーザーID、パスワード、ワンタイムパスワードによる認証が必要なWebサイトの情報をスクレイピングで取得したくなったので、比較的安全そうなやり方として1Passwordから情報を引っ張りながら認証を突破する方法を考えてみました。
WLS2上で動くUbuntu20.04.1で実行しています。
利用するツールは以下の通り。

  • node.js
  • Puppeteer

基本となる上記2つに加えて、node.jsの中からchild_process.execSyncを叩いて以下のOSコマンドを利用します。

4つともググれば相当数の解説記事がヒットするので、個別の説明は控えます。

実装

準備

1Password CLIを入手してパスの通ったところにopという実行ファイルを配置した後、サインインしておきます。

op signin <1Passwordの認証ドメイン> <メールアドレス> <SECRET KEY> 

node.jsの中からopコマンドを呼び出して1Passwordのロックを解除する

まずは1Passwordのロックを解除し、1Passwordのセッショントークンを取得します。
execSyncじゃなくてspawnを使えばexpectに頼らずに対話式の認証を突破できそうな雰囲気があったけど、少し理解に手間取ったので諦めてexpectを使いました。。。(分かる人いたら教えてください)
1Passwordのロック解除に使うパスワードはスクリプト実行時の引数として渡す想定です。結局このパスワードの管理という新しい問題が出てくるのですが、今は1PasswordのパスワードをRundeckのKey Storageに入れて、Rundeckからスクリプトを実行するようにしています。

import { execSync } from 'child_process';

const opPassword = process.argv[2];
const opSession = execSync(`expect -c 'spawn op signin --raw; expect -re Enter; send -- "${opPassword}\\r"; expect EOF exit 0'`, { shell: '/bin/bash' }).toString().split(/[\r\n]+/)[2];

1Passwordから認証情報を取得する

↑で取得したセッショントークンを使って以下のように認証情報を取得できます。

// 1Password上の全アイテムを取得
const items = JSON.parse(execSync(`op list items --session ${opSession} --categories Login`).toString());

// 目的のアイテムを探し、アイテムのuuidを取得
const uuid = items.filter(e => e.overview.title === '目的のアイテムのタイトル')[0].uuid;

// ユーザ名を取得
const username = execSync(`op get item ${uuid} --session ${opSession} --fields username`).toString();

// パスワードを取得
const password = execSync(`op get item ${uuid} --session ${opSession} --fields password`).toString();

// ワンタイムパスワードを取得(標準出力そのままだと改行が含まれてしまうので除去)
const otp = execSync(`op get totp ${uuid} --session ${opSession}`).toString().replace(/[\r\n]/g, '');

Puppeteerでログインする

認証情報が取得できるところまで行ったら、あとはPuppeteerに頑張ってもらうだけです。

// Puppeteerの準備
import * as puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
            args: ['--no-sandbox', '--disable-setuid-sandbox'],
            headless: true,
        });
const page = await browser.newPage();
page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36");
const gotoOpt = { waitUntil: ['load', 'networkidle0', 'domcontentloaded'] };

// ログインボタン等をクリックして次の画面に遷移するのに以下のような関数を用意しておくと便利
async function clickAndWaitForNavigation(selector) {
        const wait = page.waitForNavigation();
        const elem = await page.$(selector);
        await elem.click();
        await wait;
}

// ログインページを開く
await page.goto('ログインページのURL', gotoOpt);

// ユーザ名を1Passwordから取得して入力
const username = execSync(`op get item ${uuid} --session ${opSession} --fields username`).toString();
await (await page.$('input[type=email]')).type(username); // セレクタは適宜変更してください

// パスワードを1Passwordから取得して入力
const password = execSync(`op get item ${uuid} --session ${opSession} --fields password`).toString();
await (await page.$('input[type=password]')).type(password); // セレクタは適宜変更してください

// ↑で定義した関数を使ってログインボタンをクリックし、ワンタイムパスワード入力画面に遷移
await clickAndWaitForNavigation('input[type=submit]'); // セレクタは適宜変更してください

// ワンタイムパスワードを1Passwordから取得して入力
const otp = execSync(`op get totp ${uuid} --session ${opSession}`).toString().replace(/[\r\n]/g, '');
await (await page.$('input[type=tel]')).type(otp); // 今回試したサービスではなぜか`type=tel`だった。セレクタは適宜変更してください

// もう一度ログインボタンをクリック
await clickAndWaitForNavigation('input[type=submit]'); // セレクタは適宜変更してください

あとは目的のページに移動して煮るなり焼くなりするだけなのですが、この状態だとサービスによってはスクレイピングバッチ処理を動かすたびにログインしたよメールが届くので、Cookieを保管してログインを省略するようにしたほうがいいかもしれません。

ログイン後にCookieを保存し、次回以降のログインを省略する

// Cookieの保存先
import * as fs from 'fs'
const cookie_path = 'puppeteer/cookie.txt;

// Puppeteerの準備(省略)

// Cookieのファイルが存在していれば内容をpageにセットする
if (fs.existsSync(cookie_path )) {
    const savedCookies = JSON.parse(fs.readFileSync(cookie_path, 'utf-8'));
        for (let cookie of savedCookies ) {
            await page.setCookie(cookie);
        }
}

// ログアウトのリンクが存在しているか否かによってログイン状態を判定する関数を作成
async function isNotLoggedIn() {
    const logoutLink = await page.$('a[href="/sign_out"]'); // セレクタは適宜変更してください
    return !logoutLink;
}

// スクレイピングしたいページに移動
await page.goto('目的のURL');

if (isNotLoggedIn()) {
    // ログイン処理(省略)
    // ログイン後にCookieを保存
    const cookies = await page.cookies();
    fs.writeFileSync(cookie_path , JSON.stringify(cookies));
}

// スクレイピングの処理

iPhoneのショートカットで定形メールを作成する

背景

毎日終業後に社用のiPhoneから日報をメール送信しているが、定形メールを送る機能が標準メールアプリにもOutlookにもなく、過去メールからのコピペによる非効率なメール作成を強いられていた。
耐えかねてiPhone用の定形メール送信アプリを探していたが、下記の要件に対応できるものが見つからなかった。

  • タイトルに日付を入れる
  • 定形とはいえ、業務内容など一部項目は日によって異なる内容を書くことができる

自分でアプリ作ったり簡単なHTML書いてmailtoのリンクでゴニョゴニョすることを考えたが、運用込みのコストが高すぎるように思えた。
色々探しているとAppleが出しているショートカットというアプリを使えばコードレスで簡単なプログラム作ってホーム画面のアイコンから実行できるみたいだったので試してみた。
純正アプリなのも会社のインストール禁止アプリに引っかからなくて使いやすいポイント。

ショートカット

ショートカット

  • Apple
  • 仕事効率化
  • 無料

ショートカット自体の説明はググれば出てくるけど、エンジニアならあんまり調べる必要もないかもしれない。
当方はソフトウェアエンジニア経験3年弱でブランク1年弱だけど何も調べなくても1時間ぐらい弄っていたらだいたい期待通りのものが作れた。

やり方

テキストやらフォーマットした日付やらの組み合わせでメールのタイトルやら本文やらを作り、 ms-outlook://compose?to=宛先&cc=写し&subject=題名&body=本文 という形式でOutlookURI Schemeを作って開く感じにした。
URLエンコード という部品があり、それを使えば日本語とか改行とか悩まずに済む。

スクリーンショット貼り付けるのも微妙なので表形式でステップを書き出してみる。

部品
(正式な呼び名知らん)                                
パラメータ名                                 パラメータ値                                 コメント
テキスト テキストの内容 <自分の名前> 同僚に配れるように名前の入力を先頭に切り出して改変しやすくした
変数に追加 変数名 名前
テキスト テキストの内容 現在の日付 名前 日報 現在の日付というプリセットの変数があり、フォーマットも指定できる。今回はカスタムフォーマットでM/dにした
URLエンコード モード エンコード 常にこれをやっておくのが無難
変数に追加 変数名 題名
テキスト テキストの内容 nippou-teishutsu-saki@my-company.com
変数に追加 変数名 TO
テキスト テキストの内容 各位

お疲れ様です。名前です。
本日の日報です。

URLエンコード モード エンコード 常にこれをやっておくのが無難
変数に追加 変数名 本文
テキスト テキストの内容 ms-outlook://compose?to=TO&subject=題名&body=本文
URLを開く Safariで開けばOutlookが立ち上がる

これを日報っていう名前で作成してホーム画面に配置すれば、日報アイコンをタップするだけであらかた出来上がった状態でメール送信画面が立ち上がる。

課題

これを同僚に簡単に配布できるようにしたいんだけど、ファイルとして共有してOutlookで送ったやつをショートカットアプリで読み込むと壊れているとか言われる。
メールアドレスとか入ってるからiCloudで公開できないし、どうしたらいいのか分からない。。

雑記欄があるんだけど、天気の情報を取得して「今日はいい天気だった」とかうまいこと自動生成できないだろうか。日報ってまじで不毛だ。