将(一些)Unicode 非间距标记与相关字母组合以进行统一处理

Combining (some) Unicode nonspacing marks with associated letters for uniform processing

我正在用 C# 编写文本处理 Windows 应用程序。该应用程序处理许多纯文本文件以计算字符、单词等。为此,该应用程序遍历每个文件中的字符。我发现一些文本文件使用 Unicode 字符 U+00E1(带尖音符号的小写字母 A)表示重音字母,例如 á,而其他文本文件使用简单的无重音符号 a(U+0061,小写字母A) 后跟 U+0301(组合尖音)。记事本或我用过的其他编辑器在屏幕上呈现文本的方式没有视觉差异,但底层字符流明显不同。

我想以同样的方式检测和处理这两种情况。换句话说,我希望我的应用程序将一个字母后跟一个组合代码点组合成等效的独立字符。例如,我想将序列 U+0061 U+0301 组合成 U+00E1。据我所知,除了对纯字母和组合字符的所有可能组合进行大量且容易出错的查找 table 之外,没有简单的算法可以做到这一点。

是否有更简单、更直接的算法来执行此组合?

您指的是 Unicode normalization forms。该页面介绍了一些有趣的细节,但要点是代表例如重音字母作为单个代码点(例如 á 作为 U+00E1)是规范化形式 C 或 NFC,作为单独的代码点(例如 á 作为 U+0061 U+0301)是 NFD。

Unicode specification goes into the gory details of how to implement it, with some extra details here.

的第 3.11 节

幸运的是,您不需要自己实现:string.Normalize() 已经存在。

"\u00E1".Normalize(NormalizationForm.FormD); // \u0061\u0301
"\u0061\u0301".Normalize(NormalizationForm.FormC); // \u00E1

也就是说,我们只是触及了“角色”是什么的皮毛。一个很好的例子是使用表情符号,但它也适用于各种脚本:有现代脚本,其中普通字符由两个代码点组成,并且没有可用的单个组合代码点。这会弹出例如泰米尔语和泰语,以及一些东欧语言 (IIRC)。

我最喜欢的例子是 ‍,或“女消防员:中等肤色”。想猜猜它是如何编码的吗?没错,4个不同的码位:U+1F469 U+1F3FD U+200D U+1F692.

  • U+1F469 是,女人表情符号。
  • U+1F3FD 是“Emoji Modifier Fitzpatrick Type-4”,它将之前的 emoji 修改为棕色肤色,呈现为单独出现时的效果。
  • U+200D 是一个“零宽度连接器”,用于将代码点粘合到同一个字符中
  • U+1F692 是,消防车表情符号。

所以你拿一个女人,加上棕色肤色,把她粘在消防车上,你就会得到一个棕色皮肤的女消防员。

(只是为了好玩,尝试将‍粘贴到各种编辑器中,然后在上面使用退格键。如果它正确呈现,一些编辑器将它变成然后然后删除它,而另一些则跳过各个部分。但是,你select 它作为单个字符。这反映了在某些脚本中编辑复杂字符的工作方式。

(另一个有趣的金块是国旗表情符号。Unicode 定义了“区域指示符号字母 A-Z”(U+1F1E6 到 U+1F1FF),国旗被编码为国家的 ISO 3166-1 alpha-2 字母国家使用这些指标符号的代码。So 后跟 。将 粘贴到 之后,出现一个标志!)

当然,如果您逐个代码点迭代此代码点,您将单独访问 U+1F469 U+1F3FD U+200D U+1F692,这可能不是您想要的。

如果你逐个字符地迭代这个字符,你会做得更糟,因为 surrogate pairs:像 U+1F469 这样的代码点太大了,无法使用单个 16-位字符,所以我们需要使用其中两个。这意味着如果您尝试遍历 U+1F469,您实际上会发现您有两个字符:0xD83D(高代理项)和 0xDC69(低代理项)。

相反,我们需要引入extended grapheme clusters, which represent what you'd traditionally think of as a single character. Again there's a bunch of complexity if you want to do this yourself, and again someone's helpfully done it for you: StringInfo.GetTextElementEnumerator. Note that this was a bit buggy pre-.NET 5, and didn't properly handle all EGCs

但是在 .NET 5 中:

// Number of chars, as 3 of the codepoints need to use surrogate pairs when
// encoded with UTF-16
"‍".Length; // 7

// Number of Unicode codepoints
"‍".EnumerateRunes().Count(); // 4

// Number of extended grapheme clusters
GetTextElements("‍").Count(); // 1

public static IEnumerable<string> GetTextElements(string s)
{
    TextElementEnumerator charEnum = StringInfo.GetTextElementEnumerator(s);
    while (charEnum.MoveNext())
    {
        yield return charEnum.GetTextElement();
    }
}

我在这里使用表情符号作为一个可以理解的例子,但这些问题也会出现在现代脚本中,使用文本的人需要注意这些问题。