在Linux上使用pyodbc向MSSQL的nvarchar字段插入Unicode或UTF-8字符

23 投票
4 回答
17924 浏览
提问于 2025-04-15 12:01

我正在使用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 个回答

1

我在尝试绑定unicode参数时遇到了同样的问题:'[HY004] [FreeTDS][SQL Server]无效的数据类型 (0) (SQLBindParameter)'。

我通过升级freetds到0.91版本解决了这个问题。

我使用的是pyodbc 2.1.11。为了让它支持unicode,我不得不应用这个补丁,否则我会偶尔遇到内存损坏的错误。

2

我用的是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。

22

我记得以前用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驱动相关。抱歉。

撰写回答