Unicode字符串带有变音符号,按字符拆分

14
我有一个Unicode字符串:Ааа́Ббб́Ввв́Г㥴Дд 我想按字符拆分它。目前,如果我尝试循环遍历所有字符,我会得到类似这样的结果: A a a ' Б ... 有没有一种正确的方法将此字符串拆分为字符:А а а́

你是如何循环遍历字符的? - Nivas
@Nivas并不重要,从JavaScript的角度来看,"а́"是由2个字符组成的。"а" + "́" === "а́" - Esailija
@Esailija 不用在意。由于某种原因,我以为这是一个Java问题。没有读标签(也没有标题)... - Nivas
2
@Nivas 自从ES6发布以来,你如何迭代实际上会产生很大的差异,因为for..of使用String.prototype[Symbol.iterator],它按代码点步长进行迭代(有时超过一个字符长度),而使用方括号进行索引则不会。 - ygormutti
6个回答

12
为了正确地做到这一点,你需要使用在UAX 29中定义的计算字形簇边界的算法。不幸的是,这需要从Unicode字符数据库中了解哪些字符属于哪些类别,而JavaScript并没有提供这些信息(*). 所以你必须在你的脚本中包含一个UCD的副本,这将使它非常臃肿。
如果你只需要考虑拉丁或西里尔语使用的基本重音符号,另一个选择是仅使用组合用途记号块(U+0300-U+036F)。这对于其他语言和符号可能会失败,但对于你想要做的事情可能已经足够了。
function findGraphemesNotVeryWell(s) {
    var re= /.[\u0300-\u036F]*/g;
    var match, matches= [];
    while (match= re.exec(s))
        matches.push(match[0]);
    return matches;
}

findGraphemesNotVeryWell('Ааа́Ббб́Ввв́Г㥴Дд');
["А", "а", "а́", "Б", "б", "б́", "В", "в", "в́", "Г", "г", "Ґ", "ґ", "Д", "д"]

(*: 也许可以通过让浏览器呈现字符串并测量其选择位置的方法来提取信息...但这肯定会非常混乱和难以在各种浏览器中运行。)


9

关于这个问题的最新更新。

随着ES6的到来,有了新的字符串方法和处理字符串的方式。 这里有两个问题的解决方案。

1) 表情符号和代理对

表情符号和其他Unicode字符位于基本多语言平面(BMP)以上(范围在Unicode“代码点”0x0000 - 0xFFFF之外),可以像ES6中的字符串一样遵循迭代器协议进行处理,因此可以这样做:

let textWithEmoji = '\ud83d\udc0e\ud83d\udc71\u2764'; //horse, happy face and heart
[...textWithEmoji].length //3
for (char of textWithEmoji) { console.log(char) } //will log 3 chars

2) 变音符号

随着您开始处理“字形簇”(一个字符及其变音符号),这将是一个更难解决的问题。在ES6中,有一种方法可以简化处理此类问题,但仍然很难操作。 String.prototype.normalize 方法可以简化工作,但正如Mathias Bynens所说:

(A)对于带有多个组合标记的代码点始终会产生单个可视字形,但可能没有规范化的形式,在这种情况下,规范化无效。

更多见解可以在以下链接中找到:

https://ponyfoo.com/articles/es6-strings-and-unicode-in-depth https://mathiasbynens.be/notes/javascript-unicode


3
自从ES6推出以来,这是最好的答案。为了完整起见,可以提及使用字符串迭代器的 Array.from - ygormutti
现在我看到这不完全是OP所要求的,但对于我来说解决了我遇到的问题(代理对)。问题标题需要改进。 - ygormutti
分割表情符号的绝佳答案。 "❤".length 是5,但是使用扩展运算符 [..."❤"].length 却是3,太棒了。 - WSBT

8

这个包可能会对您有所帮助:https://www.npmjs.com/package/runes

const runes = require('runes')

const example = 'Emoji '
example.split('') // ["E", "m", "o", "j", "i", " ", "�", "�"] 
runes(example)    // ["E", "m", "o", "j", "i", " ", ""] 

使用Grapheme(请参见我的答案),即使是表情符号也可以正确地分割/找到。(在Firefox和V8上进行了测试) - Clemens Tolboom

0
如果您正在编写一个需要从Node.js流中消耗数据块的应用程序,那么您可能只需通过utf8-stream进行管道传输以防止这种情况:

https://github.com/substack/utf8-stream


0

使用Unicode属性Grapheme_Base

"Ааа́Ббб́Ввв́Г㥴Дд".match(/\p{Grapheme_Base}/gu)
> ['А', 'а', 'а', 'Б', 'б', 'б', 'В', 'в', 'в', 'Г', 'г', 'Ґ', 'ґ', 'Д', 'д']

Grapheme_Extend一起

"Ааа́Ббб́Ввв́Г㥴Дд".match(/\p{Grapheme_Extend}/gu)
> ['́', '́', '́']

将它们合并
"Ааа́Ббб́Ввв́Г㥴Дд".match(/\p{Grapheme_Base}\p{Grapheme_Extend}|\p{Grapheme_Base}/gu)
> ['А', 'а', 'а́', 'Б', 'б', 'б́', 'В', 'в', 'в́', 'Г', 'г', 'Ґ', 'ґ', 'Д', 'д']

尝试: "אַּׁ" .match(/ \ p {Grapheme_Base} \ p {Grapheme_Extend} * / gu) - o17t H1H' S'k
如果你执行 "אַּׁ".match(/\p{Grapheme_Base}/gu),除了 "אַּׁ".match(/\p{Grapheme_Extend}/gu) 之外,你会得到匹配结果。后者是 ['ׁ', 'ּ', 'ַ']。不确定为什么你的星号(*)无法工作。这是什么语言? - Clemens Tolboom
如果你除了 "אַּׁ".match(/\p{Grapheme_Extend}/gu) 之外还使用 "אַּׁ".match(/\p{Grapheme_Base}/gu),你会得到匹配结果。后者是 ['ׁ', 'ּ', 'ַ']。不确定为什么你的 */星号没有起作用。这是什么语言? - Clemens Tolboom
希伯来语。什么出了问题? - o17t H1H' S'k

-1

你的字符串问题在于它包含了代理对("a" "́),只有在浏览器显示时才会组合成单个字符。对于你的情况,如果你将\u0301附加到前一个字符上就足够了,但这绝不是一般解决方案。

var a="Ааа́Ббб́Ввв́Г㥴Дд",
    i =0,
    chars=[];

while(a.charAt(i)) {
  if (a.charAt(i+1) == "\u0301") {
    chars.push(a.charAt(i++)+a.charAt(i++));
  } else {
    chars.push(a.charAt(i++));}}

为了澄清问题,请前往阅读Mathias Bynens的博客文章


你的代码存在严重缺陷——除了有一个错误,a.fromCharCode(i),真的吗?——它不支持组合,所以你又回到了起点... - dda
谢谢提醒。已经更正。 - Aleš Kotnik
1
charCodeAt(index)在UTF-16代码单元方面工作吗?所以对于BMP之外的任何内容都不起作用。 - bames53
问题是如何将Unicode字符串拆分为单个Unicode字符的数组,而代码正是这样做的。请检查chars数组。 - Aleš Kotnik
chars 数组仍然返回每个单独的字符,而不是将 "а" + "́" === "а́" 组合起来。 - Gapipro
1
代理对与组合字符是完全不同的东西。代理对是指在UTF-16中,两个连续的16位值组合成一个32位代码点。而组合字符是完整的代码点,它们与前面的基本代码点组合形成一个用户感知字符,称为“字形簇”。 - hippietrail

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接