10. 日语字符的处理

10.1 若包含汉字的脚本输出乱码或无法正常运行时,该如何处理?

在ruby 1.6以后的版本中,默认情况下不会对汉字代码进行特殊的解释。若想处理汉字时,必须使用ruby -Ke等来设置$KCODE

若想在Windows上使用SJIS时,应该设为ruby -Ks;若想在UNIX系列OS上使用EUC时,应该设为ruby -Ke

另外,若在脚本首行中添加如下代码时

#! ruby -Ks

就可以将设定的选项置入脚本之中。恐怕这就是最常用的解决方法。

10.2 选项-K和$KCODE有什么不同?

产生效果的时机不同。

例如,在SJIS编码文件中出现下列代码的话

$KCODE = 'SJIS'
s = "表"

在设定$KCODE的值时,脚本的解析过程已经结束。(因为$KCODE的默认值是"NONE")所以字符串没有被看作是多字节内容。

如果使用选项-K来指定汉字编码的话,则在读入脚本之前已经生效,所以解析脚本时汉字代码会被正确识别。

另外,若包含汉字代码的脚本出现问题时,多半是因为JIS编码的汉字代码中包含与反斜杠("\")相同的代码所致。

10.3 可以使用日语标识符吗?

只要正确设置了-K选项,就可以使用日语标识符。以日语汉字开头的变量名相当于以小写字母开头的变量名。但它的可移植性较低,我们不推荐您这样做。

在Hash中,也可以使用日语标识符,而且比较安全。

var = {'变量' => '值'}
var['变量'] = 1

10.4 如何从包含日语字符的字符串中依次抽出1个字符?

设定$KCODE之后,使用split(//)或scan(/./)即可。

10.5 tr("あ","a")运作不正常,应如何处置?

内部的String#tr在进行变换时,是以字节为单位的。在添加了require "jcode"之后,将以日语字符为单位进行处理。 (请参考jcode.rb)

10.6 如何对平假名进行排序?

下例是对平假名进行排序的例子(忽略了浊音、半浊音、拗音和拨音)。

require "jcode"

a = "ぁぃぅぇぉがぎぐげござじずぜぞだぢづでど" \
    "ばびぶべぼぱぴぷぺぽゃゅょっゎ"
b = "あいうえおかきくけこさしすせそたちつてと" \
    "はひふへほはひふへほやゆよつわ"

ary = %w(ふー ばー ばず)

p ary.sort
p ary.collect{|l| [l.tr(a,b), l]}.sort.collect!{|e| e[1]}

# => ["ばー", "ばず", "ふー"]
#    ["ばー", "ばず", "ふー"]

另外,在1.7以后的版本中可以使用Enumerable#sort_by来这样改写最后一行

p ary.sort_by {|l| l.tr(a,b)}

即可。

10.7 如何用空白来替代SJIS中从84BF到889F之间的系统相关代码?

尽管您可以使用正则表达式中表示范围的[あ-ん],但直接写出系统相关字符会影响程序的可读性。但也不能写成

gsub(/[\x84\xbf-\x88\x9f]/s, ' ')

这样。此时应该使用如下的小技巧

gsub(Regexp.compile("[\x84\xbf-\x88\x9f]", nil, 's'), ' ')
或
gsub(/#{"[\x84\xbf-\x88\x9f]"}/s, ' ')

用数值来表示2字节的代码,并将其替换为空白字符。(其实并非替换成空白字符,而是("〓"))

10.8 如何进行全角-半角字符的变换?

标准方法是使用nkf.so库或jcode.rb库进行变换。另外,还可以使用[RAA:Kakasi]库进行变换。

请参考[ruby-list:10505], [ruby-list:25839], [ruby-list:31238], [ruby-list:31240], [ruby-list:31508]等等

10.9 关于半角假名的问题

Ruby不支持半角假名。

# 在下例中,请将"ア"看作半角假名
ruby -Ks -e 'p "あア"'
=> "あ\261"

据说现在开发中的M17N版ruby就不会出现这种问题。

10.10 怎样从包含日语字符的字符串中抽出n字节的内容?

抽出多字节字符时,可能会将字符一分为二。此时,若正确设定了$KCODE的话,/./就不会匹配拆散的字符了。应对其加以充分利用

$KCODE = "e"
p /./ =~ "あ"[0,1]    # => nil

# 注: 如果它并不是汉字的构成要素的话,就会进行匹配。

p /./ =~ "\xff"       # => 0

下例中定义了一个jleft方法,它会从字符串左侧起抽出至多len字节的内容。

class String
  def jleft(len)
    return "" if len <= 0

    str = self[0,len]
    if /.\z/ !~ str
        str[-1,1] = ''
    end
    str
  end
end

$KCODE = 'e'
s = "あいうえお"
for i in -2 .. s.size+2
    p [i, s.jleft(i)]
end

=> [-2, ""]
   [-1, ""]
   [0, ""]
   [1, ""]
   [2, "あ"]
   [3, "あ"]
   [4, "あい"]
   [5, "あい"]
   [6, "あいう"]
   [7, "あいう"]
   [8, "あいうえ"]
   [9, "あいうえ"]
   [10, "あいうえお"]
   [11, "あいうえお"]
   [12, "あいうえお"]

若限定使用EUC编码时,可以使用下面的方法。

class String
  def jleft(len)
    return "" if len <= 0

    str = self[0, len]

    if str.count("\xa1-\xfe") % 2 == 1
      str[-1, 1] = ''
    end
    str
  end
end

注:上述方法都不支持3字节字符。

10.11 怎么让日语文本在第n个字处换行?

可以使用NKF-f选项。

require 'nkf'
p NKF.nkf("-ef11", "あいうえお、かきくけこ")

但是NKF会自动进行断字处理和空格调整,使我们无法进行精确控制。

若想自己来实现的话,可以使用10.10的方法。

class String
  def jfold(len)
    return "" if len <= 0

    right = self.delete("\r\n")
    while right and not right.empty?
      left, right = right.unpack("a#{len} a*")

      if /.\z/ !~ left
          right[0,0] = left[-1,1]
          left[-1,1] = ''
      end
      yield left
    end
  end
end

"あいうえお、かきくけこ".jfold(11) {|s|
  puts s
}
# =>  あいうえお
      、かきくけ
      こ

实际上,它是每过n字节就进行一次folding,但并未考虑到TAB的位置问题。与nkf比较起来,它的速度非常慢。