多语言字符集系列文章-- 第二篇 各种技术对字符集使用的声明
各种技术在交换数据的时候,就需要知道对方给的数据使用什么字符集和字符编码,否则很可能就解码错了。这里列举了医疗行业常见的数据交换技术方式和它们对字符集使用的声明方式。
2.1 文件
文件是字符型数据最常见的交换方式,文本编辑工具通常在保存时都会让用户选择保存成什么字符编码。对于不同的字符编码,文件是如何保存的呢?
通常会在文件头使用字节顺序标志(BOM,Byte Order Mark)来标记文件的编码。下表是常见的编码格式对应的BOM,注意ANSI并不需要BOM,我把它列在这里的目的是希望一目了然。
字节 |
编码格式 |
00 00 FE FF |
UTF-32, big-endian |
FF FE 00 00 |
UTF-32, little-endian |
FE FF |
UTF-16, big-endian |
FF FE |
UTF-16, little-endian |
EF BB BF |
UTF-8 |
空 |
ANSI |
例如,汉字的“中”的各种编码如下:
用Windows的写字板软件将这个汉字分别保存成UTF-8、ANSI、Unicode编码,在UltraEdit打开其16进制模式查看,就可以看到如下的输出:
这里ANSI其实保存的是GBK码(使用GBK代码页936),Unicode其实保存的是UTF16编码。试图以ANSI保存文件时,对超出了GBK编码范围的汉字,Windows写字板软件会提示包含Unicode文字,以ANSI保存会丢失数据,如下图:
对于ANSI保存的包含汉字的文件,代码页信息并不在文件里。并不是所有的文本编辑器和文字处理代码都能正确解析这样的文件,因为它们并不知道代码页。这是可能造成文件中文乱码的一个原因。
另外,前面提到通过BOM可以确定文件编码方式,但并不是所有的文件都使用了BOM。因此特定的文本编辑器和文字处理代码对中文都可能产生显示乱码。
2.2 HTTP
对于HTTP消息内容,包括SOAP、RESTful,浏览器/客户端和服务器怎么知道字符编码呢?
HTTP头的Content-Type可以通过参数charset指定文字编码。例如:
Content-Type: text/html; charset = UTF-8
如果没有正确配置charset,就可能产生乱码。例如网页表单提交的中文数据,服务器没有正确解码从而产生乱码。
不同的Web服务器都可以设置默认的charset,例如Apache可以修改httpd.conf文件,通过配置AddDefaultCharset指定默认字符集编码。
2.3 XML
XML文件当然可以使用前面提到的文件编码方式设置,以HTTP传递的XML数据也可以使用HTTP的文字编码设置。同时,XML规范自己也定义了文字编码声明的方式,从而保证通过任何方式传递(例如TCP)的XML都可以被正确解析。
XML定义的编码方式是设置encoding属性,例如:
<?xml version="1.0" encoding="UTF-8"?>
注意,JSON并没有声明文字编码的设置。通常JSON数据都是通过HTTP传递的,因此使用HTTP的文字编码设置。
2.4 数据库连接
数据库连接也是造成文字乱码的一个重灾区。客户端从数据库服务器获取数据、向数据库服务器提交数据,怎么知道数据的文字编码呢?
和数据库相关的文字编码有2部分:数据库内码、数据库连接使用的字符编码。
数据库内码:
由于Unicode码历史并不悠久,数据库管理系统的历史远早于Unicode,直到最近这20年,数据库厂商才开始支持Unicode内码。当然,每个厂商的Unicode内码编码也不一样,例如InterSystems的Caché和IRIS是UTF-16格式,而Oracle是UTF-8和UTF-16。因为UTF-8处理效率低,大多数Unicode数据库都使用UTF-16。现在依然能看到不是Unicode内码的老版本数据库。
很多数据库相关的字符编码问题和数据库内码设置有关。例如国内不少的Oracle安装时没设置过字符编码,数据库内码NLS_CHARACTERSET默认为US7ASCII,客户端字符集NLS_LANG默认为AMERICAN_AMERICA.US7ASCII。这并不会造成中文无法保存,因为数据库只是将客户端的数据逐位保存下来,无论什么编码。但这样保存的中文数据只有Oracle自己的客户端能正常显示,其它的客户端可能就按ASCII处理从而造成乱码。因此正确设置数据库的内码很关键。
数据库连接使用的字符编码:
ODBC:
直到ODBC 3.5标准(1997年)之前绝大多数的ODBC连接的函数调用和字符串编码都是ANSI(单字节或双字节),所以中文生僻字大多都会处理异常。
ODBC 3.5 规定ODBC驱动管理器要能够透明地处理Unicode和ANSI之间的转换,从而让ANSI和Unicode的数据库客户端都可以正确地向数据库获取和提交数据。
当然,并不是所有的Unicode字符都能转为ANSI码,例如中文“”字,所以ANSI的数据库客户端依然会遇到生僻字乱码。
JDBC:
Java对字符串使用UTF-16编码,如果数据库内码也使用UTF-16,那么不会有乱码问题,例如InterSystems IRIS。但很多数据库并非使用UTF-16,这些数据库的JDBC驱动就需要支持UTF-16和数据库内码之间的转换。通常可以通过设置JDBC的连接字符串的特定属性来实现。
例如mysql的连接字符串中指定characterEncoding:
jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
其它连接方式:
另外,很多数据库还提供XDBC之外的连接方式,这些连接方式有自己的字符编码逻辑。
2.5 HL7 V2
在医疗行业,HL7 V2是目前最为广泛使用的消息交换标准。它可以通过文件、TCP、HTTP、SOAP 等多种通道交换。因此,HL7 V2消息标准中,也有设置消息字符编码的字段:MSH-18。正确设置该字段有助于不同的系统正确地处理HL7 V2消息里的字符数据。
例如下面的HL7消息在MSH段设置其字符编码为UTF-8:
MSH|^~\&||PHLS|||||ORU^R01|||2.5|||AL|||UTF-8|
PID||S2345|S2345^^^PHLS^MR|C9876^^^COR^XX~S45008787^^^MA^DL|张^三||19301019|M|||1 Memorial Drive^^剑桥市^MA^02142||||||||063070516
PV1||O|||||ISCGP001^建国^李|||||||EO|||||HSVN00008|||||||||||||||||||||||||20200912090700|20200912090700
ORC||00265-001|0606:H00550R||||^^^202009120910||202009120910|||ISCGP001^建国^李|PHLS||||||||PHLS||||||||LAB
OBR||00265-001|0606:H00550R|CBCD^血常规^L|||202009121049|||||||202009120937|Blood|ISCGP001^Moore^James||||||202009121227|||F
OBX||NM|WBC^WHITE BLOOD CELL COUNT||6.24|10(9)/L|4.0-10.6||||F|||202009121049
OBX||NM|RBC^RED BLOOD CELL COUNT||4.99|10x12/L|4.5-5.9||||F|||202009121049
OBX||NM|HGB^HEMOGLOBIN||13.6|g/dL|12.0-16.0||||F|||202009121049
OBX||NM|HCT^HEMATOCRIT||41.6|Percent|36.0-46.0||||F|||202009121049
2.6 其它
在上面提到的和字符集相关的乱码之外,有时我们会混淆一些其它的、并非真正乱码的情况。下面这些中文显示的“乱码”,并非乱码:
URL编码:
根据RFC 3986,如果URL的路径中有URL的保留字,就需要对URL路径中的保留字使用转义符% 进行转码,也叫做百分号编码。例如ASCII中下面的字符都需要转义:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
对于非ASCII码字符,按其UTF-8编码字节顺序加%转义。例如“https://cn.community.intersystems.com/post/多语言字符集系列文章-第一篇-多语言字符集和相关标准简史”会被转义为:
https://cn.community.intersystems.com/post/%E5%A4%9A%E8%AF%AD%E8%A8%80%E5%AD%97%E7%AC%A6%E9%9B%86%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0-%E7%AC%AC%E4%B8%80%E7%AF%87-%E5%A4%9A%E8%AF%AD%E8%A8%80%E5%AD%97%E7%AC%A6%E9%9B%86%E5%92%8C%E7%9B%B8%E5%85%B3%E6%A0%87%E5%87%86%E7%AE%80%E5%8F%B2
其中%E5%A4%9A就是“多”字的UTF-8编码E5A49A的3个字节加上%。
在InterSystems技术平台上,可以用$ZCVT("%E5%A4%9A","I","URI")将URL编码翻译为中文字符,或使用$ZCVT("多","O","URI")将汉字进行URL编码。
HTML实体编码(转义):
HTML实体编码(HTML Entity Encode)将字符编码为&开头;结尾的字符串,例如"<"编码为"<"。而中文字符就会被按其unicode编码进行转义,例如“广”字的unicode 10进制码为24191,所以它的HTML实体编码是"广"。在InterSystems技术平台上,可以用$ZCVT("广","I","HTML")将其翻译为中文字符。
Base64 编码:
当使用SOAP传递的数据是一个XML字符串或其它应被视为二进制类型的数据时,通常使用Base64对其进行编码,从而不会破坏XML结构解析。所以它也不是乱码。