PHP 字符串与 HTML 实体编码解码

HTML 转义与解码 (十进制)

下面这个文本是经过 HTML 编码的文本, 表示的是一些汉字的 Unicode 编码。在 HTML 中使用 Unicode 编码可以用 &# 后跟 Unicode 码点的十进制值来表示特定字符。HTML 中的 &# 编码 (称为字符实体)

# 发邀请区版规
发邀区版规

UTF-16 与 UCS-2 的区别

UCS-2 和 UTF-16 都是 Unicode 的字符编码

Unicode 介绍

Unicode 通过明确的名称和称其为代码点的整数来识别字符。例如, © 字符被命名为 “copyright sign”, 其码位为 U+00A9 (0xA9 , 十进制为 169)

Unicode 代码空间分为 17 个平面 (plane), 每个平面有 2^16 (65,536) 个码位 (code point)。

第一个 Unicode 平面 (码位从 U+0000U+FFFF) 包含了最常用的字符, 该平面被称为基本多文种平面 (Basic Multilingual Plane), 缩写为 BMP。其他平面称为辅助平面 (Supplementary Planes)

只需要记住, 有 BMP 字符和非 BMP 字符, 后者也称为补充字符或星体字符。

UCS-2

UCS-2 (2-byte Universal Character Set) 是固定长度编码方式, UCS-2 仅简单的使用一个 16 位代码单元来表示码位, 也就是 00xFFFF 码位范围内 (即 BMP), 它和 UTF-16 基本一致

不能正确处理 emoji (4 字节字符), 因为它依赖于 UCS-2 编码, 这种编码方式不支持 3 字节及以上的字符

UTF-16

UTF-16 (16-bit Unicode Transformation Format) 是 UCS-2 的拓展, 它产生一个可变长度结果。它可以表示 BMP 以外的字符。UTF-16 使用一个或者两个 16 位的码元来表示码位, 这样就可以从 00x10FFFF 范围内的码位进行编码

简单的说, UTF-16 可看成是 UCS-2 的父集。在没有辅助平面字符 (surrogate code points) 前, UTF-16 与 UCS-2 所指的是同一的意思。 (严格的说这并不正确, 因为在 UTF-16 中从 U+D800U+DFFF 的码位不对应于任何字符, 而在使用 UCS-2 的时代, U+D800U+DFFF 内的值被占用) 但当引入辅助平面字符后, 就称为 UTF-16 了。

Unicode 编码函数

此函数将原始中文字符串 (支持指定编码) 转换为 Unicode 编码字符串, 格式为指定前缀和后缀 (默认为 &#;), 可用于显示特殊字符或多语言场景下的 Unicode 表示

GBK 转 HTML 实体

假设所有字符都在基本多文种平面 (BMP) 内, 即每个字符正好占用两个字节, 而无需考虑代理对

使用 UCS-2BE, 保证字符按大端序编码 (big-endian)。这将确保每个字符的高位字节在前、低位字节在后, 与 Unicode 码点顺序一致

/**
 * $str 原始中文字符串
 * $encoding 原始字符串的编码, 默认 GBK
 * $prefix 编码后的前缀, 默认 "&#"
 * $postfix 编码后的后缀, 默认 ";"
 */
function unicode_encode($str, $encoding = 'GBK', $prefix = '&#', $postfix = ';') {
    $str = iconv($encoding, 'UCS-2BE', $str);
    $arrstr = str_split($str, 2);
    $unistr = '';
    for ($i = 0, $len = count($arrstr); $i < $len; $i++) {
        $dec = hexdec(bin2hex($arrstr[$i]));
        $unistr .= $prefix . $dec . $postfix;
    }
    return $unistr;
}

这段代码会将字符串转换为 UCS-2BE 编码, 但 UCS-2BE 是一个 2 字节编码, 只适用于基本的多语言平面 (BMP) 字符。它 无法正确处理 3 字节或 4 字节的 UTF-8 字符, 如汉字 和 emoji (通常是 4 字节的 Unicode 字符)。因此, 任何 4 字节的字符 (比如 emoji) 在转换过程中会丢失或被错误编码。

UTF-8 转 HTML 实体

可以使用 mb_convert_encoding 将字符串转换为 HTML-ENTITIES 十进制编码

function unicode_encode($str, $encoding = 'UTF-8') {
    // 使用 mb_convert_encoding 将 UTF-8 字符串转换为 HTML-ENTITIES
    $str = mb_convert_encoding($str, 'HTML-ENTITIES', $encoding);
    return $str;
}

直接处理 UTF-8 字符

mb_convert_encoding 函数不可用代替方案, 可能是因为 PHP 没有安装或启用 mbstring 扩展。你可以尝试另一种方法, 不依赖 mbstring

不使用 mb_convert_encoding, 而是直接处理 UTF-8 编码的字符串, 将每个字符逐字节转换为十进制 Unicode 值

function unicode_encode($str, $encoding = 'UTF-8', $prefix = '&#', $postfix = ';') {
    $str = iconv($encoding, 'UTF-8', $str);
    $unistr = '';

    // 遍历每个 UTF-8 字符
    for ($i = 0, $len = strlen($str); $i < $len; $i++) {
        $ord = ord($str[$i]);

        // 根据 UTF-8 编码的首字节判断字符字节长度
        if ($ord < 128) {  // 1字节字符 (ASCII)
            $unistr .= $prefix . $ord . $postfix;
        } elseif ($ord < 224) {  // 2字节字符
            $code = (($ord & 0x1F) << 6) | (ord($str[++$i]) & 0x3F);
            $unistr .= $prefix . $code . $postfix;
        } elseif ($ord < 240) {  // 3字节字符
            $code = (($ord & 0x0F) << 12) | ((ord($str[++$i]) & 0x3F) << 6) | (ord($str[++$i]) & 0x3F);
            $unistr .= $prefix . $code . $postfix;
        } else {  // 4字节字符 (不常见, 但包括一些 emoji 等)
            $code = (($ord & 0x07) << 18) | ((ord($str[++$i]) & 0x3F) << 12) | ((ord($str[++$i]) & 0x3F) << 6) | (ord($str[++$i]) & 0x3F);
            $unistr .= $prefix . $code . $postfix;
        }
    }

    return $unistr;
}

这个代码能够逐字节地解析 UTF-8 编码字符, 并根据字符的字节长度 (1 到 4 字节) 来确定如何解析它们。对于 4 字节字符 (如 emoji), 它使用正确的位运算将其转换为相应的 Unicode 编码。因此, 它能够处理包含 emoji 的字符串, 并输出正确的 Unicode 编码。

emoji 字符通常是 UTF-8 编码的 4 字节字符, 代码能够识别并正确转换为 Unicode 编码。例如, 😍 (心形眼睛 emoji) 会被正确转换为 &#128525;

Unicode 解码函数

HTML 实体 转 GBK

该函数将 Unicode 编码字符串 (带指定前后缀) 还原为指定编码的原始字符串。示例中的默认编码为 GBK

/**
 * $str Unicode编码后的字符串
 * $decoding 原始字符串的编码, 默认 GBK
 * $prefix 编码字符串的前缀, 默认 "&#"
 * $postfix 编码字符串的后缀, 默认 ";"
 */
function unicode_decode($unistr, $encoding = 'GBK', $prefix = '&#', $postfix = ';') {
    $arruni = explode($prefix, $unistr);
    $unistr = '';
    for ($i = 1, $len = count($arruni); $i < $len; $i++) {
        if (strlen($postfix) > 0) {
            $arruni[$i] = substr($arruni[$i], 0, strlen($arruni[$i]) - strlen($postfix));
        }
        $temp = intval($arruni[$i]);
        $unistr .= ($temp < 256) ? chr(0) . chr($temp) : chr($temp / 256) . chr($temp % 256);
    }
    return iconv('UCS-2BE', $encoding, $unistr);
}

HTML 实体 转 UTF-8

要将通过 unicode_encode 编码后的 Unicode 编码字符串解码回原始的字符 (包括 emoji), 可以通过解析这些 Unicode 编码并将它们转换回相应的字符

function unicode_decode($str, $encoding = 'UTF-8') {
    $str = mb_convert_encoding($str, $encoding, 'HTML-ENTITIES');
    return $str;
}

直接处理 UTF-8 字符

不依赖 mb_convert_encoding

function unicode_decode($str, $encoding = 'UTF-8', $prefix = '&#', $postfix = ';') {
    $pattern = '/' . preg_quote($prefix) . '(\d+)' . preg_quote($postfix) . '/';
    return preg_replace_callback($pattern, function ($matches) use ($encoding) {
        $code = intval($matches[1]);

        if ($code >= 0x10000) {
            // 对于大于 0xFFFF 的 Unicode 编码 (4 字节字符, 如 emoji)
            $code -= 0x10000;
            $highSurrogate = 0xD800 | ($code >> 10);
            $lowSurrogate = 0xDC00 | ($code & 0x3FF);
            $packed = pack('v', $highSurrogate) . pack('v', $lowSurrogate);
            return iconv('UTF-16LE', $encoding, $packed);
        } else {
            // 对于小于 0xFFFF 的 Unicode 字符
            return iconv('UCS-4LE', $encoding, pack('V', $code));
        }
    }, $str);
}

其他实现

实现更简洁, 专门针对 &#...; 格式的 Unicode 实体解码, 编码固定为 UTF-8

function unicode_decode($unistr) {
    $arr = preg_split("/(&#[0-9]*;)/", $unistr, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
    $restr = '';
    foreach ($arr as $key => $value) {
        if (strstr($value, '&#')) {
            $unistr = '';
            $arruni = explode('&#', $value);
            $arruni = substr($arruni[1], 0, strlen($arruni[1]) - 1);
            $temp = intval($arruni);
            $unistr .= ($temp < 256) ? chr(0) . chr($temp) : chr($temp / 256) . chr($temp % 256);
            $restr .= iconv('UCS-2BE', 'UTF-8', $unistr);
        } else {
            $restr .= $value;
        }
    }
    return $restr;
}

测试

源代码: example.php

UTF-8 字符串测试

header('Content-Type: text/html; charset=UTF-8');

function pr($data) {
    echo '<xmp>';
    var_dump($data);
    echo '</xmp>';
}

$str = '哈哈😍';
pr($str);

$unistr = unicode_encode($str);
pr($unistr);

$str2 = unicode_decode($unistr);
pr($str2);

输出结果

string(10) "哈哈😍"
string(25) "&#21704;&#21704;&#128525;"
string(10) "哈哈😍"

GBK 字符串测试

注意: GBK 在 UTF-8 下显示的乱码!可以在 php 文件头加入 header()

GBK 编码不支持 emoji

header('Content-Type: text/html; charset=GBK');

$str = '哈哈';
pr($str);

$unistr = unicode_encode($str);
pr($unistr);

$str2 = unicode_decode($unistr);
pr($str2);

$gbk_str = iconv('UTF-8', 'GBK', $str);
pr($gbk_str);

$gbk_unistr = unicode_encode($gbk_str, 'GBK');
pr($gbk_unistr);
  
$gbk_str2 = unicode_decode($gbk_unistr, 'GBK');
pr($gbk_str2);

输出结果

string(6) "鍝堝搱"
string(16) "&#21704;&#21704;"
string(6) "鍝堝搱"
string(4) "哈哈"
string(16) "&#21704;&#21704;"
string(4) "哈哈"

其它后缀、前缀测试

$str = '哈哈😍';
pr($str);

$prefix_unistr = unicode_encode($str, 'UTF-8', "\\u", '');
pr($prefix_unistr);

$profix_unistr2 = unicode_decode($prefix_unistr, 'UTF-8', "\\u", '');
pr($profix_unistr2);

输出结果

string(10) "哈哈😍"
string(22) "\u21704\u21704\u128525"
string(10) "哈哈😍"

原文

php 中文unicode 互转
UTF-16与UCS-2的区别
JavaScript 的内部字符编码:UCS-2 还是 UTF-16?
在线字符统计:支持Unicode、URL、CSS实体、HTML实体、Base64 等编码/解码 - 工具 - CodePlayer
PHP 在线工具 | 菜鸟工具

最后更新于 2024-11-11
使用 Hugo 构建
主题 StackJimmy 设计