【问题标题】:javascript and string manipulation w/ utf-16 surrogate pairs带有 utf-16 代理对的 javascript 和字符串操作
【发布时间】:2011-07-30 20:45:03
【问题描述】:

正在开发一个 twitter 应用程序,并且偶然发现了 utf-8(16) 的世界。似乎大多数 javascript 字符串函数都像我一样对代理对视而不见。我必须重新编码一些东西以使其具有宽字符意识。

我有这个函数可以将字符串解析为数组,同时保留代理对。然后我将重新编写几个函数来处理数组而不是字符串。

function sortSurrogates(str){
  var cp = [];                 // array to hold code points
  while(str.length){           // loop till we've done the whole string
    if(/[\uD800-\uDFFF]/.test(str.substr(0,1))){ // test the first character
                               // High surrogate found low surrogate follows
      cp.push(str.substr(0,2)); // push the two onto array
      str = str.substr(2);     // clip the two off the string
    }else{                     // else BMP code point
      cp.push(str.substr(0,1)); // push one onto array
      str = str.substr(1);     // clip one from string 
    }
  }                            // loop
  return cp;                   // return the array
}

我的问题是,我还缺少什么更简单的东西吗?我看到很多人重申 javascript 原生处理 utf-16,但我的测试让我相信,这可能是数据格式,但函数还不知道。我错过了一些简单的东西吗?

编辑: 为了帮助说明问题:

var a = "0123456789"; // U+0030 - U+0039 2 bytes each
var b = "????????????????????????????????????????"; // U+1D7D8 - U+1D7E1 4 bytes each
alert(a.length); // javascript shows 10
alert(b.length); // javascript shows 20

Twitter 将这两个字符都视为 10 个字符。

【问题讨论】:

  • 你实际上需要做什么?
  • 基本操作。 Twitter 不返回内联链接,只返回纯文本和 url 以及 url 所属位置的索引。索引基于代码点而不是 16 位字符。我还有一个用于格式化推文的文本区域。 Javascript 将简单的字符计数视为 16 位大块的计数,而不是单个代码点。我可以解决,只是不想在不询问专业人士是否有更简单的事情的情况下就走错方向。
  • Javascript 在内部使用 UCS-2,不是 UTF-16。因此在 Javascript 中处理 Unicode 非常困难,并且我不建议尝试这样做。至于 Twitter 做什么,您似乎是在说它是按代码点而不是按代码单元疯狂计数。
  • @tchrist:你这是什么意思?对开发人员可见的 JavaScript 字符串是 UTF-16 编码的。
  • @Tim 它们显示为单独代码单元的 UCS-2 字符串,而不是代码点的 Unicode 字符串。您可以使用正则表达式向自己证明这一点。试着在一个模式中写[????-????],看看会发生什么。它只是坏了。如果 Javascript 实际使用 UTF-16,我将能够编写 document.write(String.fromCharCode(0x1D49C)) 并且不必编写 也不允许编写 document.write(String.fromCharCode(0xD835,0xDC9C)) 代替它。这是破坏 UCS-2 的废话。

标签: javascript string unicode twitter utf-16


【解决方案1】:

Javascript 字符串迭代器可以为您提供实际字符而不是代理代码点:

>>> [..."0123456789"]
["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
>>> [..."??????????"]
["?", "?", "?", "?", "?", "?", "?", "?", "?", "?"]
>>> [..."0123456789"].length
10
>>> [..."??????????"].length
10

【讨论】:

    【解决方案2】:

    Javascript 在内部使用 UCS-2,而不是 UTF-16。因此,在 Javascript 中处理 Unicode 非常困难,我不建议尝试这样做。

    至于 Twitter 做什么,您似乎是在说它是按代码点而不是按代码单元疯狂计数。

    除非您别无选择,否则您应该使用真正支持 Unicode 的编程语言,并且该语言具有代码点接口,而不是代码单元接口。正如您所发现的那样,Javascript 还不够好。

    它有 UCS-2 诅咒, 甚至比 UTF-16 诅咒, 已经够糟糕了。我在 OSCON 演讲中谈到了所有这些,? Unicode Support Shootout: ? The Good, the Bad, & the (mostly) Ugly ?

    由于其可怕的 Curse,您必须在 Javascript 中使用 UCS-2 手动模拟 UTF-16,这简直是疯了。

    Javascript 也遭受各种其他可怕的 Unicode 问题。它不支持字素或规范化或整理,所有这些都是您真正需要的。它的正则表达式被破坏了,有时是由于诅咒,有时只是因为人们弄错了。例如,Javascript 无法表达像[?-?] 这样的正则表达式。 Javascript 甚至不支持大小写折叠,所以你不能编写像 /ΣΤΙΓΜΑΣ/i 这样的模式并让它正确匹配 στιγμας

    你可以尝试使用the XRegEXp plugin,但你不会那样消除诅咒。只有更改为支持 Unicode 的语言才能做到这一点,而?????????? 不是其中之一。

    【讨论】:

    • 我对graphanemes或名词化一无所知,但如果我可以在javascript中模拟一个基本的宽字符感知字符串,我当前的问题将得到解决:)
    • @BentFX:我已经编写了代码来完成您想要的基本操作。看我的回答。
    • EcmaScript 5 说实现可以是 UTF-16 或 UCS-2。 “本标准的一致实施应按照 Unicode 标准 3.0 版或更高版本和 ISO/IEC 10646-1 以 UCS-2 或 UTF-16 作为采用的编码形式来解释字符,实施级别 3。”来自chapter 2 paragraph 2
    • @tchrist,我同意。如果您想使用许多解释器,则不能依赖所有将补充代码点表示为 UTF-16 的解释器。如果只需要一个或几个口译员,可以测试:var div = document.createElement("DIV"); div.innerHTML = "&#0x10000;"; var isUtf16 = div.firstChild.nodeValue.charCodeAt(0) == 0xd800;
    • /ΣΤΙΓΜΑΣ/i.test('στιγμας') 在我或多或少最新版本的 Chrome、Firefox、Edge 甚至 Internet Explorer 中返回 true。当前 ECMA-262 v9.0 定义,该字符串必须使用 UTF-16。 Current Javascript 也有 String.fromCodePointString.prototype.codePointAt ,它们实际上适用于 BMP 以上的代码点。也许您可以更新您的答案并提及现代 javascript 使用 UTF-16?
    【解决方案3】:

    我已经拼凑了一个 Unicode 字符串处理对象的起点。它创建了一个名为UnicodeString() 的函数,该函数接受JavaScript 字符串或表示Unicode 代码点的整数数组,并提供lengthcodePoints 属性以及toString()slice() 方法。添加正则表达式支持会非常复杂,但是像indexOf()split()(不支持正则表达式)应该很容易实现。

    var UnicodeString = (function() {
        function surrogatePairToCodePoint(charCode1, charCode2) {
            return ((charCode1 & 0x3FF) << 10) + (charCode2 & 0x3FF) + 0x10000;
        }
    
        function stringToCodePointArray(str) {
            var codePoints = [], i = 0, charCode;
            while (i < str.length) {
                charCode = str.charCodeAt(i);
                if ((charCode & 0xF800) == 0xD800) {
                    codePoints.push(surrogatePairToCodePoint(charCode, str.charCodeAt(++i)));
                } else {
                    codePoints.push(charCode);
                }
                ++i;
            }
            return codePoints;
        }
    
        function codePointArrayToString(codePoints) {
            var stringParts = [];
            for (var i = 0, len = codePoints.length, codePoint, offset, codePointCharCodes; i < len; ++i) {
                codePoint = codePoints[i];
                if (codePoint > 0xFFFF) {
                    offset = codePoint - 0x10000;
                    codePointCharCodes = [0xD800 + (offset >> 10), 0xDC00 + (offset & 0x3FF)];
                } else {
                    codePointCharCodes = [codePoint];
                }
                stringParts.push(String.fromCharCode.apply(String, codePointCharCodes));
            }
            return stringParts.join("");
        }
    
        function UnicodeString(arg) {
            if (this instanceof UnicodeString) {
                this.codePoints = (typeof arg == "string") ? stringToCodePointArray(arg) : arg;
                this.length = this.codePoints.length;
            } else {
                return new UnicodeString(arg);
            }
        }
    
        UnicodeString.prototype = {
            slice: function(start, end) {
                return new UnicodeString(this.codePoints.slice(start, end));
            },
    
            toString: function() {
                return codePointArrayToString(this.codePoints);
            }
        };
    
    
        return UnicodeString;
    })();
    
    var ustr = UnicodeString("f??bar");
    document.getElementById("output").textContent = "String: '" + ustr + "', length: " + ustr.length + ", slice(2, 4): " + ustr.slice(2, 4);
    &lt;div id="output"&gt;&lt;/div&gt;

    【讨论】:

    • 感谢您的努力。我对 javascript 对象结构真的很陌生,将从您的示例中学到很多东西。真的,我的编码是娱乐性的,我很喜欢解决这个难题。似乎 Javascript 中的 Unicode 就像一个魔方,只是正确的解决方案较少。 :)
    • 当 StackOverflow 的 4 个赞成回答是处理 UTF-16 Unicode 的最佳方式时,这是对当前 Javascript 状态的一个可悲的声明。不过在这方面做得很好!完美地完成我当前的任务(切片包含表情符号图标的推文)
    【解决方案4】:

    这里有几个脚本在处理 JavaScript 中的代理对时可能会有所帮助:

    【讨论】:

      【解决方案5】:

      符合我的要求。它需要更好地支持不同的字符串函数。当我添加它时,我会更新这个答案。

      function wString(str){
        var T = this; //makes 'this' visible in functions
        T.cp = [];    //code point array
        T.length = 0; //length attribute
        T.wString = true; // (item.wString) tests for wString object
      
      //member functions
        sortSurrogates = function(s){  //returns array of utf-16 code points
          var chrs = [];
          while(s.length){             // loop till we've done the whole string
            if(/[\uD800-\uDFFF]/.test(s.substr(0,1))){ // test the first character
                                       // High surrogate found low surrogate follows
              chrs.push(s.substr(0,2)); // push the two onto array
              s = s.substr(2);         // clip the two off the string
            }else{                     // else BMP code point
              chrs.push(s.substr(0,1)); // push one onto array
              s = s.substr(1);         // clip one from string 
            }
          }                            // loop
          return chrs;
        };
      //end member functions
      
      //prototype functions
        T.substr = function(start,len){
          if(len){
            return T.cp.slice(start,start+len).join('');
          }else{
            return T.cp.slice(start).join('');
          }
        };
      
        T.substring = function(start,end){
          return T.cp.slice(start,end).join('');
        };
      
        T.replace = function(target,str){
          //allow wStrings as parameters
          if(str.wString) str = str.cp.join('');
          if(target.wString) target = target.cp.join('');
          return T.toString().replace(target,str);
        };
      
        T.equals = function(s){
          if(!s.wString){
            s = sortSurrogates(s);
            T.cp = s;
          }else{
              T.cp = s.cp;
          }
          T.length = T.cp.length;
        };
      
        T.toString = function(){return T.cp.join('');};
      //end prototype functions
      
        T.equals(str)
      };
      

      测试结果:

      // plain string
      var x = "0123456789";
      alert(x);                    // 0123456789
      alert(x.substr(4,5))         // 45678
      alert(x.substring(2,4))      // 23
      alert(x.replace("456","x")); // 0123x789
      alert(x.length);             // 10
      
      // wString object
      x = new wString("??????????");
      alert(x);                    // ??????????
      alert(x.substr(4,5))         // ?????
      alert(x.substring(2,4))      // ??
      alert(x.replace("???","x")); // ????x???
      alert(x.length);             // 10
      

      【讨论】:

      • 这看起来与我的努力相当相似。
      • @Tim 是的,结构不同,但主要是对所需功能进行原型设计。主要区别在于您将代码单元编码为代码点。我选择不这样做,因为我对真正的代码点没有用处,javascript 无法显示它们,所以为什么要麻烦。对于我的用途,只需将它们分开就足够了,这样它们就可以在合理的点上进行计数和拆分。玩得开心!
      • 很公平,如果您不需要真正的代码点,请不要使用它们。如果您需要向服务器发送 Unicode 字符串,您可能需要它们。
      • 来自 angular js: "...".replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, function(value) { var hi = value.charCodeAt( 0); var low = value.charCodeAt(1); return '' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'; }) 这是创建一个实体编码值可以安全插入到属性或元素主体中。
      猜你喜欢
      • 2023-03-27
      • 1970-01-01
      • 1970-01-01
      • 2012-02-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-31
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多