JS charCodeAt等效于PHP(具有完整的unicode和emoji兼容性)

时间:2016-11-28 09:40:07

标签: javascript php unicode character-encoding

我在JS中有一个简单的代码,如果涉及特殊字符,我无法在PHP中复制。

这是JS代码(请参阅JSFiddle输出):

var str = "t↙️"; //char "t" and special characters, emojis, etc..
document.write("Length is: "+str.length); // Length is: 19
for(var i=0; i<str.length; i++) {
  document.write("<br> charCodeAt(" + i + "): " + str.charCodeAt(i));
}

第一个问题是PHP strlen()mb_strlen()已经提供了与JS (strlen:39,mb_strlen:11)不同的结果,但是我设法得到了相同的结果使用自定义JS_StringLength功能(感谢this SO回答)。

这是我到目前为止在PHP中所拥有的内容(请参阅phpFiddle了解输出):

<?php

function JS_StringLength($string) {
    return strlen(iconv('UTF-8', 'UTF-16LE', $string)) / 2;
}

function JS_charCodeAt($str, $index){
    //not working!

    $char = mb_substr($str, $index, 1, 'UTF-8');
    if (mb_check_encoding($char, 'UTF-8'))
    {
        $ret = mb_convert_encoding($char, 'UTF-32BE', 'UTF-8');
        return hexdec(bin2hex($ret));
    } else {
        return null;
    }
}

$str = "t↙️";

echo $str."\n";
//echo "Length is: ".strlen($str)."\n"; //wrong
echo "Length is: ".JS_StringLength($str)."\n"; //OK
for($i=0; $i<JS_StringLength($str); $i++) {
    echo "charCodeAt(".$i."): ".JS_charCodeAt($str, $i)."\n";
}

经过一整天的Google搜索,and trying out everything我发现,没有任何结果与JS相同。 使用类似的性能JS_charCodeAt应该将相同的输出作为JS?

试验#1:
将我的字符串输入https://r12a.github.io/app-conversion/(很棒的东西)。看起来JS使用 UTF-16代码单元(19)和PHP strlen计算 UTF-8代码单元(39)。

试验#2:
当我在我的字符串上使用json_encode()时 - 结果几乎就是这样,JavaScript可能会使用什么。我甚至检查了json_encode的原始PHP源代码以及如何json_encode escapes strings,但是......好吧..

在标记为重复之前,请确保使用上面示例中的字符串(或随机emojis)测试解决方案,因为在stackoverflow上找到的所有charCodeAt实现都与大多数特殊字符一起使用,但是不是用表情符号。

3 个答案:

答案 0 :(得分:2)

好的,差不多两天后,我想我自己也找到了答案。 基本思想是button.addActionListener(thing) enterbutton.addActionListener(thing) 以一种形式逃避多字节Unicode字符,JS使用它们(如= json_encode())进行字符计数,"\ud83d\ude18"函数等等。所以如果我们JSON对字符串进行编码,我们可以提取一个简单字符数组,并转义多字节字符。这样,我们可以很容易地将原始字符串的字符计为UTF-16代码单元(就像JS一样)。当然,我们可以返回&#34; charCodeAt&#34;值(charCodeAt表示简单字符,并将\ uXXXX十六进制转换为多字节字符处的dec。)

问题:如果我想获得&#34; JS charCodeAt&#34; for循环中每个字符的值(所以基本上将字符串转换为charcode列表),这段代码在长文本上会很慢,因为ord()中的preg_match_all将为每个字符运行一次。 /> 解决方法:不是每次调用getUTF16CodeUnits,而是将matches数组存储在变量中,然后使用它。更多详情:FASTER VERSION (backup)

代码和demo

getUTF16CodeUnits

我们非常感谢改进,修复和评论!

答案 1 :(得分:1)

JS处理UTF-16的方式并不理想; charCodeAt正在为您挑选代码单元,包括表情符号中的代理。如果你想要每个角色的真实代码点,String.codePointAt()将是更好的选择。也就是说,由于您的用例没有得到解释,这可以实现您最初的要求而无需json相关功能:

<?php

$original = 't↙️';
$converted = iconv('UTF-8', 'UTF-16LE', $original);

for ($i = 0; $i < iconv_strlen($converted, 'UTF-16LE'); $i++) {
    $character = iconv_substr($converted, $i, 1, 'UTF-16LE');
    $codeUnits = unpack('v*', $character);

    foreach ($codeUnits as $codeUnit) {
        echo $codeUnit . PHP_EOL;
    }
}

这会将(假设的)UTF-8字符串转换为UTF-16,然后遍历每个字符。在UTF-16中,每个字符的大小为2或4个字节。使用v重复格式化器解包将在前一种情况下返回一个short,或者在后一种情况下返回2(v是无符号短格式器)。

也可以通过循环遍历UTF-8并逐个转换每个字符来实现;但它并没有产生很大的不同。使用mb_ *函数也可以实现相同的效果。

修改

由于您已经询问了更快的方法,将上述内容与nw​​ellnhof提供的解决方案相结合可以提供更好的性能:

<?php

$original = 't↙️';
$converted = iconv('UTF-8', 'UTF-16LE', $original);

for ($i = 0; $i < strlen($converted); $i += 2) {
        $codeUnit = ord($converted[$i]) + (ord($converted[$i+1]) << 8);
        echo $codeUnit . PHP_EOL;
}

首先,这会将UTF-8字符串转换为UTF-16LE。我们对编写UTF-16 代码单元(根据行为charCodeAt())感兴趣,并且这些代码由16位表示。循环一次只跳2个字节。对于每次迭代,它将获取该位置的字节的数值,并将其添加到下一个字节,左移8位。左移是因为我们正在处理小端格式化的UTF- 16。

举例来说,考虑角色BENGALI DIGIT ONE()。这由单个UTF-16代码单元2535表示。首先描述如何将其编码为UTF-16BE更容易。该字符的单个代码单元将消耗16位:

0000100111100111 (2535)

在PHP中,字符串实际上是字节数组。因此,PHP将其视为:

$converted[0] = 00001001 (9)
$converted[1] = 11100111 (231)

鉴于上面的2个字节,我们如何获得代码单元?我们真正想做的是:

   0000100100000000 (2304)
+          11100111 (231)
=  0000100111100111 (2535)

但我们无法做到这一点,因为我们只有一个字节可供使用。一种方法是处理这是使用整数,给我们一个完整的64位(8字节)..我们想要以整数形式表示代码单位,所以这似乎是一个合理的路线。我们可以通过ord()获取每个字节的数值:

ord($converted[0]) == 0000000000000000000000000000000000000000000000000000000000001001 == 9
ord($converted[1]) == 0000000000000000000000000000000000000000000000000000000011100111 = 231

左移第一个值8:

   0000000000000000000000000000000000000000000000000000000000001001 (9) 
<< 0000000000000000000000000000000000000000000000000000000000001000 (8)
=  0000000000000000000000000000000000000000000000000000100100000000 (2304)

然后像以前一样总结:

   0000000000000000000000000000000000000000000000000000100100000000 (2304)
+  0000000000000000000000000000000000000000000000000000000011100111 (231)
=  0000000000000000000000000000000000000000000000000000100111100111 (2535)

因此我们现在拥有正确的代码单位值2535。与UTF-16LE的唯一区别是字节的顺序是相反的。因此,我们不需要将第一个字节左移8,而是需要左移第二个字节。

P.S:执行此步骤的等效方法是执行

for ($i = 0; $i < strlen($converted); $i += 2) {
        $codeUnit = unpack('v', $converted[$i] . $converted[$i+1]);
        echo $codeUnit . PHP_EOL;
}

unpack函数将完全按照所描述的那样提供v格式化程序,它告诉它期望16位以小端排列。如果您对优化速度感兴趣,可能值得对2进行基准测试。

答案 2 :(得分:1)

如果确实需要等同于JavaScript的charCodeAt方法,请尝试:

function JS_charCodeAt($str, $index) {
    $utf16 = mb_convert_encoding($str, 'UTF-16LE', 'UTF-8');
    return ord($utf16[$index*2]) + (ord($utf16[$index*2+1]) << 8);
}

charCodeAt存在问题,应替换为codePointAt。大多数JavaScript代码处理补充Unicode平面中的字符,如Emojis和使用charCodeAt可能是错误的。您可以在问题UTF-8 safe equivalent of ord or charCodeAt() in PHP的答案中找到模仿codePointAt的代码。