Ruby,将字符串与 UTF-8 字符进行比较时出现问题

Ruby, problems comparing strings with UTF-8 characters

我有这 2 个 UTF-8 字符串:

a = "N\u01b0\u0303"
b = "N\u1eef"

它们看起来很不一样,但渲染后是一样的:

irb(main):039:0> puts "#{a} - #{b}"
Nữ - Nữ

a版本是我存到DB里的那个。 b 版本是来自浏览器的 POST 请求,我不知道为什么浏览器发送不同的 UTF8 字符组合,它是并非总是发生,我无法在我的开发环境中重现该问题,它发生在生产环境中并且占总请求的一定百分比。

情况是我尝试比较它们,但它们 return false:

irb(main):035:0> a == b
=> false

我尝试了不同的方法,例如强制编码

irb(main):022:0> c.force_encoding("UTF-8") == a.force_encoding("UTF-8")
=> false

另一个有趣的事实是:

irb(main):005:0> a.chars
=> ["N", "ư", "̃"]
irb(main):006:0> b.chars
=> ["N", "ữ"]

如何比较这些字符串?

这是 Unicode equivalence 的问题。

字符串的 a 版本由字符 ư(U+01B0:带有喇叭的拉丁文小写字母 U)和 U+0303 COMBINING TILDE 组成。顾名思义,第二个字符是 combining character,在呈现时它与前一个字符组合以生成最终字形。

字符串的 b 版本使用字符 (U+1EEF,带有喇叭和波浪线的拉丁文小写字母 U),它是单个字符,并且是 相当于之前的组合,但使用不同的字节序列来表示它。

为了比较这些字符串,您需要对它们进行规范化,以便它们对这些类型的字符使用相同的字节序列。 Ruby 的当前版本内置了此功能(在早期版本中,您需要使用第三方库)。

所以目前你有

a == b

这是 false,但如果你这样做

a.unicode_normalize == b.unicode_normalize

你应该得到 true.

如果您使用的是 Ruby 的旧版本,则有几个选项。 Rails 有一个 normalize 方法作为其多字节支持的一部分,所以如果你使用 Rails 你可以这样做:

a.mb_chars.normalize == b.mb_chars.normalize

或者类似的东西:

ActiveSupport::Multibyte::Unicode.normalize(a) == ActiveSupport::Multibyte::Unicode.normalize(b)

如果您不使用 Rails,那么您可以查看 unicode_utils gem,然后执行以下操作:

UnicodeUtils.nfkc(a) == UnicodeUtils.nfkc(b)

(nfkc指归一化形式,与其他技巧中默认相同。)

标准化 unicode 字符串有多种不同的方法(即使用分解版本还是组合版本),本示例仅使用默认值。我会把研究差异留给你。

你可以看到这些是不同的字符。 First and second. In the first case, it is using a modifier "combining tilde".

维基百科对此有一个部分:

Code point sequences that are defined as canonically equivalent are assumed to have the same appearance and meaning when printed or displayed. For example, the code point U+006E (the Latin lowercase "n") followed by U+0303 (the combining tilde "◌̃") is defined by Unicode to be canonically equivalent to the single code point U+00F1 (the lowercase letter "ñ" of the Spanish alphabet). Therefore, those sequences should be displayed in the same manner, should be treated in the same way by applications such as alphabetizing names or searching, and may be substituted for each other.

The standard also defines a text normalization procedure, called Unicode normalization, that replaces equivalent sequences of characters so that any two texts that are equivalent will be reduced to the same sequence of code points, called the normalization form or normal form of the original text.

似乎Ruby支持这种归一化,但是only as of Ruby 2.2:

http://ruby-doc.org/stdlib-2.2.0/libdoc/unicode_normalize/rdoc/String.html

a = "N\u01b0\u0303".unicode_normalize
b = "N\u1eef".unicode_normalize

a == b  # true

或者,如果您在 Rails 上使用 Ruby,似乎有一个 built-in method 用于标准化。