背景
使用Django3.2.5作为后端开发框架,数据库为Oracle11.2。据Django官方文档说明,要求Oracle Database的最低版本为12c。阅读过Django中ORM(对象关系模型)的实现,ORM中常用的自增id、sql查询分页等数据库特性在Oracle11.2均没有实现,从Oracle12c版本开始才有这些特性,这也是Django框架要求Oracle版本最低为12c的主要原因。但是项目的数据库Oracle11.2已不可能更换。这也为后面各种业务的开发在某些具体方面带来了技术问题。本文涉及的cx_Oracle字符集问题就是其中之一。此外,后续我将就此背景分享一些其他问题及解决方案,因为期间遇到的问题在网上很少有涉及,自己花费了不少精力去解决。
问题
Django页面在保存中文字符时出现乱码。
分析
知识背景
根据以往的经验,中文字符保存后出现乱码基本上是字符集转换出错引起。
Django后端对不同的数据库客户端进行了一次封装,以保证上层ORM调用数据库访问接口时的统一。Oracle的python客户端为cx_Oracle,其本质就是将Oracle客户端库进行python封装。这里为了简化Django里的问题排查过程,我将直接使用cx_Oracle访问Oracle11.2,并使用一个单独的表“test”用于测试。
SQL> create table test(id number(9), txt nvarchar2(128), txt2 varchar2(128));
这里创建varchar2和nvarchar2的原因是Oracle数据库中有数据库字符集和国家字符集的概念,这两种字符集用途不一样,受服务端和客户端字符集的影响,这个我稍后具体介绍。
这里额外啰嗦下字符集及python下的字节串和字符串。
字符集可以这么理解,就是将某些字符汇总在一起,然后给它们编个号,这个号就是码位(code point),一个码位与一个字符对应,计算机在处理字符时,实际就是处理这些码位值。比如美国使用的英文字母及符号汇总在一起,然后编号,最后为了能让计算机方便存储,用一个字节(8个比特位)的空间按码位存储,这里的编号就是为字符分配码位,用一个字节存储就是对码位进行编码,只不过刚好码位跟编码的结果一样,这就是我们熟知的ASCII码(美国标准信息交换码)。再比如将中国使用的汉字及符号汇总起来,编号,这就是中国国标字符集,最后为了方便计算机存储和运算,使用两个字节的空间按码位直接存储,这就是我们熟知的GBK编码。很多国家按照这个思路提供了自己的字符集标准。
互联网的崛起,为了方便各个地区的人访问,需要统一字符集,于是就有了Unicode字符集。Uicode字符集囊括世界几乎所有国家的字符,而且每年还有增加,显然用一个字节的空间存储是不行的,通常情况下使用两个字节就能够涵盖大部分各个国家常用的字符。两个字节对于字母地区的人来说,显然又比较浪费存储空间,于是就有了大家熟知utf-8编码方案,该方案用一个字符存储ASCII字符集的字符,三个字节存储汉字等字符集。
python(这里指python3)的默认编码方案为utf-8,即python代码从磁盘文件被加载值虚拟机时,按utf-8编码进行解码。比如磁盘存储的字符串“你”,字节序列为0xe4 0xbd 0xa0,被加载至虚拟机内存时,字节序列为0x4f 0x60,实际上0x4f 0x60就是“你”的Unicode码位值,也就是说python中的字符串实际是Unicode的码位串。我们对这个码位进行utf-8编码后,生成一个字节串,也就是utf-8编码后的字节序列,这个字节序列通常用于存储和传输,这也是utf(Unicode格式传输转换编码)名称的由来。
>>> a = '你'
>>> a
'你'
>>> ord(a)
20320
>>> hex(ord(a))
'0x4f60'
>>> b = a.encode(encoding='utf-8')
>>> b
b'\xe4\xbd\xa0'
实验一
言归正传,我们现在直接往test表插入数据,并查询数据,看看能得到什么结果。
import cx_Oracle
import os
def str_to_hex(s):
if s is None:
return '<null>'
return r' '.join([hex(ord(c)) for c in s])
db = cx_Oracle.connect(dsn='orcl', user='xx', password='xx')
cursor = db.cursor()
cursor.execute("update test set txt=:param, txt2=:param2", param='你', param2='你')
db.commit()
rows = cursor.execute('select txt,txt2 from test')
for row in rows:
print(type(row[0]), type(row[1]))
print('txt=', row[0], 'txt2=', row[1].decode(encoding='utf-8') if isinstance(row[1], bytes) else row[1])
print('txt=', str_to_hex(row[0]), 'txt2=', row[1] if isinstance(row[1], bytes) else str_to_hex(row[1]))
db.close()
执行上述python代码后,从表test获取到的值在