March 31, 2008

EC2のデータをS3にバックアップを取る

ためには、どうしたらいいの?ということで調査中。

正直なところ私はバックエンドに自信がないのだが、せっかくの機会だということで、Amazon EC2にチャレンジしている。そして調べていくうちに、この環境ってすげぇな、と思うようになった。こんな構成よく思いついたね。天才じゃない?

ということで、EC2を使ってホスティングをする予定なのだが、使用しているAMIのインスタンスを落としてしまう(落ちてしまう)と、データは全部ふっ飛んでしまうので、バックアップを取る必要があるのだ。一般のサーバならハードが残っていればなんとか復旧できたりしそうな気もするが、EC2においては、そんなことはできない。というよりも、問題が起きたときには、ハードをゴニョゴニョして復旧をさせるというアプローチはもうやめようぜ、という立場なのだろう。つまり、ちゃんとしたバックアップ構成を組んでシステムを運用するべきだ、と。

そして、そのバックアップのために(それだけのためではないが)、S3というサービスも展開している。でも、どうやってそのバックアップを取ったらいいのだろう、ということがわからなかったので最近はそれを調べていた。

まだ調査中だが、二つの方法があることがわかった。たぶん。

AMIのインスタンスをそのままイメージ化してS3に退避

環境をすべてバックアップする。Amazon Elastic Compute Cloud Getting Started Guide Creating an Imageに書かれているように、構成をすべてバックアップとるので、EC2で運用しているサーバを落としても、AMIの選択で、バックアップしてあるイメージを指定すれば、復旧できる。

s3syncにてディレクトリを指定してS3に退避

必要なディレクトリのみをバックアップする。Using Amazon S3 from Amazon EC2 with Rubyに書かれているように、s3syncというRubyのスクリプトを使って、指定したディレクトリを指定したバケットにバックアップをする。

私が言っているバックアップというのは、サーバに乗っけた自作のWebアプリ自体やそこで使用しているデータベースのことなので、s3syncの方を採用するのだろうな。しかし、リリース時点のサーバの構成のバックアップは取っておくのはいい考えだと思うので、その際には、イメージ化してS3に退避するという方法を採用する必要がありそうだ。

というわけで、使い分ける必要がありそうだ。間違っていたら指摘よろしこ。

September 3, 2007

というわけで、YouTubeのAPIがGData対応になりましたん。

私は、ニコニコよりもYouTube派です。

で、どうAPIが変わったか、というところの詳細はここ見てね。
YouTube Data API Developer’s Guide: Protocol
なんかすでにクライアント用のライブラリのリストもあったり。。。
Google Data APIs Client Libraries

July 23, 2007

Competeを使ってみたが。。

Services_Competeに投票しようかみるために、まず、Competeって何?ってとこから始めたよ。

ほむほむ。Webサイトのトラフィックの比較とかをするらしい。
で、Compete Aboutを読むと、

Is this website safe from spyware and other threats like phishing?
How many people visit this site and how does it compare to other sites?
Are there promotion codes for this site that can save me money?

なるほどね。しかし、なんだか、ググルタソができそうなネタのような気もするが、

Today, search engines help us find sites, but they fall short of showing how safe, popular and valuable a site is.

と書いてあるように、今の段階ではしていないみたい。で、Competeは、なんだか独自の方法で評価する仕組みを持っているみたい。それが何かはよくわからんが、何百万もの人の動向を反映しているんだとさ。うーん。ググルタソが少し力を入れれば、速攻できそうな気もするが、その辺どうなんかな。つか、そこにお金儲けの仕組みがあると判断されれば、買収が一番早いのかもしれん。

で、だ。今回使用してみたかったのが、Services_Competeなのだが、CompeteのAPIをラップしたものということだ。CompeteのAPIでは、Compete Site Analyticsで手に入るようなデータをもらえるようだ。そして、それをうまいことマッシュアップしてみたらどうかね、といったものか。

まず、自分のサイトganchiku.comでやってみたけど

Sorry, we don’t have any data for ganchiku.com. With more data, we can cover more sites.

とか言われた。なにー!どうせトラフィックなんてないですよ。うちのサイトは。

というわけで、結構大きなサイトを比較するのがいいみたい。Site Analyticsのページを開くと、mlb.comとnfl.comとnhl.comの比較をしているのがわかる。確かにネットではどのスポーツが見られる人気が高いのかわかるわけだ。なるほどー。ということは、だ。同じようなサービスでどれだけシェアを持っているのかを見るのにいいんじゃね、ということで、SBMサービスを提供しているところを比較してみた。diggとdel.icio.usとhatena。しかし、だ。バグ発見。。。

hatena.ne.jpがne.jpと判断されてるじゃねーか!つーわけで、Contact Usからバグ報告してみた。yahoo.co.jpとかならいけるんだけどね。なぜかne.jpはダメ。バリデーションが変なところで切ってしまっているのだろう。まぁ、おそらく他にもダメなものがあるのだろうね。

本当は、hatena.ne.jpをServices_Competeから使ってみたら、ne.jpと認識されていたみたいで、ソースを見たのだけど、どうも間違っていないっぽかったので、Site Analyticsで試したら、やはり、ということだったのだ。話をうまいことそれに持っていくことができなかったので、無理矢理話を作っちゃった。

(more…)

July 4, 2007

phptubeだって。

phptubeというのを発見した。
こういうアプローチの方がいいのかな。なんつーか、ハックって感じで。でも、公式APIではないので、変えられたら面倒そうだけどね。ファイルのアップロードとダウンロードができるようだ。まぁ、両方ともphpでHTTPクライアントを書いてみたってだけだけど、中途半端なAPIを対応するよりこっちの方が需要がありそう。ソース見たけど、まぁ、思った通りか。

つーか、YouTubeのAPIがGDataに対応したら、PEARから下ろしてもらうように言おうかしらん。Zend_FrameworkはGoogleと一緒にZend_GDataを開発していたみたいなので、勝負しても意味ないし。しかし、ZendとPEARの調整なんとかならんかね。

他にもいくつかのフレームワークは依存するのが嫌のようで、独自でいろいろ作ったりしている。symfonyも自分で勝手にlimeというテスティングフレームワーク作って組み込んでるしね。

んー。今の流れは小技ハックを出していくのではなくて、大物オープンソースを出していくことなのかしらん。ちょいと考えてみますか。

June 9, 2007

YouTubeの次期APIには、GDataを採用

YouTube APIの開発者ブログを見ていたら、そんなことが決定されたようだ。
YouTube API Blog: The Future
ついでにビデオも挙がっている。なげーから、見てないけど。まぁ、当然の流れだな。

今までのAPIも残すけども、そちらには、機能追加はしないとのこと。そして、GDataを使用したAPIに機能追加していくようだ。

Zend FrameworkにGDataのライブラリがある。その名もZend_Gdata
Zend Framework Zend_Gdata
つーか、Zend Frameworkのページのtitle要素がほとんどZend Frameworkなのは、イケテナイと思った。もう少しちゃんとページのタイトルを表すべきでしょ。って、ぜんぜん関係ないが。

まぁ、確かに現在のAPIにはあまりがんばっている様子がなかった(マニュアル古くて、正しい動作がわからん。)ので、GData版のAPIに期待だな。つーか、そうなるとServices_YouTubeも存在意義がなくなるなー。まぁ、この手の新しいサービスは、古い仕組みにしがみ付いていたらあかんですよ。しかし、現在のAPIのドキュメントに載ってないものを一つずつ調べて、それに対応したものをリリースしていこうと思ったけど、なんかやる気無くなっちゃったYO

March 5, 2007

Services_YouTubeのバージョンアップができん。

つーか、早いとこバージョンアップをしようと思ったのだけども、なかなかできん。
原因は、旅行に来ていることも確かにあるけども、YouTubeのドキュメントが不完全なのだ。http://youtube.com/dev_docsにページングができるようになったと書いてあるのに、詳細のドキュメントを見たらできなかったり。。list_friendsとかlist_by_userとか。というわけで、最近はYouTube API Developer Forumにたまに顔を出したり。。。

APIによっては、per_pageパラメータを与えても、反映されなかったりしているので、中途半端なのだ。ユニットテストをつけているのだけども、そもそも正しい動きをしているのかよくわからん。もう少し時間がかかりそうだな。プノンペン辺りでバージョンアップができたらいいな。

つーか、フーコック島良すぎです。サオビーチ最高っす。バイクを借りて、訪れる価値ありっす。

February 19, 2007

YouTubeのAPIに追加されていたので、Services_YouTubeをアップする予定。

YouTubeのDeveloperサイトを確認していたら、APIが追加されていました。
http://youtube.com/dev_docs

Services_YouTubeのバージョンを近々上げます。プログラムの方はできたのだけども、まだ反映はしていません。テストがまだ不十分なので。

今は、ハノイのゲストハウスにいるのだけども、無線ができるので、非常に快適。明日からこのゲストハウスを出てしまうのは、とても残念だけどもフエでもインターネットができる場所に滞在する予定なので、そこで反映させる予定。

しかし、list_friendsやlist_by_userのページャができるようになったって書いてあるけど、詳細のドキュメントには反映されてないのね。動作を確認してもなんか怪しいし。ちゃんとメンテできるかな。。。

December 26, 2006

食べログとGoogle Mapsもやってみた。

HotpepperのAPIと食べログのAPIをうまく重ねることができないかなー、と試行錯誤中なのだけども、さーて、どうしたものか。

で、HotpepperのAPIの検索結果をGoogle Mapsにプロットすることができたので、ついでに食べログもしてみた。(まぁ、本当は食べログのサイトにすでにGoogle Mapsとのマッシュアップがあるんだけどね。。。)
こちらは、私の見た目を重視していない食べログAPIとGoogle Mapsのマッシュアップ
UIはHotpepperのサンプルと少し違う。ここでは、都道府県と評価をもとに検索をかけて、20件だけGoogle Mapsにプロットする感じでやってみた。本当はページャを付けたかったのだけども、食べログAPIでは、検索件数を返してくれないので、できない。YouTubeのAPIと同じだね。私にとっては、使いにくい。「次のページ」があるかないかわからないのに、「次のページ」のリンクを出すのは嫌なので。
しかし、よくよく考えてみると、上位20位以下のレストランなんてあえて検索する必要ないんでないかい?上位20位以内で、十分な感じがする。「『よーし、パパ57位のレストランに行っちゃうゾー』とか言っているの。もう見てらんない。」からねw

Hotpepperは、クーポンがあるかどうかが重要なので、上位20位とか関係なしにページャが必要だけども、食べログの方はより良いサービス、味を追求するためのモチベーションとして店ががんばればいいんじゃない?で、だ。やっぱり、食べログの持つ情報とHotpepperの持つ情報は、やはり質が違うんだよね。評価の高いレストランが常にクーポンがあるとは限らない。評価が高ければ、ぶっちゃけHotpepperに載せて、来てもらう必要がないしね。

と言いつつも、一つ考えたのは、電話番号をキーにして検索してみたりしてはどうかな、と。食べログで検索して、その店の数分、電話番号で、Hotpepperの検索をかけてみる。引っかかったら、表示できるので。一度のページ表示で、HotpepperのAPIに20件もリクエストが飛ぶのでなんか嫌な感じだが。それに上に書いたようにそもそも情報の質が違うので、意味がないかもね。って思えてきてもいる。

で、いつもサンプルスクリプトを見て思うんだけども、見た目が重要っていうのは激しくわかっているんだけど、なんかやる気がしないだよね。サンプルスクリプト用のCSSやら、イメージやらをがんばって作るのが面倒すぎて。。。

December 25, 2006

とりあえず、ソースコード吐いていいかね。Services_HotPepper-0.1.0?(訂正)

というわけで、ライブラリが出てきたっぽいので、そちらにリンクを貼って、こっちのソースコードは消すよー。だいたい同じ感じのAPIで、そちらの方が完成度が高そうだったので。でも、少しだけ使用方法が違ったので、それを修正したよー。
過去と他人はかえられないが、未来と自分はかえられる - Services_HotPepper-0.1.1

とりあえず、ライセンスは放棄しておく。なので、自由に改変してもいいし、配布してもいいです。ライセンス周りは結構面倒なのよ。なので、やる気が出てちゃんとメンテする気が出るまでライセンスは放棄でよろしく。単にGoogle Mapsと連携してみたかっただけなので。

で、ライブラリの方のソースコードは、Exception周りをちゃんとチェックしていないので、よくわからないけど、今回作ったHotPepperのAPIとGoogle Mapsのマッシュアップのソースの公開でもしてみる。適当に自己責任で使ってくれ。名前を変更して、追加開発しても構いませんよ。前回のポストにも書いたけど、すでに名前があると、なんかややこいけど、いいライブラリが出てきたらServices_の接頭辞を消しますよー。例によって、PHP5で、Curlが必要。Cache_Liteもあると激しくよい。つーか、パッケージ化すればいいのか。なんかやる気がないのが困ったものだ。需要があれば、してみるけど、ちょっと様子見。

アプリの方は、Pagerがないとダメ。あと、0.1.0 では、

で、これを使うと、マスタ系APIとグルメサーチができる。

  1. $hotpepper = new Services_HotPepper();
  2.  
  3. // 大サービスエリアコードの内容を取得 詳細: http://api.hotpepper.jp/reference.html#021
  4. $data = $hotpepper->getLargeServiceArea();
  5. var_dump($data);
  6. // サービスエリアコードの内容を取得 詳細: http://api.hotpepper.jp/reference.html#022
  7. $data = $hotpepper->getServiceArea();
  8. var_dump($data);
  9. // 大エリアコードの内容を取得 詳細: http://api.hotpepper.jp/reference.html#023
  10. $data = $hotpepper->getLargeArea();
  11. var_dump($data);
  12. // 中エリアコードの内容を取得 詳細: http://api.hotpepper.jp/reference.html#024
  13. $data = $hotpepper->getMiddleArea();
  14. var_dump($data);
  15. // 小エリアコードの内容を取得 詳細: http://api.hotpepper.jp/reference.html#025
  16. $data = $hotpepper->getSmallArea();
  17. var_dump($data);
  18. // お店ジャンルコードの内容を取得 詳細: http://api.hotpepper.jp/reference.html#026
  19. $data = $hotpepper->getGenre();
  20. var_dump($data);
  21. // 予算のコード内容を取得 詳細: http://api.hotpepper.jp/reference.html#027
  22. $data = $hotpepper->getBudget();
  23. var_dump($data);
  24.  
  25. // グルメサーチAPIの内容を取得
  26. // $parametersの内容は、リクエストパラメータのハッシュ http://api.hotpepper.jp/reference.html#002
  27. $data = $hotpepper->getGourmetSearch($parameters);
  28. var_dump($data);

これだけではありがた味がわからないので、それを使って、検索結果の店の住所からGoogle Mapsにプロットしてみるのが次のコード。はい。ツッコミされまくりそうですね。

  1. <?php
  2. require_once 'Services/Hotpepper.php';
  3. require_once 'Pager.php';
  4.  
  5. class HotPepperApp
  6. {
  7.     private $hotpepper;
  8.     private $searchResult;
  9.     private $parameters;
  10.  
  11.     const DEFAULT_ADDRESS = '東京';
  12.  
  13.     public function __construct($parameters = array())
  14.     {
  15.         $this->hotpepper = new Services_Hotpepper();
  16.         $this->hotpepper->setResponseFormat('array');
  17.         $this->hotpepper->setUseCache(true);
  18.         $this->parameters = $parameters;
  19.         if (isset($this->parameters['Start'])) {
  20.             if (!isset($this->parameters['Count'])) {
  21.                 $this->parameters['Count'] = 10;
  22.             }
  23.             $this->parameters['Start'] *= (int)$this->parameters['Count'];
  24.         }
  25.         // 2006-12-25T16:03:16 修正 parametersに使えない値があるとInvalidParamerExceptionを吐くので、とりあえずそれ以外は削る。
  26.         $availables = array(
  27.           'ShopIdFront', 'ShopNameKana', 'ShopName', 'ShopTel', 'ShopAddress',
  28.           'KtaiCoupon', 'LargeServiceAreaCD', 'ServiceAreaCD', 'LargeAreaCD',
  29.           'MiddleAreaCD', 'SmallAreaCD', 'Keyword', 'GenreCD', 'Order',
  30.           'Start', 'Count');
  31.         foreach ($this->parameters as $key => $value) {
  32.             if (!in_array($key, $availables) or $value == '') {
  33.                 unset($this->parameters[$key]);
  34.             }
  35.         }
  36.     }
  37.  
  38.     public function getOptions($master)
  39.     {
  40.         $method = 'get' . $master;
  41.         $result = $this->hotpepper->$method();
  42.  
  43.         $output = '<select name="' .$master. 'CD">';
  44.         $output .= '<option value="">&nbsp;-&nbsp;</option>';
  45.         foreach ($result[$master]  as $item) {
  46.             $output .= '<option value="' . $item[$master.'CD'] . '"';
  47.             if (isset($_REQUEST[$master.'CD']) and $_REQUEST[$master.'CD'] == $item[$master.'CD']) {
  48.                 $output .= ' selected="selected"';
  49.             }
  50.             $output .= '>' . $item[$master .'Name'] . '</option>';
  51.         }
  52.         $output .= '</select>';
  53.         return $output;
  54.     }
  55.  
  56.     public function getResult()
  57.     {
  58.         if (!isset($_REQUEST['search'])) { return array(); }
  59.         $output = '';
  60.         if (empty($this->searchResult)) {
  61.             $this->searchResult = $this->hotpepper->getGourmetSearch($this->parameters);
  62.         }
  63.         if (is_null($this->searchResult['NumberOfResults'])) { return array(); }
  64.         return $this->searchResult['Shop'];
  65.     }
  66.  
  67.     public function getPager()
  68.     {
  69.         if (!isset($this->parameters)) { return ''; }
  70.         if (empty($this->searchResult)) {
  71.             $this->searchResult = $this->hotpepper->getGourmetSearch($this->parameters);
  72.         }
  73.         $options = array(
  74.             'mode'         => 'Sliding',
  75.             'urlVar'       => 'Start',
  76.             'totalItems'   => (int)$this->searchResult['NumberOfResults'] - (int)$this->searchResult['DisplayPerPage'],
  77.             'perPage'      => (int)$this->searchResult['DisplayPerPage'],
  78.             'currentPage'  => (int)$this->searchResult['DisplayFrom'] / (int)$this->searchResult['DisplayPerPage'],
  79.         );
  80.  
  81.         $pager = Pager::factory($options);
  82.         return $pager->links;
  83.     }
  84.  
  85.     public function getShopAddress()
  86.     {
  87.         return isset($_REQUEST['ShopAddress']) ? htmlspecialchars($_REQUEST['ShopAddress']) : self::DEFAULT_ADDRESS;
  88.     }
  89.  
  90.     // Helper Method
  91.     public static function getTabHtml($result)
  92.     {
  93.         $url          = htmlspecialchars($result['ShopUrl']);
  94.         $name         = htmlspecialchars($result['ShopName']);
  95.         $address      = htmlspecialchars($result['ShopAddress']);
  96.         $noKtaiCoupon = (boolean)$result['KtaiCoupon'];
  97.         $access       = htmlspecialchars($result['Access']);
  98.         $genre        = htmlspecialchars($result['GenreName']);
  99.         $catch        = htmlspecialchars($result['ShopCatch']);
  100.         $budget       = htmlspecialchars($result['BudgetDesc']);
  101.         $capacity     = htmlspecialchars($result['Capacity']);
  102.         $ktaiShopUrl  = htmlspecialchars($result['KtaiShopUrl']);
  103.         $image        = htmlspecialchars($result['PictureUrl']['PcMiddleImg']);
  104.  
  105.         $ktaiText = $isKtaiCoupon ? '携帯NG!' : '携帯OK!';
  106.  
  107.         $base =<<<eof
  108. <a href='$url'>$name($ktaiText)<br />$catch<br /><hr />$address<br />
  109. <!-- HotPepperのページからメールを送ればいいんじゃない?
  110. <form>
  111. <lable>携帯にアクセス先アドレスを:<input type='text' name='email'/>
  112. <input type='hidden' name='url' value='$ktaiShopUrl'/>
  113. <input type='submit' value='送信!'/>
  114.  
  115. -->
  116. EOF;
  117.         $detail =<<<eof
  118. <div style='float:left;width=200;text-align:center;'>
  119. <img src='$image' alt='$name' /><br />写真提供:ホットペッパー.jp<br />
  120.  
  121. <div>
  122. ジャンル($genre)<br />予算($budget)<br />総席数($capacity)
  123. <hr />
  124. $access
  125. </div>
  126. EOF;
  127.         $tab = array(
  128.             '店情報'   => str_replace("\n", "", $base),
  129.             '詳細情報' => str_replace("\n", "", $detail)
  130.         );
  131.         return $tab;
  132.     }
  133. }
  134.  
  135. if (isset($_REQUEST['search'])) {
  136.     $app = new HotPepperApp($_REQUEST);
  137. } else {
  138.     $app = new HotPepperApp;
  139. }
  140.  
  141. ?>
  142. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  143. <html xmlns="http://www.w3.org/1999/xhtml">
  144.   <head>
  145.     <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
  146.     <title>Google Maps JavaScript API Example</title>
  147.     <!-- http://localhost/ -->
  148.  
  149. <script src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=ABQIAAAAvkm9X7SZvAyCad0c7GjwBhT2yXp_ZAY8_ufC3CFXhHIE1NvwkxRo7UhGG5tiSNLbTXH8AFUQzXffZg"
  150.     type="text/javascript"></script>
  151.  
  152. <!-- http://www.ganchiku.com/ -->
  153. <!--
  154.     <script src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=ABQIAAAAvkm9X7SZvAyCad0c7GjwBhR3SquV7UZw5piZBM9SKA5Ap0BlHxRIXFNr6SBh14IfS1Z5EVaMbEE3jw"
  155.       type="text/javascript">
  156. -->
  157. <script type="text/javascript">
  158.  
  159. //<![CDATA[
  160.  
  161. function load() {
  162.     if (GBrowserIsCompatible()) {
  163.         var map = new GMap2(document.getElementById("map"));
  164.         var geocoder = new GClientGeocoder();
  165.         var address = '<?php echo $app->getShopAddress(); ?>';
  166.         map.addControl(new GLargeMapControl());
  167.         map.addControl(new GMapTypeControl());
  168.         map.addControl(new GOverviewMapControl());
  169.         geocoder.getLatLng(address, function(point) {
  170.             if (!point) {
  171.                 alert("存在する住所(一部可)を入力してください。");
  172.             } else {
  173.                 map.setCenter(point, 13);}
  174.         });
  175.     }
  176.     var shopAddress = '';
  177.     <?php foreach ($app->getResult() as $result) : ?>
  178.     shopAddress = '<?php echo array_shift(explode(" ", $result['ShopAddress'])); ?>';
  179.     geocoder.getLatLng(shopAddress, function(point) {
  180.         if (point) {
  181.             var marker = new GMarker(point);
  182.             var windowTabs = [
  183.                 <?php foreach (HotPepperApp::getTabHtml($result) as $tab => $content) : ?>
  184.                 new GInfoWindowTab("<?php echo $tab; ?>", "<?php echo $content; ?>"),
  185.                     <?php endforeach; ?>
  186.                     ];
  187.             map.addOverlay(marker);
  188.             GEvent.addListener(marker, "click", function() {
  189.                 marker.openInfoWindowTabsHtml(windowTabs);
  190.             });
  191.         }
  192.     });
  193.     <?php endforeach; ?>
  194. }
  195.  
  196. //]]>
  197. </script>
  198.   </head>
  199.   <body onload="load()" onunload="GUnload()">
  200.     <div id="form">
  201.     <form method="get" action="hotpepper.php">
  202.     <input type="text" name="ShopAddress" value="<?php echo $app-/>getShopAddress(); ?>" size="64" maxlength="128">
  203.     <?php echo $app->getOptions('Genre'); ?>
  204.     <select name="Count">
  205. <?php
  206. for($i = 0; $i <10; $i++)  {
  207.     $value = ($i+1) * 10;
  208.     echo '<option value="'.$value.'"';
  209.     if (isset($_REQUEST['Count']) and $_REQUEST['Count'] == $value) {
  210.         echo ' selected="selected"';
  211.     }
  212.     $output .= '>' . $item[$master .'Name'] . '';
  213.     echo '>'.$value.'件';
  214. }
  215. ?>
  216.     </select>
  217.     <input name="search" type="submit" value="検索"/>
  218.     </form>
  219.     <?php if (isset(<