SQLite,Python,Unicode和非UTF数据

69
我开始尝试使用python在sqlite中存储字符串,结果收到以下消息:

sqlite3.ProgrammingError: You must not use 8-bit bytestrings unless you use a text_factory that can interpret 8-bit bytestrings (like text_factory = str). It is highly recommended that you instead just switch your application to Unicode strings.

好的,我切换到Unicode字符串。然后我开始收到以下消息:

sqlite3.OperationalError: Could not decode to UTF-8 column 'tag_artist' with text 'Sigur Rós'

在尝试从数据库检索数据时。进一步调查后,我开始使用utf8进行编码,但是'Sigur Rós'开始变成了'Sigur Rós'

注意:正如@ John Machin指出的那样,我的控制台设置为以'latin_1'显示。

这是什么情况?阅读this之后,描述了与我处于完全相同的情况,似乎建议忽略其他建议,最终仍然使用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. 您要处理的字符串的编码以及您要将其转换为的编码。
  2. 系统编码。
  3. 控制台编码。
  4. 源文件的编码
详细说明:
(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,则我认为您的控制台将根据默认系统编码显示。由于某种原因,我正在使用带有Eclipse的pydev,因此我必须进入项目设置,编辑测试脚本的启动配置属性,转到Common选项卡,并将控制台从latin-1更改为utf-8,以便我可以视觉确认我的操作是否有效。
(4) 如果您想要一些测试字符串,例如:
test_str = "ó"

在您的源代码中,如果使用了非默认编码方式,那么您需要告诉Python该文件使用了哪种编码方式。(FYI:当我输入错误的编码方式时,我不得不按ctrl-Z,因为我的文件变得无法读取。)这可以通过在源代码文件顶部放置以下行来轻松完成:
# -*- coding: utf_8 -*-

如果你没有这个信息,Python 默认会尝试以 ASCII 编码解析你的代码,因此:
SyntaxError: Non-ASCII character '\xf3' in file _redacted_ on line 81, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details

当您的程序正常工作时,如果您没有使用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

注意,'original'字符现在可以正确显示,并且内置的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个回答

35

我仍然不知道是否有一种正确的方法将拉丁-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-tilde后跟上标3。
我已经切换到Unicode字符串。
您所说的Unicode字符串是什么?UTF-16吗?
怎么回事?在阅读描述与我完全相同的情况后,似乎建议是忽略其他建议,最终仍然使用8位字节串。
我无法想象您是如何这样认为的。传达的故事是Python中的unicode对象和数据库中的UTF-8编码是正确的方法。但是马丁回答了原始问题,并为OP提供了一种方法(“文本工厂”)以便能够使用latin1 - 这并不构成推荐!
更新:针对评论中提出的进一步问题:
我没有理解Unicode字符仍包含隐式编码。我说得对吗?
不。编码是Unicode与其他内容之间的映射,反之亦然。Unicode字符没有编码,无论是隐式还是明确的。
在我的看来,当使用repr()评估时,unicode("\xF3")和"\xF3".decode('latin1')是相同的。
说什么?在我的看来并不是这样的:
>>> 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)做的事情相同... 包括在提供不适当的编码时引发错误。
“这是一个愉快的巧合吗?” 第一个256个Unicode字符与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 aka latin1,应该是Windows-1252 aka cp1252;不要浪费资源尝试解码gb2312,请改用gbk)。你可以从网站的国籍/语言中获得线索。
UTF-8总是值得一试。如果数据是ascii,则可以正常工作,因为ascii是utf8的子集。使用非ascii字符编写并使用其他编码进行编码的文本字符串几乎肯定会在尝试将其解码为utf8时失败并引发异常。
以上所有的启发和更多的统计数据都被封装在chardet中,这是一个用于猜测任意文件编码的模块。它通常很有效。然而,你不能让软件傻瓜化。例如,如果你将使用编码A和编码B编写的数据文件连接起来,并将结果提供给chardet,则答案可能是编码C,并且置信度降低,例如0.8。始终检查答案中的置信度部分。
如果所有其他方法都失败了:
(1) 尝试在此处询问,并提供来自数据前面的小样本...print repr(your_data[:400])...以及您拥有的关于其来源的任何相关信息。
(2) 最近俄罗斯对恢复遗忘密码的技术进行的研究似乎非常适用于推断未知编码。
更新2:顺便说一下,是不是该打开另一个问题了?-)
还有一件事:显然,Windows为某些字符使用Unicode,但这不是该字符的正确Unicode,因此,如果要在其他期望该字符在正确位置的程序中使用它们,则可能必须将这些字符映射到正确的字符。
这不是Windows在做,而是一群疯狂的应用程序开发人员。您可能更容易理解而不是转述effbot文章的开头段落,该文章是您所提到的:
某些应用程序会向标记为ISO 8859-1(Latin 1)或其他编码的文档中添加CP1252(Windows,西欧)字符。这些字符不是有效的ISO-8859-1字符,并且可能在处理和显示应用程序中引起各种问题。
背景:
Unicode中U + 0000至U + 001F范围内的字符被指定为“C0控制字符”。它们也存在于ASCII和latin1中,具有相同的含义。它们包括回车、换行、响铃、退格、制表符等常见字符以及很少使用的其他字符。
Unicode中U + 0080至U + 009F范围内的字符被指定为“C1控制字符”。它们也存在于latin1中,并包括32个除了unicode.org之外没有人能想象出任何可能用途的字符。
因此,如果您对Unicode或Latin1数据进行字符频率计数,并发现该范围内的任何字符,则表示您的数据已损坏。没有通用解决方案;它取决于它如何变得损坏。这些字符可能与相同位置的cp1252字符具有相同的含义,因此effbot的解决方案将起作用。在我最近研究的另一种情况中,可疑字符似乎是由于连接以UTF-8编码的文本文件和另一种需要根据文件所写的(人类)语言中的字母频率推断出的编码而引起的。

我之前的终端设置为latin1,所以我进行了更改。当你问我正在使用Unicode字符串是什么意思时,我是在读取的str上使用unicode()方法。我不明白Unicode字符仍然包含隐式编码。我的表达方式正确吗?在我看来,当使用repr()评估时,unicode("\xF3") 和 "\xF3".decode('latin1') 是相同的。这是一个偶然的情况还是unicode("str")总是返回正确的解码?如果不是,当我收到可能包含任何字符集的Python str时,如何知道如何对其进行解码? - Nathan Spears
再次感谢。我猜你的问题"What are you calling Unicode strings? UTF-16?" 的答案是"无论 oacute_unicode 是什么"。我是否忽略了你问题中应该理解的某个方面?我以这种形式解码了从文件名得到的字符串并将其发送到数据库中。我还订购了十个五角大楼手鼓,所以很快我就会拥有军事解码能力,Python 将不再必要。 - Nathan Spears
我只是在确认您真的是指Python Unicode对象== oacute_unicode,而不是SQLite数据库中另一种可能性编码UTF-16,有些人称之为“Unicode”。 - John Machin
我不会在这里再问任何问题,但我不想在新问题中发布关于这个问题的答案。我更新了有关Windows开发人员的内容。 - Nathan Spears

22

SQLite数据库的默认编码是UTF-8。例如在"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))")

针对您数据库中的每个文本列。


那真是非常有用。虽然我现在不需要它,因为我只是试图理解这些东西并且我的数据库是一次性的,但是它确实因其有用性而值得点赞。 - Nathan Spears
我编写了一个简短的脚本,使用这种技术重新编码目标数据库中所有表格的所有textclobchar列。http://stackoverflow.com/a/29048500/1191425。 - Li-aung Yip

19

我通过设置解决了这个pysqlite问题:

conn.text_factory = lambda x: unicode(x, 'utf-8', 'ignore')

默认情况下,text_factory被设置为unicode(),它将使用当前默认的编码(在我的计算机上是ascii)。


尽管我遇到了“text_factory”相关的错误,但我在SQL Alchemy v0.7.6在线文档(http://docs.sqlalchemy.org/en/latest/)中找不到它的任何参考资料。 - RobM
我已经这样做了,但它仍然在抱怨。 - fiatjaf
太好了!在我的情况下只需要做一个小改变:删除带有重音符号的字符...所以我使用了 conn.text_factory = lambda x: x.decode('latin-1') - luc

8
当然有解决办法。但是你的数据已经在数据库中损坏,所以你需要修复它:
>>> print u'Sigur Rós'.encode('latin-1').decode('utf-8')
Sigur Rós

4

我在使用Python 2.x(具体地说是Python 2.7.6)时遇到了Unicode问题,现已得到解决:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import unicode_literals
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

它也解决了您在帖子开头提到的错误:

sqlite3.ProgrammingError: 除非您使用Unicode字符串,否则不得使用8位字节串 ...

编辑 sys.setdefaultencoding 是一个肮脏的hack。是的,它可以解决UTF-8问题,但一切都有代价。有关详细信息,请参阅以下链接:

1
不要使用 sys.setdefaultencoding()。那是可怕的迷信行为,只会暂时掩盖问题。它会破坏其他东西。你只是在掩盖伤痕而不是停止被打击。相反,停止被打击并正确处理Unicode。 - Martijn Pieters

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接