読者です 読者をやめる 読者になる 読者になる

あと味

たくさん情報を食べて、たくさん発信すると、あとになって味わい深い。

アマグラマーが初めてのAjaxのプログラムを作成するまでの道のり全容

言及 Tips ひとりごと

ここ数日、会社の勉強会でjQueryをすることになって、そのデモとしてAjaxを使ったプログラムを作っていました。まともにAjaxさわったのは初めて。ノウハウが必要そうです。
jQueryの勉強会の内容は、後日整理してアップしますので、お楽しみに。

で、プログラムを作るまでの道のりを備忘録もかねて投稿しようと思います。ぶっちゃけソースレベルは低いです。まだまだ改善すべき点は山ほどありますが、作ったプログラムはたぶん修正しないので、不完全ながら公開します。

作ったプログラム

scriptタグを埋め込めば、会社の制作事例を再生するブログパーツのようなもの

<script id="js" type="text/javascript" src="http://higashizm.sakura.ne.jp/parts/js/loading.js"></script>

これをHTMLのbody内に埋め込むと事例が再生されます。

妥協したこと

  • 年度を選択する機能(単に勉強会までに実装する時間がありませんでした)
  • クロスブラウザ(IEでエラーが出る?)
  • 意味不明な動作の解読

かなり妥協してますね。。。時間的な制限もあったので、ある程度は割り切りました。

開発方法

会社の事例はデータベースから引っ張ってくれば手っ取り早いのですが、俺、プログラマじゃないのでデータベースにアクセスさせてもらえません。なので、必要な事例のデータは会社のサイトからスクレイピングして引っ張ってくることにします。

内側からが無理なら、外側から攻めろと。

自分で契約したさくらのサーバーで使える言語で、サーバーサイドのプログラム部分を実装します。

スクレイピング対象のページは以下のとおり。idとかほとんど振ってないので、結構めんどい。

ホームページ制作・Webコンサルティング・システム開発ならWeb制作会社サーフボード

サーバー側のプログラミング言語PerlPHPPythonRubyのどれにするか非常に迷いました。

PerlPythonではスクレーパーを書いた経験があります。Pythonのスクレーパーは下記の記事で公開しています。
こうして私も1000枚のおっぱい画像を手に入れた - あと味

この時Pythonを選んだ理由は、Google App Engineが話題になり始めていたことと、単におっぱいとPythonがとても仲良しになれる気がしたからです。二つ合わせて「おPython」とすれば、神格化できます。

ライブラリのBeautiful Soupもおっぱいっぽいし、世の中うまいことできてる

でも選んだのはRuby

中ではRubyが最も知らない言語だということと、Rubyでプログラムを書きたかったからです。新しい言語を覚える時は、極力既存の知識を使うのではなく、言語の文化や哲学を学んでから実装したいと思っているので、初めてのRubyまるごと Ruby! Vol.1は予習しました。Rubyのみならず、他の言語でも、文化を学ぶためにはオライリーの一番分厚い本とまるごと○○を読むようにしています。

スクレイピングで取得したデータをずっと試してみたかったjsonで出力し、jQueryを使って(一応、jQueryの勉強会用ネタなので)さらに加工し、サイト上に表示するようにします。

環境構築

さくらのサーバーにRubyの環境を作りました。

Rubyは最初から組み込まれていますが、その環境を使ったライブラリのインストールがよくわかんないので、$HOMEにlocalディレクトリを作って、Rubyのインストールから始めました。

で、gem入れて、必要なライブラリのインストール。ついでにvimの環境も構築(vim-rubyと.vimrc)。

サーバーサイドのプログラミング(Rubyスクレイピング

vimでコーディングして、デバッグはMacのターミナルとpメソッド。Rubyはブラウザ上でデバッグするのがなんだか面倒そうですね。スクレイピングはHpricotを使いました。

以下がコード全体。

#!/home/higashizm/local/bin/ruby
require 'rubygems'
require 'hpricot'
require 'open-uri'
require 'kconv'
require 'json'
require 'cgi'

class Uri
  def initialize(uri)
    @@uri = uri
  end
  attr_accessor :uri
end

class Document < Uri
  def get_uris
    @uris = []
    @jsons = []
    doc  = Hpricot( open(@@uri).read )
    1.upto(10) do |i|
      uris << @@uri.sub(/p=[1-9]+$/, "p=#{i}")
      @uris = uris
      break if (doc/"/html/body/div/div[4]/table//tr/td[2]/table//tr/td[2]//a[#{i}]").empty?
    end
  end
  attr_accessor :uris, :jsons

  def to_json 
    @uris.each{|uri|
      next if uri == nil
      @doc = Hpricot( open(uri).read.toutf8 )
        2.upto(16) do |i|
          doc  = @doc
          break if (doc/"/html/body/div/div[4]/table//tr/td[2]/table[#{i+1}]").empty?
          img  = (doc/"/html/body/div/div[4]/table//tr/td[2]/table[#{i}]//tr/td/a/img").to_html
 
          temp_hash = 
            {
              'title'       => (doc/"/html/body/div/div[4]/table//tr/td[2]/table[#{i}]//tr/td[2]/table//tr/td/a/font").inner_html,
              'img_uri'     => (doc/"/html/body/div/div[4]/table//tr/td[2]/table[#{i}]//tr/td/a/img").to_html[/http.*\.jpg/],
              'link_uri'    => (doc/"/html/body/div/div[4]/table//tr/td[2]/table[#{i}]//tr/td[2]/table//tr[3]/td/a").inner_html,
              'description' => (doc/"/html/body/div/div[4]/table//tr/td[2]/table[#{i}]//tr/td[2]/table//tr[5]/td").inner_html
            }
          @jsons << temp_hash
        end
        @doc = nil
    }
    @jsons = JSON.pretty_generate(@jsons)
  end

  def send_jsonp(callback)
    puts "content-type: application/json\n\n"
    puts "#{callback}(#{@jsons})"
  end
end

cgi = CGI.new
yy = cgi['yy']
mode = cgi['mode']
p = cgi['p']
callback = cgi['callback']
uri = "http://www.surfboard.jp/web100/list_year.asp?" << "yy=" << yy << "&mode=" << mode << "&p=" << p
response = Document.new(uri)
response.get_uris
response.to_json
response.send_jsonp(callback)

Hpricotのメソッド内で、Xpathの記述がありますが、これはfirebugからコピペ。コピペしているのに動作しなくて2時間消費。firebugが勝手に作るtbodyに弄ばれました。。。

Hpricotはスクレイピングすごく楽。楽しく記述できる。

Rubyイテレータや?メソッドもわかりやすい。Rubyが楽しくプログラミングできるという理由がわかった気がしました。

Rubyのjsonに関する情報がちょっと少なかったのですが、jsonライブラリを使っています。pretty_generate使って値の確認してましたけど、実際はgenerateでいいですね。アクセサとかもデバッグ用に作ったけど、結局いらない。

配列にnilが入っちゃうところがあったんですけど、イマイチわかんないのでifで回避。完全にやっつけです。

全てssh上で作業していたので気づかなかったのですが、ローカルで実行した時にクロスドメインの制約に引っかかり、jsonじゃなく、jsonp使わなきゃいけないことに気づきました。jsonpは偉大だ。

callback関数の実装は、なかなか悩みました。

プログラムはどのようにまとめればいいか全然わかんないです。しっかり設計できるようになりたい。

クライアントサイドのプログラミング(jQueryAjaxの実装)

正直あまりjQuery活用できてない気がします。IEの挙動おかしいし。

もちろんJavaScript 第5版とまるごとJavaScriptは予習済み。

jsファイルはふたつ作りました。loading.jsとcontroller.jsです。

開発環境はvimfirebugfirebugなくしてjs書けないですね。

loading.js

ソースコードは以下。

(function() {
    var jquery = document.createElement('script');
    jquery.src = 'http://higashizm.sakura.ne.jp/parts/js/jquery.js';
    jquery.type = 'text/javascript';
    var controller = document.createElement('script');
    controller.src = 'http://higashizm.sakura.ne.jp/parts/js/controller.js';
    controller.type = 'text/javascript';
    document.getElementsByTagName('head')[0].appendChild(jquery);
    document.getElementsByTagName('head')[0].appendChild(controller);
})();

匿名関数で囲んだクロージャをすぐに実行。これで、jquery.jsとcontroller.jsを読み込んでます。scriptタグをひとつで済ませるための工夫だったんですが、これでクロスブラウザの問題が発生しているのかもと思ったり。

自宅にWindowsないので、検証できません。。。

id:amachangの以下の記事を参考にしました。
動的スクリプトローディング(さんざん既出だと思うけど - IT戦記

これでクロスブラウザの問題が発生しているのでなければ、汎用性も高いし、非常に使えるテクニックだと思います。

controller.js

ソースコードは以下。

$(function(){
  var div_wrapper = document.createElement('div');
  div_wrapper.id = 'wrapper';
  $('#js').after(div_wrapper);
  var cssObj = {
    height: '50px',
    paddingTop: '25px',
    backgroundColor: '#888',
    color: '#fff',
    textAlign: 'center'
  }
  $('#wrapper').css(cssObj);
  $('#wrapper').text('Now Loading');

  $.ajax({
    url : 'http://higashizm.sakura.ne.jp/parts/inccase.cgi',    
    dataType : 'jsonp',
    data : {
      yy : '2008',
      mode : '1',
      p : '1'
    },
    success : function(jsons){
      $('#wrapper').empty();
      var cssObj = {
        height: 'auto',
        paddingTop: '0',
        backgroundColor: 'transparent',
        color: '#000',
        textAlign: 'left'
      }
      $('#wrapper').css(cssObj);

      for (var i = 0; i < jsons.length; i++) {
        var div = document.createElement('div');
        div.id = 'case' + i;
        var h2  = document.createElement('h2');
        var img = document.createElement('img');
        var a   = document.createElement('a');
        var p   = document.createElement('p');
        $('#wrapper').append(div);
        $('#case' + i).append(h2).append(img).append(a).append(p);
        div = document.createElement('div');
        $('#case' + i + '> img').wrap(div);
        $('#case' + i + '> a').wrap(div);
        $('#case' + i).css('display', 'none');
      }
        
      for( i = 0; i < jsons.length; i++ ) {
        $('#case' + i + '> h2').text(jsons[i].title);
        $('#case' + i + '> div > img').attr({ src: jsons[i].img_uri, alt: jsons[i].title });
        $('#case' + i + '> div > a').text(jsons[i].link_uri); 
        $('#case' + i + '> div > a').attr({ href: jsons[i].link_uri });
        $('#case' + i + '> p').text(jsons[i].description);
      }

      var evalTexts = [] ;
      for ( var i = 0; i < jsons.length; i++ ) {
        evalTexts.push("setTimeout(function(){$('#case" + i + "').fadeIn(3000).fadeOut(3000, function(){$('#case" + i + "').remove();})}, 6000 * " + i + ");")
      }

      for( var i = 0; i < jsons.length; i++) {
        eval(evalTexts[i]);
      }
    },
    error : function() {
      alert('error');
    }
  });
});

CSSの記述は適当です。jQueryAjaxの記述がすごく楽ですね。jsonpのコールバック関数に戸惑っただけで、後はすんなりいきました。

ただ、すごく処理に困った箇所が以下の部分。多分、今回の開発で一番時間かかった。

      var evalTexts = [] ;
      for ( var i = 0; i < jsons.length; i++ ) {
        evalTexts.push("setTimeout(function(){$('#case" + i + "').fadeIn(3000).fadeOut(3000, function(){$('#case" + i + "').remove();})}, 6000 * " + i + ");")
      }

      for( var i = 0; i < jsons.length; i++) {
        eval(evalTexts[i]);
      }

for文の箇所です。前のコードで、#caseNという要素を事例の数だけ作っています。Nの箇所に入っている数字を使ってfor文を走らせつつ、#case0から順番に事例をfadeIn()fadeOut()したかったんですけど、fadeIn()が始まる前にインクリメントが動いちゃって、すごく困りました。

別のコードにして書くとこんな感じ。これで#a1から#a3までを順番にfadeIn()fadeOut()したかった。

<script type="text/javascript">
$(function(){
for (var i = 1; i < 3; i++) {
$('a' + i).fadeIn(2000).fadeOut(3000);
}
});
</script>
<div id="a1" style="display:none;">a1 now printing</div>
<div id="a2" style="display:none;">a2 now printing</div>
<div id="a3" style="display:none;">a3 now printing</div> 

いろいろ試してみたんですけどうまくいかず、最終的に文字列形式で配列に格納して、その後evalで実行する方法を取っています。

でもこれ絶対おかしい。不自然すぎる気がする。かと言って的確な情報にも行き着かず。。。

この部分は良い解法があれば修正したいです。よくありそうなパターンなので。ついでにクロスブラウザにも対応しようかな。。。

最後に

識者の方が見ると、とんでもないコードかもしれませんが、自分なりにはがんばれました。(実質2日間しかコーディング時間とれなかったし)

やってみてJavaScriptの奥深さと、Rubyの楽しさを体験できたので、未だに決めかねていた主力言語はRubyにしようかなと思いつつあります。もちろんJavaScriptも。RubyJavaScriptは共通点があって相性が良さそうです。

ということで、今後もプログラミングを継続しつつ、2,3年を目処に自分でサービスを立ち上げるまでのレベルに持っていきます。

って俺ディレクターなんだけどw

追記1

クロスブラウザ対応を妥協するのはあんまりよろしくないですよねという指摘もあったので、後日その部分を修正することにします。*1

東京への引っ越しとjQuery勉強会のアップ準備に時間が取られるので、再来週とかかな。がんばろう。

追記2

簡易に確認するためにサーバーにアップしときました。見出し以外は基本的にscriptタグが入ってるだけです。

Demo

追記3修正版

Demo

追記3

IEのエラーはcontroller.jsを遅延ロードしてないからっぽいです。

以下のように直すとエラーは消えました。

(function() {
    var jquery = document.createElement('script');
    jquery.src = 'http://higashizm.sakura.ne.jp/parts/js/jquery.js';
    jquery.type = 'text/javascript';
    var controller = document.createElement('script');
    controller.src = 'http://higashizm.sakura.ne.jp/parts/js/controller.js';
    controller.type = 'text/javascript';
    document.getElementsByTagName('head')[0].appendChild(jquery);
    function wait(a,func) {
      var check = 0;
        try{
            eval("check =" + a);
        }catch(e){
        }
        if(check){
          func();
        }else{
          var f = function(){wait(a,func)};
          setTimeout(f,100);
        }
    }
    wait('jQuery.each',function(){document.getElementsByTagName('head')[0].appendChild(controller)});
})();

この記事を参考にさせていただきました。

JavaScriptで遅延ロードをする方法についてのおさらい - Clouder::Blogger

いずれにしてもCPUめちゃくちゃ食うし、このあたりのノウハウは今後学んで修正できるようにしておきたいところです。

*1:Safariは動いてるみたいですね。エラー出てたと思ったんだけど、どこか直した過程でエラーなくなったのかな?