如何反转包含复杂表情符号的字符串?

Posted

技术标签:

【中文标题】如何反转包含复杂表情符号的字符串?【英文标题】:How to reverse a string that contains complicated emojis? 【发布时间】:2021-01-16 01:41:17 【问题描述】:

输入:

Hello world????‍????????‍????‍????‍????

期望的输出:

????‍????‍????‍????????‍????dlrow olleH

我尝试了几种方法,但都没有给我正确的答案。

这惨遭失败:

const text = 'Hello world????‍????????‍????‍????‍????';

const reversed = text.split('').reverse().join('');

console.log(reversed);

这有点用,但它会将????‍????‍????‍???? 分成 4 个不同的表情符号:

const text = 'Hello world????‍????????‍????‍????‍????';

const reversed = [...text].reverse().join('');

console.log(reversed);

我也尝试了this question 中的所有答案,但没有一个有效。

有没有办法得到想要的输出?

【问题讨论】:

我看不到第二种解决方案的问题。我错过了什么? 所以这些表情符号实际上是某种组合表情符号,非常有趣。首先,你有一个女人脸表情符号,它本身由你的两个 字符表示,然后有一个额外的连接字符,即 charcode 8205,然后还有另外两个 - 代表“红头发”,还有那些5 个字加起来表示“红头发的女人脸” 我认为要正确反转带有组合表情符号的字符串会非常复杂。您必须检查每个表情符号是否后跟 charcode 8205,如果是,则必须将其与前一个表情符号结合,而不是将其视为自己的字符。相当复杂... javascript 让我很困惑。这是低级和高级语言概念的最奇怪的混合。它的级别在于它完全抽象了内存(没有指针,手动内存管理),但级别太低以至于将字符串视为哑代码点而不是扩展的字形簇。这真的很令人困惑,它让我不知道在使用这个东西时会发生什么。 @Alexander-ReinstateMonica 是否有任何语言默认确实通过字形拆分进行拆分? JS 只是提供了以 UTF-16 编码的标准字符串。 【参考方案1】:

如果可以,请使用lodash 提供的_.split() 函数。从version 4.0 开始,_.split() 能够拆分 unicode 表情符号。

使用原生 .reverse().join('') 反转“字符”应该可以很好地处理包含零宽度连接符的表情符号

function reverse(txt)  return _.split(txt, '').reverse().join(''); 

const text = 'Hello world?‍??‍?‍?‍?';
console.log(reverse(text));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js" integrity="sha512-90vH1Z83AJY9DmlWa8WkjkV79yfS2n2Oxhsi2dZbIv0nC4E6m5AbH8Nh156kkM7JePmqD6tcZsfad1ueoaovww==" crossorigin="anonymous"></script>

【讨论】:

您指出的更改日志提到“v4.9.0 - 确保 _.split 可与表情符号一起使用”,我认为 4.0 可能为时过早。用于拆分字符串的代码中的 cmets (github.com/lodash/lodash/blob/4.17.15/lodash.js#L261) 指的是从 2013 年开始的 mathiasbynens.be/notes/javascript-unicode。看起来它从那时起就开始了,但它确实使用了相当难以破译大量 unicode 正则表达式.我在他们的代码库中也看不到任何用于 unicode 拆分的测试。所有这些都会让我对在生产中使用它持谨慎态度。 只花了一点时间就发现这失败了reverse("뎌쉐")(2 个韩文字素),它给出了“ᅰ셔ᄃ”(3 个字素)。 这个问题似乎没有简单的本地解决方案。不想仅仅为了解决这个问题而导入一个库,但它确实是目前最可靠/一致的方法。 感谢让它正常工作? 在 Windows10 上的 Firefox 中反转书写方向仍然有点小故障(孩子们最终排在后面),所以我猜 lodash 击败了 Windows 10,这可能是预算有点低?【参考方案2】:

我采纳了 TKoL 使用 \u200d 字符的想法,并用它来尝试创建一个更小的脚本。

注意:并非所有作品都使用零宽度连接符,因此与其他作品字符会出现问题。

它使用传统的for 循环,因为我们会跳过一些迭代,以防我们找到组合的表情符号。在for 循环中有一个while 循环来检查是否有以下\u200d 字符。只要有一个,我们就添加接下来的 2 个字符,并通过 2 次迭代转发 for 循环,这样组合的表情符号就不会反转。

为了在任何字符串上轻松使用它,我将它作为字符串对象上的新原型函数。

String.prototype.reverse = function() 
  let textArray = [...this];
  let reverseString = "";

  for (let i = 0; i < textArray.length; i++) 
    let char = textArray[i];
    while (textArray[i + 1] === '\u200d') 
      char += textArray[i + 1] + textArray[i + 2];
      i = i + 2;
    
    reverseString = char + reverseString;
  
  return reverseString;


const text = "Hello world?‍??‍?‍?‍?";

console.log(text.reverse());

//Fun fact, you can chain them to double reverse :)
//console.log(text.reverse().reverse());

【讨论】:

我在想,当你在浏览器上拖动选择文本时,?‍?‍?‍?只能整体选择。浏览器如何知道它是一个字符?有内置的方法吗? @HaoWu 这就是所谓的“Grapheme Clusters”上的“Unicode Segmentation”。您的浏览器(可能使用您的操作系统提供的浏览器)将呈现并允许选择每个字素簇。您可以在此处阅读规范:unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries @HaoWu:“浏览器怎么知道它是一个字符?” ——它不是“一个字符”。它是多个字符组合形成单个字形簇,呈现为单个字形 Same as here;并非所有作品都使用零宽度连接器。 除了用 ZWJ 组成的字符外,这不会正确反转任何内容。请,不仅在这里,而且作为一般规则,请使用由知道自己在做什么的人编写的外部库,而不是破解恰好适用于一个测试用例的定制解决方案。在其他答案中推荐了 runes 和 lodash 库(我不能保证任何一个)。【参考方案3】:

由于很多原因,反转 Unicode 文本很棘手。

首先,根据编程语言,字符串以不同的方式表示,可以是字节列表、UTF-16 代码单元列表(16 位宽,在 API 中通常称为“字符”),也可以是ucs4 代码点(4 字节宽)。

其次,不同的 API 在不同程度上反映了这种内部表示。一些处理字节的抽象,一些处理 UTF-16 字符,一些处理代码点。当表示使用字节或 UTF-16 字符时,API 的某些部分通常允许您访问此表示的元素,以及执行必要逻辑以从字节(通过 UTF-8)或从UTF-16 字符到实际的代码点。

通常,执行该逻辑并因此让您可以访问代码点的 API 部分是稍后添加的,因为最初是 7 位 ascii,然后稍后大家都认为 8 位就足够了,使用不同的代码页,甚至后来 16 位对于 unicode 来说已经足够了。代码点作为没有固定上限的整数的概念在历史上被添加为用于对文本进行逻辑编码的第四个常见字符长度。

使用可让您访问实际代码点的 API 似乎就是这样。但是……

第三,有很多修饰符码点影响下一个码点或后面的码点。例如。有一个变音符号修饰符将后面的 a 变成 ä,e 变成 ë,&c。转动代码点,aë 变成 eä,由不同的字母组成。有一个直接的表示,例如ä 作为自己的代码点,但使用修饰符同样有效。

第四,一切都在不断变化。表情符号中也有很多修饰符,如示例中使用的那样,并且每年都会添加更多。因此,如果 API 允许您访问代码点是否为修饰符的信息,则 API 的版本将确定它是否已经知道特定的新修饰符。

不过,Unicode 提供了一个 hacky 技巧,因为它只涉及视觉外观:

有书写方向修饰符。在示例的情况下,使用从左到右的书写方向。只需在文本开头添加一个从右到左的书写方向修饰符,根据 API / 浏览器的版本,它看起来会正确反转?

'\u202e' 被称为从右到左覆盖,它是从右到左标记的最强版本。

见this explanation by w3.org

const text = 'Hello world?‍??‍?‍?‍?'
console.log('\u202e' + text)

const text = 'Hello world?‍??‍?‍?‍?'
let original = document.getElementById('original')
original.appendChild(document.createTextNode(text))
let result = document.getElementById('result')
result.appendChild(document.createTextNode('\u202e' + text))
body 
  font-family: sans-serif
<p id="original"></p>
<p id="result"></p>

【讨论】:

+1 非常有创意地使用 bidi (-: 使用 POP DIRECTIONAL FORMATTING char '\u202e' + text + '\u202c' 关闭覆盖更安全,以免影响后面的文字。 谢谢 ? 这是一个非常老套的技巧,我链接到的文章详细解释了为什么使用 html 属性更聪明,但这样我可以只使用字符串连接作为我的 hack ? 顺便说一句。我在这台机器上的 firefox(win 10)并没有完全正确,孩子们在从右到左书写时落后于父母,我想用这些大规模复杂的表情符号人群修饰符很难找到正确的写作方向。 .. 另一个有趣的边缘案例:用于标志表情符号的区域指示符号。如果你取字符串“??”(两个代码点 U+1F1E6,U+1F1E8,制作阿森松岛的旗帜)并尝试天真地反转它,你会得到“??”,即加拿大的旗帜。跨度> @yeoman 仅供参考:“UTF-16 字符”(正如您在此处使用的术语)也称为“UTF-16 代码单位”。 “字符”这个术语往往过于含糊,因为它可以指代很多东西(但在 Unicode 的上下文中通常是一个代码点)。【参考方案4】:

我知道!我将使用正则表达式。会出什么问题? (答案留给读者作为练习。)

const text = 'Hello world?‍??‍?‍?‍?';

const reversed = text.match(/.(\u200d.)*/gu).reverse().join('');

console.log(reversed);

【讨论】:

你的回答听起来很抱歉,但老实说,我认为这个答案接近规范。它绝对优于尝试手动执行相同操作的其他答案。基于字符的文本操作是正则表达式的设计和擅长的领域,Unicode 联盟明确标准化了必要的正则表达式功能(在这种情况下,ECMAScript 恰好正确实现了这些功能)。也就是说,它无法处理组合字符(IIRC 正则表达式应该使用. 通配符处理)。 不适用于未使用 U+200D 构建的组合,例如?️‍?。值得注意的是,Emijoi 世界之外也确实存在组合角色…… @StevenPenny ?️‍? 包含两个组合,其中一个不使用U+200D。很容易验证?️‍?不适用于此答案的代码...... @Holger 虽然 ?️‍? 确实包含一个不是用 U+200D 构建的组合,但它是一个非常糟糕的例子,因为它还包含一个用 U+200D 构建的组合。更好的例子是 ?? 或 ?️ 与此处的其他 cmets 相反,并非每次使用零宽度连接符都应视为单个字素簇。例如,unicode 13 字素测试 (unicode.org/Public/13.0.0/ucd/auxiliary/GraphemeBreakTest.txt) 的最后三行显示了三个非常相似的情况,其中 ZWJ 的处理方式不同。【参考方案5】:

替代解决方案是使用runes 库,小而有效的解决方案:

https://github.com/dotcypress/runes

const runes = require('runes')

// String.substring
'?‍?‍?‍?a'.substring(1) => '�‍?‍?‍?a'

// Runes
runes.substr('?‍?‍?‍?a', 1) => 'a'

runes('12?‍?‍?‍?3?✓').reverse().join(); 
// results in: "✓?3?‍?‍?‍?21"

【讨论】:

这是最好的答案。所有这些其他答案都有失败的情况,这个库(希望)满足所有边缘情况。 这很有趣,这样一个“简单的问题”乍一看变成了一个不容易解决的任务。同意 Carson 的观点 - 希望随着表情符号的不断发展,图书馆将随着更新和变化而向前发展。 这个好像有 3 年没更新了。 Unicode 11 大约在那个时候发布,但从那以后情况发生了变化,Unicode 13 稍后发布。 13 中的扩展字素规则发生了一些变化。因此可能存在一些无法处理的边缘情况。 (我没有看过代码 - 但值得小心) 我同意@MichaelAnderson,这个库似乎使用了一种幼稚或旧的算法。要正确执行此操作,应使用grapheme segmentation algorithm specified in Unicode。【参考方案6】:

您不仅对表情符号有问题,而且对其他组合字符也有问题。 这些感觉像单个字母但实际上是一个或多个 unicode 字符的东西被称为“扩展字素簇”。

将字符串分解成这些簇很棘手(例如,请参阅这些unicode docs)。我不会依赖自己实现它,而是使用现有的库。谷歌将我指向grapheme-splitter 库。该库的文档包含一些nice examples,它们会导致大多数实现出错:

使用这个你应该可以写:

var splitter = new GraphemeSplitter();
var graphemes = splitter.splitGraphemes(string);
var reversed = graphemes.reverse().join('');

旁白:对于来自未来的访客,或那些愿意生活在最前沿的人:

有一个proposal 可以将字形分段器添加到 javascript 标准中。 (它实际上也提供了其他分段选项)。 它目前处于接受审核的第 3 阶段,目前在 JSC 和 V8 中实现(请参阅https://github.com/tc39/proposal-intl-segmenter/issues/114)。

使用此代码将如下所示:

var segmenter = new Intl.Segmenter("en", granularity: "grapheme")
var segment_iterator = segmenter.segment(string)
var graphemes = []
for (let segment of segment_iterator) 
    graphemes.push(segment)

var reversed = graphemes.reverse().join('');

如果你比我懂更多现代的 javascript,你可能会做得更整洁......

有一个implementation here - 但我不知道它需要什么。

注意:这指出了一个有趣的问题,其他答案尚未解决。分段可以取决于您使用的语言环境 - 而不仅仅是字符串中的字符。

【讨论】:

看起来代码大约有 2 年没有更新了 - 所以它的表可能不是最新的。因此,您可能需要搜索更新的内容。 看起来这个库的更新版本可以在github.com/flmnt/graphemer获得 我很惊讶我不得不向下滚动这么远才能看到实际上正确的答案。 对于提案示例,您可以使用const graphemes = Array.from(segment_iterator, (segment) =&gt; segment)【参考方案7】:

我只是为了好玩才决定这样做,这是一个很好的挑战。不确定它是否在所有情况下都正确,因此使用风险自负,但这里是:

function run() 
    const text = 'Hello world?‍??‍?‍?‍?';
    const newText = reverseText(text);
    console.log(newText);


function reverseText(text) 
    // first, create an array of characters
    let textArray = [...text];
    let lastCharConnector = false;
    textArray = textArray.reduce((acc, char, index) => 
        if (char.charCodeAt(0) === 8205) 
            const lastChar = acc[acc.length-1];
            if (Array.isArray(lastChar)) 
                lastChar.push(char);
             else 
                acc[acc.length-1] = [lastChar, char];
            
            lastCharConnector = true;
         else if (lastCharConnector) 
            acc[acc.length-1].push(char);
            lastCharConnector = false;
         else 
            acc.push(char);
            lastCharConnector = false;
        
        return acc;
    , []);
    
    console.log('initial text array', textArray);
    textArray = textArray.reverse();
    console.log('reversed text array', textArray);

    textArray = textArray.map((item) => 
        if (Array.isArray(item)) 
            return item.join('');
         else 
            return item;
        
    );

    return textArray.join('');


run();

【讨论】:

好吧,实际上它很长,因为调试信息。我真的很感激 @AndrewSavinykh 不是代码高尔夫,但正在寻找更优雅的解决方案。可能不像单行字那么疯狂,但容易记住。比如regex solution 是一个非常好的恕我直言。【参考方案8】:

你可以使用:

yourstring.split('').reverse().join('')

它应该把你的字符串变成一个列表,把它反转然后再把它变成一个字符串。

【讨论】:

你读过这个问题吗?您的代码正是问题中被 OP 证明错误的代码。【参考方案9】:

const text = 'Hello world?‍??‍?‍?‍?';

const reversed = text.split('').reverse().join('');

console.log(反转);

【讨论】:

以上是关于如何反转包含复杂表情符号的字符串?的主要内容,如果未能解决你的问题,请参考以下文章

如何将包含表情符号的字符串拆分为数组?

如何搜索包含表情符号的文本?

如何使用 javascript 检测表情符号

如何使用 javascript 检测表情符号

如何从字符串中删除表情符号字符?

Regex:过滤特殊字符(如日语),但保留表情符号