プログラマでありたい

おっさんになっても、プログラマでありつづけたい

Ruby製のクローラー Anemoneの文字化け対策

 何度も取り上げていますが、Ruby製のクローラーであるAnemoneについてです。もう2年ほどメンテナンスされていないものの、Rubyの中のクローラー・フレームワークとしては未だに一番の完成度です。しかし、残念ながら幾つかの問題点があります。その中で日本語を扱う我々にとっては一番大きな問題は、文字化けです。

Anemoneの文字化けの原因



 Anemoneの文字化けの原因は、ずばりUTF-8以外の考慮が何もされていないためです。Anemoneが利用するHTMLパーサーであるNokogiriは、もともと内部的な文字コードをUTF-8として扱います。UTF-8以外の文字コードを扱う場合は、文字コードを指定して渡す必要があります。それにもかかわらず、AnemoneがHTMLをパースする時は、次のような実装になっています。

#
# Nokogiri document for the HTML body
#
def doc
  return @doc if @doc
  @doc = Nokogiri::HTML(@body) if @body && html? rescue nil
end

 この実装ではUTF-8やASCII以外の場合は、文字化けが発生します。

小手先のAnemoneの文字化け対策



 対策の1つとしては、AnemoneがパースしたNokogiriドキュメントを使わない方法があります。
取得先のページの文字コードをみて、String#encodeを使って明示的に変換します。

require 'anemone'

urls = []
urls.push("http://www.amazon.co.jp/gp/bestsellers/books/466282/")

Anemone.crawl(urls, :depth_limit => 0) do |anemone|
  anemone.on_every_page do |page|
    doc = Nokogiri::HTML.parse(page.body.encode("utf-8","shift_jis"))
    puts doc.xpath("//title").text
  end
end

 実行すると、この通り文字化けせずに結果を取得できます。

$ ruby anemone-convert.rb 
Amazon.co.jp ベストセラー: ビジネス・経済 の中で最も人気のある商品です

抜本的なAnemoneの文字化け対策



 一方で、折角Anemone内部でNokogiriドキュメントを作っているのに、それを切り捨てて新たなNokogoriドキュメントを作るのは無駄以外の何者でもありません。また、複数のサイトをクローリングする際は、決め打ちの文字コードの対処はできません。抜本的な対策としては、Anemone内部の問題を解決するのが良いでしょう。

 AnemoneがNokogiriドキュメントを生成しているのは、Anemone::Pageクラスのdocメソッドで実体はlib/anemone/paage.rbにあります。問題は、変換の際の文字コードに何を指定すれば良いかという点です。Anemoneが扱うのは、HTMLです。なので、HTML中からcharsetを抜き出し、それをセットすることにしましょう。幸い、Anemoneにはcontent_typeというメソッドがあり、HTMLヘッダーからcontent-typeを抜き出してくれています。ここからcharsetを抜き出せば大丈夫です。

#
# Nokogiri document for the HTML body
#
def doc
  return @doc if @doc
  if @body && html?
if charset == 'utf-8' || charset.nil?
  body = @body
else
  body = @body.encode("UTF-8", charset, :invalid => :replace, :undef => :replace) rescue nil
end
@doc = Nokogiri::HTML(body) if body
  end
end

#
# The content-type returned by the HTTP request for this page
#
def content_type
  headers['content-type'].first
end

#
# The charset returned by the content-type request for this page
#
def charset
  matcher = content_type.match(/charset=[\"]?([a-zA-Z\_\-\d]*)[\"]?/)
  matcher[1].downcase if matcher
end

 実装している途中で気がついたのですが、anemoneのPull Requestの中にほぼ同じ実装がありました。totothinkという方によるForkです。
totothink/anemone · GitHub
 
 私の方では、少し実装を変えています。content_typeのマッチの部分が「_」が含まれていなかった為にShift_JISに対応出来かなったので追加しています。また、もともとのHTMLのbodyが格納されている@body変数も、変換することなく元のままで残しております。その方が、後々使いやすいと考えたからです。

追記 2014/04/07
 content_typeだけで大丈夫なのというご指摘ありました。その通りだと思います。が、必要に応じて、変更していけば良いと思います。(MetaタグとかLangとか優先順位をつけて判定)ちなみに@bodyに入っている文字列をString#encodingで判定しようとしたのですが、必ずUS_ASCIIと判定されるので使えません。

Fork版のGitHubからのインストール



 GitHubにForkして、修正版を置いています。興味あれば使ってください。

git clone https://github.com/takuros/anemone.git
cd anemone
gem build anemone.gemspec
gem install anemone-0.7.2.gem

まとめ



 Pythonには、ScrapyというCrawlerのフレームワークがあります。今でも開発は活発です。RubyのCrawlerフレームワークとして網羅的な機能を持つのはAnemoneしかありません。Anemoneは残念ながら、もう開発されていない模様です。Pull Requestも幾つもあるけど、取り込まれる様子はありません。Forkで何か新しいの作っても良いのかもしれませんね。


See Also:
あらためてRuby製のクローラー、"anemone"を調べてみた
オープンソースのRubyのWebクローラー"Anemone"を使ってみる
JavaScriptにも対応出来るruby製のクローラー、Masqueを試してみる
複数並行可能なRubyのクローラー、「cosmicrawler」を試してみた
takuros/anemone · GitHub


参照:
chriskite/anemone · GitHub
anemone RDoc


Rubyによるクローラー開発技法

Rubyによるクローラー開発技法

Spidering hacks―ウェブ情報ラクラク取得テクニック101選

Spidering hacks―ウェブ情報ラクラク取得テクニック101選