在Linux上使用pyodbc向MSSQL的nvarchar字段插入Unicode或UTF-8字符
我正在使用Ubuntu 9.04
我安装了以下版本的软件包:
unixodbc and unixodbc-dev: 2.2.11-16build3
tdsodbc: 0.82-4
libsybdb5: 0.82-4
freetds-common and freetds-dev: 0.82-4
我把/etc/unixodbc.ini
配置成这样:
[FreeTDS]
Description = TDS driver (Sybase/MS SQL)
Driver = /usr/lib/odbc/libtdsodbc.so
Setup = /usr/lib/odbc/libtdsS.so
CPTimeout =
CPReuse =
UsageCount = 2
我把/etc/freetds/freetds.conf
配置成这样:
[global]
tds version = 8.0
client charset = UTF-8
我从http://github.com/mkleehammer/pyodbc
下载了pyodbc的版本31e2fae4adbf1b2af1726e5668a3414cf46b454f
,并通过"python setup.py install
"安装了它。
我在本地网络上有一台装有Microsoft SQL Server 2000的Windows机器,正在监听本地IP地址10.32.42.69。我创建了一个名为"Common"的空数据库。用户"sa"的密码是"secret",并且拥有所有权限。
我使用以下Python代码来设置连接:
import pyodbc
odbcstring = "SERVER=10.32.42.69;UID=sa;PWD=secret;DATABASE=Common;DRIVER=FreeTDS"
con = pyodbc.connect(s)
cur = con.cursor()
cur.execute('''
CREATE TABLE testing (
id INTEGER NOT NULL IDENTITY(1,1),
name NVARCHAR(200) NULL,
PRIMARY KEY (id)
)
''')
con.commit()
到目前为止,一切正常。我在服务器上使用SQL Server的企业管理器,新的表格已经存在。现在我想在表格中插入一些数据。
cur = con.cursor()
cur.execute('INSERT INTO testing (name) VALUES (?)', (u'something',))
但是这失败了!我得到的错误是:
pyodbc.Error: ('HY004', '[HY004] [FreeTDS][SQL Server]Invalid data type
(0) (SQLBindParameter)'
因为我的客户端配置为使用UTF-8,所以我想通过将数据编码为UTF-8来解决这个问题。这样确实有效,但我得到的却是奇怪的数据:
cur = con.cursor()
cur.execute('DELETE FROM testing')
cur.execute('INSERT INTO testing (name) VALUES (?)', (u'somé string'.encode('utf-8'),))
con.commit()
# fetching data back
cur = con.cursor()
cur.execute('SELECT name FROM testing')
data = cur.fetchone()
print type(data[0]), data[0]
这没有错误,但返回的数据和发送的数据不一样!我得到的是:
<type 'unicode'> somé string
也就是说,pyodbc不直接接受unicode对象,但却返回给我unicode对象!而且编码搞混了!
现在我有个问题:
我想要代码能在NVARCHAR和/或NTEXT字段中插入unicode数据。当我查询时,希望能得到我插入的相同数据。
这可以通过不同的系统配置,或者使用一个能够在插入或检索时正确转换数据的包装函数来实现。
这要求也不算高,对吧?
4 个回答
我在尝试绑定unicode参数时遇到了同样的问题:'[HY004] [FreeTDS][SQL Server]无效的数据类型 (0) (SQLBindParameter)'。
我通过升级freetds到0.91版本解决了这个问题。
我使用的是pyodbc 2.1.11。为了让它支持unicode,我不得不应用这个补丁,否则我会偶尔遇到内存损坏的错误。
我用的是UCS-2来和SQL Server进行交互,而不是UTF-8。
更正一下:我修改了.freetds.conf的设置,让客户端使用UTF-8。
tds version = 8.0
client charset = UTF-8
text size = 32768
现在,对于UTF-8编码的字符串,绑定值工作得很好。驱动程序在数据服务器端使用的UCS-2和客户端传递的UTF-8编码字符串之间自动转换。
这是在Solaris 10上使用pyodbc 2.0,运行Python 2.5和FreeTDS freetds-0.82.1.dev.20081111,以及SQL Server 2008的环境下。
import pyodbc test_string = u"""Comment ça va ? Très bien ?""" print type(test_string),repr(test_string) utf8 = 'utf8:' + test_string.encode('UTF-8') print type(utf8), repr(utf8) c = pyodbc.connect('DSN=SA_SQL_SERVER_TEST;UID=XXX;PWD=XXX') cur = c.cursor() # This does not work as test_string is not UTF-encoded try: cur.execute('INSERT unicode_test(t) VALUES(?)', test_string) c.commit() except pyodbc.Error,e: print e # This one does: try: cur.execute('INSERT unicode_test(t) VALUES(?)', utf8) c.commit() except pyodbc.Error,e: print e
这是测试表的输出(我通过管理工作室手动输入了一些测试数据)。
In [41]: for i in cur.execute('SELECT t FROM unicode_test'): ....: print i ....: ....: ('this is not a banana', ) ('\xc3\x85kergatan 24', ) ('\xc3\x85kergatan 24', ) ('\xe6\xb0\xb4 this is code-point 63CF', ) ('Mich\xc3\xa9l', ) ('Comment a va ? Trs bien ?', ) ('utf8:Comment \xc3\xa7a va ? Tr\xc3\xa8s bien ?', )
我能够通过“编辑前200行”对话框直接在管理工作室中将一些Unicode代码点输入到表中,只需输入Unicode代码点的十六进制数字,然后按Alt-X。
我记得以前用odbc驱动的时候也遇到过这种愚蠢的问题,那时候是java和oracle的组合。
关键是,odbc驱动在把查询字符串发送到数据库时,似乎会对其进行编码。即使字段是Unicode格式,并且你提供的是Unicode,在某些情况下,这似乎也没什么用。
你需要确保驱动发送的数据和你的数据库使用的是相同的编码(不仅是服务器,还有数据库)。否则,你会看到奇怪的字符,因为客户端或服务器在编码或解码时搞混了。你知道你的服务器默认使用什么字符集(微软喜欢称之为codepoint)来解码数据吗?
排序规则和这个问题没关系 :)
比如看看这个微软页面。对于Unicode字段,排序规则只是用来定义列中的排序顺序,并不是用来指定数据是如何存储的。
如果你把数据存储为Unicode,那么就有一种独特的方式来表示它,这就是Unicode的目的:不需要定义一个与所有你将使用的语言兼容的字符集 :)
这里的问题是“当我给服务器提供的数据不是Unicode时,会发生什么?”例如:
- 当我发送一个UTF-8字符串到服务器时,它是怎么理解的?
- 当我发送一个UTF-16字符串到服务器时,它是怎么理解的?
- 当我发送一个Latin1字符串到服务器时,它是怎么理解的?
从服务器的角度来看,这三种字符串都只是字节流。服务器无法猜测你编码它们时使用的编码方式。这意味着如果你的odbc客户端最终发送的是字节字符串(一种编码的字符串)而不是Unicode数据,你就会遇到麻烦:如果这样做,服务器会使用一个预定义的编码(这就是我问的:服务器会使用什么编码?因为它不是在猜测,肯定是一个参数值),如果字符串是用不同的编码编码的,dzing,数据就会损坏。
这就像在Python中做的一样:
uni = u'Hey my name is André'
in_utf8 = uni.encode('utf-8')
# send the utf-8 data to server
# send(in_utf8)
# on server side
# server receives it. But server is Japanese.
# So the server treats the data with the National charset, shift-jis:
some_string = in_utf8 # some_string = receive()
decoded = some_string.decode('sjis')
试试吧,挺有意思的。解码后的字符串应该是“Hey my name is André”,但变成了“Hey my name is Andrテゥ”。é被替换成了日文的テゥ。
所以我建议:你需要确保pyodbc能够直接发送数据作为Unicode。如果pyodbc无法做到这一点,你会得到意想不到的结果。
我描述的是从客户端到服务器的问题。但从服务器到客户端的通信也可能出现类似的问题。如果客户端无法理解Unicode数据,你可能会遇到麻烦。
FreeTDS为你处理Unicode。
实际上,FreeTDS会为你处理这些事情,并将所有数据转换为UCS2 Unicode。(来源)。
- 服务器 <--> FreeTDS : UCS2数据
- FreeTDS <--> pyodbc : 编码字符串,使用UTF-8编码(来自
/etc/freetds/freetds.conf
)
所以我希望你的应用程序在将UTF-8数据传递给pyodbc时能够正常工作。实际上,正如这个django-pyodbc的问题所述,django-pyodbc与pyodbc之间是用UTF-8通信的,所以你应该没问题。
FreeTDS 0.82
不过,cramm0说FreeTDS 0.82并不是完全没有bug,并且0.82和官方修补的0.82版本之间有显著差异,官方版本可以在这里找到。你可能应该尝试使用修补过的FreeTDS。
编辑: 删除了与FreeTDS无关的旧数据,这些数据仅与Easysoft商业odbc驱动相关。抱歉。