Mechanize(2.5.1) のエンコーディング周りに関するメモ
Mechanize#get などによって取得されたリソースは、Mechanize::Page のインスタンスとして返される。その際の Mechanize::Page の初期化プロセスの中で、取得したリソースの文字エンコーディングを推定している。
エンコーディング候補
以下で得られるエンコーディングを、エンコーディング候補として @encodings に格納する。
ボディのデータから推定する
- Mechanize::Util.detect_charset(body)
NKF.guess を利用して文字エンコーディングを推定する。
推定できた場合は ISO-2022-JP, EUC-JP, SHIFT_JIS, UTF-8, UTF-16, UTF-32 の様なエンコーディング文字列が得られる。
推定できなかった場合はデフォルト値として ISO-8859-1 を得る。
レスポンスヘッダから推定する
- Mechanize::Page.response_header_charset(response)
Content-Type ヘッダの値に charset という文字列が含まれている場合に、その値部分を取り出す。取り出した値が none という文字列であった場合は、取り出せなかったものとして扱う。
force_encoding
String#force_encoding が使える場合、ボディに対して ASCII-8BIT で force_encoding しておく。
body.force_encoding 'ASCII-8BIT' if body.respond_to? :force_encoding
meta 要素(1)
- Mechanize::Page.meta_charset body
charset 属性を持つ meta 要素があれば、その属性値をエンコーディングとして取り出す。
charset 属性を持つ meta 要素がなければ、http-equiv 属性値が content-type である meta 要素を探す。見つかればその content 属性値を取り出し、その中から charset= に続く部分をエンコーディングとして取り出す。
class << self def charset content_type charset = content_type[/;(?:\s*,)?\s*charset\s*=\s*([^()<>@,;:\\\"\/\[\]?={}\s]+)/i, 1] return nil if charset == 'none' charset end alias charset_from_content_type charset end def self.meta_charset body # HACK use .map body.scan(/<meta .*?>/i).map do |meta| if meta =~ /charset\s*=\s*(["'])?\s*(.+)\s*\1/i then $2 elsif meta =~ /http-equiv\s*=\s*(["'])?content-type\1/i then meta =~ /content\s*=\s*(["'])?(.*?)\1/i m_charset = charset $2 if $2 m_charset if m_charset end end.compact end
default_encoding
Mechanize#default_encoding がセットされていれば、それもエンコーディング候補としておく。
エンコード(パース)
以下は Mechanize::Page#parser のソース。
def parser return @parser if @parser return nil unless @body if @encoding then @parser = @mech.html_parser.parse html_body, nil, @encoding elsif mech.force_default_encoding then @parser = @mech.html_parser.parse html_body, nil, @mech.default_encoding else @encodings.reverse_each do |encoding| @parser = @mech.html_parser.parse html_body, nil, encoding break unless encoding_error? @parser end end @parser end
Mechanize::Page#encoding は内部で Mechanize::Page#parser.encoding を実行している。つまり、エンコーディングは最終的にHTMLパーサによって決められる。
ケース(1)
Mechanize::Page の @encoding は、Mechanize::Page#encoding= で指定する事が出来る(標準ではnil)。これで指定していると、そのエンコーディングを使用する事になる。
@parser = @mech.html_parser.parse html_body, nil, @encoding
ケース(2)
Mechanize#force_default_encoding を真にしている場合、Mechanize#default_encoding をエンコーディングとして強制する。
@parser = @mech.html_parser.parse html_body, nil, @mech.default_encoding
ケース(3)
ケース1,2に該当しない場合、先に推定しておいたエンコーディング候補で順にパースを試し、エンコーディングによるエラーが発生しなければそれが正しいエンコーディングであるとする。
@encodings.reverse_each do |encoding| @parser = @mech.html_parser.parse html_body, nil, encoding break unless encoding_error? @parser end
エンコーディングによるエラーが発生しているかは以下で判断する。
def encoding_error?(parser=nil) parser = self.parser unless parser return false if parser.errors.empty? parser.errors.any? do |error| error.message =~ /(indicate\ encoding)| (Invalid\ char)| (input\ conversion\ failed)/x end end
まとめ
Mechanize のエンコーディング周りについてざっくりと見てみました。特にまとめられる事もありませんが、あえてあげるとすれば「Nokogiriによるパースが文字コード関係で失敗すると、別のエンコーディングとして処理される事がある」という事でしょうか。
UTF-8のHTMLがあるとして、その中でUTF-8ではない文字が埋め込まれている場合、そのHTMLをMechanizeに処理させると ISO-8859-1 として扱われたりします。
他にも、内部的に iconv に依存しているため、iconv で扱えないエンコーディングは扱えない等といった問題もあった気がしますが、そこは整理できていません。
(OSX 10.6 の iconv が Windows-31J に対応していなくて例外終了するなど)