2010年11月14日星期日

Encoding,还是Encoding

话说,上次发现台湾用big5-uao很多,于是和某台湾人士讨论了一下,而下面要说的问题就是在讨论过程中遇到的。

嗯,该问题虽然还是跟Encoding有关,不过这次倒不能说是Ruby的错。

以下是代码:

require 'mechanize'

agent = Mechanize.new
# 请自备梯子
agent.set_proxy('127.0.0.1', '8118')
# 该页面有用big5-uao编码的字符串
url = 'http://www.ptt.cc/man/Japanese-B95/index.html'
page = agent.get(url)
t = page.root.css('#finds > p')[0]

puts t.text.encoding
p t.text
p t.text.encode('gbk')
puts t.to_s.encoding
p t.to_s
p t.to_s.encode('gbk')

# 以下为程序输出:

#UTF-8
#"\u60A8\u73FE\u5728\u7684\u4F4D\u7F6E\u662F Japanese-B95 -          "
#"您現在的位置是 Japanese-B95 -          "
#Big5
#"<p>\x{B17A}\x{B27B}\x{A662}\x{AABA}\x{A6EC}\x{B86D}\x{AC4F} Japanese-B95 -          </p>"
#"<p>您現在的位置是 Japanese-B95 -          </p>"
#[#<Nokogiri::XML::SyntaxError: input conversion failed due to input error, bytes 0x93 0xAB 0xC7 0x66>, #<Nokogiri::XML::SyntaxError: input conversion failed due to input error, bytes 0x93 0xAB 0xC7 0x66>, #<Nokogiri::XML::SyntaxError: htmlCheckEncoding: encoder error>, #<Nokogiri::XML::SyntaxError: input conversion failed due to input error, bytes 0x93 0xAB 0xC7 0x66>, #<Nokogiri::XML::SyntaxError: encoder error>]

以我对Mechanize和Nokogiri的理解,该脚本的输出很正常。因为Nokogiri在其内部使用libxml解析HTML,而libxml使用libiconv处理编码问题。libiconv用big5去解码big5-uao字符串并尝试转换为utf-8,遇到不认识的部分时中断处理并丢弃剩余的部分,libxml则补完HTML的关闭标签并返回给Nokogiri。

(注:用Nokogiri去抓有编码问题的HTML页面时并不会主动报错,因此必须去检查page.root.errors,这很重要。)

然而这么一个符合我认知的脚本在某人的Mac上却跑出了不一样的结果。以下是稍微修改了一点的代码:

puts t.text.encoding
p t.text.lines.first.force_encoding('utf-8')
p t.text.lines.first.force_encoding('utf-8').encode('gbk', :undef=>:replace)
puts t.to_s.encoding
p t.to_s.lines.first
p t.to_s.lines.first.force_encoding('big5-uao').encode('gbk')

# 以下为程序输出:

#ASCII-8BIT
#"\u60A8\u73FE\u5728\u7684\u4F4D\u7F6E\u662F Japanese-B95 -          \uE66B\uF735\uE6C3\uF735\u4E2D\n"
#"您現在的位置是 Japanese-B95 -          ????中\n"
#Big5
#"<p>\x{B17A}\x{B27B}\x{A662}\x{AABA}\x{A6EC}\x{B86D}\x{AC4F} Japanese-B95 -          \x93\x{ABC7}f\x94D\x{C766}\x{A4A4}\n"
#"<p>您現在的位置是 Japanese-B95 -          読み込み中\n"
#[]

t.text.encoding不知为何变成了ascii-8bit,内容倒仍然是utf-8,可是却包含无效字符。而且libxml解析该页面没有发现错误(page.root.errors为空),t.to_s还居然能拿到正确的内容。

伊妹来伊妹去,最后我们俩在gtalk上兜了半天圈子,终于找到了问题所在。原来,他的Nokogiri用的并不是libiconv,而是ICU(IBM的那个,OSX里边默认安装),就这样而已。

(插花,t.text.encoding变成ascii-8bit的问题应该是Nokogiri的bug,这个还没去追的说。)

big5-uao只是一个big5的扩展,也就是在big5的private use area里安排了很多本不存在于big5里的内容。libiconv用严格符合big5的码表去解码big5-uao,于是就出错了。

而ICU的处理策略跟libiconv不同。在ICU的big5码表中,将big5的PUA与Unicode的PUA一一对应(第二个脚本输出的“\uE66B\uF735\uE6C3\uF735”这四个字符就在PUA里)。这样做的好处是即使字符串内包含非法字符,big5到utf-8再到big5也能还原到原始字符串而不会丢失信息。坏处么,ICU和别的编码转换工具混用会变成一件非常危险的事情。

虽然非常偏门,不过对处理big5-uao有需要的同学,可以试试这个:http://www.boxcn.net/shared/iu0uo12l70

压缩包里有我弄的一个为libiconv增加big5-uao的补丁(该补丁非常初步,不过正确性应该不成问题)以及编译好的dll,还有libxml以及相关dll。如果是动态链接的库,换上libiconv就行,只是Windows里gem安装的Nokogiri是静态编译,要处理big5-uao还需要继续改。

打开Nokogiri里的lib/nokogiri/ffi/libxml.rb,开头的部分改成这样:

# :stopdoc:
module Nokogiri
  module LibXML
    extend FFI::Library
    if RbConfig::CONFIG['host_os'] =~ /(mswin|mingw)/i
      raise(RuntimeError, "Nokogiri requires JRuby 1.4.0 or later on Windows") if RUBY_PLATFORM =~ /java/ && JRUBY_VERSION < "1.4.0"

再把压缩包里的所有dll文件放到lib/ext/nokogiri下,gem装上ffi,就可以这么用了:

ENV['NOKOGIRI_FFI'] = '1'
require 'mechanize'

agent = Mechanize.new
agent.set_proxy('127.0.0.1', '8118')

url = 'http://www.ptt.cc/man/Japanese-B95/index.html'
page = agent.get(url)
page.encoding = 'big5-uao'

t = page.root.css('#finds > p')[0]
puts t.text.encoding # => ASCII-8BIT,这里还是不对,仍然需要force一下
puts t.to_s.encoding # => Big5-UAO

貌似我应该去找找text的问题,然后给Nokogiri提交两个补丁…

git代码库里的Nokogiri已经将ffi的部分移除,以后如果要处理big5-uao,看来只能自己编译Nokogiri了。

没有评论 :