Persistence

技術メモなど

最近思いついたアイディア

貯めこんでるだけでは何も起こらないので、twitterに投稿してみました。

photo by Martin Whitmore

ちょっと前に読んだ本で、「アイディアを公表するとパクられると思うかもしれませんが、世の中のディレクターにあなたのアイディアをパクらせてみてください。すっごい難しいですよ!」って書いてあったんだけど何で読んだかを忘れでしまった。

自作サイト(ポートフォリオ)

今まで作ってきたWebサービスポートフォリオです。

※ほとんどが作っただけで終わっているので、現在は動作しないものもあります。

Doll Face Detector

f:id:bisque3311:20150617220045j:plain

項目 内容
概要 ドールの類似顔検索サービスです
詳細 DollFaceDetectorというサービスを作りました。
サイトURL http://doll-face-detector.herokuapp.com/
ソースコード 非公開
作成時期 2015/05
スキルセット Node.js(Express), AngularJS, Heroku, AWS(S3, dynamodb, lambda)

npm

oxford-face-api

MicrosoftAzureのFaceAPIsのラッパーモジュールです。

RubyGems

media_arts_db | RubyGems.org | your community gem host

メディア芸術データベースのラッパーモジュールです。

すごいディーラーリスト for ドルパ31

f:id:bisque3311:20140515232735p:plain

項目 内容
概要 ドールズパーティ31のディーラーリストを俺が考える最強の~にしました
詳細 すごいディーラーリストを作った
サイトURL http://dp31.herokuapp.com/
ソースコード https://github.com/bisque33/DollsPartyDealerViewer
作成時期 2014/04
スキルセット Ruby on Rails, MongoDB, Heroku

JSON.parse(NAVERまとめ);

f:id:bisque3311:20140403020609p:plain

項目 内容
概要 NAVERまとめから位置情報を含むスポット情報をJSON形式で取得します
詳細 NAVERまとめから位置情報を含むスポット情報をJSON形式で取得する
サイトURL http://dp31.herokuapp.com/
ソースコード https://github.com/bisque33/DollsPartyDealerViewer
作成時期 2014/03
スキルセット Node.js, Ajax, javaScript, Heroku

Beatube-IIDX

f:id:bisque3311:20140403020203p:plain

項目 内容
概要 beatmaniaIIDXのゲーム画面を模した動画検索・プレイヤー
サイトURL http://beatube-iidx.herokuapp.com/
ソースコード https://github.com/bisque33/beatube_for_iidx
作成時期 2014/01
スキルセット Node.js, MongoDB, WebSocket, jQuery, Ajax, javaScript, Heroku

Node.js + Socket.io の技術学習として作った。他にも、MongoDBを使ったり、jQueryをゴリゴリ書いたのもこれが初めてで、いい勉強になった。現在リファクタリング中...で止まっている。

RTA Studio

レンタルサーバを解約してしまったためソースコードのみ。

項目 内容
概要 テキスト専用のGitHubのようなものです
ソースコード https://github.com/bisque33/rta-studio
作成時期 2013/07
スキルセット Ruby, Ruby on Rails, Nginx, PostgreSQL, Git, Linux

RTA(リアルタイムアタック)は、ゲームをリアルタイムに最速クリアをする遊び方で、主にニコニコ動画などで人気のカテゴリの1つです。このRTAの面白さにハマり、チャートと呼ばれる攻略方法をサイト上で情報共有して育てていこうというコンセプトで作成しました。しかし、使い方ががわかりにくく、テストユーザにすら使ってもらえなかったという苦い経験で終わりました。

工夫した点としては、チャートはテキストファイルなので裏でGitを使って履歴や差分を見たり、チャート自体をクローンできるようにしたこと。GitHubのクローンのようなもので、Gitを知らない層の人にGItの便利な部分を使えるようにしたいと考えました。

Dealer Market Guide 〜みんなで作るドールディーラー情報サイト〜

f:id:bisque3311:20140403015620p:plain

項目 内容
概要 ドール服のディーラー情報を管理するサイト
サイトURL http://dm-guide.herokuapp.com/
ソースコード https://github.com/bisque33/dealer-market-guide
作成時期 2013/01
スキルセット Ruby, Ruby on Rails, PostgreSQL, amazon S3, Heroku

趣味のドールの情報サイト。イベントに参加するディーラーを素早く検索・確認できるようにしたいとの思いから作りました。内容としては画像投稿サイト。各イベント、各ディーラから画像などの情報を検索できます。

現在ドール熱が少々冷めてしまい、サイトはベータレベルで止まっているが、ユーザビリティを改善してちゃんとしたものにしたい。

RubyGems Guides for Japanese

f:id:bisque3311:20140403021025p:plain

項目 内容
概要 RubyGems-Guidesの和訳サイト
サイトURL http://rubygems-guides-jp.herokuapp.com/
ソースコード https://github.com/bisque33/RubyGems-Guides-for-Japanese
作成時期 2012
スキルセット Ruby, Sinatra

OSSへの貢献と、英語の勉強、Rubyの勉強を兼ねて、RubyGems Guidesを和訳したもの。翻訳率は50%程度。(とりあえず読みたい部分だけ訳して満足してしまった。。)

ソースコードGithubで公開していたら、見ず知らずの人が翻訳を手伝ってくれたりしたのは嬉しい経験でした。

AKB Search

f:id:bisque3311:20140403023108p:plain

項目 内容
概要 AKBを題材にしたアフィリエイトサイト
サイトURL http://akb-search.herokuapp.com/
ソースコード 非公開
作成時期 2012/07
スキルセット Ruby, Sinatra, PostgreSQL, Heroku, 各種WebAPI

AKB48のファンと言うわけではなく、完全にアフィリエイト目的のサイトを作ってみようという思いで作ったサイト。AmazonYoutubeなど各種APIをいろいろと使いました。が、愛が足りなかったようです。

and more...

これ以前はLAMP環境でいろいろなサイトを作っていましたが、レンタルサーバを解約したのでもう残っていません。

音ゲーの始め方

前回の記事で「音ゲーの楽しみ方」について書きましたので、今回は実際にどうやって始めたらいいかについて書きます。

好きなゲームを選ぶ

前回の記事でも書きましたが、今や音ゲーはゲームセンターにあるだけでも十数種類、さらに家庭用ゲーム機(PS,WIIなど)、PC、最近ではスマホアプリでもあります。

さてこの中からどう選ぶかですが、おすすめする方法は、まずゲームセンターに行ってみることです。なぜなら、家庭用ゲーム機やPC、スマホアプリなどの多くは、ゲームセンターにあるゲームをコンシューマ向けにしたものだからです。それと、音ゲー体感ゲームですから、どのゲームが見ていて一番「楽しそう」「かっこいい」と思えるかも重要なポイントだと思います。

一番初めに行くゲームセンターを選ぶポイントとしては、なるべく大型店(ラウンドワンなど)に行くと、ゲームのラインナップが多いので良いと思います。

何度かやってみる

面白そうと思うゲームがあったら、1回やってみましょう。人前でプレイするのは恥ずかしいと思う人もいるみたいですが、誰も下手なプレイなど見ていないから大丈夫です。どうしても恥ずかしいと思うのなら、友だちを連れて一緒にプレイしたらいいともいます。

1回目はなんだかよく分からないまま終わってしまうかもしれませんが、4,5回やるとだんだん要領がわかってくるのとともに、自分ができるラインとできないラインの壁が見えてくると思います。この壁を「超えてやりたい」と思えるならきっと上手くなります。そうまで思わなくても、「楽しかったからまたやりたい」と思えれば十分です。逆に「自分には向いていない」と思ったらそこまででしょう。

師匠を見つける

音ゲーを楽しみながら上手くなるには、良い師匠を早く見つけることが一番だと思います。もし知り合いに音ゲーマーがいれば、その人を師にしてもいいですし、その人のコミュニティーに参加させてもらうのもいいと思います。

また、知り合いが一人もいない場合は、なるべくゲームセンターにいる中で一番上手そうな人(もちろん人柄も大事ですけど)に声をかけてみましょう。上手い人というのは、往々にして誰かに教えた経験があり教えるのが上手いはずです。また、上手い人は近いレベルの人たちのコミュニティを持っていることが多く、そこに入れてもらうことで「上手いことが当たり前」の環境になるので、上達も早いと思います。

家で練習

やりたいゲームがあって、教えてくれる人も見つかりました。でもゲームセンターでプレイしていると当然お金がかかります。特に最初のうちは失敗してすぐゲームオーバになってしまったりするので余計にお金がかかります。なので、できればコンシューマ向けゲームを買ってお金を気にすることなくガッツリ練習するのがいいと思います。

残念なことに、ゲームによってはメーカーがコンシューマ向けゲームを発売することにあまり積極的ではなく、古いバージョンしかなかったり、価格が高騰していたりすることもありますので、そのゲームに詳しい人に聞いてみてください。

ちなみに、「Beatmania IIDX」ですと一番新しいPS2のソフトとコントローラを中古で買うとだいたい2万円くらいでしょうか。Amazonはめちゃくちゃ高いのでヤフオクとかで探すといいです。(それでも定価よりかなりお高め)。またPC環境があるなら無料で遊べるBMSというソフトもあります。(最初はちょっと敷居が高いかと思いますが。)

ビートマニア2 DX専用コントローラ

ビートマニア2 DX専用コントローラ

まとめ

月並みな言葉ですが、百聞は一見に如かずということで、まずはやってみましょう!

音ゲーの楽しみ方

「楽しさを伝える」というお題で記事を書いてみました。

はじめに

音ゲーとは

基本的には、音楽に合わせて出てくるオブジェクトをタイミングよく処理するのが音ゲーです。 具体的な例としては、ビートマニアダンスダンスレボリューション太鼓の達人、などがあり、現在ゲームセンターで稼働している主なタイトルでも十数種類はあります。

書きたいこと

音ゲーを全く知らない人を対象に、音ゲーに共通して言える楽しみ方と、私は音ゲーの中でも「Beatmania IIDX」というタイトルをメインでプレイするので、それについての楽しみ方を書きます。

楽しみ方

音ゲー全般における楽しみ方

冒頭で述べたとおり、音ゲーには様々なタイトルがありますが、その中でも共通した楽しみ方について書きます。

音楽そのものを楽しむ

一言で言うと「音ゲーにはゲーム音楽の域を超えたありとあらゆる音楽」があります。

【ジャンル】

音ゲーには多種多様なジャンルの曲が数千曲以上あります。メージャどころではトランス、テクノ、ユーロビート、ハウス、J-POP、クラシック、など。マイナージャンルになってくると本当に今までであったことのないような曲に出会えたりします。また、音楽を作るアーティストが勝手にジャンルを創作してしまうこともよくあります。ですから、何かしら自分の好きな音楽に出会うことができます。

【アーティスト】

ジャンルと同様に、音楽を提供しているアーティストも数百人以上いますから、お気に入りのアーティストに出会うことができます。

クリアする達成感を楽しむ

音ゲーはゲームなので、ゲーム性(つまり、経験値を得て成長できる仕組み)があります。最初はみんな下手なので誰でもクリアできる簡単なものから、超上級者をも飽きさせない超難しいものまであります。初心者が超上級者のプレイしているところを見ると、エベレストの麓から頂上を見ているような感じになりますが、曲がたくさんあるように曲のレベルもたくさんあるので、目の前の目標に向かって一歩ずつ着実に登っていくことで達成感を得られます。

ただし、人によりますが大抵の場合、成長速度は本当にゆっくりで、超上級者の域に達するには何年もかかります。それに、一度練習を止めてしまうと、成長が停滞したり腕が落ちたりします。音ゲーがよく「部活」に例えられるのはこのようなことがあるからです。

コミュニケーション

これは音ゲーに限った話してはありませんが、共通したタイトルのプレイヤー同士で話し合ったり、プレイヤーを目標(ライバル)として切磋琢磨できることです。特に、上達することを目標にする人にとっては、音ゲーは「部活」のようなものなので、仲間は大切な存在であり、それにより良いコミュニケーションが生まれます。

Beatmania IIDX」における楽しみ方

photo by Dan Dickinson

演奏感

音ゲーは何かを体感させる型のタイトルが多く、「Beatmania IIDX」ではDJシミュレーションゲームと呼ばれているようにDJの操作パネルを模擬したインターフェイスになっています。これによって、実際に楽譜が読めるわけでもなくDJができるわけでもないのに、自分が演奏している気分を味わうことができるのが楽しいところです。また、集中すると本当に無心になって演奏できるあの感覚(スポーツなどで言う、「ゾーン状態」に近いかな?)は、体験した人でないとわからない快感があります。

段位認定

曲をクリアするのとは別に、実力を図るための段位認定というものがあります。課題曲を連続して全てクリアすると、その段位の実力が認定されます。この一段一段がひとつの目標となり、達成感となります。一つの段位を登るのに、人によりますが数ヶ月から数年かかることもあります。

7つの鍵盤と、1つの皿(スクラッチ)

人の指は10本なので、7+1の操作対象に対して1本ずつ指を割り当てると2本余る計算になりますが、位置の関係でそのように単純にはならないところが面白いところです。

例えば、曲のレベルが上がるに連れ、だんだんと対処できない譜面が登場します。そこでどうするかというと、運指(うんし)と呼ばれる「手の使い方の型」のようなものを練習します。この運指にはいくつかの型があり、どれがベストかという議論はずっとされてきましたがどれも一長一短なので、自分にあったものを選びます。そしてひたすら運指を極めていきます。

見えないものが見えるようになる

運指と並んでもう一つ成長するものが、目、つまり動体視力です。音ゲーをやったことがない人によく勘違いされることが、譜面を「暗記している」と思われることですが、暗記なんてそれこそ絶対に無理です。でも、見たことがある人なら分かると思いますが、あんなに早く落ちてくるオブジェクトを見るのも絶対無理!と思うかと思いますが、人間の能力ってすごいもので、練習するとちゃんと見えるようになります。こればかりは体験したことがない人に説明するのは無理です。そういう譜面をプレイしているとよく変人扱いされますが、それもまた楽しいところです。

スコア狙いとクリア狙い

スコア狙いは最終的には満点を目指す遊び方、クリア狙いは最終的にはノーミスを目指す遊び方です。ある程度上達してくると、大体の人がどちらかに傾きます。私はクリア狙い寄りなのでそちらの楽しみ方を書きます。

このゲームの場合、クリアにはいくつかの段階があります。一番段階の低いクリアをするだけなら感覚的にはテストで60点取れれば合格ですが、そこから70点、80点、90点とあり、ノーミスは100点満点しないと得られません。テストを60点から100点にする難しさを想像してもらえると分かると思いますが、すごく大変なことです。何度も何度も98点や99点を取りますが認めてもらえません。だから初めて100点をとった時はものすごい達成感を得られます。

スコア狙いも似たようなもだと思います。とにかく満点はどんなに簡単な曲であっても相当難しいことです。

終わりがない超上級譜面

クリア狙いをしていると、当然どんどん上手くなります。だからと言って、難しい譜面が尽きることはありません。先ほど段位認定を説明しましたが、Beatmania IIDXでは、7級から始まり、、、1級、初段、二段、、、十段、皆伝という18段階があります。皆伝でも相当難しいですが、実際、皆伝も取って全曲クリアしてしまう人は結構います。

それでもまだ難しさを求める場合はどうするかというと、BMSというPCで遊べるビートマニアのクローンソフトがあります。こちらは、曲や譜面を誰でも作れるようになっており、果てしなく難しい譜面があります。(BMSについてはBeatmania IIDXから外れていくのでここでは詳しくは書きません)

様々なプレイスタイル

難しさを追求する以外に、様々なプレイスタイルがあります。例えば、ダブルプレイと言って、一人で二人分の鍵盤+皿を操作するスタイル(公式にダブルプレイ用の譜面が用意されています)や、片手だけでプレイするスタイルなど、遊び方は1つだけではないということも楽しいところです。

まとめ

私は難しさを追求するタイプなので、難しさが強調されてしまったかもしれませんが、初心者でも楽しめるよう配慮されていますので安心してください。それに、何度も述べているように楽しみ方は1つではないので、音ゲーをやってみて自分なりの楽しみ方を見つけてください。

私の音ゲーの楽しさをまとめると、音ゲーは体感型のゲームなので「成長が実感できる」という点が他のジャンルのゲームよりあるかなと思います。あと、音楽の趣味が広がるのも楽しいところです。

NAVERまとめから位置情報を含むスポット情報をJSON形式で取得する

現在開発中の別のサービスのために、位置情報を含むスポット情報が欲しかったので、たまたま見つけたNAVERまとめからデータを取得するツールを作ってみました。

URL

概要

NAVERまとめのURLを入力すると、そのページにあるタイトル・説明と位置情報付きのスポット情報をJSON形式で返します。

(実行例)

入力:

http://matome.naver.jp/odai/2134519339022942401

出力:

{
    "matome": {
        "title": "京都旅行で「カップルデート」にオススメの人気観光スポット",
        "discription": "日本国内でも、特に人気のデートスポットが京都です。京都には、古き良き町であったり、オイシイ食文化が残っています。京都旅行でオススメの人気観光スポットを紹介します!!"
    },
    "location": [
        {
            "spotName": "京都タワー",
            "address": "日本, 〒600-8216 京都府京都市下京区東塩小路町 京都タワー",
            "discription": "京都駅から下車してすぐの場所にあるので、アクセスは抜群です。1964年に建てられた古いタワーですが、京都のシンボルとして親しまれています。\n\n展望室営業時間:9:00~21:00\n大人:770円\nhttp://www.kyoto-tower.co.jp/",
            "image": {
                "thumbnailUrl": "http://rr.img.naver.jp:80/mig?src=http%3A%2F%2Fimgcc.naver.jp%2Fkaze%2Fmission%2FUSER%2F20120818%2F13%2F1169533%2F7%2F540x405x8c76a58e48ef57fe4f9388ef.jpg%2F300%2F600&twidth=300&theight=600&qlt=80&res_format=jpg&op=r",
                "originalUrl": "http://tkpkyoto.net/images/map_gckyoto_540.jpg",
                "sourceUrl": "http://tkpkyoto.net/access.shtml"
            },
            "lat": 34.98752,
            "lng": 135.759301
        },
        {
            "spotName": "東本願寺",
            "address": "日本, 〒600-8505 京都府京都市下京区常葉町 東本願寺",
            "discription": "巨大な建物の数々を見れば、度肝を抜かれる事は間違いなし、本当に大きい木造建築は圧巻\n\n京都の寺が詰まらないと思っている人は、最初に京都駅から近い東本願寺に行きましょう。京都駅から歩いて行けて、世界最大の木造建築に驚きます。\n\nhttp://www.higashihonganji.or.jp/",
            "image": {
                "thumbnailUrl": "http://rr.img.naver.jp:80/mig?src=http%3A%2F%2Fimgcc.naver.jp%2Fkaze%2Fmission%2FUSER%2F20120820%2F13%2F1169533%2F28%2F1280x960xa5a79e5818e4780010691a1.jpg%2F300%2F600&twidth=300&theight=600&qlt=80&res_format=jpg&op=r",
                "originalUrl": "http://userdisk.webry.biglobe.ne.jp/001/247/64/N000/000/000/117603302339416302404.JPG",
                "sourceUrl": "http://kontomo.at.webry.info/200704/article_1.html"
            },
            "lat": 34.9912688,
            "lng": 135.7587804
        },
        *長いので以下省略*
}

フォーム入力だけでなく、APIもあります。

コーディング

構成は Node.js + Express + Heroku です。 前に作った Beatube IIDX ~ver.SPADA~ のソースをかなり流用したため、書いた量自体はほんのわずかですが、その中でポイントを2点ほど紹介します。

関数プロパティによるメモ化パターン

いわゆるキャッシュです。 同じURLに対するリクエストが2回以上来た時、(ページが更新されていなければ)結果は同じなのに毎回ページを取得して処理するのは無駄になります。 なので、関数のプロパティにURLをキーとしたハッシュを持たせておき、同じキーがある場合はその値を返すようにしました。

シングルトン

シングルトンとは、あるクラスのインスタンスを一つだけにする(何回newしても必ず同じインスタンスが作られる)というデザインパターンです。 今回のケースですと、インスタンス化した関数にキャッシュを持たせているため、どこかでもう一つ別のインスタンスが作られた場合に、また一からキャッシュをためることになります。 なので、このパターンを当てはめました。

exports.Scraping = function Scraping(){
        // singleton
    // インスタンスをキャッシュする
    var instance = this;

    // ローカル変数
    var request = require('request');
    var cheerio = require('cheerio');

    // 変換データのキャッシュ
    var cache = {};

    /**
   * htmlのbodyを解析して位置情報のリストを返す
   * 
   * @method getLocation
   * @return {Array} 位置情報のリスト
   */ 
    this.getLocation = function(url, callback){

        if(cache[url]){
            return callback(cache[url]);
        }

        _getBody(url, function(error, body){
            if(error) {
                return callback({});
            }

            var $ = cheerio.load(body);

            var matomte = {
                title: $(".mdHeading01Ttl a").text() || "",
                discription: $(".mdHeading01DescTxt").text() || ""
            };

            var location = [];
            $(".MdMTMWidgetList01 ._jWidgetData").each(function(){
                var json = JSON.parse($(this).attr("data-contentdata"));
                // console.log(json);
                if(json.location){
                    var lo = {
                        spotName: json.location.spotName || json.title,
                        address: json.location.address,
                        discription: json.description,
                        image: {
                            thumbnailUrl: json.thumbnailUrl,
                            originalUrl: json.url,
                            sourceUrl: json.sourceUrl
                        },
                        lat: json.location.lat,
                        lng: json.location.lng
                    };
                    location.push(lo);
                }
            });

            var result = {
                matome: matomte,
                location: location
            };

            cache[url] = result;
            return callback(result);
        });
    };

    /**
   * リクエストのbody要素を返却する
   * 
   * @method _getBody
   * @return {String} body要素
   */
    function _getBody(url, callback){
        request({url: url}, function(error, response, body) {
            if(error || response.statusCode != 200){
                // console.log(error);
                return callback(error);
            }
            // console.log(body);
            return callback(null, body);
        });
    }

    // Singleton
    // コンストラクタを書き換える
    Scraping = function(){
        return instance;
    };
};

今回参考としたページ

JavaScriptパターン ―優れたアプリケーションのための作法

JavaScriptパターン ―優れたアプリケーションのための作法

  • p44 3.2 カスタムのコンストラクタ関数
  • p79 4.8 関数プロパティによるメモ化パターン
  • p147 7.1 シングルトン

余談

Herokuに登録するとき、何を間違えたか無意識にURLを「json-parse-hatena.herokuapp.com」にしていて、デプロイした後に気づきましたw


この記事は、習慣化の手助けをするためのアプリ「TheHabit」に背中を押してもらって書いています。 (ブログを書くタスク:3回目)

JavaScriptリファクタリング実践1

JavaScriptパターン ―優れたアプリケーションのための作法 を読みながら先日紹介したBeatubeのソースをリファクタリングしました。

リファクタリングの例として説明する処理は、曲やシリーズ名の情報をとあるサイトから取得してDBに格納するというものです。 この処理をcronモジュールで定期的に実行させます。

リファクタリング

まずはリファクタリング前のソースコードから。

var request = require('request');
var cheerio = require('cheerio');

var db = require('./lib/db.js');
var Musics = db.Musics;
var Series = db.Series;

// コレクションの全データを削除
function reset_data(){
    Musics.remove({},function(error){
        if(error) return console.log(error);
    });

    Series.remove({},function(error){
        if(error) return console.log(error);
    });
}

// 全曲表からデータ取得
function get_data(){
    var requestUrl = 'http://iidxsd.sift-swift.net/view/all';
    request({url: requestUrl}, function(error, response, body) {
        if(!error && response.statusCode == 200){
            var $ = cheerio.load(body);
            var series_count = 0;

            $("#datatable .data_title tr td h3").each(function(){
                var series = new Series();
                series.series = series_count;
                series.title = $(this).text();
                series.save(function(error){
                    if(error) return console.log(error);
                });
                
                console.log(series);
                series_count++;
            });
            console.log("get series finished.");

            series_count = 0;
            $("#datatable .data_body").each(function(){
                $(this).children("tr").each(function(){

                    var music = new Musics();
                    music.series = series_count;
                    music.title = $(this).children("td").first().text().replace(/^詳/,"");
                    music.genre = $(this).find(".genre").text();
                    music.artist = $(this).children(".artist").text();
                    music.bpm = $(this).children(".bpm").text();
                    music.dif_n7 = zeroCheck($(this).children(".difn7").text());
                    music.dif_h7 = zeroCheck($(this).children(".difh7").text());
                    music.dif_a7 = zeroCheck($(this).children(".difa7").text());
                    music.dif_n14 = zeroCheck($(this).children(".difn14").text());
                    music.dif_h14 = zeroCheck($(this).children(".difh14").text());
                    music.dif_a14 = zeroCheck($(this).children(".difa14").text());
                    music.notes_n7 = zeroCheck($(this).children(".notesn7").text());
                    music.notes_h7 = zeroCheck($(this).children(".notesh7").text());
                    music.notes_a7 = zeroCheck($(this).children(".notesa7").text());
                    music.notes_n14 = zeroCheck($(this).children(".notesn14").text());
                    music.notes_h14 = zeroCheck($(this).children(".notesh14").text());
                    music.notes_a14 = zeroCheck($(this).children(".notesa14").text());
                    music.save(function(error){
                        if(error) return console.log(error);
                    });

                    console.log(music);
                });
                series_count++;
            });
            console.log("get musics finished.");
        }
        else{
            if(error) return console.log(error);
        }
    });
};

function zeroCheck(value){
    return value.match(/[^0-9]+/) ? 0 : value;
}

exports.scraping = function(){
    reset_data();
    get_data();
};
var scraping = require('./scraping.js');

// cron設定 ==========================
var cronJob = require('cron').CronJob;
var cronTime = "00 00 00 */7 * *";    // 7日間隔で実行
// var cronTime = "00 */1 * * * *";  // 1分間隔で実行

job = new cronJob({
  cronTime: cronTime

    // The function to fire at the specified time.
    , onTick: function() {
        scraping.scraping();
        //console.log(new Date());
    }

  // Specified whether to start the job after just before exiting the constructor.
  , start: false
}).start();

問題点として、以下のことが上げられます。

  • データ取得とDB格納が一つの関数に混在しており、可読性やメンテナンス製が悪い。
  • また、上記の理由により単体テストがしにくい。
  • 変数や関数が散らかってる。

リファクタリング1回目

var SCRAPING = {
    request: require('request'),
    cheerio: require('cheerio'),

    SCRAPING_REQUEST_URL: 'http://iidxsd.sift-swift.net/view/all',

    getSeries: function(callback){
        var cheerio = this.cheerio;
        var series_count = 0;
        var series = [];

        this._getBody(function(error, body){
            if(error) return callback(series);

            var $ = cheerio.load(body);
            $("#datatable .data_title tr td h3").each(function(){
                var s = {
                    series: series_count,
                    title: $(this).text()
                };
                series.push(s);
                series_count++;
            });
            return callback(series);
        });
    },

    getMusics: function(callback){
        var cheerio = this.cheerio;
        var zeroCheck = this._zeroCheck;
        var series_count = 0;
        var musics = [];

        this._getBody(function(error, body){
            if(error) return callback(musics);

            var $ = cheerio.load(body);
            $("#datatable .data_body").each(function(){
                $(this).children("tr").each(function(){
                    var m = {
                        series: series_count,
                        title: $(this).children("td").first().text().replace(/^詳/,""),
                        genre: $(this).find(".genre").text(),
                        artist: $(this).children(".artist").text(),
                        bpm: $(this).children(".bpm").text(),
                        dif_n7: zeroCheck($(this).children(".difn7").text()),
                        dif_h7: zeroCheck($(this).children(".difh7").text()),
                        dif_a7: zeroCheck($(this).children(".difa7").text()),
                        dif_n14: zeroCheck($(this).children(".difn14").text()),
                        dif_h14: zeroCheck($(this).children(".difh14").text()),
                        dif_a14: zeroCheck($(this).children(".difa14").text()),
                        notes_n7: zeroCheck($(this).children(".notesn7").text()),
                        notes_h7: zeroCheck($(this).children(".notesh7").text()),
                        notes_a7: zeroCheck($(this).children(".notesa7").text()),
                        notes_n14: zeroCheck($(this).children(".notesn14").text()),
                        notes_h14: zeroCheck($(this).children(".notesh14").text()),
                        notes_a14: zeroCheck($(this).children(".notesa14").text())
                    };
                    musics.push(m);
                });
                series_count++;
            });
            return callback(musics);
        });
    },
    /**
   * リクエストのbody要素を返却する
   * 
   * @method _getBody
   * @return {String} body要素
   */
    _getBody: function(callback){
        this.request({url: this.SCRAPING_REQUEST_URL}, function(error, response, body) {
            if(error || response.statusCode != 200){
                // console.log(error);
                return callback(error);
            }
            // console.log(body);
            return callback(null, body);
        });
    },


    _zeroCheck: function(value){
        return value.match(/[^0-9]+/) ? 0 : value;
    }
};

exports.scraping = SCRAPING;
var scraping = require('./scraping').scraping;
var db = require('./db');
var cronJob = require('cron').CronJob;

var cron_time = "00 00 00 */7 * *";   // 7日間隔で実行
// var cron_time = "*/10 * * * * *"; // 10秒間隔で実行(debug)

var job = new cronJob({
    cronTime: cron_time,

    // The function to fire at the specified time.
    onTick: function() {
        console.log("cron start");
        // seriesのinsert。データが既にある時はupdate。
        scraping.getSeries(function(series){
            var i, max;
            for(i = 0, max = series.length; i < max; i++){
                db.Series.update({number: series[i].number}, series[i], {upsert: true}, function(error, numberAffected, raw){
                    if(error) console.log(error);
                });
            }
        });

        // musicsのinsert。データが既にある時はupdate
        scraping.getMusics(function(music){
            var i, max;
            for(i = 0, max = music.length; i < max; i++){
                db.Musics.update({series_number: music[i].series_number,title: music[i].title},
                    music[i], {upsert: true}, function(error, numberAffected, raw){
                    if(error) console.log(error);
                });
            }
        });
    },

    // Specified whether to start the job after just before exiting the constructor.
    start: false
}).start();

見ての通り、スクレイピング部分をオブジェクトとしてひとまとまりにし、DB格納部分と分けました。

しかしもう少し良くできそうです。

リファクタリング2回目

/**
* 曲リストのあるサイトからデータを取得し、配列にして返す
* 
* @class Scraping
*/
exports.Scraping = function Scraping(){
    // ローカル変数
    var request = require('request');
    var cheerio = require('cheerio');
    var SCRAPING_REQUEST_URL = 'http://iidxsd.sift-swift.net/view/all';

    /**
   * シリーズの名前のリストを返す
   * 
   * @method getSeries
   * @return {Array} シリーズの名前のリスト
   */ 
    this.getSeries = function(callback){
        var series_count = 0;
        var series = [];

        _getBody(function(error, body){
            if(error) {
                callback(null);
                return [];
            }

            var $ = cheerio.load(body);
            $("#datatable .data_title tr td h3").each(function(){
                var s = {
                    series: series_count,
                    title: $(this).text()
                };
                callback(s)
                series.push(s);
                series_count++;
            });
            return series;
        });
    };

    /**
   * 曲の情報のリストを返す
   * 
   * @method getMusics
   * @return {Array} 曲の情報のリスト
   */ 
    this.getMusics = function(callback){
        var series_count = 0;
        var musics = [];

        _getBody(function(error, body){
            if(error) {
                callback(null);
                return [];
            }

            var $ = cheerio.load(body);
            $("#datatable .data_body").each(function(){
                $(this).children("tr").each(function(){
                    var m = {
                        series: series_count,
                        title: $(this).children("td").first().text().replace(/^詳/,""),
                        genre: $(this).find(".genre").text(),
                        artist: $(this).children(".artist").text(),
                        bpm: $(this).children(".bpm").text(),
                        dif_n7: _zeroCheck($(this).children(".difn7").text()),
                        dif_h7: _zeroCheck($(this).children(".difh7").text()),
                        dif_a7: _zeroCheck($(this).children(".difa7").text()),
                        dif_n14: _zeroCheck($(this).children(".difn14").text()),
                        dif_h14: _zeroCheck($(this).children(".difh14").text()),
                        dif_a14: _zeroCheck($(this).children(".difa14").text()),
                        notes_n7: _zeroCheck($(this).children(".notesn7").text()),
                        notes_h7: _zeroCheck($(this).children(".notesh7").text()),
                        notes_a7: _zeroCheck($(this).children(".notesa7").text()),
                        notes_n14: _zeroCheck($(this).children(".notesn14").text()),
                        notes_h14: _zeroCheck($(this).children(".notesh14").text()),
                        notes_a14: _zeroCheck($(this).children(".notesa14").text())
                    };
                    callback(m);
                    musics.push(m);
                });
                series_count++;
            });
            return musics;
        });
    };

    /**
   * リクエストのbody要素を返却する
   * 
   * @method _getBody
   * @return {String} body要素
   */
    function _getBody(callback){
        request({url: SCRAPING_REQUEST_URL}, function(error, response, body) {
            if(error || response.statusCode != 200){
                // console.log(error);
                return callback(error);
            }
            // console.log(body);
            return callback(null, body);
        });
    }

    /**
   * 数値でない値の場合、0にして返す
   * 
   * @method _zeroCheck
   * @param {String} value
   * @return {String} 0 または value
   */
    function _zeroCheck(value){
        return value.match(/[^0-9]+/) ? 0 : value;
    }
};
/**
* cronの設定
*/

var db = require('./db');
var Scraping = require('./scraping').Scraping;
var CronJob = require('cron').CronJob;

//var cronTime = "00 00 00 */7 * *"; // 7日間隔で実行
var cronTime = "*/10 * * * * *";  // 10秒間隔で実行(debug)

var job = new CronJob({
    cronTime: cronTime,

    // The function to fire at the specified time.
    onTick: function() {
        console.log("Cron start.");

        var scraping = new Scraping();
        
        // seriesのinsert。データが既にある時はupdate。
        scraping.getSeries(function(series){
            // console.log(series);
            db.Series.update({number: series.number}, 
                series, {upsert: true}, function(error, numberAffected, raw){
                if(error) console.log(error);
            });
        });

        // musicsのinsert。データが既にある時はupdate
        scraping.getMusics(function(music){
            // console.log(music);
            db.Musics.update({series_number: music.series_number,title: music.title},
                music, {upsert: true}, function(error, numberAffected, raw){
                if(error) console.log(error);
            });
        });
    },

    // Specified whether to start the job after just before exiting the constructor.
    start: false
}).start();

まず、Scrapingオブジェクトをコンストラクタ関数にしました。 これにより、記述しやすくなり、オブジェクトのパブリック(this.〜)とプライベート(var 〜)を機能的に分けることができます。

また、1回目のソースコードでは、getSeriesやgetMusicsの戻り値をcallbackでまとめて配列で返していましたが、今回は1オブジェクトごとcallbackで返すようにしました。これにより、DB格納側の処理でfor文を使ってループする処理が省けました。

以上のようなことが書いてあるので、JavaScriptをステップアップしたい人は読んでおいて間違いないと思います。

JavaScriptパターン ―優れたアプリケーションのための作法

JavaScriptパターン ―優れたアプリケーションのための作法


この記事は、習慣化の手助けをするためのアプリ「TheHabit」に背中を押してもらって書いています。 (ブログを書くタスク:2回目)

『リーン・スタートアップ』

スタートアップに限らず、何かを生み出すときの考え方の手本となるバイブル的な本です。

考え方

  • スタートアップとは、不確定な状態で新しい製品やサービスを作り出さなければならない人的組織である
  • スタートアップにおける生産性とは、ものを作ることではなく、いかに顧客が欲しいものに早くたどり着くかである
  • 起業とはすなわちマネジメントであり、スタートアップのマネジメントはスタートアップに適したマネジメント方法にする必要がある。
  • 価値とは顧客にとってメリットを提供する物であり、それ以外は全て無駄である

構想段階

  • ビジョン(目的)を設定する
  • ビジョンはぶれないようなものにする。
  • 問うべきなのは「この製品は作るべきか」であり、「このような製品を中心に持続可能な事業が構築できるか」である
  • 仮説を組み立てる
  • 価値仮説:顧客にとっての価値がどのように提供できるのか
  • 成長仮説 :製品がどのように広がっていくか

構築ー計測ー学習

  • このサイクルを短くすることが大事 -「かんばん」によるサイクルの管理では、計測する際の因果関係がわかりやすいように検証は1度に1つ

構築

  • 最も重要な(=リスクの高い)仮説から必要最小限の製品(MVP・学びの中間目標)が何かを考える
  • 顧客が分からないうちは何が品質かも分からないのでMVPに品質を求めては行けない
  • 必要以上に作らないで、少しでも迷ったらシンプルにする
  • アーリーアダプター(見込み客)に試しに使ってもらう
  • MVPは動く物でなくてもよい。例えば以下の方法がある。
  • 技術的に課題がある:動画で紹介する
  • 需要があるか調べたい:製品の紹介と事前登録サイトを作って反応を伺う
  • どこまでの機能があれば使われるかを検証したい:期間限定で無料利用にする
  • コンシェルジュ型サービス:手動でコンシェルして試してみる

計測(革新会計)

  • 仮説と実測を比較する
  • MVPから得られた具体的な数値をベースラインとする
  • コホート分析
  • 虚栄の評価基準にまとわされないようにする
  • エンジンのチューニング
  • 数値目標を明確にして改善を繰り返し、ベースラインから数値を上げていく

学習

  • 顧客と話をする
  • 否定的な意見が多くても悲観的になる必要は無い。意見を聞けば良い。
  • 要望として上がったものと、マイルストーンが一致していればよい
  • 要望として上がらなかったものがロードマップにある場合、それはさして重要ではない物である
  • 顧客は製品が提示される前にどういう物が欲しいか分からないことが多いので、反応を観測するとよい

方向転換(ピポット)

  • ピポットするには正確な測定が必要
  • ピポットするタイミングはプロジェクトによるが、あらかじめ時期を決めておくと良い
  • ズームイン型ピポット
  • ズームアウト型ピポット
  • 顧客セグメント型ピポット
  • 顧客ニーズ型ピポット
  • プラットフォーム型ピポット
  • 事業構造型ピポット

スピードアップ

  • バッチサイズは小さくする。まとめて行うと、かえって効率が悪くなる
  • 成長のエンジン(成長仮説)が正しく働いていること
  • 失敗したら5回のなぜを問う。原因を人にしてはいけない。原因は仕組みにあり、それを改善する

アンチパターン(失敗する方法)

  • 計画を忠実に実行した結果、「失敗を達成」する
  • 一つしか試さない場合、アントレプレナーではなく政治家が増えてしまいます
  • 思い込みだけのアイディアで長い期間プロトタイプを作成する
  • 最初の失敗(否定的な意見)だけで諦めてしまうこと
  • 最後の最後まで諦めず心中すること

リーン・スタートアップ

リーン・スタートアップ