воскресенье, 2 февраля 2014 г.

Гадости мира юникода - акценты и нормализация

Есть проблема:

на диске лежит файл "андрей.png"
Мы пытаемся открыть этот файл (код на пайтоне, f = open("андрей.png",'r'))

пример отлично отрабатывает на Mac OSX, однако если перенести файл и скрипт на Linux (Ubuntu) то файл не открывается, пишет что не найден. Здорово?

Здорово. Я раньше думал, с юникодом все просто. Ан нет. "и краткое" может быть закодировано в utf-8 двумя различными способами - с акцентом или без. Остальные диакритические символы с галочками, крыжиками и умляутами - тоже.

при этом, "по умолчанию" разные операционные системы используют разные пути нормализации юникодных имен файлов - Linux, Windows и другие системы используют NFC (нормальная форма С, символ с акцентом - один знак), MacOS - NFD (нормальная форма D, акценты отдельно от символа). один и тот же символ Й, закодированный в разных нормальных формах - это два разных набора бит (на примере UTF-8)


  • NFC: "\xd0\xb9"
  • NFD: "\xd0\xb8\xcc\x86"


И как следствие, файл не находится, если он записан например в NFC, а в open() передана строка в NFD. Такая вот гадость на ровном практически месте.

Есть несколько способов решить проблему. мне нравится способ отказаться от юникода в именах файлов вообще (pip install unidecode и пользуйся на здоровье).

Либо для кросс-платформенности использовать unicodedata и делать две попытки открыть файл - один с normalize(NFC) а другой - с normalize(NFD)

по второму варианту - вот такая простая функция

 # check for unicode  
 # it supports unicode normal forms NFC and NFD  
 # see here: http://yuryskaletskiy.blogspot.ru/2014/02/blog-post.html  
 # returns properly normalized filename if file exists, otherwise None  
 def unicode_filename_check(name):  
      name_u = name  
      if not isinstance(name, unicode):  
           name_u = name.decode('utf-8')  
      if os.path.isfile(name_u):  
           return name_u  
      #try with NFC  
      name_u = normalize('NFC', name_u)  
      if os.path.isfile(name_u):  
           return name_u  
      #try with NFD  
      name_u = normalize('NFD', name_u)  
      if os.path.isfile(name_u):  
           return name_u  
      return None  

ссылки по теме

- википедия http://en.wikipedia.org/wiki/Unicode_normalization
- статейка http://vemod.net/filenames-and-unicode-normalization-forms
- про эти же грабли http://nedbatchelder.com/blog/201106/filenames_with_accents.html


Комментариев нет: