Akata Works

東京エンジニア。主にRuby,Go,たまにAWSとiOS。ゲーム音楽が好きです。連絡はTwitterかakata.onen@gmail.comまで

Base64エンコードとBase64URLエンコードについて

GoogleさんのProtocol Buffersでエンコードしたデータをクエリパラメータで含めようと思ったので改めてBase64エンコードとBase64URLエンコードについて調べてみました

Base64エンコーディングについては以下の記事でも軽く触っていますが、データを印字可能な64種類のデータで表現するエンコード方式です
よくマルチバイト文字やバイナリデータをポストするのに使っているイメージがします

ただ、URLの一部として利用する場合は62 indexの+と63 indexの/,パディングの=がURLセーフではない(RFC3986とapplication/x-www-form-urlencoded)ため、
URLセーフにエンコードするべきです
パーセントエンコーディングはデカいしキモいのでなるべく避けたいっすね

Base64URL Encoding

+-に、/_に変更するのが仕様みたいです。パディングは省略されます

RFC 4648 - The Base16, Base32, and Base64 Data Encodings

このようにBase64の中でもこのようにURLセーフなものをBase64URLと区別します

Ruby

RubyではBase64.#urlsafe_encode64にて提供されますが、内部的にはRFC4648で変換した後、後から+/を置換します

https://apidock.com/ruby/Base64/urlsafe_encode64

def urlsafe_encode64(bin)
  strict_encode64(bin).tr("+/", "-_")
end

Go

Goではbase64.RawURLEncodingにて提供されます。Paddingを含むbase64.URLEncodingもあります
Rubyとは違って置換ではなくはcharsetが予めURLセーフになっている点が無駄がないですね

const encodeURL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"

// ...

var RawURLEncoding = URLEncoding.WithPadding(NoPadding)

MongoDB TTLのexpireAfterSecondsを変更する

MongoDBにはドキュメントサイズで削除するキャップ付きコレクションと有効期限で削除するTTLインデックスがあります

TTLインデックスの有効期限であるexpireAfterSecondsを変更する方法には二種類あり、dropIndexからのcreateIndexでインデックスを張り直すというのも一つの手ですが、backgroundオプションがなければロックがかかりますし、あってもbackgroundオプションありで作られたインデックスは最適化されるまで時間がかかります

なので、runCommandとcollModを使った方法をおすすめします

db.runCommand({collMod: <collection_name>, index: {keyPattern: <key_pattern>, expireAfterSeconds: <expire_after_seconds>}})
db.runCommand({collMod: 'example_colls', index: {keyPattern: {datetime: 1}, expireAfterSeconds: 86400}})

らくちん

参考URL

Expire Data from Collections by Setting TTL — MongoDB Manual 3.4

Bitcodeバージョンの違いで発生するInvalid bitcode version (Producer: xxxx Reader: yyyy)

タイトルイコールほぼ答えみたいなものですが、Facebook SDKなど外部のフレームワークを使用する場合、SDKをビルドした際のBitcodeバージョンとそれを使用したアプリをビルドした際のBitcodeバージョンが異なればアーカイブ時にタイトルのエラー
が出ます(xxxxとyyyyはBitcodeのバージョン)

Bitcodeについては下記の過去記事で解説しています。よく分からない人はどうぞ


対応策としてはTARGETSの設定からBuild Settings => Build Options => Enable BitcodeをNoにすることでBitcodeを無効にすることで解消できますが、
Bitcodeをオフにすることで何らかの問題が発生する可能性がないとは言い切れませんし、最適化のためにあまりよくありません

基本的に新しい外部SDKはその時の最新のXcodeでビルドされていると思いますので、Xcodeコードのバージョンを上げるのが一番いいと思います
どうしてもXcodeのバージョン上げたくなければ古いSDKを使用するか、
設定でオフにするといいんじゃないでしょうか

XcodeバージョンとBitcodeバージョンの対応表どっかにないかな・・

僕が引っかかったときのBitcodeバージョンは以下でした

Xcode 8.3.3: 802.0.42.0_0
Xcode 8.2.1: 800.0.42.1_0

参考URL

Rails Console(irb)のアウトプット待ちがメンドイときに消す方法

小ネタです。Rails Console(irb)で適当な処理を実行すると返却値が出力されますよね?例えば以下のeachメソッドの返却値はレシーバ自身なので、eachブロックでゴニョゴニョ処理をするとレシーバが出力されます

> %w/hoge huga foo bar/.each{|e| e}
=> ["hoge", "huga", "foo", "bar"]

ちょっとした実行なら便利です


が、たまにステージングエリアのRails Consoleで大量のリソースをeachブロックで処理をする時とかに、これが少し邪魔になることになります

> Resource.where(statements).each{|record| record}
  Resource Load ...
=> [#<Resource id: 1, hoge: "aaaa", huga: "bbbb", foo: "cccc", bar: "dddd">, ...(以下略

返却値のActiveRecord::Relationオブジェクトが小さければいいですが、大きいと出力に時間がかかりますし、過去の情報が流れてしまいます

解決策

irbの出力は最後に実行された文の返却値みたいなので単純に; 0を追加すればかなりさっぱりします

> Resource.where(statements).each{|record| record}; 0
  Resource Load ...
=> 0

あースッキリした

参考URL

RailsのStatsタスクに調査対象ディレクトリを追加してみる

どうもです。rake statsコマンドを使っていますか?これは一言で言えばプロジェクトの大まかな統計情報を取るためのコマンドです。リファクタリングすべきかどうか、テストコードを書くべきかどうかをざっくり判断したいときに使ったりします

$ rake stats
+----------------------+-------+-------+---------+---------+-----+-------+
| Name                 | Lines |   LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+
| Controllers          |   xxx |   xxx |     xxx |     xxx | xxx |   xxx |
| Helpers              |   xxx |   xxx |       0 |     xxx | xxx |   xxx |
| Models               |   xxx |   xxx |     xxx |     xxx | xxx |   xxx |
| Libraries            |   xxx |   xxx |     xxx |     xxx | xxx |   xxx |
| Integration tests    |     0 |     0 |       0 |       0 |   0 |     0 |
| Functional tests     |   xxx |   xxx |     xxx |       0 |   0 |     0 |
| Unit tests           |   xxx |   xxx |     xxx |     xxx |   0 |   xxx |
| Cucumber features    |   xxx |   xxx |       0 |     xxx |   0 |   xxx |
+----------------------+-------+-------+---------+---------+-----+-------+
| Total                |   xxx |   xxx |     xxx |     xxx | xxx |   xxx |
+----------------------+-------+-------+---------+---------+-----+-------+
  Code LOC: xxx       Test LOC: xxx      Code to Test Ratio: xxx:xxx

※xxxは実際の数字をぼかしているだけです


各項目の具体的な意味は上記を参考に・・


ただ、上記で対象となっているディレクトリはデフォルトのものとGemをインストールしたときに追加されたものしか含まれていません
独自に作ったlib/request/以下を対象としたい場合などに困りますね

調査対象を独自に追加する

具体的な実装はrails/code_statistics.rbに、拡張方法はRSpecのコードなどを見れば大体わかります(内容は端折ります) 調査対象は::STATS_DIRECTORIESという2次元配列に

> ::STATS_DIRECTORIES
=> [["Controllers", "xxx/app/controllers"],
略
 ["Cucumber features", "features"]]

みたいな感じに表示名とパスが入っているので単純に対象を追加してやればいいです


例えば、lib/tasks/hoge_huga.rbファイルに以下のような記述をします

namespace :hoge_huga do
  task :statsetup do
    require 'rails/code_statistics'
    ::STATS_DIRECTORIES << %w(Custom\ Requests lib/requests) if File.exist?('lib/requests')
  end
end
task stats: 'hoge_huga:statsetup'


こうすれば既存のstatsタスクが実行される前に必ずhoge_huga:statsetupタスクが実行されるので対象が追加されます


実行結果は以下のとおり

$ rake stats
+----------------------+-------+-------+---------+---------+-----+-------+
| Name                 | Lines |   LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+
| Controllers          |   xxx |   xxx |     xxx |     xxx | xxx |   xxx |
| Helpers              |   xxx |   xxx |       0 |     xxx | xxx |   xxx |
| Models               |   xxx |   xxx |     xxx |     xxx | xxx |   xxx |
| Libraries            |   xxx |   xxx |     xxx |     xxx | xxx |   xxx |
| Integration tests    |     0 |     0 |       0 |       0 |   0 |     0 |
| Functional tests     |   xxx |   xxx |     xxx |       0 |   0 |     0 |
| Unit tests           |   xxx |   xxx |     xxx |     xxx |   0 |   xxx |
| Cucumber features    |   xxx |   xxx |       0 |     xxx |   0 |   xxx |
| Custom Requests      |   xxx |   xxx |     xxx |     xxx | xxx |   xxx |
+----------------------+-------+-------+---------+---------+-----+-------+
| Total                |   xxx |   xxx |     xxx |     xxx | xxx |   xxx |
+----------------------+-------+-------+---------+---------+-----+-------+
  Code LOC: xxx       Test LOC: xxx      Code to Test Ratio: xxx:xxx


ちなみに対象がテストコードである場合、テスト系の項目に数値を反映させるために ::CodeStatistics::TEST_TYPESに表示名を追加する必要があります

namespace :hoge_huga do
  task :statsetup do
    require 'rails/code_statistics'
    ::STATS_DIRECTORIES << %w(Custom\ Requests lib/requests) if File.exist?('lib/requests')
    ::CodeStatistics::TEST_TYPES << "Custom Requests" if File.exist?('lib/requests')
  end
end
task stats: 'hoge_huga:statsetup'

まあ、これを追加する必要のある人は少ないと思いますが・・

参考

Mac上にGoのバージョン管理ツールgvmをインストールしてみた

追記(2017/04/19): MacOS Sierraだとコンパイルに使用するGo 1.4が動作しないため失敗するらしいです

公式のバイナリpkgならうまくいきます

Downloads - The Go Programming Language


仕事でGoを使う機会に巡り会えそうだったので、ちょっとフライング気味に勉強してみることにしました
となるとやはり初めはバージョン管理ツールの導入なのだが、Go界隈の流行りはどうなっているんだろう

調べる感じRubyとかほどバージョン管理ツールは使われていないのかな?情報が少ない印象を受けました

お馴染みの命名規則でgoenvとかgvmとかいったものがあるらしいですがGoogle トレンドを見る感じでは、gvmのほうが使われている印象を受けました(データ量が少ないけど・・)

https://www.google.co.jp/trends/explore?date=today%2012-m&geo=JP&q=goenv,gvm

同名のgoenvでこんなのもありましたが、こっちはプロジェクト作成ツールっぽい?(あとで見る)

という訳で一旦gvmを使うことに決定しました!

導入済みのanyenvは名前の通りgoenvしか使えないので一からインストールします

Macにgvmをインストールする

とりあえず先に依存ツールをすべてインストールします

xcode-select --install
brew update
brew install mercurial

本体のインストールは以下のコマンドです(zshの人は先頭の"bash"を"zsh"にしてください)

bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)

もしzshを使用しているのに間違えてbashで実行してしまったら
"ERROR: Already installed!"
と表示されてインストールできなくなってしまいます
そのときは一旦、"~/.gvm/"ディレクトリを削除してください

ターミナルを再起動するか下記を実行してgvmコマンドを実行できるようにします

source ~/.gvm/scripts/gvm

これにてgvm本体のインストールは完了です

$ gvm version
Go Version Manager v1.0.22 installed at ~/.gvm

Go 1.6をインストールする

参考にした書籍が1.6を推奨していたので1.6.3をインストールしようとしましたが・・

$ gvm install go1.6.3
Downloading Go source...
Installing go1.6.3...
 * Compiling...
ERROR: Failed to compile. Check the logs at ~/.gvm/logs/go-go1.6.3-compile.log
ERROR: Failed to use installed version

エラー通り".gvm/logs/go-go1.6.3-compile.log"を見てみます

##### Building Go bootstrap tool.
cmd/dist
ERROR: Cannot find ~/.gvm/gos/go1.4/bin/go.
Set $GOROOT_BOOTSTRAP to a working Go tree >= Go 1.4.

どうやら1.5からはセルフホスティングされているため、ビルドに1.4以上が必要らしいです

そこで1.4をインストールしようとしましたが・・

$ gvm install go1.4
Installing go1.4...
 * Compiling...
ERROR: Failed to compile. Check the logs at ~/.gvm/logs/go-go1.4-compile.log
ERROR: Failed to use installed version

エラー通り"~/.gvm/logs/go-go1.4-compile.log"を見てみます

# Building C bootstrap tool.
cmd/dist

# Building compilers and Go bootstrap tool for host, darwin/amd64.
lib9
libbio
liblink
cmd/cc
cmd/gc
cmd/6l
cmd/6a
cmd/6c
~/.gvm/gos/go1.4/src/cmd/6c/txt.c:995:28: error: shifting a negative signed value is undefined [-Werror,-Wshift-negative-value]
~/.gvm/gos/go1.4/src/cmd/6c/txt.c:1045:28: error: shifting a negative signed value is undefined [-Werror,-Wshift-negative-value]
go tool dist: FAILED: clang -Wall -Wstrict-prototypes -Wextra -Wunused -Wno-sign-compare -Wno-missing-braces -Wno-parentheses -Wno-unknown-pragmas -Wno-switch -Wno-comment -Wno-missing-field-initializers -Werror -fno-common -ggdb -pipe -Wuninitialized -O2 -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -mmacosx-version-min=10.6 -c -m64 -I ~/.gvm/gos/go1.4/include -I ~/.gvm/gos/go1.4/src/cmd/6c -o ~/.gvm/gos/go1.4/pkg/obj/cmd/6c/txt.o ~/.gvm/gos/go1.4/src/cmd/6c/txt.c

上のIssueを見る感じMacだとインストールできないとかなんとか(ちゃんと読んでいないです)

そこでバイナリからインストールします

gvm install go1.4 -B
gvm use go1.4
export GOROOT_BOOTSTRAP=$GOROOT

改めて1.6.3をインストールします

gvm install go1.6.3
gvm use go1.6.3

ようやく無事にインストールできた模様フー ( ̄‥ ̄) = =3

$ gvm list

gvm gos (installed)

   go1.4
=> go1.6.3
   system

Hello World!

最後にちょろっとHello World!だけ動かしてみました

package main

import (
  "fmt"
)

func main() {
  fmt.Println("Hello World!")
}
$ go run hello.go
Hello World!

コンパイラ言語なのにこの手軽さはいいですね!!

参考URL

一昨日に開催された第2回 botソンに参加して画像レスbotを作成した話

2016年07月09日、株式会社トレタ様で開催されたbotハッカソンこと通称botソンに参加してきました。個人でもくもく開発して最後に発表といった感じの勉強会です

この記事はその勉強会で作った簡単に画像レスをするためのSlack botの紹介みたいなもので、Herokuの使い方などについては触れませんのでご了承ください

背景

最近、Slack Teamを作ったのですが、やっぱり文字やアイコンばかりだと盛り上がりにかけ、誰も発言しなくなる => 過疎のコンボに繋がると思いました

かといって、下記にもあるようにおもしろ画像をセコセコと探しているのを誰かに見られるとものすごい萎えます(ちなみに僕は画像レス用のディレクトリをDropboxに持っていますが、そこから画像を選別しているのを見られるのも嫌です)

Emacsからネタ画像を検索したい - Qiita

オモシロ画像を貼るためにせっせとマウスでURLをコピペする姿も、ウケるために必死な感じが出てしまいますしあまり人に見られたくありません

なので今回は、なるべく少ない手数で、それこそLINEスタンプ感覚で画像レスができるように、 レス画像検索サイトTiqavのAPIGoogle Custom Search APIを使ってメッセージのとあるキーワードに反応して自動的に画像レスを行ってくれるSlack Botを作りたいと思いました

構成

1年ほど前からHerokuは無料プランでは1日18時間しか使用できなくなったし・・普段RubyばっかでNodeほとんど書かないし、Rubotyとかあるし・・とか思ったのですが、やっぱり一番情報が集めやすそうなHerokuとHubotという構成にしました

近年のベストプラクティスみたいなものがあればぜひ教えていただきたいです

無料プランはほっておくとSleepに入ってしまうため、New RelicでPingを送ってあげたり、1日18時間しか使用できないので、Process Schedulerというアドオンを使って意図的に眠らしてあげたりする必要があります

Tiqav API

Tiqavは画像レスを検索するためのサイトでAPIも公開しています。大体の画像が漫画系なのもいいです

Tiqav

Tiqav API

http://dev.tiqav.com/

今回はSearch APIとサムネイル用のImage URLを使います

Search API

GET search

http://api.tiqav.com/search.json?q=[query]&callback=[fucntion_name]

GET search/random

http://api.tiqav.com/search/random.json?&callback=[fucntion_name]

Example response

[
 {"id":"3om","ext":"jpg","height":1442,"width":1036,"source_url":"http://example.com/image1.jpg"}, 
 {"id":"1eb","ext":"jpg","height":171,"width":250,"source_url":"http://example.com/image2.jpg"}
]

Image URL

Search APIのResponseにはTiqav上の画像URLが含まれていません(Source URLはすでにNot Foundになっていることが多い)
そこで、ResponseのIDとEXTを組み立ててTiqav上の画像のURLに変換します

http://img.tiqav.com/[id].[ext]

所感

Tiqavの画像はほとんどが漫画画像だと思われるのでキーワードに対する漫画画像のレスポンス率は非常に高いです(tsで発火するようにしてた)

f:id:akatakun:20160710230654p:plain

ただし、検索が結構あいまいなところがあり、下記のように意思疎通でできていないことも多いです(getsで発火するようにしてた)

f:id:akatakun:20160710230700p:plain

また、Googleと違ってフィルタリングができないため、「ピー」な画像を引くこともたまにあります
誰かに見られたらたまりません

というような問題があったためTiqavに対応してからGoogleにも対応することにしました

Google Custom Search API

無料枠だと1日100回までしかリクエストしか受け取れず、結構さんさんたる言われようですが、検索精度は流石だと思います

なお、Custom Search APIを使用するにはCustom Search API KeyとAPIで使用するCustom Search Engineを指定する必要があるため、API KeyとEngine IDをそれぞれ取得する必要があります

1. Custom Search API

2. Custom Search Engine

Sign in - Google Accounts

それぞれこちらの方が非常に分かりやすく解説してくださっています

また、Custom Search APIのドキュメントはこちらになります

API Reference  |  Custom Search  |  Google Developers

Search API

GET search

https://www.googleapis.com/customsearch/v1

Parameters

とりあえず以下のパラメータはつけておきましょう

Name Description
key ①で取得したCustom Search API Key
cx ②で取得したCustom Search Engine ID
q 検索キーワード
searchType 検索タイプ

qには任意の文字列を、searchTypeには画像を検索するために"image"を指定します

Example response

すみません、レスポンスは少し長いので省略します

ただ、Google Search APIは使用制限があるため、実際に使用する前に①のサイトのこの API を API Explorer で試すというリンクでしっかりテストをするといいと思います

Filtering

こちらはTiqavと違ってただのGoogle画像検索なので「ピー」な画像を引く可能性も高く、漫画画像以外の画像もいっぱいレスポンスに含まれてしまっています
前者に関してはGoogleにはセーフサーチという機能があるのでそれにあやかります

後者に関してもImageMagickで画像の彩度を図ってフィルタリングしようとしたのですが、そもそもGoogle Custom Engineにカラーのフィルタリング機能も備わっていたためこれを使用することで解決しました(Google検索すげー)

Name Description
safe 検索セーフレベル("high", "medium", "off")
imgColorType 画像カラータイプ("color", "gray", "mono")

それぞれ"high"と"gray"を選択しました。"mono"より"gray"がちょうどよかったです

セーフサーチはお好みで外してください

所感

検索精度の高さ、検索結果の多さがTiqavの比ではないため、漫画のセリフを入れると大体帰ってくるのがすごいですし、紹介した以外にもたくさんフィルタリング機能があるように見えます

f:id:akatakun:20160711000847p:plain

ただし、漫画画像に特化した検索エンジンというわけではないので、グレースケールに近い画像が多く含まれそうなキーワードを入れると、漫画画像以外が返ってきます

f:id:akatakun:20160711073407p:plain

なので、うまく使い分けれれば大体のキーワードはなんとかなりそうな気がしました

ソースコード

ソースコードは僕のGit Hubに公開しているので、自由に見ることができますが、一部だけ簡単な解説を入れようと思います

shuffle = (array) ->
  # For clone
  arr = array.slice()
  i = arr.length
  while --i
    j = Math.floor(Math.random() * (i + 1))
    tmp = arr[i]
    arr[i] = arr[j]
    arr[j] = tmp
  arr

# Avoid to be cached same url image by Slack
get_timestamp = () ->
  (new Date()).toISOString().replace(/[^0-9]/g, '')


send_with_tiqav = (msg, path, query = {}) ->
  msg.http(path).query(query).get() (err, res, body) ->
    json = JSON.parse body
    if json.length > 0
      items = shuffle json
      msg.send "http://img.tiqav.com/#{items[0].id}.th.#{items[0].ext}?#{get_timestamp()}"


send_with_google = (msg, path, query = {}) ->
  msg.http(path).query(query).get() (err, res, body) ->
    json = JSON.parse body
    if json.items.length > 0
      items = shuffle json.items.slice(0, 3)
      msg.send "#{items[0].link}?#{get_timestamp()}"


module.exports = (robot) ->
  robot.hear /(.*?) tr/i, (msg) ->
    send_with_tiqav msg, 'http://api.tiqav.com/search/random.json'

  robot.hear /(.*?) ts/i, (msg) ->
    keyword = msg.match[1]
    send_with_tiqav msg, 'http://api.tiqav.com/search.json', {q: keyword}

  robot.hear /(.*?) gs/i, (msg) ->
    keyword = msg.match[1]
    send_with_google msg, 'https://www.googleapis.com/customsearch/v1', {key: process.env.GCS_KEY, cx: process.env.GCSE_ID, q: keyword, searchType: 'image', imgColorType: 'gray', safe: 'high'}

1

毎回同じ検索結果が表示されると面白くないため、検索結果からランダム取得をしてきています。JavaScriptで配列をシャッフルする方法にもいろいろとあるのですが、Math.random()だけだと偏るので、Fisher-Yatesアルゴリズムを使っています
下記サイトでアルゴリズムごとの偏りがグラフで見れるので是非確認してみてください

Will It Shuffle?

f:id:akatakun:20160711074354p:plain

2

Slackは画像URLを自動的に画像に展開してくれるため、Content-Typeを指定する必要もなく楽でいいですが、同じ画像が使用されると

Pssst! I did not unfurl #{url} because it was already shared in this channel quite recently (within the last hour) and I didn't want to clutter things up.

といって画像を展開してくれないため、画像URLにパラメータとしてTimestampを付与しています(これはどっちでもいいかな)

さいごに

結果として、そこそこの頻度で期待している画像レスが返せるようになったので満足しています

あとはブラックリストを作ったり、画像解析をやっていくともっとやれることもありそうなのですが、その辺りはおいおいやってみて、もしかしたら記事にするかもしれません

参考URL

qiita.com

qiita.com

qiita.com