SimpleWikipediaAPI を ruby から使う

WikipediaAPI - ウィキペディア情報をサイトで利用できるAPI
Wikipediaの項目をひいて,XML やら JSON で返してくれる素晴らしい APIPerl から使えるライブラリは既にある(YappoLogs: WebService::SimpleAPI::Wikipedia)ので,ruby から使えるようにしてみた。基本的には川o・-・)<2nd life - TDD で作る RakuAPI ライブラリの真似というかコピペというか。元の SimpleWikipediaAPI には色々オプションがあるのですが,とりあえずキーワードだけ与えて検索結果が XML で返るだけです(id:secondlife さんのをソックリ真似したから…)。そんでもって TDD も真似っ子。

# 上の階層の lib ディレクトリをライブラリパスに追加
$LOAD_PATH << File.dirname(__FILE__) + '/../lib'
require 'test/unit'
require 'simple_wikipedia_api'

class SimpleWikipediaAPITest < Test::Unit::TestCase

	def setup
		@simple_wikipedia = SimpleWikipediaAPI.new({:proxy_host => 'foo', :proxy_port => 8000, :timeout => 10})
	end

	def test_instance
		assert_instance_of SimpleWikipediaAPI, @simple_wikipedia
	end

	def test_keyword
		results = @simple_wikipedia.keyword('YouTube')
		assert_instance_of Array, results
		results.each do |result|
			# 構造体かどうか
			assert_kind_of Struct, result
			# 構造体メンバーの型チェック
			struct_methods_call result, %w(sid length) do |method|
				assert_instance_of Fixnum, method
			end
			struct_methods_call result, %w(language url title body) do |method|
				assert_instance_of String, method
			end
			assert_instance_of Time, result.datetime
			# url は http で始まってるかどうか
			assert_match /^http/, result.url
		end
	end

	def test_show
		@simple_wikipedia.keyword('YouTube').each do |result|
			puts result.body.tosjis
		end
	end

	def struct_methods_call(struct, methods)
		methods.each do |method|
			yield struct.send(method)
		end
	end

end

id:secondlife さんも絶賛している(川o・-・)<2nd life - ruby のスクレイピングツールキット scrAPI)scrapi を使っています。しかし proxy に対応していなかったので,パッチを当てました。Reader クラスの read_page メソッドに以下のようなコードを追加。

        http = Net::HTTP.new(uri.host, uri.port, options[:proxy_host], options[:proxy_port])

Base クラスのオプションの定義も以下のように修正しました。

      READER_OPTIONS = [:last_modified, :etag, :redirect_limit, :user_agent, :timeout, :proxy_host, :proxy_port]

これで上記のテストコードが通るようになります。
ruby の実体はこんな感じ。今のところ動いているけど。

require 'kconv'
require 'uri'
require 'scrapi'

class SimpleWikipediaAPI

	WIKIPEDIA_API_URI = 'http://wikipedia.simpleapi.net/api'

	class SimpleWikipediaScraper < Scraper::Base

		elements = %w(language sid url title body length redirect strict datetime)
		elements.each {|el| process el, el => :text }

		def collect
			self.sid = sid.to_i
			self.length = length.to_i
			self.redirect = (redirect == 1)
			self.strict = (strict == 1)
			self.datetime = Time.iso8601(datetime)
		end

		result *elements
	end

	attr_accessor :options
	def initialize(options = {})
		@options = {
			:parser => :html_parser
		}.update options
	end

	def keyword(keyword)
		uri = URI.parse(WIKIPEDIA_API_URI)
		uri.query = queryize({:keyword => keyword.toutf8})
		Scraper.define do
			process 'result', 'results[]' => SimpleWikipediaScraper
			result :results
		end.scrape(uri, self.options)
	end

	private
	def queryize(hash)
		hash.map {|i| i.map {|j| URI.escape j.to_s }.join '=' }.join('&')
	end

end

まあ,ほとんど id:seconflife さんのコードのままです。ただ id メソッドって ruby で使われているので,なんか不具合がある気がして sid とか逃げてます。ちゃんとオプションを渡せるようにしなきゃいけないんだけど,きっと誰かがやってくれるだろう。つーかもうやっていると見た。

SimpleWikipediaAPI を ruby から使う(2)

上記のスクリプトでは返ってきた xmlruby の Struct オブジェクトに変換されますが,SimpleWikipediaAPI ではオプションで XML 以外の出力形式を指定できます。この場合は返ってきた文字列そのままもらえた方がいいでしょう。scrapi を使わないで,単純に url たたいたレスポンスのボディを使うと。JSON と callback は ruby で意味あるのかなあ。

SimpleWikipediaAPI を ruby から使う(3)

ハッシュでパラメータを与えて,レスポンスをそのまま返すメソッド api を追加。こんな感じ。
open-uri を require してあります。

	def api(params)
		protocol = 'http://' if /^http/ !~ self.options[:proxy_host]
		uri = URI.parse(WIKIPEDIA_API_URI)
		params.update({:keyword => params[:keyword].toutf8})
		uri.query = queryize(params)
		uri.read({:proxy => "#{protocol}#{self.options[:proxy_host]}:#{self.options[:proxy_port]}"})
	end

テストをいい加減に書く。徐々に厳しいテストを追加していくつもり。シフトJISにして出力しているのは,僕が Windows でテストしているためです。
rss は標準の rss パーサに与えてみた。XML は REXML に与えればいいのかな。

	def test_api_kw
		result = @simple_wikipedia.api({:keyword => 'YouTube'})
		assert_instance_of String, result
		puts result.tosjis
	end

	def test_api_xml
		result = @simple_wikipedia.api({:keyword => 'YouTube', :output => 'xml'})
		assert_instance_of String, result
		puts result.tosjis
	end

	def test_api_rss
		require 'rss/2.0'
		result = @simple_wikipedia.api({:keyword => 'YouTube', :output => 'rss'})
		assert_instance_of String, result
		rss = RSS::Parser.parse(result)
		puts result.tosjis
	end

	def test_api_html
		result = @simple_wikipedia.api({:keyword => 'YouTube', :output => 'html'})
		assert_instance_of String, result
		puts result.tosjis
	end

	def test_api_javascript
		result = @simple_wikipedia.api({:keyword => 'YouTube', :output => 'javascript'})
		assert_instance_of String, result
		puts result.tosjis
	end
	
	def test_api_json
		result = @simple_wikipedia.api({:keyword => 'YouTube', :output => 'json'})
		assert_instance_of String, result
		puts result
	end

	def test_api_json_callback
		result = @simple_wikipedia.api({:keyword => 'YouTube', :output => 'json', :callback => 'cb_func'})
		assert_instance_of String, result
		puts result
	end

テストの結果。どうやら日付に ISO8601 形式を使っていると解釈できないらしい。rss パーサに問題があるのか,rss の形式に問題があるのか今のところ不明。

Finished in 3.406 seconds.

  1) Error:
test_api_rss(SimpleWikipediaAPITest):
RSS::NotAvailableValueError: value <2006-09-23T13:14:35+09:00> of tag <pubDate> is not available.
    D:8:in `pubDate='
    D:/dev/ruby-1.8/lib/ruby/1.8/rss/parser.rb:393:in `__send__'
    D:/dev/ruby-1.8/lib/ruby/1.8/rss/parser.rb:393:in `start_get_text_element'
    D:/dev/ruby-1.8/lib/ruby/1.8/rss/parser.rb:329:in `call'
    D:/dev/ruby-1.8/lib/ruby/1.8/rss/parser.rb:329:in `tag_end'
    D:/dev/ruby-1.8/lib/ruby/1.8/rexml/parsers/streamparser.rb:26:in `parse'
    D:/dev/ruby-1.8/lib/ruby/1.8/rexml/document.rb:185:in `parse_stream'
    D:/dev/ruby-1.8/lib/ruby/1.8/rss/rexmlparser.rb:22:in `_parse'
    D:/dev/ruby-1.8/lib/ruby/1.8/rss/parser.rb:163:in `parse'
    D:/dev/ruby-1.8/lib/ruby/1.8/rss/parser.rb:78:in `parse'
    D:/Scripts/ruby/simple_wikipedia_api/test/test_simple_wikipedia_api.rb:57:in `test_api_rss'

10 tests, 27 assertions, 0 failures, 1 errors