Nokogiriに渡す前の文字コード判別

Nokogiriが確実に処理できるように、htmlを先に決め打ちでutf-8に変換する方法を模索してみた。
方針としては、

  • httpヘッダのcharsetは中身と一致しているとは限らないため参照しない。
  • metaタグのcharsetは信用する。
  • metaタグが無かったら自動判定。
  • 文字コードを変換したらcharsetが書いてあるmetaタグを抜く。

という方向で文字コード判定関数を書いた。判定は適当に試したところ特に問題はなかった。
次に、検出したコードを元にUTF8に変換するのだが、
Encoding::UndefinedConversionError
に遭遇。
http://charset.7jp.net/sjis.html
googlesjisと検索すると先頭に来るサイトですが0x81ADのコードで死ぬ。
Encoding::Shift_JISをEncoding::CP932に変えてみたが無駄であった。
(そもそも文字が無いところか?)

で、調べてみると
http://d.hatena.ne.jp/kitamomonga/20090701

Ruby 1.8 の String#tosjis などは「変換先の文字エンコーディングにない文字は切り捨てるか適当に変換」という処理をしていましたが、Ruby 1.9 の String#encode は変換先にない文字があった場合とりあえず例外を出します。

という記述が。
まさに探していた情報が見つかったので解決。
ついでにString#encodingの:undefと:replaceの技をいただきました。

#!/usr/bin/env ruby
# coding: utf-8
# 文字コードを判別してUTF-8に変換する

require 'strscan'
require 'open-uri'
require 'kconv'

# metaタグを探してcharsetを返す。見つかった場合はmetatagのバイト位置も返す。
def detect_encode(ascii8)
  s=StringScanner.new(ascii8)
  while true do
    # 捨て
    s.scan(/[^<]*/)
    # tag
    pos=s.pos
    tag=s.scan(/<([^>]*)>/i)
    if not tag then
      break
    end
    if tag[1, 4].downcase=='meta' then
      is_ready=false
      tag.scan(%r!([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]*))!i) do |m|
        key=m[0]
        value=m[1] or m[2] or m[3]
        if is_ready then
          charset=value.match(/charset=([\w-]*)/i)[1]
          # 後でmetaタグを捨てたり書き換えるために場所も返す
          return Encoding::find(charset), pos, tag.bytesize
        end
        if key.downcase=='http-equiv' and value.downcase=='content-type' then
          is_ready=true
        else
          break
        end
      end
    end
  end
  return Kconv.guess(ascii8), 0, 0
end

if $0 == __FILE__ then
  if ARGV.empty? then
    html=open('sjis.html', 'r:ascii-8bit').read
  else
    html=open(ARGV[0], 'r:ascii-8bit').read
  end
  encoding, pos, len=detect_encode(html)
  p encoding
  puts html[pos, len]

  if encoding==Encoding::Shift_JIS then
    # 決め打ち
    encoding=Encoding::CP932
  end

  utf8=html.encode(Encoding::UTF_8, encoding, 
                   #:undef => :replace, :replace => '・')
                   # 全角はよろしくない
                   :undef => :replace, :replace => '_')

  open('utf8.html', 'wb').write utf8
end