我正在使用URI.unescape取消转义文本,不幸的是,我遇到了奇怪的错误:

 # encoding: utf-8
 require('uri')
 URI.unescape("%C3%9Fą")

结果是
 C:/Ruby193/lib/ruby/1.9.1/uri/common.rb:331:in `gsub': incompatible character encodings: ASCII-8BIT and UTF-8 (Encoding::CompatibilityError)
    from C:/Ruby193/lib/ruby/1.9.1/uri/common.rb:331:in `unescape'
    from C:/Ruby193/lib/ruby/1.9.1/uri/common.rb:649:in `unescape'
    from exe/fail.rb:3:in `<main>'

为什么?

最佳答案

对于非ASCII输入,URI.unescape的实现被破坏了。 1.9.3 version看起来像这样:

def unescape(str, escaped = @regexp[:ESCAPED])
  str.gsub(escaped) { [$&[1, 2].hex].pack('C') }.force_encoding(str.encoding)
end

使用的正则表达式为/%[a-fA-F\d]{2}/。因此,它通过字符串寻找百分号,后跟两个十六进制数字。块$&中的将是匹配的文本(例如'%C3'),而$&[1,2]是没有前导百分号的匹配文本('C3')。然后,我们调用 String#hex 将十六进制数转换为Fixnum(195),并将其包装在Array([195])中,以便我们可以使用 Array#pack 为我们进行字节处理。问题是pack给了我们一个二进制字节:
> puts [195].pack('C').encoding
ASCII-8BIT

ASCII-8BIT编码也称为“二进制”(即没有特定编码的纯字节)。然后,该块返回该字节, String#gsub 尝试将str正在处理的gsub插入UTF-8编码的副本中,并且出现错误:



因为您不能(通常)仅将二进制字节填充到UTF-8字符串中;您通常可以摆脱它:
URI.unescape("%C3%9F")         # Works
URI.unescape("%C3µ")           # Fails
URI.unescape("µ")              # Works, but nothing to gsub here
URI.unescape("%C3%9Fµ")        # Fails
URI.unescape("%C3%9Fpancakes") # Works

一旦开始将非ASCII数据混合到URL编码的字符串中,事情就会开始崩溃。

一个简单的解决方法是在尝试解码之前将字符串切换为二进制:
def unescape(str, escaped = @regexp[:ESCAPED])
  encoding = str.encoding
  str = str.dup.force_encoding('binary')
  str.gsub(escaped) { [$&[1, 2].hex].pack('C') }.force_encoding(encoding)
end

另一个选择是将force_encoding推送到块中:
def unescape(str, escaped = @regexp[:ESCAPED])
  str.gsub(escaped) { [$&[1, 2].hex].pack('C').force_encoding(encoding) }
end

我不确定gsub为什么在某些情况下会失败,而在其他情况下会成功。

10-07 19:14