Приводим русские тексты на Mac OS X в одну кодировку Python-скриптом

    Случилось мне иметь ноут на OS X, комп на Linux и одного из друзей с Windows. И вот через dropbox обмениваются все эти три компа документами разными. В том числе и текстовыми, в которых хранятся разные заметки, задачи и т.п. И вот незадача: тексты написанные на MacOSx плохо читаются в блокноте Винды, а виндовые в textedit на MacOSx.

    И вся причина в том, что на винде блокнот использует кодировку Windows 1251, а на OS X используется по умолчанию MACCYRILLIC. Причем обе программы без проблем работают с UTF-8 кодировкой.
    Вот только конвертировать из одной кодировки в другую как-то неудобно, лишнее время тратить на открытие терминала и набор заветных команд iconv…

    Пораздумав, написал небольшой скрипт, который сам определяет используемую кодировку и конвертирует в UTF-8 все txt-файлы.


    Что использую для всего:
    Python 2.7
    Mac OS X 10.7.5
    PyCharm IDE

    Изначально сделал определение кодировки самостоятельно, без дополнительных модулей. Но по совету ad3w решил переписать с использованием готового модуля chardet для определения кодировки.
    Кому интересно, предыдущий
    ужасный код
    Определение происходит простым перебором кодировок и выбором той, в которой не будет лишних символов. А набор символов определяете Вы. Конечно этот способ не подойдет для файлов с DOS-графикой, но в обычных целях использования txt его вполне хватит.

    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    __author__ = 'virtustilus'
    
    import os
    import sys
    
    #Автоматический режим при запуске с параметрами
    automatic=False
    
    #общие данные
    appdata={'enc':'','curfile':''}
    
    #Массив файлов для конвертации
    toconvert=[]
    
    #массив на загрузку
    r=[]
    if len(sys.argv)>1:
        r=sys.argv[1:]
        automatic=True
    else:
        i=raw_input(u'INPUT PATH:')
        r+=[i]
    
    
    def print1(s):
        """
        Функция печатает данные, если не в автоматическом режиме
        """
        if not automatic:
            print s
    
    
    #Строка с возможными символами
    utfrustring=u'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя'
    utfrustring+=u'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
    utfrustring+=u'1234567890-=—+_}{][\\"|;:\'/?><.,`~§±!@#$%^&*()№ \r\n « » \u0009 \u2013 \u201c \u201d'
    
    
    def checkline(s,encoding=''):
        """
        проверка одной строки на правильность символов
        """
        hist=''
        b=True
        for i in range(0,len(s)):
            c=s[i]
            try:
                if not c in utfrustring:
                    #Отладка: если в UTF-8 кодировке неверный символ, лучше его показать
                    if encoding==u'UTF-8':
                        hist+= c + u' ' + str(hex(ord(c))) + u' at ' + str(i) + u' in: ' + s + '\n'
                    b=False
                    break
            except:
                if encoding==u'UTF-8':
                    hist+=u'error encoding \n'
                b=False
                break
        return (b,hist)
    
    
    
    
    def check_all_lines(lines, encoding=''):
        """
        Проверка массива строк для файла
        """
        foundenc=appdata['enc']
    
        if foundenc:
            return foundenc
        if encoding=='':
            foundenc=u'UNICODE'
        else:
            foundenc=encoding
        x=lines[:]
        for j in x:
            if encoding!='':
                try:
                    j=unicode(j,encoding)
                except:
                    foundenc=''
                    break
            cl=checkline(j,encoding)
            if not cl[0]:
                if cl[1]!='':
                    print1(u'Error in:'+appdata['curfile'])
                    print1(cl[1])
                foundenc=''
                break
        appdata['enc']=foundenc
    
    
    #Если передана директория, а не файл, собрать все входящие текстовые файлы
    if len(r)==1 and os.path.isdir(r[0]):
        a=r[0]
        r[:]=[]
        for i in os.walk(a):
            p=i[2]
            for j in p:
                if j.endswith('.txt'):
                    r+=[i[0]+'/'+j]
    
    
    
    if len(r)>0:
        for i in r:
            i=unicode(i,u'UTF-8')
            #обратите внимание, тут конвертация имени файла в UNICODE и проверка txt также в UNICODE: u'.txt'
            if i.endswith(u'.txt'):
                f=file(i,'r')
                lines=f.readlines()
                f.close()
    
                appdata['curfile']=i
    
                #Проверяем некоторые кириллические кодировки
    
                check_all_lines(lines,'')
                check_all_lines(lines,u'MACCYRILLIC')
                check_all_lines(lines,u'CP866')
                check_all_lines(lines,u'CP1251')
                check_all_lines(lines,u'KOI8R')
                check_all_lines(lines,u'CP10007')
                check_all_lines(lines,u'UTF-8-MAC')
                check_all_lines(lines,u'UTF-8')
                check_all_lines(lines,u'UTF-8-MAC')
                check_all_lines(lines,u'UTF-16')
                check_all_lines(lines,u'UTF-16BE')
                check_all_lines(lines,u'UTF-7')
                check_all_lines(lines,u'CP1252')
                check_all_lines(lines,u'KOI8-U')
                check_all_lines(lines,u'KOI8-RU')
                check_all_lines(lines,u'ISO-8859-5')
    
    
                if not appdata['enc']:
                    toconvert.append((i,u'NOT FOUND ENCODING'))
    
                if appdata['enc'] and appdata['enc']!=u'UTF-8':
                    toconvert.append((i,appdata['enc']))
    
            else:
                print1(u'\nFile '+i+u' is not text file. \n\n')
    
    
        if toconvert:
            c=0
            for i in toconvert:
                if i[1]!=u'NOT FOUND ENCODING':
                    c+=1
            if c>0:
                print1(u'\n\n FOUND FILES TO CONVERT: ')
                for i in toconvert:
                    print1(i[0] + u' in encoding ' + i[1])
    
                bt=True
                if not automatic:
                    w=raw_input(u'Convert '+str(c)+u' files? (N)')
                    bt= (w=='Y' or w=='y' or w=='Д' or w=='д' or w=='да' or w=='Да')
                if bt:
                    for i in toconvert:
                        if i[1]!=u'NOT FOUND ENCODING':
                            f=file(i[0],'r')
                            x=f.readlines()
                            f.close()
                            x=[ unicode(k,i[1]) for k in x ]
                            x=[ k.encode(u'UTF-8') for k in x]
                            f=file(i[0],'w')
                            f.writelines(x)
                            f.close()
                            print1(u'FILE '+i[0]+u' CONVERTED SUCCESSFULLY :) ')
                else:
                    print1(u'Bye!')
            else:
                print1(u'NO FILES TO CONVERT')
                for i in toconvert:
                    print1(i[0] + u' in encoding ' + i[1])
    
        else:
            print1(u' ALL ENCODING IS OK (UTF-8)!!! :)')
    
    
    
    else:
        print1(u'NO ONE TXT FILE')
    
    
    



    Скачиваем модуль chardet 1.1,
    Распаковываем и устанавливаем:
    python setup.py install
    


    Создаем свой скрипт для перекодировки файлов:
    Предыдущая редакция
    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    __author__ = 'virtustilus'
    
    import os
    import sys
    import chardet
    
    files=sys.argv[1:]
    #если запуск без параметров, запрашиваем путь
    if len(files)==0: files=[raw_input(u'INPUT PATH:')]
    
    #собираем текстовые файлы и проходим по папкам
    files_to_convert=[]
    for i in files:
        if os.path.exists(i):
            if os.path.isdir(i):
                for w in os.walk(i):
                    for wfile in w[2]:
                        if wfile.lower().endswith('.txt'):
                            files_to_convert+=[w[0]+'/'+wfile]
            elif os.path.isfile(i):
                #если был выбран файл специально, не важно какое расширение
                files_to_convert+=[i]
    
    if len(files_to_convert)>0:
        for i in files_to_convert:
            f=file(i,'r')
            text=''.join(f.readlines())
            f.close()
            enc=chardet.detect(text).get('encoding')
            #Чтобы лишний раз не перезаписывать файл (dropbox), проверяем его кодировку
            if enc!='UTF-8':
                #Обходим ошибку перекодировки файла при пустом файле или другом случае
                try:
                    text=text.decode(enc).encode('UTF-8')
                    f=file(i,'w')
                    f.write(text)
                    f.close()
                except:
                    pass
    
    
    



    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    __author__ = 'virtustilus'
    
    import os
    import sys
    import chardet
    
    def may_be_1251(text_not_changed, encoding):
        """
        Функция проверяет возможность Win1251, если chardet написал, что это MacCyrillic
        И возвращает кодировку
        Например в данном тексте chardet считает, что это MacCyrillic:
    
        1. ”становить пробную Advanced-версию
        2. ”бедитьс€, что программа закрыта (в т.ч. и агент в области уведомлений)
        3. —копировать библиотеку mfc100u.dll в папку с программой и согласитьс€ на замену
        """
        simbols = u'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя'
        simbols += u'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
        simbols += u'1234567890-=—+_}{][\\"|;:\'/?><.,`~§±!@#$%^&*()№ \r\n'
        if encoding.lower() == 'maccyrillic':
            err_mac = err_win = 0
            try:
                t_win = text_not_changed.decode('cp1251')
            except:
                err_win += 1000000
            try:
                t_mac = text_not_changed.decode('MacCyrillic')
            except:
                err_mac += 1000000
            for i in t_win:
                if i not in simbols and i != u'\n':
                    err_win += 1
            for i in t_mac:
                if i not in simbols:
                    err_mac += 1
            if err_mac > err_win:
                encoding = 'cp1251'
        return encoding
    
    paths = sys.argv[1:]
    
    #если запуск без параметров, запрашиваем путь
    if len(paths) == 0: paths = [raw_input(u'INPUT PATH:')]
    
    #выбираем директории и файлы (файлы любые, т.к. они были выбраны пользователем "не зря")
    dirs = [i for i in paths if os.path.exists(i) and os.path.isdir(i)]
    files = [i for i in paths if os.path.exists(i) and os.path.isfile(i)]
    
    #рекурсивно проходим по поддиректориям
    for i_dir in dirs:
        for wpath, wdirs, wfiles in os.walk(i_dir):
            files += [wpath + '/' + i for i in wfiles if i.lower().endswith('.txt')]
    
    for i in files:
        with open(i, 'r') as f:
            text = ''.join(f.readlines())
        enc = may_be_1251(text, chardet.detect(text).get('encoding'))
        #Чтобы лишний раз не перезаписывать файл (dropbox), проверяем его кодировку
        if enc and enc.lower() != 'utf-8':
            #Обходим ошибку перекодировки файла при пустом файле или другом случае
            try:
                text = text.decode(enc)
                #На новой версии OS X 10.8 текстедиту не нравятся символы \r
                text = text.replace(u'\r', '').encode('utf-8')
                #Обнаружил проблему: если питон увидит кодировку, то он в ней и будет работать. Нам это не нужно, поэтому удаляем файл
                os.unlink(i)
                with open(i, 'w') as f:
                    f.write(text)
            except:
                pass
    
    
    
    


    Далее необходимо сделать удобным запуск данного скрипта прямо из папки в OS X.

    Открываем Automator и создаем Службу.
    Вверху выбираем пункты, чтобы получилось «Служба получает файлы и папки в Finder.app».

    Далее ставим действие «получить выбранные объекты Finder».
    Далее «Запустить Shell-скрипт» в настройках его «Передать ввод: как аргументы» и в нем содержание:

    for f in "$@"
    do
    	python /ПУТЬ_К_ВАШЕМУ_СКРИПТУ/convert_encoding.py "$f" 2>/dev/null
    done
    


    Дописал 2>/dev/null, чтобы автоматор не останавливал выполнение при выводе ошибки модуля chardet.

    И последний пункт «Show Growl Notification» (в нем можно написать, что конвертация произведена).


    Сохраняем с именем латинскими буквами (с русскими у меня почему-то пункт в меню не появлялся, пока не переименовал) и проверяем.

    Новый пункт меню появится в Finder в меню файлов и папок в подменю Сервисы.

    P.S. Это уже 5-ая редакция скрипта после комментариев и опыта его использования.
    P.S. Обнаружил проблему: если питон увидит кодировку файла, в который пишет, то он в ней и будет работать. Нам это не нужно, поэтому удаляем файл перед сохранением.
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 19
    • +2
      А можно сразу настроить мак на работу в UTF-8?
      • 0
        После обновлений версия OS X у меня 10.7.5 и TextEdit делает файлы уже в UTF-8. Но все-таки остаются файлы в Windows-1251, которые делает винда и он не хочет нормально их показывать.
      • 0
        Почему то я такой проблемы не испытываю. Создавая файл в TextEdit спокойно с ним работаю на винде и на маке. ЧЯДНТ?
        • 0
          *хотел сказать на винде и на линуксе

          Я вот себе утащу что бы кодировку в сабах с трекеров конвертить, подправлю чуток и думаю всё окей будет, а то питона я тока нчал учить. Так что спасибо.
          • 0
            Линукс не использует MACCYRILLIC по умолчанию
          • +5
            Это самый ужасный код на питон, который я когда-либо видел.
            Совет автору: уберите его подальше от людей, пока этим кто-нибудь не воспользовался.
            Совет людям, которым нужно перевести файл в UTF-8: enconv -L ru -x UTF-8 <filename>
            • 0
              в os x enconv по умолчанию нет, в портах тоже не нашел
              • 0
                % port search enca
                enca @1.13 (textproc)
                character set analyser
                • 0
                  есть iconv
                • 0
                  Да, согласен, код был ужасен. Написал его не думая о красоте, поэтому второй вариант сделал красивым, насколько смог в данный момент.
                • +3
                  Для питона есть chardet и все сводится к этому:

                  import chardet
                  
                  
                  text = text.decode(chardet.detect(text).get('encoding')).encode('utf-8')
                  
                  • 0
                    Недостаточно хорошо погуглил, надо было искать готовый модуль. Спасибо, переписал с использованием chardet.
                    • 0
                      Вот и наступил момент, когда chardet неправильно определил кодировку Win1251, решив, что это MacCyryllic и испортил файл. Я нашел из-за какого символа он так подумал — это была большая русская буква «С», которая на мак выглядит, символ тире.

                      Добавляю дополнительную проверку между ними.
                    • 0
                      Тут читал доки по питону, они рекомендуют с файлами работать так:

                      with open('path', encoding = 'xxx', mode='w') as fout:
                      blah blah blah

                      Такая конструкция сама правильно хендилот закрытие файла.
                      • 0
                        У меня OS X 10.8.2, growlа тут нет, я поставил terminal-notifier. Скрипт работает, но уведомление не выдается. В automator причем при запуске выдает, а на живом файле нет( подскажите плз куда ковырять?
                        • 0
                          С помощью какого действия передаете в terminal-notifier сообщение? Можно просто в действии шелл-скрипт внизу попробовать дописать

                          /Applications/terminal-notifier.app/Contents/MacOS/terminal-notifier -message "Task Finished"
                          
                          • 0
                            с помощью display notification center alert (оно появляется после установки terminal-notification). Рекомендация не помогла, т.к. приложения нет в папке Applications, я так понял оно сразу в терминал что-ли свои либы добавляет.
                            • 0
                              тогда сделайте в терминале:

                              find / -name "terminal-notifier*" 2>/dev/null
                              


                              и узнаете где он
                        • 0
                          Большое спасибо за вашу функцию may_be_1251 :)

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.