UCS2 или UCS4? — pyodbc и работа с utf16 данными в MSSQL

в 6:51, , рубрики: linux, odbc, python, virtualenv, Песочница, метки: , , ,

Проблема

Для работы с базой данных 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. Что это за собой тянет:

  1. unixODBC вызывая соответствующие функции работы с базой данных с аппендиксом W (например, SQLExecDirectW()), получает результаты. в которых один символ текста занимает 2 байта(UCS2)
  2. pyodbc получает результаты от ODBC-драйвера, и в свою очередь сохраняет результаты в переменную с типом unicode
  3. Таким образом 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

* - обязательные к заполнению поля