Pull to refresh

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

Reading time2 min
Views1.3K

Проблема


Для работы с базой данных 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
Tags:
Hubs:
Total votes 9: ↑6 and ↓3+3
Comments0

Articles