Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SQLite, python, unicode, and non-utf data

I started by trying to store strings in sqlite using python, and got the message:

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.

Ok, I switched to Unicode strings. Then I started getting the message:

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

when trying to retrieve data from the db. More research and I started encoding it in utf8, but then 'Sigur Rós' starts looking like 'Sigur Rós'

note: My console was set to display in 'latin_1' as @John Machin pointed out.

What gives? After reading this, describing exactly the same situation I'm in, it seems as if the advice is to ignore the other advice and use 8-bit bytestrings after all.

I didn't know much about unicode and utf before I started this process. I've learned quite a bit in the last couple hours, but I'm still ignorant of whether there is a way to correctly convert 'ó' from latin-1 to utf-8 and not mangle it. If there isn't, why would sqlite 'highly recommend' I switch my application to unicode strings?


I'm going to update this question with a summary and some example code of everything I've learned in the last 24 hours so that someone in my shoes can have an easy(er) guide. If the information I post is wrong or misleading in any way please tell me and I'll update, or one of you senior guys can update.


Summary of answers

Let me first state the goal as I understand it. The goal in processing various encodings, if you are trying to convert between them, is to understand what your source encoding is, then convert it to unicode using that source encoding, then convert it to your desired encoding. Unicode is a base and encodings are mappings of subsets of that base. utf_8 has room for every character in unicode, but because they aren't in the same place as, for instance, latin_1, a string encoded in utf_8 and sent to a latin_1 console will not look the way you expect. In python the process of getting to unicode and into another encoding looks like:

str.decode('source_encoding').encode('desired_encoding') 

or if the str is already in unicode

str.encode('desired_encoding') 

For sqlite I didn't actually want to encode it again, I wanted to decode it and leave it in unicode format. Here are four things you might need to be aware of as you try to work with unicode and encodings in python.

  1. The encoding of the string you want to work with, and the encoding you want to get it to.
  2. The system encoding.
  3. The console encoding.
  4. The encoding of the source file

Elaboration:

(1) When you read a string from a source, it must have some encoding, like latin_1 or utf_8. In my case, I'm getting strings from filenames, so unfortunately, I could be getting any kind of encoding. Windows XP uses UCS-2 (a Unicode system) as its native string type, which seems like cheating to me. Fortunately for me, the characters in most filenames are not going to be made up of more than one source encoding type, and I think all of mine were either completely latin_1, completely utf_8, or just plain ascii (which is a subset of both of those). So I just read them and decoded them as if they were still in latin_1 or utf_8. It's possible, though, that you could have latin_1 and utf_8 and whatever other characters mixed together in a filename on Windows. Sometimes those characters can show up as boxes, other times they just look mangled, and other times they look correct (accented characters and whatnot). Moving on.

(2) Python has a default system encoding that gets set when python starts and can't be changed during runtime. See here for details. Dirty summary ... well here's the file I added:

\# 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')   

This system encoding is the one that gets used when you use the unicode("str") function without any other encoding parameters. To say that another way, python tries to decode "str" to unicode based on the default system encoding.

(3) If you're using IDLE or the command-line python, I think that your console will display according to the default system encoding. I am using pydev with eclipse for some reason, so I had to go into my project settings, edit the launch configuration properties of my test script, go to the Common tab, and change the console from latin-1 to utf-8 so that I could visually confirm what I was doing was working.

(4) If you want to have some test strings, eg

test_str = "ó" 

in your source code, then you will have to tell python what kind of encoding you are using in that file. (FYI: when I mistyped an encoding I had to ctrl-Z because my file became unreadable.) This is easily accomplished by putting a line like so at the top of your source code file:

# -*- coding: utf_8 -*- 

If you don't have this information, python attempts to parse your code as ascii by default, and so:

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 

Once your program is working correctly, or, if you aren't using python's console or any other console to look at output, then you will probably really only care about #1 on the list. System default and console encoding are not that important unless you need to look at output and/or you are using the builtin unicode() function (without any encoding parameters) instead of the string.decode() function. I wrote a demo function I will paste into the bottom of this gigantic mess that I hope correctly demonstrates the items in my list. Here is some of the output when I run the character 'ó' through the demo function, showing how various methods react to the character as input. My system encoding and console output are both set to utf_8 for this run:

'�' = 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 

Now I will change the system and console encoding to latin_1, and I get this output for the same input:

'ó' = 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 

Notice that the 'original' character displays correctly and the builtin unicode() function works now.

Now I change my console output back to 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 

Here everything still works the same as last time but the console can't display the output correctly. Etc. The function below also displays more information that this and hopefully would help someone figure out where the gap in their understanding is. I know all this information is in other places and more thoroughly dealt with there, but I hope that this would be a good kickoff point for someone trying to get coding with python and/or sqlite. Ideas are great but sometimes source code can save you a day or two of trying to figure out what functions do what.

Disclaimers: I'm no encoding expert, I put this together to help my own understanding. I kept building on it when I should have probably started passing functions as arguments to avoid so much redundant code, so if I can I'll make it more concise. Also, utf_8 and latin_1 are by no means the only encoding schemes, they are just the two I was playing around with because I think they handle everything I need. Add your own encoding schemes to the demo function and test your own input.

One more thing: there are apparently crazy application developers making life difficult in 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) 

Much thanks for the answers below and especially to @John Machin for answering so thoroughly.

like image 440
Nathan Spears Avatar asked Mar 06 '10 14:03

Nathan Spears


People also ask

Does SQLite support Unicode?

SQLite fully supports storage and retrieval of Unicode strings, encoded as UTF-8, UTF-16LE, or UTF-16BE. The only aspect of Unicode that SQLite does not support is case-insensitive matching of characters.

Is UTF-8 and Unicode the same?

The Difference Between Unicode and UTF-8Unicode is a character set. UTF-8 is encoding. Unicode is a list of characters with unique decimal numbers (code points).

What is Unicode data type in Python?

Type 'unicode' is meant for working with codepoints of characters. Type 'str' is meant for working with encoded binary representation of characters. A 'unicode' object needs to be converted to 'str' object before Python can write the character to a file.

What encoding does SQLite use?

The default encoding will be UTF-8 for databases created using sqlite3_open() or sqlite3_open_v2(). The default encoding for databases created using sqlite3_open16() will be UTF-16 in the native byte order.


2 Answers

I'm still ignorant of whether there is a way to correctly convert 'ó' from latin-1 to utf-8 and not mangle it

repr() and unicodedata.name() are your friends when it comes to debugging such problems:

>>> 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' >>> 

If you send oacute_utf8 to a terminal that is set up for latin1, you will get A-tilde followed by superscript-3.

I switched to Unicode strings.

What are you calling Unicode strings? UTF-16?

What gives? After reading this, describing exactly the same situation I'm in, it seems as if the advice is to ignore the other advice and use 8-bit bytestrings after all.

I can't imagine how it seems so to you. The story that was being conveyed was that unicode objects in Python and UTF-8 encoding in the database were the way to go. However Martin answered the original question, giving a method ("text factory") for the OP to be able to use latin1 -- this did NOT constitute a recommendation!

Update in response to these further questions raised in a comment:

I didn't understand that the unicode characters still contained an implicit encoding. Am I saying that right?

No. An encoding is a mapping between Unicode and something else, and vice versa. A Unicode character doesn't have an encoding, implicit or otherwise.

It looks to me like unicode("\xF3") and "\xF3".decode('latin1') are the same when evaluated with repr().

Say what? It doesn't look like it to me:

>>> 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' >>> 

Perhaps you meant: u'\xf3' == '\xF3'.decode('latin1') ... this is certainly true.

It is also true that unicode(str_object, encoding) does the same as str_object.decode(encoding) ... including blowing up when an inappropriate encoding is supplied.

Is that a happy circumstance

That the first 256 characters in Unicode are the same, code for code, as the 256 characters in latin1 is a good idea. Because all 256 possible latin1 characters are mapped to Unicode, it means that ANY 8-bit byte, ANY Python str object can be decoded into unicode without an exception being raised. This is as it should be.

However there exist certain persons who confuse two quite separate concepts: "my script runs to completion without any exceptions being raised" and "my script is error-free". To them, latin1 is "a snare and a delusion".

In other words, if you have a file that's actually encoded in cp1252 or gbk or koi8-u or whatever and you decode it using latin1, the resulting Unicode will be utter rubbish and Python (or any other language) will not flag an error -- it has no way of knowing that you have commited a silliness.

or is unicode("str") going to always return the correct decoding?

Just like that, with the default encoding being ascii, it will return the correct unicode if the file is actually encoded in ASCII. Otherwise, it'll blow up.

Similarly, if you specify the correct encoding, or one that's a superset of the correct encoding, you'll get the correct result. Otherwise you'll get gibberish or an exception.

In short: the answer is no.

If not, when I receive a python str that has any possible character set in it, how do I know how to decode it?

If the str object is a valid XML document, it will be specified up front. Default is UTF-8. If it's a properly constructed web page, it should be specified up front (look for "charset"). Unfortunately many writers of web pages lie through their teeth (ISO-8859-1 aka latin1, should be Windows-1252 aka cp1252; don't waste resources trying to decode gb2312, use gbk instead). You can get clues from the nationality/language of the website.

UTF-8 is always worth trying. If the data is ascii, it'll work fine, because ascii is a subset of utf8. A string of text that has been written using non-ascii characters and has been encoded in an encoding other than utf8 will almost certainly fail with an exception if you try to decode it as utf8.

All of the above heuristics and more and a lot of statistics are encapsulated in chardet, a module for guessing the encoding of arbitrary files. It usually works well. However you can't make software idiot-proof. For example, if you concatenate data files written some with encoding A and some with encoding B, and feed the result to chardet, the answer is likely to be encoding C with a reduced level of confidence e.g. 0.8. Always check the confidence part of the answer.

If all else fails:

(1) Try asking here, with a small sample from the front of your data ... print repr(your_data[:400]) ... and whatever collateral info about its provenance that you have.

(2) Recent Russian research into techniques for recovering forgotten passwords appears to be quite applicable to deducing unknown encodings.

Update 2 BTW, isn't it about time you opened up another question ?-)

One more thing: there are apparently characters that Windows uses as Unicode for certain characters that aren't the correct Unicode for that character, so you may have to map those characters to the correct ones if you want to use them in other programs that are expecting those characters in the right spot.

It's not Windows that's doing it; it's a bunch of crazy application developers. You might have more understandably not paraphrased but quoted the opening paragraph of the effbot article that you referred to:

Some applications add CP1252 (Windows, Western Europe) characters to documents marked up as ISO 8859-1 (Latin 1) or other encodings. These characters are not valid ISO-8859-1 characters, and may cause all sorts of problems in processing and display applications.

Background:

The range U+0000 to U+001F inclusive is designated in Unicode as "C0 Control Characters". These exist also in ASCII and latin1, with the same meanings. They include such familar things as carriage return, line feed, bell, backspace, tab, and others that are used rarely.

The range U+0080 to U+009F inclusive is designated in Unicode as "C1 Control Characters". These exist also in latin1, and include 32 characters that nobody outside unicode.org can imagine any possible use for.

Consequently, if you run a character frequency count on your unicode or latin1 data, and you find any characters in that range, your data is corrupt. There is no universal solution; it depends on how it became corrupted. The characters may have the same meaning as the cp1252 characters at the same positions, and thus the effbot's solution will work. In another case that I've been looking at recently, the dodgy characters appear to have been caused by concatenating text files encoded in UTF-8 and another encoding which needed to be deduced based on letter frequencies in the (human) language the files were written in.

like image 130
John Machin Avatar answered Oct 14 '22 03:10

John Machin


UTF-8 is the default encoding of SQLite databases. This shows up in situations like "SELECT CAST(x'52C3B373' AS TEXT);". However, the SQLite C library doesn't actually check whether a string inserted into a DB is valid UTF-8.

If you insert a Python unicode object (or str object in 3.x), the Python sqlite3 library will automatically convert it to UTF-8. But if you insert a str object, it will just assume the string is UTF-8, because Python 2.x "str" doesn't know its encoding. This is one reason to prefer Unicode strings.

However, it doesn't help you if your data is broken to begin with.

To fix your data, do

db.create_function('FIXENCODING', 1, lambda s: str(s).decode('latin-1')) db.execute("UPDATE TheTable SET TextColumn=FIXENCODING(CAST(TextColumn AS BLOB))") 

for every text column in your database.

like image 45
dan04 Avatar answered Oct 14 '22 03:10

dan04