新しいアプリを作りました。

リリースしました!

Ruriというアプリをリリースしました。
http://www.martianstalk.com/
(サイトがまだcoming soon状態なので書き換えねば…)

日本ではまだ未発売なのですが、とりあえず1st verはできたので、振り返ってどういう事を気をつけたかを書きます。
ただ、さすがに全部は書ききれないので、今回は前職を経験していたからこそ判断できたことを中心に書いていこうかなと思います。

前職ではITベンチャーで、サーバー側のプログラムなどを書いてました。

個人で開発する

前職

前職では、20人ぐらいの人達と働いてました。チームプレーは、苦手ではないと思います。前職の後半は、コードを書くよりも人と話すことのほうが多かったです。

今回

自分は英語が厳しいので、利用規約などの部分は手伝ってもらいました。それ以外、つまりプロダクト自体は、今回全て1人でやってます。

主張したいこととしては、「1人は良い」ということではなく、「企画と開発とデザインを通してできることは良い」ということです。
この体制の良いところの1つは、判断の精度が高くなることだと思います。例えば、開発が難しいこのデザインは、果たしてペイするだけの価値を持つか? といった問いに容易に結論が出せます。一般的には「開発の難易度」と、「デザイン品質」と、「ペイするかの判断」は全く別の人が考えますが、これらをシームレスに繋げることができます。また、職をまたいだ柔軟性が簡単に発揮できます。例えば、あるロールに偏った難しさがある場合は、別のロールで解決するといった方法を容易に取れます。開発とデザイン、企画が分断されていないので、デコボコした難易度や工数をおしなべることが可能です。

印象として、馬力が足りていないようには思いません。基本的には「簡単だけど効果のあることをやる」という戦略でやっています。そこを上手くに選ぶことが強みであり、逆に上手く選べないと一瞬で馬力が足りなくなると思います。

多言語化をしない

前職

前職では、多言語化を2回ほどやりました。多言語化は大変です。

まず、初回の翻訳にアホみたいにコストがかかります。単に訳せばOKといっても、例えば、「アイテムが3こ」の”こ"の部分とかも変換が必要なので、日本語表だけを見ながら翻訳するのは至難の技です。”こ"だけ書かれていても、どう訳せば良いか分かりません。そこで、使われてる画面を確認したいのですが、どこに使われてるのか翻訳者は分かりません。よって開発者に確認が入りますが、開発側も正直忘れているので、grepしないと分かりません。そんなこんなで、訳者も開発者もみんな大変です。

また、維持も大変です。流行りのLean開発では、細かく文言を変えるのは当たり前です。しかし、そのスピード感に、翻訳作業はついていけません。開発者の母国語以外を変更するには、少なくとも訳者のチェックが必要です。ひとまず日本版だけ、とやった箇所が増えていき、プログラムとして許容できなくなったり、抜け漏れが多くなったりします。多言語をちゃんとメンテナンスするには、かなりのコストが必要です。

今回

多言語化しないという割り切りもありますが、今回は多言語化する選択をしました。今回は、たまたま見つけたお客さんが偶然買ってくれることに期待しているので、市場が小さいとヒット数が少なすぎて食べていけないという事情があります。そのために、最低限英語は押さえておく必要がありました。日本語版を開発するか迷いましたが、マーケティングなどの要素が絡んだ場合、母国語以外では効果的に施策が打ちづらいので、手札として日本語も準備しています。

今回は多言語化をするかわりに、文字をなるべく埋め込まないということに心血を注いでいます。ほとんどをアイコンで表現しています。1st verには12個ほど文言を入れていて、あとはアイコンです。アイコンは最高です。さらに言えば、vector形式が最高です。どの解像度でも1種類で済みます。

当然、説明の力が足りていないアイコンもあります。どうしているかというと、どうもしてません。より良いアイコンが浮かぶまで、そのままです。文字で表現しないと成り立たないものは別ですが、多少わかりづらいぐらいであれば、アイコンにしています。

ユーザが多少わかりやすく使える"かもしれない"程度では、多言語化するコストを支払えません。このコストには、単純な工数というだけでなく、管理の苦痛、アテンションを張る辛さなども含んでいます。

個人情報 and クラウドを避ける

前職

前職では、個人情報をバリバリ使うアプリを作っていました。そして、個人情報をクラウドのサーバで管理していました。

怖いことはたくさんとあります。攻撃は怖いし、セキュリティホールも怖いし、バグで他人のデータが見えるのも怖い。発行先のメールアドレスを間違うのも怖い。何もかもが怖いです。流出すると全ての苦労が弾けてしまうので、慎重にならざるをえません。そうしたことに精神を使うのは、怖がりの自分には合っていませんでした。

今回

全てをローカルに保存しています。まぁクラッシュログとかそういう話は置いといて、情報を自社サーバに置く気は今後も全くありません。
ローカルのデメリットとして、バグでデータを損失させた場合に取り返しがつかないということ、複数端末が当然の時代でユーザの利用価値が下がるということ、などがあります。これらを流出の恐怖と天秤にかけました。その結果、今回はサーバに何も置かないという判断になりました。

まだまだ頑張ります

まだ食っていけるほど稼げてないので、まずは食えるように頑張ります。前職と1番違うのは、モノが完成するまで給料が0、ということですね…。

UnityでPull Requestベースで開発するとコンフリクト酷いよね問題

Unity(ここでは暗黙にUnity4のことを言う)でゲーム作ってます! GitとGitHubを使って開発できるようにはなった! ブランチ作って、Pull Request使って開発してます!

という場合でぶち当たる壁としてあるのは、壮絶なるコンフリクトとの戦い。

そのことについて、個人的にこうしてるよーというのを置いておく。 結論としては、「Unity4ではシーン上には何も置かないほうがいいと思う」であり、以下はなぜそう思うのかを、くどくど書いてある。

酔った勢いで公開しているので、あまりに真に受けられても困るというエクスキューズはある。

そもそも論

チームメイクで重要な事柄はいくつかあるが、製品のクオリティの担保や、組織の学習を維持すること、知識の属人化を防ぐことなどは特に重要な事項だ。 このために、中長期のプロジェクトは相互にレビューをするのが一般的であると思う。

こうした営みは、GitHubではPull Requestを使って行われる。

コンフリクト

通常、出したPull Requestが一瞬でマージされることはない。masterとPull Requestで出すブランチは次第に離れていく。

だから、編集頻度の高いファイルが存在すると、コンフリクト地獄へ突入する。Pull Requestは(原則小さくあるべきではあるが)ある一定のサイズで投げられる宿命にあるため、コンフリクトが発生する可能性が高いという面もある。

対策として、普通のプログラミングでは責務できっちり分割し、設計を上手く行うことなどが挙げられるだろう。が、Unityの場合はどんなにしっかり設計しようとも、シーンファイルがコンフリクトしてしまう。 シーンに配置したものは、どんなにprefabで分割しようと、シーンファイルに変更を及ぼしてしまう(ただしUnity 5で改善されるとか)。

コンフリクトを受け入れる

コンフリクトがある。どうするか。 当然、コンフリクトを受け入れ、シーンのマージを手動で行うという選択肢がある。

…あるにはあるが、実際にやったことのある方なら分かると思うが、かなりキツイ。修正が2, 3なら良いが、100近くなった日には涙がこぼれ落ちてしまう(100個の修正は普通にある)。 Pull Request単位なので時間の経過でmasterと全然違うことになっていたり、追加したファイルが多かった日には…。

masterを優先させるにしても、自分が入れていた値は全て飛んでしまうので、Pull Request1個分はさすがにきつい。 動作チェックをしてレビューも通っている状態になったのに、コンフリクト解消時に入れた値が間違っていたり、バグを埋め込んでしまうこともありえる。

特にコンフリクト解消時にエンバグしてしまうケースは、発見されないままになってしまう可能性が高く、危険である。 実際、コンフリクトを直した後に、直し方をミスったままリリースしてしまった、という例を見たことがある。

とにかく、コンフリクトを受け入れるという選択肢はない。可能な限り、コンフリクトを避けたい。

コンフリクトを避ける

有名なところだと、シーンを分割する方法があるようだが、この方法は実際のプロジェクト進行にマッチしない可能性がある。

実際のissue(BTSではチケット)はどういう単位で問題が切られるかと言うと、

  • キャラAを弱体化したい(HPを下げる)
  • 滑らかに移動したい(マップの凹凸を滑らかにする)
  • 武器Bの溜め状態を分かりやすくしたい(エフェクトを出す)

というような、価値や機能ベースでissueが切られる。 このような場合、位置でシーン分割をしても横断する可能性が高く、影響が複数シーンに渡ってしまう(と思う)。

よって、我々もユニティちゃんライブと同じように、シーン上には基本何も置いていない。

ただ、作っているのがそもそも敵をチマチマ配置するようなゲームではないので、シーン上に見えている必要性があまりないというのもある。

何も置かないケースの発展型として、メンバがそれぞれ作業用シーンを持つというパターンが考えられるが、こうした個々のメンバにメンテが任されるツールというのは、決まってメンテ不足なメンバが出てバグの温床になるので、採用しない方が良いと思う。

prefab単位のコンフリクト

prefab単位でコンフリクトするのはしょうがない。適切な大きさでprefabを分けているのであれば、十分解決できる範囲だというのが実感だ。

週に5〜15程度のPull Requestがmasterにマージされているが、そもそもコンフリクトはほぼ発生していないし、発生しても2, 3箇所直して終了することが多い。

KindleStoreの新刊を出すサイト使った

サイトできたよー

漫画やラノベの新刊を、KindleStoreから買うためだけのサービスを作った。

KindleStoreって、新刊コーナーが最近電子書籍化された古い漫画で埋め尽くされたりいるなど、諸々辛い点がある。現実の本屋の新刊コーナーのように、新しい書籍かつ一般的に売れてる書籍だけ並ぶようなものが欲しかった。

ただ、Amazonさんのアソシエイトの審査が通らないから、URLはここに書かないしこのままお蔵入りかもしれんね。それについては後述。

使用環境

使ってる環境は以下。

言語とフレームワーク以外は別のにしたかったのだけど(MariaDBとかにしたかった…)、時間がもったいなかったので、経験があるものにしておいた。

使用gem

主な使用gemは以下の通り。

# Product Advertising APIを使うため
gem 'amazon-ecs'
# 半角 <=> 全角 の変換
gem 'moji'
# cronに作ったバッチ(rakeのコマンド)を設定するため
gem 'whenever', :require => false

あとは自力でゴリゴリと。MySQLのgemとかは略してます。

実装

ざっくり言うと、以下のような実装をした。

  1. AmazonからAPIを通して電子書籍のデータを取ってくる
  2. データが怪しいので、紙の本(オリジナル)を再検索してデータを照合・訂正する
  3. 諸条件でフィルタリング(古すぎる本などは表示しないため)
  4. 表示

サーバのセットアップとか合わせて、全部で50時間ぐらいかかったと思う。2番と3番がわりとキツくて、半分以上の時間を取ったのではないかと。かなりの表記ゆれ等があって、それの対処が辛かった。

入ってるデータがテキトーすぎる件

例えば、APIから取れる本のデータには、出版日とリリース日というものがある。多くのデータは、出版日を紙の本が出た日、リリース日をStoreで販売開始した日としている。

…のだが、何割かのデータは出版日が間違っている。1998年に出版された本の電子書籍版が、出版日とリリース日ともに2014年になっていたりする。今回のサービスでは、これを頑張って訂正するプログラムを書く必要があるのだ…。

他にも色々とあった。その一部をご紹介しよう!

  • リリース日より出版日が未来になっている(本来は出版日が必ず古くなる)
  • 出版日に日数がない(2014-06-24となるはずが、2014-06までしかない)
  • 電子書籍には巻数があるのに、紙の本には巻数がないラノベ(サブタイトルのみある)
  • 電子書籍版と紙の本で、著者の姓と名の間の空白があったりなかったり、全角だったり半角だったりする
  • 電子書籍版と紙の本で、レーベル名があったりなかったりする

などなど…。

ここに書いていない事含め、いくつかは辛すぎて放置していることを、ここで懺悔する。

今後

AmazonAPIを使うには、アソシエイトのIDが必要であって、ちゃんと運営するのであればアソシエイトの審査をパスすることは必要条件になる。

ただ、審査に全然通らないので…。10回審査に落ちたら諦めようと思う。今は3回落ちたので、あと7回はチャレンジしてみるかな。

次はiPad向けのアプリを作ろうと思っているので、そっちをやりつつ、様子を見て(色々変えつつ)もう一回審査に出してみようと思う。

ItemSearchする時に、powerで条件を加える

ニートせいかつ

ニートになって収入0に今更怯えていたり、Webエンジニアとして3年やってきたのにHTML/CSSを書く機会が3回ぐらいしかなくて全く書けないので1から勉強したり、ビュー周りをゴリゴリ書いていた。

そのあたりの話題は書いてもしょうがないので更新が途絶えていたが、アプリの裏側にやっと手がまわせるようになったので、ネタができた。ので書く。

特定の月のデータに絞る

これまでの成果により、書籍情報は取れるようになった。次は、詳細に条件を絞っていくことにしよう。

ある月のデータを取得する方法について考える。

ItemSearchでは、検索する時にpowerというパラメータを付与できる。powerでは色々できるのだが、今回はその中で出版日の指定を行う。

options = {
    country: 'jp',
    sort: 'daterank',
    browse_node: '2410280051',
    response_group: 'Medium',
    power: 'pubdate:during 05-2014' # 2014年5月に出版されたものを検索
}
res = Amazon::Ecs.item_search("", options)

pubdateが出版日の指定で、値をduring 05-2014とすることで、2014年5月に出版されたものを取得している。

特定の出版社も合わせて絞る

出版社でも絞りたいが、これまたpowerでpublisherを指定すれば取れる。powerを2つ以上指定する時は、andでつなげておけば良いらしい。

options = {
    country: 'jp',
    sort: 'daterank',
    browse_node: '2410280051',
    response_group: 'Medium',
    power: 'publisher:KADOKAWA and pubdate:during 03-2014' # 2014年5月に出版されたKADOKAWAが出版しているものを検索
}
res = Amazon::Ecs.item_search("", options)

試した感じだと、publisherでは指定した文字列が含まれていれば良いようだ。例えば、正式には「KADOKAWA / 富士見書房」のようなpublisherの場合、「富士見書房」で検索すれば、ちゃんと取れる。

ページと得られる結果の数

1回のリクエストで得られる商品の数は、10個と決まっている。もちろんページがあって上限10ページまでで、それ以上はエラーが返ってくるようになっている。つまり、1つの同じパラメータで得られるデータは100個が限界。

ページの指定は下記のようにする。上のコードにさらに付け足してみた。

options = {
    country: 'jp',
    sort: 'daterank',
    browse_node: '2410280051',
    response_group: 'Medium',
    power: 'publisher:KADOKAWA and pubdate:during 03-2014',
    item_page: 2 # 2ページ目を検索
}
res = Amazon::Ecs.item_search("", options)

ページの始まりは1から。10ページ以上のデータは、MoreSearchResultsURLという項目を活用して、Amazonへの誘導URLを貼ることができる。

ユーザが直接APIを叩くのでなければ、アイテムがなるべく100以下になるように調節することも可能だろう。browse_nodeのIDやpowerのpubdate、publisherなどの指定で細分化させて、それぞれ取ってくる形にすれば良いと思う(反則かはよくわからない)。browse_nodeなんかはnodeの値がAPIを叩けばすぐ分かるから、おそらく自動的に掘り下げるプログラムも書けないわけじゃないと思う。

Amazonから詳しい書籍情報を取得し、DBに入れる

書籍の詳細情報をDBに入れる

これまでの成果により、ラノベの新着が取れるようになったので、次は取ってきた情報をDBに突っ込むようにする。

スキーマ

テキトーなので深く考えていないが、以下ぐらいは欲しい。

  • 著者
  • タイトル
  • 作品紹介
  • 値段
  • 出版日
  • 画像URL
  • 商品詳細のURL

注意:キャッシュ制限

Amazon利用規約によると、キャッシュは24時間までしか認められていない。値段や商品リストが変動するからとのこと。

ResponseGroup

上で挙げた作品紹介や画像URLなどは、デフォルトではAPIのレスポンスに含まれていないため、含んでもらえるように指定する必要がある。この指定をResponseGroupと言う。

ResponseGroupに指定できる値は複数存在し、例えば、画像URLが欲しい、値段が欲しい、などと細かく指定できる。グループは組み合わせで指定することも可能。

デフォルトではSmallというグループになっていて、これはいくつかのグループの組み合わせになっている。今回はその1つ上のMediumを使う。

http://docs.aws.amazon.com/AWSECommerceService/latest/DG/CHAP_ResponseGroupsList.html

注意:KindleStoreの商品の値段は取得できない

なんというか、驚くべきゴミさだと思うのだが、2014/06/02時点では、KindleStoreの商品はからは値段を取得できない。取得できないのである。驚くべきことに、取得できないのだ。

開発者から言わせてもらえば、正直「お前は何を言っているんだ」レベルだが、かなり長期に渡って放置されているので、戦略的に放置されているのではないかと思う。ないものねだりをしてもしょうがないので、値段はあきらることにした。

ちなみに、抜け道を探すために2時間ぐらい本件を調べていた。見た限りは抜け道はなく、徒労感が半端ではない。

出来上がったもの

スキーマができた。ポイントとしては、

  1. 著者は1つの本に対して複数存在する(共著であったり、イラストレーターなどが存在するので)。
  2. 画像URLはサイズで複数存在する。

ぐらいか。これらは1つの本に対して多関係にしておいた。もちろん、実際のモデルやDBスキーマはアプリケーションに合わせてもう少し複雑になっているのだが、ここで個別のケースについて話してもしょうがないので省略する。

プログラムは以下のようにresponse_groupを加えた形になる。

options = {
    country: 'jp',
    sort: 'daterank',
    browse_node: '2410280051',
    response_group: 'Medium',
}
res = Amazon::Ecs.item_search("", options)

ニートはお腹が減った。アニメも見たい。そろそろお昼休憩すべきである。

Amazonから電子書籍の新着ラノベを取得する

下準備

アソシエイトプログラムだの、Product Advertising APIの登録だの、めんどくさい事が待ち受けている。

ここでは省略するが(俺がもうやることはないだろうから)、かわりに、よくまとまっているリンクを置いておく。

  1. http://www.ajaxtower.jp/ecs/pre/
  2. http://blog.makotokw.com/2014/05/21/get-aws-secret-access-key-for-amazon-product-advertising-api/

言うことがあるとしたら、ほとほとうんざりだ、ということぐらいか。

Product Advertising APIRubyから叩く

直接叩きたくはないので、先人のありがたいGemを使わせてもらう。 https://github.com/jugend/amazon-ecs

シンプルでなかなかよい。

config/initializers/amazon-ecs.rbに設定を置く。

Amazon::Ecs.configure do |options|
  options[:associate_tag] = 'your associate tag'
  options[:AWS_access_key_id] = 'your access key'
  options[:AWS_secret_key] = 'your secret key'
end

ひとまず商品検索してみた

何はともあれ、検索できなければお話しにならない。

適当にgeneratorでtaskを作って、中身を書く。

namespace :hello do
  desc "Sample"

  task :my_puts => :environment do
    options = { country: 'jp' }
    res = Amazon::Ecs.item_search("惑星のさみだれ", options)
    puts res.doc.to_s
  end
end

データは取れた。ちゃんとリスト返ってきてるっぽい。

KindleStoreから新着順で取得する

新着順で取得する

新着にするには、オプションにSortをdaterankで指定すれば良い。厳密には、商品のカテゴリ(と国?)によって指定できるsortが異なるが、本(BooksとKindleStore)ならdaterankはあるので、細かいことは考えずに済む。

http://docs.aws.amazon.com/AWSECommerceService/latest/DG/JPSortValuesArticle.html

KindleStoreから取る方法

KindleStoreのみから情報を取るには、SearchIndexかBrowseNodeを指定して、特定のカテゴリで絞る必要がある。

SearchIndexではKindleStoreでざっくり絞ることができるが、どうせこの後にラノベで細かく絞るのであまり意味がない。細かく絞るのはBrowseNodeで指定できるので、今回のケースだとこちらだけ使っておけば良い(と思う)。

別の観点で言うと、先ほどは検索ワード(惑星のさみだれ)を入れたものの、最終的には検索ワードなしで新着順で取りたい。この場合はSearchIndex単体だと取れないので、BrowseNodeを指定する。何故かは記述が面倒なので省略。

BrowseNodeのID

実は、KindleStoreのラノベのNodeIDは何か、ということをプログラム等で割り出す必要がある。

公式のNodeID一覧ページがどれもこれも腐っており、役に立つ資料が無い(いくつかページがあるが、JP用のIDリストでKindleStoreのラノベまで載っているものは2014/06/02時点では存在しないと思う)。

割り出し方はここでは省略するが、BrowseNodeLookupを使う。もしくは、Amazonのサイトに行って注意深くURLを見れば、そのページのタグIDが分かるかもしれない。

http://docs.aws.amazon.com/AWSECommerceService/latest/DG/BrowseNodeIDs.html

出来上がったもの

options = {
  country: 'jp',
  sort: 'daterank',
  browse_node: '2410280051'
}
res = Amazon::Ecs.item_search("", options)
puts res.doc.to_s

俺が見てるAmazonの新着と同じだ。よしよし。

あとは

ページネーションして数ページ入手したいところだが、ニートはもう寝る時間である。寝る。

Railsの初期プロジェクトを作ってgithubに入れる

プロジェクトを作る

既にgithub側は準備ができているので、次はrailsのプロジェクトを作る。

今回はRubyMineから作ってみた。ボタンぽちぽちなので略。見慣れたディレクトリがいっぱい自動生成された。

なお、その見慣れたディレクトリの中には.keepというファイルがあるが、これはディレクトリをkeepするファイルのようだ。gitはディレクトリを管理下に置けないので、こういう工夫が必要になる。

作ったプロジェクトをgithubの管理下に置く

たぶん、RubyMineから、現在のプロジェクトをgithubリポジトリに設定する方法があるんだと思うが、10秒探して分からんかったので、コンソールから叩いた。

$ git init
$ git commit -a
$ git remote add origin git@github.com:kazekyo/hoge.git
$ git push -u origin master

諸々のディレクトリが、githubに入った。

とはいえRubyMineから弄れないのは意味が無い。

RubyMineからVCS -> Enable Version Control Integration(だったか?)を選ぶと、gitが選べるので、それを押す。これだけで連携できた。

最初からこれをしておけば良かったのか…?