光的旅行 發表於 2026-3-1 19:06:00

对于 UTF-16 的高低代理项码点的解析

<h3 id="起因">起因:</h3>
<p>​        首先是我写的这一段代码, 这是一个手搓的 Json 解析器内部的一个, 把转义字符还原成 UTF-16 的一个逻辑代码:</p>
<pre><code class="language-CSharp">case 'u': //Unicode 字符
    if (i + 4 &lt; text.Length)
    {
      byte[] bytes;
      string u = text.Substring(i + 1, 4);
      if (Command.string2bytes(u, out bytes) == true)
      {
            i = i + 4; //这里不因该是 i + 5, 因为下一次循环会 i++
            tmp_str = tmp_str + (string)char.ConvertFromUtf32((int)Command.sum4bytes(bytes));
      }
      else
      {
            tmp_str = tmp_str + "\\u";
      }
    }
    else
    {
      tmp_str = tmp_str + "\\u";
    }
break;
</code></pre>
<p>​        正常来说它可以把一些类似于 <code>\u5bd2\u5bd2\u5929\u4e0b\u7b2c\u4e00\u53ef\u7231\uff01</code> 这样的 Unicode 给转换成 <code>寒寒天下第一可爱!</code>, 但是遇到 emoji 的文本时, 就抛出异常了:</p>
<blockquote>
<p>System.ArgumentOutOfRangeException:“有效的 UTF32 值介于 0x000000 和 0x10ffff 之间(包括这两者),而且不能包含代理项码位值(0x00d800 ~ 0x00dfff)。”</p>
</blockquote>
<p>​        嗯, 没见过的异常, 头一次见到代理项码位值, 然后在网路上查了一下这个玩意</p>
<h3 id="什么是代理项">什么是代理项:</h3>
<p>​        在常规 UTF-16 编码中, 文本是以 2 字节单位储存的, 满足一些常用字符的显示. 但是对于 emoji 和其他生僻字符却不够用, 所以, 在原有的 0x0000 至 0xFFFF 区间, 划分了一些区域给代理项使用, 然后通过代理项转换最后映射为补充码点.</p>
<ul>
<li>
<p>首先是高代理项码点, 它是识别拓展字符的关键, 区间在 0xD800 至 0xDBFF.</p>
</li>
<li>
<p>尾随高代理项的是低代理项码点, 区间在 0xDC00 至 0xDFFF.</p>
</li>
<li>
<p>补充码点, 用于拓展一些不常见的生僻的字符, 或者 emoji 及其他符号, 代理项最终要映射在此区间, 区间在 0x010000 至 0x10FFFF.</p>
</li>
</ul>
<h3 id="怎么解析">怎么解析:</h3>
<h5 id="补充代码点--代理项代码点">补充代码点 =&gt; 代理项代码点</h5>
<pre><code>TMP = INPUT - 0x10000
HSCP = (TMP / 0x400) + 0xD800
LSCP = TMP % 0x400 + 0xDC00
</code></pre>
<h5 id="代理项代码点--补充代码点">代理项代码点 =&gt; 补充代码点</h5>
<pre><code>OUTPUT = ((HSCP - 0xD800) * 0x0400) + (LSCP - 0xDC00) + 0x010000
</code></pre>
<p>​        按照以上流程, 用 C# 写的话, 如下所示</p>
<pre><code class="language-CSharp">case 'u': //Unicode 字符
    if (i + 4 &lt; text.Length)
    {
      byte[] bytes;
      string u = text.Substring(i + 1, 4);
      if (Command.TryString2Bytes(u, out bytes) == true)
      {
            int hex = (int)Command.Sum4Bytes(bytes);
            //对于代理位解析, 参看一下连接, 感谢!
            //https://zhuanlan.zhihu.com/p/147339588
            //https://www.cnblogs.com/sailJs/p/18397567
            if (hex &gt; 55295 &amp;&amp; hex &lt; 56320) // D800 到 DBFF //高代理位 //emoji 那些
            {
                if(i + 10 &lt; text.Length &amp;&amp; text == '\\' &amp;&amp; text == 'u')
                {
                  u = text.Substring(i + 7, 4);
                  if (Command.TryString2Bytes(u, out bytes) == true)
                  {
                        int HSCP = hex;
                        int LSCP = (int)Command.Sum4Bytes(bytes);
                        if (LSCP &lt; 56320 || LSCP &gt; 57343) //低代理位, 在 DC00 到 DFFF
                        {
                            tmp_str = tmp_str + "\\u";
                        }
                        else
                        {
                            //HSCP = HSCP - (int)Command.Sum4Bytes(Command.String2Bytes("D800"));
                            HSCP = HSCP - 55296;
                            HSCP = HSCP * 1024;
                            //LSCP = LSCP - (int)Command.Sum4Bytes(Command.String2Bytes("DC00"));
                            LSCP = LSCP - 56320;
                            //hex = HSCP + LSCP + (int)Command.Sum4Bytes(Command.String2Bytes("010000"));
                            hex = HSCP + LSCP + 65536;
                            i = i + 10;
                            tmp_str = tmp_str + (string)char.ConvertFromUtf32(hex);
                        }
                  }
                  else
                  {
                        tmp_str = tmp_str + "\\u";
                  }
                }
                else
                {
                  tmp_str = tmp_str + "\\u";
                }
            }

            else
            {
                i = i + 4; //这里不因该是 i + 5, 因为下一次循环会 i++
                tmp_str = tmp_str + (string)char.ConvertFromUtf32(hex);
            }
      }
      else
      {
            tmp_str = tmp_str + "\\u";
      }
    }
    else
    {
      tmp_str = tmp_str + "\\u";
    }
    break;
</code></pre>
<p>​        总之就是, 在俺不了解代理项之前, 俺一直以为一个 \uXXXX 代表一个 char, 了解代理项之后就明白, 只要遇到一个处于高代理项的字符时, 它的后面必定会有一个低代理项 (即两个 char).</p>
<h3 id="参考连接">参考连接:</h3>
<ul>
<li>
<p>Unicode | 代理项(Surrogate)</p>
</li>
<li>
<p>Unicode编码 - 代理区和4字节codePoint</p>
</li>
</ul><br><br>
来源:https://www.cnblogs.com/yuhang0000/p/19656382
頁: [1]
查看完整版本: 对于 UTF-16 的高低代理项码点的解析