2010年11月7日星期日

Encoding,又是Encoding

话说,前几天在用Ruby的Mechanize抓一个台湾网站的时候,又遇到了一系列令人无语的事情。详细的过程我就不复述了,以下只是精简版。

假设我们用如下脚本:

agent = Mechanize.new
page = agent.get(url)
puts page.root.inner_html.encode('gbk') if page.root.css('#ln2').length == 0

去抓这么一个HTML页面:

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=big5" />
<title>TT</title>
</head>

<body>
<p id="ln1">豬八戒照鏡子——裏外不是人</p>
<p id="ln2">/你看不見我/</p>
</body>
</html>

程序输出的结果是:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=big5">
<title>TT</title>
</head>
<body>
<p id="ln1">豬八戒照鏡子——</p>
</body>
</html>

大陆的童鞋们可能不熟,不过对岸的童鞋肯定一眼就能看出问题在哪里。最早的Big5是非常不完善的一种编码,缺字缺字符,譬如“裏”字就没有。而根据我从Linux里学来的经验,CP950应该靠谱。因为CP950就是Big5外加一些扩充字符,而且大部分HTML页面都是在Windows里写的。于是代码和输出变成这样:

agent = Mechanize.new
page = agent.get(url)
page.encoding = 'cp950'
node = page.root.css('#ln2')
puts node.text.encoding # => UTF-8
puts node.text.encode('gbk') # => /你看不見我/
puts puts node.inner_html # => 乱码,不过内容是对的
######### 以下是诡异的部分 ###########
puts node.inner_html.encoding # => Big5
puts node.inner_html.encode('gbk') # => Encoding::UndefinedConversionError: "\xA1\xFE" to UTF-8 in conversion from Big5 to UTF-8 to GBK
puts node.inner_html.force_encoding('cp950').encoding # => Big5

首先,node.text的值是对的,证明page.encoding的设置生效了,可node.inner_html.encoding为什么会是Big5?然后,force_encoding无效?

第一个问题,难道是先get再设置encoding所导致,也就是Mechanize的错?可mechanize/page.rb里的代码显示,Mechanize是无辜的:

def encoding=(encoding)
  @encoding = encoding

  if @parser
    parser_encoding = @parser.encoding
    if (parser_encoding && parser_encoding.downcase) != (encoding && encoding.downcase)
      # lazy reinitialize the parser with the new encoding
      @parser = nil
    end
  end

  encoding
end

既然Mechanize没问题,那就去翻Nokogiri吧。一层又一层追下去之后,在nokogiri/xml/node.rb里找到了这么一段:

def serialize *args, &block
  options = args.first.is_a?(Hash) ? args.shift : {
    :encoding   => args[0],
    :save_with  => args[1] || SaveOptions::FORMAT
  }

  encoding = options[:encoding] || document.encoding

  outstring = ""
  if encoding && outstring.respond_to?(:force_encoding)
    outstring.force_encoding(Encoding.find(encoding))
  end
  io = StringIO.new(outstring)
  write_to io, options, &block
  io.string
end

包括上面提到的force_encoding无效的问题,答案呼之欲出了:

puts Encoding.find('CP950') # => Big5
puts Encoding.aliases['CP950'] # => Big5
puts Encoding.aliases['CP936'] # => GBK

总结下来,问题有两个:

  1. CP936和GBK很相似,但还是有不一样的地方,CP950和Big5差得就更多了,Ruby却混为一谈;
  2. Nokogiri内部使用libxml解析HTML,但同样的编码名称在libxml和Ruby眼里不是一样的东西。

第一个问题我觉得应该能算作Bug。不过从Ruby 1.9 Feature #1784,还有这里这里看来,Big5的复杂和纠结远超我过去的了解。假如Big5-UAO的码表能兼容CP950的话,或许将Big5-UAO当成CP950来用也不错。

第二个问题再次说明了ANSI是多么万恶的一种东西。直接用Nokogiri的话还可以在parse前先用Iconv将HTML转换为utf-8躲过去,而Mechanize就只能打补丁了。

(Mechanize其实也应该打补丁,很多简体的HTML把Charset声明为GB2312,可想而知会有什么毛病。只是大陆UTF-8推广还不错,问题不彰而已。)

好吧,反正我对Ruby 1.9使用ANSI表示不理解也不是第一次了。

11 条评论 :

minamimaster 说...

话说..em官方给你邮件了?

其实python也是ansi..

oCameLo 说...

怎么回事,不是说py3k只分str和bytes了么?

好吧,你也是py党。仔细想想,我相熟的人里边喜欢ruby多过rails和python的人,还真的很少很少很少很少……

顺便,可不可以问一下,你是因为em所以逛到这里来的?我这里向来很冷清,突然有个人冒出来,还蛮好奇的……

em的事情不太想在这里聊的说,有机会私下跟你讲吧。

Angus Chou 说...

python众飘过。说不上喜欢不喜欢,只是接触并学习的第一个脚本语言就是py罢了,之后就并没有需求和动力去学新的语言。高质量的python教程很多,在网上和阅读器里也经常见到宣传py的志愿者。ruby相比较之下要小众很多了吧。
我替minamimaster回答你吧...因为订阅了你的rss所以你有什么新的文章就会上来瞟一眼啊。
Em的事怎么啦?看到你在论坛上的帖子了,本来想跟个贴建议Yutaka用你的翻译的。等你有空跟你讨论讨论可以吗?我对软件业发生的事情一直比较感兴趣的。。。
联系邮箱zhouzh2 at google mail dot com

oCameLo 说...

要说我接触最早的脚本语言,究竟该说是perl呢,还是vbscript?反正python肯定是排不上的,更不要说ruby了……

喜欢哪一门语言是蛮靠缘分的事情。我接触python其实比ruby早很多,不过就是用不惯,没办法。

至于em,好吧,既然你们俩都想知道。

我和em作者邮件就俩来回,他的回信每次都很短,所以有些话他没明说,是我猜的。

第一封信里他问我能不能做一个繁体版,又问我是否期待一些报酬。我跟他解释说简繁体并不仅仅是写法不同,表达方式、词汇等方面的差异也非常大。虽然有些程序可以根据转换表转换一部分词汇,但转换结果个人阅读没什么,作为商用肯定是不行的,建议他找个台湾人做繁体版。

而报酬的问题,em似乎是有计划在未来的版本里内置多语言支持,所以对方的意思大概是这样:要么我把翻译免费送给他,包括版权在内;要么,我去应征他发布的招聘。

前者我不乐意。而后者,他把简繁体的翻译当成一个活儿跟韩文翻译并列,即使我解释了那一大通,他似乎也没有改变想法的意思。在第二封信里他就说,如果我想要获得这份工作,那就需要把简繁体翻译一起提交。还说要是我自己觉得转换结果足够好的话,可以用机器翻译来做。

事情大概就是这样了。说对方不尊重简繁体用户的使用习惯什么的,也有点过,毕竟一个外国人理解这个问题有难度。可要我用同文堂什么的转一下直接当成繁体版去提交,作为工作,我觉得太没职业道德了些。

嗯,这事基本上可以说是黄了吧。

minamimaster 说...

L10n这东西外人确实难理解,
港澳繁体直接繁化了(貌似?)勉强还能用,
但是台湾国语就不行,语法词汇差太远,表达方式也根本不和大陆一个样,
接触过的都知道.

p.s.可惜了众多em fan,以后在官网上永远找不到中文版了.
p.s.2要不干脆想办法找个做繁化的平分了这份工资?
p.s.3这东西确实送不出去,包含了几个人的工作

-------
只要把概念和建模弄清楚了,
哪个语言其实不太重要

3k是很美好,
问题在于gae只能2.x,
你就不得不面对永恒的默认ansi并和乱码打交道


其实我也是ruby用户(只会puts

有兴趣的话欢迎聊天~
minamimaster<% puts "@gm" %>ail.com

minamimaster 说...

如果对方是日本人的话,
我以前认识的几个日本人就是以繁体为标准学习中文的,
在他们看来(可能)繁体要比简体容易学得多,他们可能会认为所谓的繁体简体互转只是简单的转换一下就行了,不需要费啥事.

相比之下以前在的自由软件L10n小组的印欧语系外国人几乎每个都知道繁体和简体是两码事.

oCameLo 说...

嘛?难道你混11区的?


em的作者就是日本人啊,不过他好像不在日本而是在雷蒙德,不知道是不是混微软的。

英语美语都有差了,不太明白外国人为啥不理解两岸的文化差异这件事。

香港的科技用语以前都是跟着台湾,后来也掺和了不少大陆的翻译,再加上他们自己的,也蛮乱就是。

找个台湾人一起做这事儿不好操作的说,上哪去找啊……

em如果真由官方提供多语言支持也是好事,以后都不用自己再去找汉化包了。不过我总觉得em这两年的发展有点儿太快了些,小毛病越来越多,这可不是好现象。


我个人是觉得喜欢哪一门语言蛮重要的。虽然忘光了,不过照着手册让我写python也成,可我写的东西你们py众一看肯定就知道是外国人弄出来的,因为语法是会影响思维习惯的。而且除开语法本身,缺点、bug、第三方库等等,这些只能靠时间去慢慢了解和沉淀。


gae以后应该会支持3k的吧?嗯,就不知道要后到啥时候。

py作者现在在google,可google自己的东西都不用3k,这事儿说出来还真是不好听。


呃,希望你表介意……

“minamimaster<% puts "@gm" %>ail.com”在rails里能不能获得预期的结果我不知道,不过erb里这么写是错的。puts是向stdout输出,而erb相当于格式化字符串,所以应该用“<%= %>”才对……


3Q这么一闹腾,gtalk又被不少人捡起来乐。只是google的本地软件也太不给力了,你莫非也老是一个gmail开着?

minamimaster 说...

找个台湾人一起做这事儿不好操作的说,上哪去找啊……
>各大web2.0网站发布征集文...
用病毒式传播把人挖出来
台湾肯定有人用em

--

不在11区.

----

很早以前就一直用gtalk

----

只是发现自己很适应pythonic,
以后有时间肯定要把ruby深入学一下,

适应pythonic之后会发现ruby语句"很怪",
可能是END的原因,
另外发现有点不适应TMTOWTDI

minamimaster 说...

找到一个译本
http://forum.cpatch.org/viewthread.php?tid=12411

但是这个又是怎么回事?
http://www.ahasoft.com.tw/Emurasoft/index.htm

p.s.
一般提到的'中文化'多指繁体区的
'汉化'多指简中

oCameLo 说...

ahasoft那个,呃,可能算是em v6的授权经销商?…

我去联系另外那个人看看。不过就算他愿意,怎么分赃也是件麻烦的事…


end的事前几天我还跟人提起,python代码缩进层数一多看起来就会非常有喜感。嗯,果然是各花入各眼啊。

(ps:ruby里的end和END是不同的东西…)

不过吧,我真不觉得强制缩进以及缩进具有语法意义这件事算是python的优点。强调可读性是很好的,但是靠强制缩进来实现的话,有点想当然了。我就遇到过空格tab混用,解释器能认识,我却折腾半天不知道作者在写什么的脚本。另外,非要缩进用nano和notepad这种东西来写py也会变得非常难受。


我自己对TMTOWTDI的体会不深,但我也承认ruby语法糖是很多,能玩出好些magic出来。这一点上py更像java,的确更适合工业用。这应该是一个度的问题吧,比刻板谁又比得过java,这不应该是脚本语言的终极追求才对。


哈,欢迎来ruby国观光游览,不过我觉得你应该不会有想要移民的念头…

kevin li 说...

您好,我抓gb2312的时候也出问题了,内容只能抓一半,能否忽略掉这个错误啊。。