| @WWW

connpass で募集されていた勉強会に参加したところ、イベント終了後にイベント管理者から一方的にメールマガジン的なものが送られてくるようになった。勉強会直前の確認メッセージや次回の勉強会の告知ならよく他のイベントからも届くが、そうではなく主催者の私信的な内容だったのでこのようなメールは不要だった。大抵の一斉送信メールにはフッターなどに購読解除ページへのリンクなどがあるが、 connpass から届くメールにはそのような文言はなかった。

スクリーンショット 2018-01-11 21.50.31.png

最初なぜこのような私信メールが届き続けるのかわからなかったが、自分が管理者になっているイベントで確認してみたところ、管理画面からイベント参加者に任意のメッセージを一斉送信することができるようだった。 connpass はイベントに参加登録すると自動的にイベントのグループに追加されるので、グループの管理者はグループメンバーにメッセージを一斉送信する仕組みがあるようだ。勉強会に参加しただけで一方的にグループのメンバーにされてしまうのは違和感がある。

とりあえず今回はメールマガジンは受信したくないと思ったので該当のイベントページから主催者に「勉強会に参加したときにこのようなメールを送り続けるという説明はなかった、メールマガジンを受信したくない」旨の問い合わせを行い、あわせて connpass にも「勉強会参加後に定期的にメールが届くようになった、同意なしにこのようなメールを送り続けるのを規約上容認しているのか」を問い合わせた。イベント主催者の方からは即座に返信があり、今後メールが届かないようにしてもらえた。 connpass の方(運営元のビープラウド社)からは一週間後に返信があり、自分でイベント主催者とやりとりしろという旨の文章と利用規約ページへのリンクが貼ってあるのみで、自分が尋ねた規約上容認しているかどうかへの回答は書かれてなかった。

そもそも自分が情弱だったのだが、勉強会参加後に自動的に登録されるグループから退会すれば主催者からの一斉送信メールは届かなくなるようだ。自分の場合には当該グループから退会すればよかったのにイベント主催者に苦情めいた問い合わせを行ってしまい申し訳なかった。ただ退会ボタンはぱっと見 disabled されており、到底クリックできるようには見えないし、そもそも退会することでメールが届かなくなるかどうかはわからない(ヘルプページをよく読まない限り退会することで何が起こるのかわからない)。

スクリーンショット 2018-01-11 8.36.11.png

そもそもメールのみの受信設定を行えるような画面があってもよいと思うのだが、調べてみたところ新しいイベントが公開されたときや資料がアップロードされたときの通知設定は行えるが、イベント主催者からのメッセージを受け取らないようにする設定項目は存在しなかった。

スクリーンショット 2018-01-11 8.36.31.png

connpass 内全体の各グループで適用される受信設定も同様で、イベント主催者からの恣意的なメッセージを受け取るかどうかの設定項目はなかった。

スクリーンショット 2018-01-11 8.36.40.png

いらないものを拒否する手順への説明が記されておらず、自分の意図に反して送られてき続けるメールは全て迷惑メールだと思うが、運営元に問い合わせる限りはこれらは規約違反でもなくイベント管理者は自由に行えるようなので、 IT 勉強会を主催している各社の採用担当の皆さんは一度でも勉強会に参加したことがあるカモの皆さんに求人メールを定期的に送り続けてやれば良いのではないでしょうか。

| @散財

壊れた MDR-1RBT

イヤーパッドカバーがぼろぼろになっても自分で取り替えたりしながら割と大事に使ってきた SONY の MDR-1RBT のヘッドアームが折れて使えなくなってしまった。落としたりぶつけたりして壊したというより経年劣化でプラスチックがもろくなっていたっぽい。

音を出すという機能は生きているのに頭に装着するという機能が壊れて使えなくなったのは残念。修理もできるようだが家からかなり離れた車でしか行けないようなところにある郊外の電器屋に平日か土曜の日中に持ち込まないといけない&修理料金が高いっぽいので修理するか逡巡してる。天神にソニーストアあるのにここで修理頼めないのは極めて謎。タカビーな Apple でも Apple Store に持ち込んだら古い Mac でも見てくれるのになぁ。あまり修理はして欲しくないのかも知れない。

しょうがないのでいまは iPhone に付いてくる Apple の純正イヤフォンを使ってる。ワイヤレスヘッドフォンに慣れきってたので有線は引っ張られる感じがして使い心地が悪い。 Anker の安い Bluetooth イヤフォンもあるにはあるが、電池のもちがよくないし何より自分は耳垢がしめってるネチョ耳クソ野郎なのでインイヤータイプのイヤフォンはあまり長時間付けたくない。頭の上に載っけるタイプのヘッドフォンが一番使い心地よかった。

ヘッドフォン、仕事道具という認識なかったけど失ってみて集中したいときなどに結構依存していたことがわかる。ワイヤレスヘッドフォンは偉大だった。

| @雑談

納豆味噌汁漬け物弁当

今年の目標はとにかく金を貯めることにしようと思った。 いい歳なのに恥ずかしいくらい金がなくてちょっと風が吹いただけで生活が破綻しそうな予感がある。贅沢はしてないつもりだけどとにかく金がない。とりあえず三ヶ月間は給料がなくても生活していけるくらいの金は貯めたい。

💸金がない原因

車を買ってからずっとこの調子で、車を買ったことで維持費や保険料がかかるようになったことに加え、車によって行動範囲が広がったことが問題だと思う。郊外に引っ越して飲みに行く機会は減ったが、しょうもない外食をする機会が増えた。幼稚園の支払いも高い。うちは子供一人しかいないのにとにかく園納金の支払いが厳しい(年間40万円くらい)。子どもが二人以上いる普通の家庭はどうやってやりくりしているのだろう。生命保険や固定資産税の支払いも厳しい。一戸建てに引っ越したことで電気代も高くなった。郊外に家を買ってしまった以上車を手放すのは無理だし、子どもを幼稚園に通わせないわけにもいかない。電気代に関しては嫁さんが一晩中暖房をつけたまま居間でテレビを見ながら寝るのでこれをやめてもらえれば多少は改善されるだろうが、寝室で寝て欲しいと言っても「お前の頭皮と息が臭いしあたしには長谷川博己や山田裕貴が出てるドラマを見ることくらいしか楽しみがないんだよ👿」と言われるので自分ではどうすることもできない。加えて車、家、保険、教育費などもライフステージに起因する出費なのである程度はしょうがないものなのかも知れない。

💰対策

とりあえず自分に出来る範囲で以下のことをやるようにしたい。

👀保険の見直し

生命保険、親が選んだやつに惰性で入り続けてるけど明らかに損している気がする。特に医療保険は無駄がでかい気がする。解約して県民共済とかにした方がよさそう。今年分の保険料は払ってしまったので今年中にデカめの手術して保険金もらったあと解約したい。そもそも金があれば医療保険は入る必要ないと思うし金貯めて保険とか入らなくても大丈夫な状況にしたい。

🚫🍻金がかかる飲み会には参加しない

会社持ちの無料の飲み会にしか行かないことにする。二次会にも行かない。酒飲みに行ったらその時は楽しいけど飲みすぎて翌日破滅するし凄まじい勢いで金がなくなる。友達が減るのはしょうがないとあきらめるしかない。

🚫🍺第三のビールの六缶パックを買わない

家に缶ビールがあることでついつい飲んでしまう。酒がなければないでなんとかなる。どうしても飲みたいときにその日飲む一、二本だけ買うようなスタイルに改める。第三のビールとはいえ金がかかることには変わりはない。宵越の酒は持たないようにする。

🍜弁当がない日はスーパーのカップ麺で済ませる

弁当を作ってもらえなかった日の昼食代は 100 円以下に抑える。不健康になるかも知れないが早死にしたら住宅ローンがチャラになるし保険料も払わなくて済むようになるのでむしろ好都合だと思うことにする。

🚫🏪コンビニで買い物しない

前の会社は給料高い人が多くて、みんな朝の出社時に 500 円くらいするスターバックスのコーヒー、昼飯食ったあとに 300 円くらいするスターバックスのコーヒー、三時のおやつに 400 円くらいするスターバックスのコーヒーを買ってた。自分はスターバックスのコーヒーは買えなかったけど周りに流されてコンビニコーヒーを買う癖がついてしまった。セブンイレブンのコーヒーは一杯 150 円でも量が少なくて二回くらい飲みたくなってしまい一日で 300 円使ってしまうことになる。毎日買ったら月 6000 円になる。一生懸命頑張る自分へのご褒美、みたいな感じで朝から高いコーヒー飲んだりよい昼飯食ったりするという考え方もあるとは思うが、自分はそんなに仕事頑張ってないし稼ぎもよくないのでこういう思考とは距離を置くべきだと思う。ときどき朝食にコンビニで総菜パン買って食べるのもやめる。

🚫💻GitHub の有料プラン解約

なんかうぇぶさーびすを作って一発当てようと自分にハッパかけるつもりで Developer Plan に加入してるけど大したコード書いてないし人様に見せられないコードの置き場所は GitHub から Bitbucket に変える。

🚫🛒コストコに行かない

一昨年の 9 月に会員になって去年の 9 月に失効した。余裕ができたら更新したいと思っていたけどコストコ行くと 2 万とか平気で使ってしまうので貯金が目標金額に到達するまでコストコ断ちすることにする。

🙅🏻‍♂️抵抗

逆に嫁さんにやめろといわれて抵抗しているのは以下。

🖥さくらVPSの解約、もしくは下位プランへの変更

プログラマーなので自由に使える Linux サーバーがないと厳しい。このブログはファイルシステムを結構使うようにしてあるので Heroku だと多分動かない。 AWS は逆に高くなる。

いまはメモリ 2GB プランを使っているので下位プランへ契約変更するという手もあるが、いま動かしてるアプリケーション( puma × 2)だけでも結構メモリが足りないと感じることがあるのでさらにスペックダウンするのは厳しい。実験の場を失うことで長期的には年収を下げることにもつながりかねない。

そもそもサーバー代は Google Adsense と Amazon Associate で 2/3 くらいはまかなえてるのでここは手を付けないことにする。

🎞Amazon Prime 解約

解約してしまうと 2000 円分買わないと送料無料にならないとかいろいろ厳しい。 350 円のものが欲しいだけなのに送料無料にするために 1650 円の大して必要ではない何かを買わざるを得ないシチュエーションはとてもストレスがたまる。せっかく Apple TV で見られるようになった Amazon Prime Video を見られなくなるのも残念。このくらいは死守したい。


正月早々汚らしく金の話をしてしまって最悪だけど、 @taketin さんや @kitak@t32k さんなどといった元同僚の富裕層の皆さんのように用事もないのにヒルトンに泊まったり一回買って売ったイヤフォンをもう一回買い直したりわざと虫歯になってセラミックの歯を自慢したりできるような身分になりたい。バカすぎてブロックチェーンとかはわからないので今年は金を貯めることだけを目的にやっていきます🤑

| @技術/プログラミング

cho45 さんの以下の記事を参考に関連記事を表示するようにしてみた。

ほとんど cho45 さんの記事に書いてある SQL を実行しているだけだけど、関連記事の表示用に Lokka 側に Similarity というモデルを追加して、以下のようなスキーマにしてる。

similar-entries-erd.png

Similarity テーブルの更新は cho45 さんの記事にあるように SQLite で行った計算の結果を反映することで行う。以下のような Rake タスクを定義した。

desc "Detect and update similar entries"
task similar_entries: %i[similar_entries:extract_term similar_entries:vector_normalize similar_entries:export]

namespace :similar_entries do
  require 'sqlite3'
  desc "Extract term"
  task :extract_term do
    require 'natto'
    nm = Natto::MeCab.new
    db = SQLite3::Database.new('db/tfidf.sqlite3')
    create_table_sql =<<~SQL
      DROP TABLE IF EXISTS tfidf;
      CREATE TABLE tfidf (
        `id` INTEGER PRIMARY KEY,
        `term` TEXT NOT NULL,
        `entry_id` INTEGER NOT NULL,
        `term_count` INTEGER NOT NULL DEFAULT 0, -- エントリ内でのターム出現回数
        `tfidf` FLOAT NOT NULL DEFAULT 0, -- 正規化前の TF-IDF
        `tfidf_n` FLOAT NOT NULL DEFAULT 0 -- ベクトル正規化した TF-IDF
      );
      CREATE UNIQUE INDEX index_tf_term ON tfidf (`term`, `entry_id`);
      CREATE INDEX index_tf_entry_id ON tfidf (`entry_id`);
    SQL
    db.execute_batch(create_table_sql)

    entries = Entry.published.all(fields: [:id, :body])
    entry_frequencies = {}
    entries.each do |entry|
      words = []
      body_cleansed = entry.body.
        gsub(/<.+?>/, '').
        gsub(/!?\[.+?\)/, '').
        gsub(/(```|<code>).+?(```|<\/code>)/m, '')
      begin
        nm.parse(body_cleansed) do |n|
          next if !n.feature.match(/名詞/)
          next if n.feature.match(/(サ変接続|数)/)
          next if n.surface.match(/\A([a-z][0-9]|\p{hiragana}|\p{katakana})\Z/i)
          next if %w[これ こと とき よう そう やつ とこ ところ 用 もの はず みたい たち いま 後 確か 中 気 方 頃 上 先 点 前 一 内 lt gt ここ なか どこ まま わけ ため 的 それ あと].include?(n.surface)
          words << n.surface
        end
      rescue ArgumentError
        next
      end
      frequency = words.inject(Hash.new(0)) {|sum, word| sum[word] += 1; sum }
      entry_frequencies[entry.id] = frequency
    end
    entry_frequencies.each do |entry_id, frequency|
      frequency.each do |word, count|
        db.execute("INSERT INTO tfidf (`term`, `entry_id`, `term_count`) VALUES (?, ?, ?)", [word, entry_id, count])
      end
    end
  end

  desc "Vector Normalize"
  task :vector_normalize do
    db = SQLite3::Database.new('db/tfidf.sqlite3')

    load_extension_sql =<<~SQL
      -- SQRT や LOG を使いたいので
      SELECT load_extension('/usr/local/Cellar/sqlite/3.21.0/lib/libsqlitefunctions.dylib');
    SQL
    db.enable_load_extension(true)
    db.execute(load_extension_sql)

    update_tfidf_column_sql = <<~SQL
      -- エントリ数をカウントしておきます
      -- SQLite には変数がないので一時テーブルにいれます
      CREATE TEMPORARY TABLE entry_total AS
          SELECT CAST(COUNT(DISTINCT entry_id) AS REAL) AS value FROM tfidf;

      -- ワード(ターム)が出てくるエントリ数を数えておきます
      -- term と entry_id でユニークなテーブルなのでこれでエントリ数になります
      CREATE TEMPORARY TABLE term_counts AS
          SELECT term, CAST(COUNT(*) AS REAL) AS cnt FROM tfidf GROUP BY term;
      CREATE INDEX temp.term_counts_term ON term_counts (term);

      -- エントリごとの合計ワード数を数えておきます
      CREATE TEMPORARY TABLE entry_term_counts AS
          SELECT entry_id, LOG(CAST(SUM(term_count) AS REAL)) AS cnt FROM tfidf GROUP BY entry_id;
      CREATE INDEX temp.entry_term_counts_entry_id ON entry_term_counts (entry_id);

      -- TF-IDF を計算して埋めます
      -- ここまでで作った一時テーブルからひいて計算しています。
      UPDATE tfidf SET tfidf = IFNULL(
          -- tf (normalized with Harman method)
          (
              LOG(CAST(term_count AS REAL) + 1) -- term_count in an entry
              /
              (SELECT cnt FROM entry_term_counts WHERE entry_term_counts.entry_id = tfidf.entry_id) -- total term count in an entry
          )
          *
          -- idf (normalized with Sparck Jones method)
          (1 + LOG(
              (SELECT value FROM entry_total) -- total
              /
              (SELECT cnt FROM term_counts WHERE term_counts.term = tfidf.term) -- term entry count
          ))
      , 0.0);
    SQL
    db.execute_batch(update_tfidf_column_sql)

    vector_normalize_sql = <<~SQL
      -- エントリごとのTF-IDFのベクトルの大きさを求めておきます
      CREATE TEMPORARY TABLE tfidf_size AS
          SELECT entry_id, SQRT(SUM(tfidf * tfidf)) AS size FROM tfidf
          GROUP BY entry_id;
      CREATE INDEX temp.tfidf_size_entry_id ON tfidf_size (entry_id);

      -- 計算済みの TF-IDF をベクトルの大きさで割って正規化します
      UPDATE tfidf SET tfidf_n = IFNULL(tfidf / (SELECT size FROM tfidf_size WHERE entry_id = tfidf.entry_id), 0.0);
    SQL
    db.execute_batch(vector_normalize_sql)
  end

  desc "Export calculation result to MySQL"
  task :export do
    db = SQLite3::Database.new('db/tfidf.sqlite3')
    create_similar_candidate_sql = <<~SQL
      DROP TABLE IF EXISTS similar_candidate;
      DROP INDEX IF EXISTS index_sc_parent_id;
      DROP INDEX IF EXISTS index_sc_entry_id;
      DROP INDEX IF EXISTS index_sc_cnt;
      CREATE TABLE similar_candidate (
        `id` INTEGER PRIMARY KEY,
        `parent_id` INTEGER NOT NULL,
        `entry_id` INTEGER NOT NULL,
        `cnt` INTEGER NOT NULL DEFAULT 0
      );
      CREATE INDEX index_sc_parent_id ON similar_candidate (parent_id);
      CREATE INDEX index_sc_entry_id ON similar_candidate (entry_id);
      CREATE INDEX index_sc_cnt ON similar_candidate (cnt);
    SQL
    db.execute_batch(create_similar_candidate_sql)

    extract_similar_entries_sql = <<~SQL
      -- 類似していそうなエントリを共通語ベースでまず100エントリほど出します
      INSERT INTO similar_candidate (`parent_id`, `entry_id`, `cnt`)
          SELECT ? as parent_id, entry_id, COUNT(*) as cnt FROM tfidf
          WHERE
              entry_id <> ? AND
              term IN (
                  SELECT term FROM tfidf WHERE entry_id = ?
                  ORDER BY tfidf DESC
                  LIMIT 50
              )
          GROUP BY entry_id
          HAVING cnt > 3
          ORDER BY cnt DESC
          LIMIT 100;
    SQL

    search_similar_entries_sql = <<~SQL
      -- 該当する100件に対してスコアを計算してソートします
      SELECT
          ? AS entry_id,
          entry_id AS similar_entry_id,
          SUM(a.tfidf_n * b.tfidf_n) AS score
      FROM (
          (SELECT term, tfidf_n FROM tfidf WHERE entry_id = ? ORDER BY tfidf DESC LIMIT 50) as a
          INNER JOIN
          (SELECT entry_id, term, tfidf_n FROM tfidf WHERE entry_id IN (SELECT entry_id FROM similar_candidate WHERE parent_id = ?)) as b
          ON
          a.term = b.term
      )
      WHERE similar_entry_id <> ?
      GROUP BY entry_id
      ORDER BY score DESC
      LIMIT 10;
    SQL

    results = {}
    Entry.published.all(fields: [:id]).each do |entry|
      db.execute(extract_similar_entries_sql, [entry.id, entry.id, entry.id])
      db.results_as_hash = true
      similarities = db.execute(search_similar_entries_sql, [entry.id, entry.id, entry.id, entry.id])
      results[entry.id] = similarities
    end

    Similarity.destroy

    results.each do |entry_id, similarities|
      if similarities.present?
        similarities.each do |s|
          conditions = { entry_id: s["entry_id"], similar_entry_id: s["similar_entry_id"] }
          similarity = Similarity.new(conditions)
          similarity.score = s["score"]
          similarity.save
        end
      end
    end
  end
end

やってることとしては、全エントリーを拾ってきて本文を MeCab で品詞分解して名詞だけを取り出し記事ごとの term 一覧を作り、そこから TF-IDF を求めてベクトル正規化し、最後に関連していそうなエントリを探し出して similarities テーブル(こちらは SQLite のテーブルではない)を更新している。詳しいアルゴリズムはバカなのでわからないが、 cho45 さんが書いているやり方を Lokka のスキーマに素直に適用した感じ。

結構この処理は遅いので parallel.gem を使って高速化できないか試してみたが、スレッドによる並行処理ではあまり速くできなかった。 4 コアある CPU のうち一つが 100% で処理を実行してもまだ 3 コアは余っている。プロセスを増やして並列処理するのがよさそうだが、分散をプロセスレベルで行おうとすると MySQL server has gone というエラーが出る。 DataMapper が MySQL とのコネクションをロストするようである。 ActiveRecord であれば reconnect するだとか回避方法があるようなのだけど DataMapper は情報が少なく、対応方法が見つけられなかったので一旦並列処理はあきらめた。

何回か動かしてみて大体正しく関連記事を表示できてそうなのでさくらの VPS で稼働させたいところなのだけど、関連記事の更新はいまのところ手動でやっている。本番 DB の entries テーブルを dump してきて Mac に取り込み、 similarities テーブルを更新して今度はローカルで similarities テーブルを dump して本番にインポートするという手順をとっている。

これにはいろいろ理由があって、一つには利用している mecab-ipadic-neologd (新語にも対応している MeCab の辞書)が空きメモリ 1.5GB 以上でないとインストールできずさくらの VPS にインストールできなかったから。もう一つには cho45 さんのブログにもあるけど SQLite で LOGSQRT を使うためには libsqlitefunction.so の読み込みが必要で、 load_extension() できるようにしないといけないが、そのためには sqlite3 をソースからビルドする必要があり若干面倒だった( Mac では Homebrew で sqlite を入れた)。

関連記事の更新は自分が記事を書いたときにしか発生しないのでいまの手動運用でもまぁ問題ないが、このブログは Docker でも動くようにしてあるので Docker イメージを作ればさくら VPS でも問題なく動かせそうな気はする。正月休みにでもチャレンジしたい。

感想

関連記事表示、結構面白くてちゃんと関連性の高いエントリーが表示される。例えば人吉に SL に乗り行った記事の関連記事にはちゃんと山口に SL に乗りに行ったときの記事 が表示される。いまのところ Google Adsense の関連コンテンツよりも精度が高いようである。

無限に自分の黒歴史を掘り返すことができるのでおすすめです。

| @Mac/iPhone

旅行や街歩きに出かけたとき、移動軌跡が地図上に表示されるとその時何をしていたのかが思い出せて便利になると思う。例えば知らない街に旅行に行った数日後にこんな状況になったことはないだろうか。「昼飯を食ったあとに食器屋に行って良い皿を見たんだけど迷って買わなかったんだよなぁ、あれはなんて店なんだろう、ネット通販やってるかな🤔」。自分はよくある。こういうときに位置情報付きの行動ログが残ってれば店名を探り当てやすくなるし、目当ての商品にたどり着ける可能性が高まる。

自分は今のところ Twitter 、 Instagram への投稿と Swarm へのチェックインを IFTTT 経由で Day One に自動投稿しているので、それらの情報が手がかりにならないでもないが、それでもそのときの自分が Twitter なり Instagram なり Swarm なりに何らかの情報を投稿していないと Day One には何も残らない。やはり何らかの行動ログ的なものが記録されている方がいい。

とはいえ旅行や町歩きのときに意識的に行動ログの取得を開始するのは難しいと思う。旅行中、街を散策する前に iPhone を取り出して何らかのアプリケーションを起動し「開始」ボタンを押したりできるだろうか? 気がついたときには随分行動してしまったあとで「今から記録をとってもね😔」という感じになるのが現実だろう。この手のツールはバックグラウンドで勝手にログをとっといてくれるのが重要だと思う。

何か良いアプリはないものかと App Store で調べてみたところ、 SilentLog というやつが見つかった。評価は良かったがバッテリーの減りが速いとある。インストールしてみたところ確かにバッテリーの減りが速くなる。よくよく考えたら Google Maps のタイムラインや Day One でもバックグラウンドでの位置情報の取得はやってるので似たようなことを複数のアプリでやるのは効率が悪い。

とはいえ移動したログを一番かっちょよく見られるのは SilentLog だった。こんな風に一日のタイムラインを手軽に振り返ることができる。朝家を出て電車に乗り、9時から18時まで職場で仕事をしてなどがすぐわかる。休みの日に出かけた場所も意識せずに記録されていく。その日 iPhone で撮った写真も表示される。

SilentLog

特に良いのが1日の行動ログをほかのアプリやサービスに書き出せること。 Google Maps のタイムラインはエクスポート機能がないし、 Day One の Activity Feed は一定期間たつと消えてしまうので残しておきたいなら意識して記事を作成しないといけない上に、記事にしたとしても位置情報の履歴は残らない。

Google Maps and Day One.png

その点、 SilentLog は行動ログが地図上に表示された画像を手軽に書き出せる。とても便利なのだが、この機能を使うには App 内課金で有料チケットを購入する必要があるようだった。 240円/30日で年払いだと 1900 円とのこと。

位置情報の収集や書き出し、気持ち悪いと感じる人には気持ち悪いこと極まりないだろう。またもし個人を特定できるかたちで流出して誰かに見られてしまったら非常にまずい(浮気中の人など)。加えて SilentLog を作っている会社 はユーザーの位置情報データを売っているようである。おそらく「年収 XXX 万円くらいの 30-40 代の男性が毎週何曜の何時に新宿駅に何人くらいいる」みたいなデータをデパートとかに売ったりするのだろう。 DMP の位置情報版といったところか。個人を特定できないようにはなってると思うがあまりよい気持ちはしない。

とはいえ自分でも忘れるような行動の履歴をスマートフォンアプリが取っておいてくれてあとで振り返られるというのは絶対に便利だと思う。日記は書いておくと便利だけど書くのが非常に面倒くさい。行動のログから自動で簡易的な日記にしてくれてバッテリーの消耗が穏やかなアプリがあったらうれしいなぁ。(結構個人のプライバシーを尊重する) Apple がそういうのつくってくれないかな。

| @Mac/iPhone

先週、 Alfred で通貨換算をやる方法(💵 Alfred で手軽に通貨換算 💴 )について書いたけどその続き。

Numi icon

Numi という Alfred に似たソフトがある。 Soulver と違い多言語対応はしておらず、自然言語による入力は日本語未対応なのだが、 Soulver もまともに使えるのは英語利用時だけなので実質的には機能に差がない。タブを使えたりする分、 Numi の方が高機能かも知れない。さらに Numi は今のところ無料である(ベータ版のため)。ベータ版とはいえほとんど困ることなく利用できている。

Numi うどん

このソフトがすごいのが、 Alfred Workflow を入れると Numi による計算を Alfred から n <式> と入力することでできるようになるということ(ただし Numi.app を起動しておく必要あり)。こんな感じ。

Numi Currency conversion

いちいち Numi.app に切り替えて新規ドキュメントを作成し、ということをしなくて済む。とても手軽に計算できる。通貨換算のほかに、単位の換算もできる。 Google Fincance の Converter サービスを使うやり方よりも高機能だ。

Numi Unit conversion

このソフトを知ってから Soulver を使う機会がめっきり減ってしまった。 Alfred からも計算できるし、結果を残しておきたいような複雑な計算をするときは Numi.app に切り替えて使うこともできる。文章を書いたり考え事をしているときにささっと計算できて頭の切り替えコストが低い。 AWS を利用しているプログラマー諸氏なら EC2 インスタンスの料金なんかを計算するときにもとても便利だと思う。おすすめです。


この記事は Alfred 3 Advent Calendar 2017 - Adventar 12 日目の記事でした。明日は(明日も) @tsurusuke さんです。

| @旅行/散歩

DSC_3608

子どもがてっちゃんなので春の山口に引き続き SL を見る&撮るという目的で人吉に行った。自分が子どもの頃に阿蘇を走っていた SLあそBOY が老朽化のため立野の急勾配を上れなくなり、熊本と人吉の間を走る人吉号として余生を過ごしているのだった。

DSC_3525

朝早起きして福岡の家を出て 9 時半頃に熊本駅に到着し、駐車場に車を止めて熊本駅から SL に乗った。熊本駅では写真撮影コーナーがあって、乗務員の人にシャッターを押してもらって家族写真を撮ったりできる。カツアゲに怯えながら熊本駅近くの塾に通っていた中学生の頃が懐かしかった。

DSC_3502

SL 人吉号は SL やまぐち号よりも内装がしゃれてて綺麗だった。やまぐち号の車内は何となくすすけていて、乗車後に鼻をかむと黒い鼻くそが出てきたりしたけど、人吉号の車内は清潔かつモダンで冷房も効いており快適そのものだった。鼻くそが黒くなることもなかった。しかし人吉号は車内販売が充実しておらず、ちょっと何か食べたいと思っても飲み物と甘い物くらいしかなくてその点は不満だった。やまぐち号は乗務員の人が回ってきてスナック菓子や酒のつまみっぽいやつなど割と豊富に食べ物があった。

DSC_3504

熊本県出身ながら熊本駅以南に列車で出かけたことはほとんどなかったので、宇城や八代の平野部を走る列車の車窓は新鮮だった。緑の絨毯を SL で進む感じだった。

DSC_3513

人吉に着くまではよかったけど、人吉駅に着いて駅前の定食屋で適当に昼飯を食べ、予約しておいたレンタカー屋に駅まで迎えに来てもらってから雲行きが怪しくなってきた。レンタカー屋が宿から遠い、借りたレンタカーに USB ポートが付いていない、シガーソケットに付ける USB 充電器を忘れてきた、 Anker のモバイルバッテリーがすっからかん状態だったため iPhone の順電ができないことを嫁さんにとがめられ大げんかになり、撮影に行く途中で車を降りて別行動をとった。

DSC_3565

結局自分の iPhone も充電切れになりそうだったので、猛暑の中荷物とカメラを抱えて 2km くらい歩いてたどり着いたナフコで 2000 円もする怪しい USB 充電器を買って充電できそうなスポットを探し、近くのマクドナルドに入店してコンセントを確保し狂ったように充電しまくった。ゴミみたいな 3 時間だった。会社から課題図書として指定されていた『顧客が熱狂するネット靴店 ザッポス伝説』を読んでいた。わお。

SL 撮影から戻ってきた家族と合流し、レンタカーを返して宿に向かった。三連休でどこも混んでいるなか、三人で 1 万円ちょいで泊まれる格安の宿だった。いつか自分も元同僚の taketin さんのように一泊 2000 円の宿なのに 34000 円使うような豪遊をしてみたい。

この宿は場所はよいけど温泉は付いておらず、かわりに近隣の温泉の入浴券がもらえた。近くの球磨川沿いのあゆの里というホテルの温泉に入らせてもらうことにした。ここがすごく眺めがよくて、しかも 19 時までに入れば脱衣所で生ビールが無料で飲めるという謎のサービスも付いていた。最高すぎてここに泊まりたいと値段を調べてみたら一泊 30000 円くらいして到底庶民が泊まれるような宿ではなかった。

IMG_3419IMG_3415


人吉温泉 清流山水花 あゆの里

風呂から上がってから夕食をとる店を探したがなかなかよい店が見つからず、思い切って入った店は味はよかったが少々お高い店でげんなりした気持ちで宿に帰った。

翌朝、朝食付きプランにしていたので食堂に朝食を食べに行ったが食堂は狭くまたテーブルもセッティングされておらず少々待った。うまいのかまずいのかよく分からないまま食事を終えてデザートにスイカを食べたようとしたところ異臭がする。悪くなっていたようだった。つらい。

海の日の三連休で異常に暑かったが人吉まで来て観光しないわけにはいかないので街を歩いて回った。とても寂れた感じがしたが、なかなか味わいのある街並みだった。少し前までストリップ劇場があったらしいスカイビルというビルが何とも言えない雰囲気を放っていた。

DSC_3567

IMG_3436

DSC_3606

DSC_3604

城下町の鍛冶屋町というところを歩いているとファイナルファンタジーとかに出てきそうな包丁屋があり嫁さんが包丁を買った。恐ろしくよく切れる包丁で「あぁ俺は多分この包丁で刺されて死ぬんだろうな」と思った。その先にはみそ・しょうゆ蔵があり、味噌と醤油も買った。中の方まで自由に見学できる蔵で真夏なのに中は涼しく不思議な感じがした。お茶と漬け物を出してもらって小休止した。

DSC_3580

DSC_3588

DSC_3596

その後タクシーで駅前まで移動して青井阿蘇神社にお参りし、 SL がターンテーブルで回転するところを見物しようとしたが時間をミスってそのシーンを見逃してしまう。これでまたアホみたいに怒られた。

DSC_3624

DSC_3637

DSC_3643

慌ただしくお土産を買い、弁当を買って再び熊本に戻る SL 人吉に乗ろうとするが、なんと時間が中途半端過ぎて駅弁が売り切れ状態であり、いなり寿司を二パックしか買えなかった。往路同様、人吉号の車内販売はしょぼしょぼなので結局家族全員ひもじい状態で熊本駅まで二時間堪えなければならなかった。途中の停車駅でサバ寿司の駅売りがありこれを買って食べたが 800 円もするのにちょびっとの量でひもじさに拍車がかかった。

DSC_3646

帰りも記念写真を撮ったり景色を眺めているうちに熊本駅に到着し、車に乗り換えて熊本ラーメン(黒亭)を食べてから福岡へ帰った。初めて金峰山の西側、有明海沿いを運転したが夕日がものすごく綺麗だった。道は狭いところがあるものの、迫力のある景色が見られてなかなかよいドライブコースだと思う。

DSC_3658

KPT

Keep

  • 出身県でも地元から遠い地方にはなかなか行かないので足を伸ばすことができてよかった。
  • 熊本は阿蘇以外にもいっぱいいい温泉地ある。

Problem

  • 予定がぐだぐだ
    前回の山口旅行のときは家庭内 Kibela を活用して綿密にスケジュールを組でいたが、前回とは違って転職したばかりで慣れないこともあり事前に十分な計画を練ることができなかった。若かったり子どもがいなかったりなら場当たり的な旅行でも面白いかもしれないけど、子連れだとそれなりに事前調査してから行かないとばたばたになってしまう。

  • 酒が飲めない
    途中まで車で行って列車に乗るタイプの旅行は運転をしないといけないので列車の中で酒が飲めないのがつらい。山口のときも今回も、嫁さんだけ列車の中でうまそうにビールを飲んでいて気が狂いそうになった。

Try

  • 旅行前はきちんと計画を練る。
  • Anker のモバイルバッテリーは前日寝る前に確実に充電しておく。
  • レンタカーに乗るときはシガーソケットから充電するやつを忘れない。

このどうしようもないチラシの裏のような日記は旅行 Advent Calendar 2017 - Adventar 8 日目の記事( 1 日遅れで書いています、すみません)でした。 9 日目は nayo74 さんです。