SQLite、Python、Unicode 和非 UTF 数据
我开始尝试用Python在sqlite中存储字符串,结果收到了这样的提示:
sqlite3.ProgrammingError: 你不能使用8位字节字符串,除非你使用一个可以解释8位字节字符串的text_factory(比如text_factory = str)。强烈建议你直接将应用程序切换到Unicode字符串。
好的,我切换到了Unicode字符串。然后我又收到了这样的提示:
sqlite3.OperationalError: 无法解码UTF-8列'tag_artist'中的文本'Sigur Rós'
当我尝试从数据库中获取数据时出现了这个问题。经过更多的研究,我开始用utf8编码,但'Sigur Rós'变成了'Sigur Rós'
注意:我的控制台设置为显示'latin_1',正如@John Machin指出的那样。
这是怎么回事?在阅读了这个,描述了我所遇到的完全相同的情况后,似乎建议是忽略其他建议,最终还是使用8位字节字符串。
在开始这个过程之前,我对unicode和utf了解不多。在过去的几个小时里,我学到了很多,但我仍然不知道有没有办法正确地将'ó'从latin-1转换为utf-8而不出错。如果没有,为什么sqlite会“强烈推荐”我将应用程序切换到unicode字符串呢?
我打算更新这个问题,提供一个总结和一些我在过去24小时里学到的示例代码,以便和我有相同处境的人能有一个更简单的指南。如果我发布的信息有误或误导,请告诉我,我会更新,或者你们这些资深的可以来更新。
答案总结
让我先说明一下我的理解目标。处理各种编码的目标,如果你想在它们之间转换,就是要了解你的源编码是什么,然后使用该源编码转换为unicode,再转换为你想要的编码。Unicode是一个基础,而编码是该基础的子集的映射。utf_8可以容纳unicode中的每个字符,但因为它们的位置不同,比如说latin_1,所以用utf_8编码的字符串发送到latin_1控制台时,显示的效果可能和你预期的不一样。在Python中,从unicode转换到其他编码的过程看起来像这样:
str.decode('source_encoding').encode('desired_encoding')
或者如果字符串已经是unicode格式
str.encode('desired_encoding')
对于sqlite,我其实不想再编码一次,我想解码并保持在unicode格式。这里有四件事你可能需要了解,当你尝试在Python中处理unicode和编码时。
- 你想处理的字符串的编码,以及你想转换到的编码。
- 系统编码。
- 控制台编码。
- 源文件的编码。
详细说明:
(1) 当你从某个来源读取字符串时,它必须有某种编码,比如latin_1或utf_8。在我的情况下,我是从文件名中获取字符串,所以不幸的是,我可能会得到任何类型的编码。Windows XP使用UCS-2(一个Unicode系统)作为其本地字符串类型,这对我来说似乎有点不公平。幸运的是,大多数文件名中的字符不会由多种源编码类型组成,我认为我的文件名要么完全是latin_1,要么完全是utf_8,或者只是普通的ascii(这是这两者的子集)。所以我只是读取它们,并假设它们仍然是latin_1或utf_8编码。不过,有可能在Windows的文件名中混合了latin_1和utf_8以及其他字符。有时这些字符会显示为方框,有时看起来很混乱,有时则显示正确(带重音的字符等)。继续。
(2) Python有一个默认的系统编码,在Python启动时设置,并且在运行时无法更改。有关详细信息,请查看这里。简单总结一下...这是我添加的文件:
\# sitecustomize.py
\# this file can be anywhere in your Python path,
\# but it usually goes in ${pythondir}/lib/site-packages/
import sys
sys.setdefaultencoding('utf_8')
这个系统编码是在你使用unicode("str")函数而没有其他编码参数时使用的。换句话说,Python尝试根据默认系统编码将“str”解码为unicode。
(3) 如果你使用的是IDLE或命令行Python,我认为你的控制台将根据默认系统编码显示。我出于某种原因使用pydev和eclipse,所以我不得不进入我的项目设置,编辑我的测试脚本的启动配置属性,转到Common标签,并将控制台从latin-1更改为utf-8,以便我可以直观地确认我所做的工作。
(4) 如果你想在源代码中有一些测试字符串,例如
test_str = "ó"
,那么你需要告诉Python你在该文件中使用的编码。(顺便说一下:当我错误输入编码时,我不得不按ctrl-Z,因为我的文件变得不可读。)这可以通过在源代码文件的顶部添加一行来轻松完成:
# -*- coding: utf_8 -*-
如果你没有这些信息,Python会默认尝试将你的代码解析为ascii,因此:
SyntaxError: 文件redacted的第81行中有非ASCII字符'\xf3',但没有声明编码;请参见http://www.python.org/peps/pep-0263.html以获取详细信息
一旦你的程序正常工作,或者如果你不使用Python的控制台或其他控制台查看输出,那么你可能只关心列表中的第1项。系统默认和控制台编码并不那么重要,除非你需要查看输出,或者你使用内置的unicode()函数(没有任何编码参数)而不是string.decode()函数。我写了一个演示函数,我会把它粘贴到这个庞大内容的底部,希望它能正确演示我列表中的项目。当我通过演示函数运行字符'ó'时,以下是一些输出,显示了各种方法对该字符作为输入的反应。我的系统编码和控制台输出都设置为utf_8:
'�' = original char <type 'str'> repr(char)='\xf3'
'?' = unicode(char) ERROR: 'utf8' codec can't decode byte 0xf3 in position 0: unexpected end of data
'ó' = char.decode('latin_1') <type 'unicode'> repr(char.decode('latin_1'))=u'\xf3'
'?' = char.decode('utf_8') ERROR: 'utf8' codec can't decode byte 0xf3 in position 0: unexpected end of data
现在我将系统和控制台编码更改为latin_1,对于相同的输入,我得到了这样的输出:
'ó' = original char <type 'str'> repr(char)='\xf3'
'ó' = unicode(char) <type 'unicode'> repr(unicode(char))=u'\xf3'
'ó' = char.decode('latin_1') <type 'unicode'> repr(char.decode('latin_1'))=u'\xf3'
'?' = char.decode('utf_8') ERROR: 'utf8' codec can't decode byte 0xf3 in position 0: unexpected end of data
注意到'原始'字符显示正确,内置的unicode()函数现在也能正常工作。
现在我将控制台输出改回utf_8。
'�' = original char <type 'str'> repr(char)='\xf3'
'�' = unicode(char) <type 'unicode'> repr(unicode(char))=u'\xf3'
'�' = char.decode('latin_1') <type 'unicode'> repr(char.decode('latin_1'))=u'\xf3'
'?' = char.decode('utf_8') ERROR: 'utf8' codec can't decode byte 0xf3 in position 0: unexpected end of data
在这里,一切仍然和上次一样工作,但控制台无法正确显示输出。等等。下面的函数还显示了更多信息,希望能帮助某人找出他们理解中的差距。我知道所有这些信息在其他地方都有,而且处理得更透彻,但我希望这能为想要用Python和/或sqlite编程的人提供一个良好的起点。想法很好,但有时源代码可以节省你一两天的时间来弄清楚函数的作用。
免责声明:我不是编码专家,我把这些整理在一起是为了帮助我自己的理解。我在构建时应该开始将函数作为参数传递,以避免这么多重复的代码,所以如果可以的话,我会让它更简洁。此外,utf_8和latin_1并不是唯一的编码方案,它们只是我正在尝试的两个,因为我认为它们能处理我需要的所有内容。你可以将自己的编码方案添加到演示函数中,测试自己的输入。
还有一件事:有一些疯狂的应用开发者在Windows中让生活变得困难。
#!/usr/bin/env python
# -*- coding: utf_8 -*-
import os
import sys
def encodingDemo(str):
validStrings = ()
try:
print "str =",str,"{0} repr(str) = {1}".format(type(str), repr(str))
validStrings += ((str,""),)
except UnicodeEncodeError as ude:
print "Couldn't print the str itself because the console is set to an encoding that doesn't understand some character in the string. See error:\n\t",
print ude
try:
x = unicode(str)
print "unicode(str) = ",x
validStrings+= ((x, " decoded into unicode by the default system encoding"),)
except UnicodeDecodeError as ude:
print "ERROR. unicode(str) couldn't decode the string because the system encoding is set to an encoding that doesn't understand some character in the string."
print "\tThe system encoding is set to {0}. See error:\n\t".format(sys.getdefaultencoding()),
print ude
except UnicodeEncodeError as uee:
print "ERROR. Couldn't print the unicode(str) because the console is set to an encoding that doesn't understand some character in the string. See error:\n\t",
print uee
try:
x = str.decode('latin_1')
print "str.decode('latin_1') =",x
validStrings+= ((x, " decoded with latin_1 into unicode"),)
try:
print "str.decode('latin_1').encode('utf_8') =",str.decode('latin_1').encode('utf_8')
validStrings+= ((x, " decoded with latin_1 into unicode and encoded into utf_8"),)
except UnicodeDecodeError as ude:
print "The string was decoded into unicode using the latin_1 encoding, but couldn't be encoded into utf_8. See error:\n\t",
print ude
except UnicodeDecodeError as ude:
print "Something didn't work, probably because the string wasn't latin_1 encoded. See error:\n\t",
print ude
except UnicodeEncodeError as uee:
print "ERROR. Couldn't print the str.decode('latin_1') because the console is set to an encoding that doesn't understand some character in the string. See error:\n\t",
print uee
try:
x = str.decode('utf_8')
print "str.decode('utf_8') =",x
validStrings+= ((x, " decoded with utf_8 into unicode"),)
try:
print "str.decode('utf_8').encode('latin_1') =",str.decode('utf_8').encode('latin_1')
except UnicodeDecodeError as ude:
print "str.decode('utf_8').encode('latin_1') didn't work. The string was decoded into unicode using the utf_8 encoding, but couldn't be encoded into latin_1. See error:\n\t",
validStrings+= ((x, " decoded with utf_8 into unicode and encoded into latin_1"),)
print ude
except UnicodeDecodeError as ude:
print "str.decode('utf_8') didn't work, probably because the string wasn't utf_8 encoded. See error:\n\t",
print ude
except UnicodeEncodeError as uee:
print "ERROR. Couldn't print the str.decode('utf_8') because the console is set to an encoding that doesn't understand some character in the string. See error:\n\t",uee
print
print "Printing information about each character in the original string."
for char in str:
try:
print "\t'" + char + "' = original char {0} repr(char)={1}".format(type(char), repr(char))
except UnicodeDecodeError as ude:
print "\t'?' = original char {0} repr(char)={1} ERROR PRINTING: {2}".format(type(char), repr(char), ude)
except UnicodeEncodeError as uee:
print "\t'?' = original char {0} repr(char)={1} ERROR PRINTING: {2}".format(type(char), repr(char), uee)
print uee
try:
x = unicode(char)
print "\t'" + x + "' = unicode(char) {1} repr(unicode(char))={2}".format(x, type(x), repr(x))
except UnicodeDecodeError as ude:
print "\t'?' = unicode(char) ERROR: {0}".format(ude)
except UnicodeEncodeError as uee:
print "\t'?' = unicode(char) {0} repr(char)={1} ERROR PRINTING: {2}".format(type(x), repr(x), uee)
try:
x = char.decode('latin_1')
print "\t'" + x + "' = char.decode('latin_1') {1} repr(char.decode('latin_1'))={2}".format(x, type(x), repr(x))
except UnicodeDecodeError as ude:
print "\t'?' = char.decode('latin_1') ERROR: {0}".format(ude)
except UnicodeEncodeError as uee:
print "\t'?' = char.decode('latin_1') {0} repr(char)={1} ERROR PRINTING: {2}".format(type(x), repr(x), uee)
try:
x = char.decode('utf_8')
print "\t'" + x + "' = char.decode('utf_8') {1} repr(char.decode('utf_8'))={2}".format(x, type(x), repr(x))
except UnicodeDecodeError as ude:
print "\t'?' = char.decode('utf_8') ERROR: {0}".format(ude)
except UnicodeEncodeError as uee:
print "\t'?' = char.decode('utf_8') {0} repr(char)={1} ERROR PRINTING: {2}".format(type(x), repr(x), uee)
print
x = 'ó'
encodingDemo(x)
非常感谢下面的回答,特别是@John Machin的详细解答。
5 个回答
我通过设置解决了这个pysqlite的问题:
conn.text_factory = lambda x: unicode(x, 'utf-8', 'ignore')
默认情况下,text_factory被设置为unicode(),这会使用当前的默认编码(在我的电脑上是ascii)。
UTF-8是SQLite数据库的默认编码方式。这在一些情况下会出现,比如在执行“SELECT CAST(x'52C3B373' AS TEXT);”这样的查询时。不过,SQLite的C语言库其实并不会检查插入数据库的字符串是否是有效的UTF-8编码。
如果你插入一个Python的unicode对象(在Python 3.x中是str对象),Python的sqlite3库会自动把它转换成UTF-8。但是如果你插入的是一个str对象,它就会直接假设这个字符串是UTF-8,因为在Python 2.x中,"str"对象并不知道自己的编码方式。这就是为什么我们更倾向于使用Unicode字符串的一个原因。
不过,如果你的数据本身就有问题,这样做也没什么用。
要修复你的数据,可以执行
db.create_function('FIXENCODING', 1, lambda s: str(s).decode('latin-1'))
db.execute("UPDATE TheTable SET TextColumn=FIXENCODING(CAST(TextColumn AS BLOB))")
针对数据库中的每一个文本列。
我还不知道有没有办法正确地把'ó'从latin-1转换成utf-8,而不搞错。
在调试这类问题时,使用repr()和unicodedata.name()会很有帮助:
>>> oacute_latin1 = "\xF3"
>>> oacute_unicode = oacute_latin1.decode('latin1')
>>> oacute_utf8 = oacute_unicode.encode('utf8')
>>> print repr(oacute_latin1)
'\xf3'
>>> print repr(oacute_unicode)
u'\xf3'
>>> import unicodedata
>>> unicodedata.name(oacute_unicode)
'LATIN SMALL LETTER O WITH ACUTE'
>>> print repr(oacute_utf8)
'\xc3\xb3'
>>>
如果你把oacute_utf8发送到一个设置为latin1的终端,你会看到一个带波浪线的A,后面跟着一个上标3。
我换成了Unicode字符串。
你说的Unicode字符串是什么?是UTF-16吗?
这是怎么回事?在阅读这段描述我所处的相同情况后,似乎建议是忽略其他建议,最终还是使用8位字节字符串。
我无法想象你怎么会这么觉得。这里传达的意思是,Python中的unicode对象和数据库中的UTF-8编码是正确的选择。然而,马丁回答了最初的问题,给出了一个方法(“文本工厂”),让提问者能够使用latin1——这并不算是推荐!
更新 针对评论中提出的进一步问题:
我没明白unicode字符仍然包含隐含编码。我这样说对吗?
不对。编码是Unicode和其他东西之间的映射,反之亦然。一个Unicode字符本身并没有编码,无论是隐含的还是其他的。
在我看来,unicode("\xF3")和"\xF3".decode('latin1')在用repr()评估时是一样的。
你在说什么?我觉得它们看起来不一样:
>>> unicode("\xF3")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xf3 in position 0: ordinal
not in range(128)
>>> "\xF3".decode('latin1')
u'\xf3'
>>>
也许你想说:u'\xf3' == '\xF3'.decode('latin1')
... 这确实是对的。
同样,unicode(str_object, encoding)
和str_object.decode(encoding)
做的事情是一样的……包括在提供不合适的编码时会出错。
这是个好事吗?
Unicode中的前256个字符和latin1中的256个字符一一对应,这是个好主意。因为所有256个可能的latin1字符都映射到Unicode,这意味着任何8位字节,任何Python的str对象都可以解码成unicode而不会抛出异常。这是应该的。
然而,有些人混淆了两个完全不同的概念:“我的脚本运行完毕,没有抛出任何异常”和“我的脚本没有错误”。对他们来说,latin1是“一个陷阱和错觉”。
换句话说,如果你有一个实际上是用cp1252或gbk或koi8-u等编码的文件,而你用latin1解码,得到的Unicode将是完全无用的,Python(或其他任何语言)不会标记错误——它无法知道你犯了个愚蠢的错误。
或者说unicode("str")会总是返回正确的解码吗?
就这样,如果默认编码是ascii,它会在文件实际上是ASCII编码时返回正确的unicode。否则,它会出错。
同样,如果你指定了正确的编码,或者一个包含正确编码的超集,你会得到正确的结果。否则你会得到乱码或者异常。
简而言之:答案是否定的。
如果不是,当我收到一个包含任何可能字符集的python str时,我怎么知道该如何解码?
如果str对象是一个有效的XML文档,它会提前指定。默认是UTF-8。如果它是一个正确构建的网页,它应该提前指定(查找“charset”)。不幸的是,很多网页的作者会撒谎(ISO-8859-1也就是latin1,应该是Windows-1252也就是cp1252;别浪费资源尝试解码gb2312,直接用gbk)。你可以从网站的国籍/语言中获得线索。
UTF-8总是值得尝试。如果数据是ascii,它会正常工作,因为ascii是utf8的一个子集。使用非ascii字符编写并且用utf8以外的编码编码的文本字符串,几乎肯定会在尝试用utf8解码时出错。
以上所有的经验法则和更多的统计信息都封装在chardet模块中,用于猜测任意文件的编码。它通常工作得很好。然而,你无法让软件完全防错。例如,如果你把一些用编码A写的数据文件和一些用编码B写的数据文件连接在一起,然后把结果交给chardet,答案可能是编码C 并且置信度降低,例如0.8。一定要检查答案的置信度部分。
如果一切都失败了:
(1) 尝试在这里提问,附上你数据前面的一个小样本…… print repr(your_data[:400])
……以及你所拥有的关于其来源的任何附加信息。
(2) 最近俄罗斯的研究关于恢复遗忘密码的技术似乎对推断未知编码非常适用。
更新2 顺便问一下,是不是该开个新问题了?-)
还有一件事:显然Windows使用某些字符作为Unicode的字符,但这些字符并不是该字符的正确Unicode,因此如果你想在其他程序中使用这些字符,你可能需要将它们映射到正确的字符。
这不是Windows的问题;这是一些疯狂的应用程序开发者造成的。你可能更容易理解的是引用你提到的effbot文章的开头段落:
一些应用程序将CP1252(Windows,西欧)字符添加到标记为ISO 8859-1(Latin 1)或其他编码的文档中。这些字符不是有效的ISO-8859-1字符,可能会在处理和显示应用程序中引发各种问题。
背景:
Unicode中范围为U+0000到U+001F的字符被指定为“C0控制字符”。这些字符在ASCII和latin1中也存在,含义相同。它们包括一些熟悉的字符,如回车、换行、铃声、退格、制表符等,后者使用得比较少。
Unicode中范围为U+0080到U+009F的字符被指定为“C1控制字符”。这些字符在latin1中也存在,包括32个没有人能想象任何可能用途的字符。
因此,如果你对你的unicode或latin1数据进行字符频率统计,发现任何在该范围内的字符,你的数据就是损坏的。没有通用的解决方案;这取决于它是如何损坏的。这些字符可能与cp1252中相同位置的字符具有相同的含义,因此effbot的解决方案会有效。在我最近查看的另一个案例中,问题字符似乎是由于连接了用UTF-8和另一种编码编写的文本文件造成的,而另一种编码需要根据文件中字母的频率推断出来。