101. 音译转化Transliteration
音译转化Transliteration服务用于依据发音将Unicode字符串转化为US-ASCII字符串,这和翻译是不同的概念,对于中国人来说最直观的理解就是将中文文字转变为拼音,Unicode涵盖世界所有语言的字符,因此该服务可转换所有的语言,而不仅仅用于中文;在drupal中通常用于依据用户输入产生识别id,如在后台定义字段操作中,输入中文的标签时,系统用该服务自动产生机器名。
服务定义及使用示例:
服务定义如下:
transliteration:
class: Drupal\Core\Transliteration\PhpTransliteration
arguments: [null, '@module_handler']
第一个参数是数据目录,如果为NULL将使用类文件所在目录下的“data”目录,默认如下:
core\lib\Drupal\Component\Transliteration\data
服务获取方法:\Drupal::transliteration()
使用示例:
在控制器中运行以下代码:
$str="我是云客,很高兴认识您。";
$lang=\Drupal\Core\Language\LanguageInterface::LANGCODE_DEFAULT;
echo \Drupal::transliteration()->transliterate($str, $lang, '_');
将输出:
“woshiyunke,hengaoxingrenshinin. ”
如果想进一步得到变量名,可以这样处理:
$transliterated = \Drupal\Component\Utility\Unicode::strtolower($transliterated);
$transliterated = preg_replace('@[^a-z0-9_.]+@', '', $transliterated);
实现原理概述:
你可能会对此感到非常好奇,但实际上很简单,系统附带了一份Unicode编码与音译字符的对应文件,用户也可以依据语言自定义该映射数据,在转化时按Unicode字符码查找替换即可,在理解代码之前需要先明白一些编码知识。
Unicode与UTF-8编码:
Unicode码称为统一码,或万国码,是一种包含世界各国语言字符的编码,目前还在不断发展中以包括更多字符,UTF-8是为解决Unicode码储存浪费问题而生的再次编码方案,表示一个Unicode字符的UTF-8编码是变长的,目前代表Unicode字符的UTF-8编码最多4字节(每字节有8比特),具体有多少字节可由UTF-8编码最前面几比特推断出来,第一比特如果为0则仅一字节,如果为110、1110、11110则分别代表有2、3、4字节,规则有以下两点:
一、对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
二、对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
换句话说UTF-8编码字节数和对应的形式如下:
1字节形式:0xxxxxxx
2字节形式:110xxxxx 10xxxxxx
3字节形式:1110xxxx 10xxxxxx 10xxxxxx
4字节形式:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
如果不是这样的形式,则不是有效的UTF-8字符,UTF-8编码可表示所有的Unicode编码字符,将以上这些形式中的X按顺序提取出来形成的二进制串就是Unicode码的整数值,本文将其称为Unicode字符码,她是本篇的重点,系统据此查找并替换音译字符。
为便于读者理解,云客在这里提供以下两个辅助程序:
一个UTF-8编码字符可以通过以下函数显示其内部的二进制形式表示:
function echoUTF8($character)
{
$count = 0;
for ($i = 3; $i >= 0; $i--) {
if (isset($character[$i])) {
$count = $i;
break;
}
}
$s = '';
for ($i = 0; $i <= $count; $i++) {
$s .= str_pad(base_convert(ord($character[$i]), 10, 2), 8, '0', STR_PAD_LEFT) . " ";
}
echo $s;
}
如echoUTF8 ("客");,将输出汉字“客”的utf8内部表示:
“11100101 10101110 10100010”,
按照上述转Unicode的方法(提取模板中的x)提取二进制串将是:
“0101 101110 100010”
它的十进制表示为:23458,十六进制表示为:5ba2,Unicode编码正是其十六进制表示,记为“\u5ba2”
一个UTF-8编码字符可以通过以下程序得到Unicode字符码的十六进制表示:
function echoUnicode($character)
{
$first_byte = ord($character[0]);
$code = -1;
if (($first_byte & 0x80) == 0) {
$code = $first_byte;
}
if (($first_byte & 0xe0) == 0xc0) {
$code = (($first_byte & 0x1f) << 6) + (ord($character[1]) & 0x3f);
}
if (($first_byte & 0xf0) == 0xe0) {
$code = (($first_byte & 0x0f) << 12) + ((ord($character[1]) & 0x3f) << 6) + (ord($character[2]) & 0x3f);
}
if (($first_byte & 0xf8) == 0xf0) {
$code = (($first_byte & 0x07) << 18) + ((ord($character[1]) & 0x3f) << 12) + ((ord($character[2]) & 0x3f) << 6) + (ord($character[3]) & 0x3f);
}
if ($code == -1) {
echo "the character is not legal.";
return;
}
echo $s = '\u' . str_pad(base_convert($code, 10, 16), 4, '0', STR_PAD_LEFT);
}
调用echoUnicode("客");将输出:“\u5ba2”
源码解释:
在知道了原理后我们来看一看程序,代码以drupal组件的方式提供(意味着可独立用于其他项目),核心继承添加了模块修改功能,各方法介绍如下:
public function transliterate($string, $langcode = 'en', $unknown_character = '?', $max_length = NULL);
将字符串音译转化为US-ASCII字符串,参数含义如下:
$string:要被转化的字符串,需传入UTF-8编码的字符串,否则视为无效以未知字符代替
$langcode:被转化的字符串所属的语言的语言代码,默认为en(英语),以运用语言特定的覆写
$unknown_character :当找不到转化等价物时的代替字符串,默认为'?'
$max_length:转化后的字符串最大长度限制,默认为 NULL,代表不限制,在截取时以原字符作为单位,不会将一个字符劈开,举个例子:“云客”转化后是“yunke”,如果该参数被设置为4,结果将是“yun”,而不是“yunk”,因为“客”作为一个整体,如果截取会使其被劈开,此时将整个舍去
该方法通过正则表达式分隔函数preg_split逐个字符处理,'//u'表示按unicode(utf-8)匹配(主要针对多字节比如汉字),如果字符不是合法的UTF-8编码的unicode字符,将用传入的未知字符作为转化结果
protected static function ordUTF8($character)
返回一个UTF-8编码的Unicode字符的字符编码十进制整数表示,类似ord函数,但该方法是针对utf8的 ,如果传入的不是UTF-8编码字符将返回-1,该返回值也就是前一节所指的Unicode字符码,可将其当做一个十进制整数,比如“云客”的“客”传入该方法将返回十进制整数:23458,该返回值转化为十六进制即是传入字符的Unicode码表示,代码如下:
$int =23458;
$s = '\u' . str_pad(base_convert($int, 10, 16), 4, '0', STR_PAD_LEFT);
此时$s的值为“\u5ba2”,正是汉字“客”的Unicode码表示
该方法使用了位操作,提示:用连续的1进行与(&)操作等价于针对这些位的截取操作,为阅读方便,这里将用到的十六进制对应的二进制列出如下:
0x80 : 10000000
0xe0 : 11100000
0xc0 : 11000000
0x1f : 00011111
0x3f : 00111111
0xf0 : 11110000
0xe0 : 11100000
0x0f : 00001111
0xf8 : 11111000
0x07 : 00000111
protected function replace($code, $langcode, $unknown_character)
依据十进制的Unicode字符码返回音译替换字符,替换字符可以包含多个US-ASCII字符,如果Unicode字符码在ASCII集以内,将直接通过函数chr转换返回,如果不在则查询映射文件,映射文件有两类:默认通用映射和语言特定的覆写映射,后者优先级更高
protected function readLanguageOverrides($langcode)
加载语言覆写映射文件,文件名是将语言代码中除字母和连字符“-”以外的字符去掉的结果,php文件类型,文件位置默认在core\lib\Drupal\Component\Transliteration\data中(可以通过构造函数改变默认位置),文件内容仅需声明一个php变量$overrides,如德语(语言代码为de)的内容为:
$overrides['de'] = [
0xC4 => 'Ae',
0xD6 => 'Oe',
0xDC => 'Ue',
0xE4 => 'ae',
0xF6 => 'oe',
0xFC => 'ue',
];
键名为语言代码,键值为一个数组,称为覆写数据数组,其键名为十六进制表示的Unicode字符码,也就是前文ordUTF8方法返回的值,注意该键名不要加引号,否则就变成字符串而不是整数了,键值为音译替换字符。
该文件在方法内加载,因此加载的变量是局部变量,如果没有声明$overrides将构造一个空数组,加载的覆写数据数组被保存在属性$this->languageOverrides[$langcode]中
Drupal覆写了该方法,使得模块可以通过修改钩子修改覆写映射,钩子名如下:
transliteration_overrides
默认没有模块实现该修改钩子,修改钩子函数如下:
hook_transliteration_overrides_alter(&$overrides, $langcode);
参数$overrides为覆写数据数组,键名为十六进制表示的Unicode字符码,见上文,如果不存在覆写数据,也会派发该钩子,此时该参数为一个空数组,模块可以添加覆写数据。参数$langcode为语言代码
protected function lookupReplacement($code, $unknown_character = '?')
依据Unicode字符码返回默认的音译映射字符串,这是通过查询预先准备的默认映射数据实现的,映射数据储存方式如下:
将Unicode字符码的低8位去除,剩下的高位转化为十六进制,不足两位时在左端补0,加“x”作为前缀,以此方式得出的字符串作为文件名,php类型,文件位置和语言覆写数据相同,文件内容仅声明一个变量$base,其值为一个数组,键名为Unicode字符码低8位转化的整数,以十六进制方式表示,在默认数据中可以看到许多键名被省略,这是因为php会按顺序加一形成键名;键值为音译替换字符,可以有多个,这里仍然以“客”作为列子,其Unicode字符码为23458,十六进制表示为:5ba2,低八位是“a2”,其他高位为“5b”,那么文件名就是“x5b.php”,打开这个文件,在“0xA0”位置是chong,向右两个元素就是“0xa2”,其键值正是“ke”。
public function removeDiacritics($string)
移除变音符号,变音符号(diacritics 或accents)是标明一个词如何发音的符号,在法语和西班牙语等语言中非常常见,在英语中不常见,对于汉语来说更是陌生,详见:
https://en.wikipedia.org/wiki/Diacritic
该方法实现比较简单,不多讲
补充说明:
1、该服务不会考虑汉语拼音多音字问题,如“行走”、“行业”中的“行”都会被转化为“xing”,正确转化拼音是一个很复杂的问题,需要考虑前后上下文,需要多音字相关的额外数据
2、该服务仅处理utf-8编码的字符串,不能直接处理GBK等编码,如有需要须先通过php函数转化字符串为utf-8编码
3、该服务的实现代码有部分来自MediaWiki项目的UtfNormal类,地址:
4、如果转化结果需要当做php变量使用,需要做进一步处理,去除标点符号等特殊字符,以下中文标点符号和对应的转化结果如下:
原中文符号: ,。、;‘”:?!@#¥%……&*()——+【】{}()
转化后符号: ,.,;'":?!@#Y=%......&*()--+[()] {}()