Проблема
Для работы с базой данных MSSQL Server 2005 в кодировке UTF-16(UCS2) я использую скрипт, написанный на python. Этот скрипт использует для работы с базой данных следующий набор инструментов:
- unixODBC
- FreeTDS
- pyodbc
- sqlachemy
И тут появилась трудность: при получении строковых данных из базы (поля nvarchar, ntext) неправильно обрабатывается юникод.
Как выяснилось, установленный у меня питон был собран с UCS4 юникодом. Методы получения типа юникода в сборка python хорошо описаны в данном вопросе на stackoverflow. Т.е, если выполнить следующую строчку в терминале:
python -c "import sys;print 'UCS4' if sys.maxunicode > 65536 else 'UCS2'"
то мы получаем версию сборки юникода для python.В моем случае это было UCS4. Что это за собой тянет:
- unixODBC вызывая соответствующие функции работы с базой данных с аппендиксом W (например, SQLExecDirectW()), получает результаты. в которых один символ текста занимает 2 байта(UCS2)
- pyodbc получает результаты от ODBC-драйвера, и в свою очередь сохраняет результаты в переменную с типом unicode
- Таким образом 1 символ результата, по мнению pyodbc, составляет 4 байта(UCS4). Именно так и сохраняется результат. полученный из ODBC-драйвера.
Драйвер возвращает данные, в которых символ занимает 2 байта, а pyodbc переделывает эти данные так, что символ занимает 4 байта. Все бы хорошо, если бы было какое-либо преобразование, но данные просто сохраняются как массив байтов в переменную с типом unicode, что несет неприятные последствия: символ результата по-сути содержит 2 символа того результата, который вернул ODBC-драйвер.
Не страшные последствия, решил я и самостоятельно преобразовал результат, разделив его по-символам:
def odbcUCS4toUCS2(ustr):
u = u""
for i in range(0, len(ustr)):
u32 ord(ustr[i])
u16 = [(u32 & 0xFFFF0000) >> 16, (u32 & 0x0000FFFF)]
u += unichr(u16[1])
u += unichr(u16[0])
return u
Этого оказалось недостаточным. Появилось еще одно неприятное последствие: если длина в символах результата нечетная, то последний символ результата обрезается. Т.е. строка 'это слово' будет получена в моем скрипте как 'это слов'. Эту проблему я так и не смог решить преобразованиями: последних два байта результата фактически отсутствуют из-за неправильного сохранения юникода. Тогда я пришел к решению пересобрать python с UCS2 поддержкой юникода.
Решение
Собираем, не забыв указать в опциях --enable-unicode=ucs2. При сборке нового питона надо не забыть поставить пакетик zlib1g-dev, иначе не получится потом могут быть трудности с установкой пакетов с помощью pip.
Установка и настройка virtualenv:
$ sudo apt-get install virtualenv
$ virtualenv ~/ucs2env -p [путь к бинарнику ucs2 питона]
Если не указывать путь, то
ну и добавим в в алиасы:
echo "alias ucs2env='source ~/ucs2env/bin/activate'">>~/.bashrc
Ну вот и все. теперь данные получаются таким же образом как и хранятся в базе данных:
$ ucs2env
(ucs2env)$ python -c "import sys;print 'UCS4' if sys.maxunicode > 65536 else 'UCS2'"
UCS2
Автор: 2r2w