Усечение Юникода, чтобы он соответствовал максимальному размеру при кодировании для банковского перевода - PullRequest
25 голосов
/ 27 ноября 2009

С учетом строки Unicode и следующих требований:

  • Строка должна быть закодирована в некоторый формат последовательности байтов (например, UTF-8 или экранирование Юникода JSON)
  • Максимальная длина закодированной строки

Например, для службы push-уведомлений iPhone требуется кодировка JSON с максимальным общим размером пакета 256 байтов.

Каков наилучший способ обрезать строку, чтобы она перекодировалась в действительный Unicode и отображалась достаточно корректно?

(Понимание человеческого языка не требуется & mdash; усеченная версия может выглядеть странно, например, для объединенного символа-сироты или тайского гласного, лишь бы программное обеспечение не зависало при обработке данных.)

Смотрите также:

Ответы [ 5 ]

26 голосов
/ 30 ноября 2009
def unicode_truncate(s, length, encoding='utf-8'):
    encoded = s.encode(encoding)[:length]
    return encoded.decode(encoding, 'ignore')

Вот пример для строки Unicode, где каждый символ представлен 2 байтами в UTF-8:

>>> unicode_truncate(u'абвгд', 5)
u'\u0430\u0431'
8 голосов
/ 27 ноября 2009

Одним из свойств UTF-8 является то, что его легко синхронизировать, то есть легко находить границы символов Юникода в закодированном тестовом потоке. Все, что вам нужно сделать, это вырезать закодированную строку на максимальной длине, а затем идти назад от конца, удаляя любые байты, которые> 127 - те, которые являются частью или началом многобайтового символа.

Как написано сейчас, это слишком просто - стереть до последнего символа ASCII, возможно всю строку. Нам нужно проверить, нет ли усеченного двухбайтового (начиная с 110yyyxx) трехбайтового (1110yyyy) или четырехбайтового (11110zzz)

Реализация Python 2.6 в открытом коде. Оптимизация не должна быть проблемой - независимо длины, мы проверяем только последние 1-4 байта.

# coding: UTF-8

def decodeok(bytestr):
    try:
        bytestr.decode("UTF-8")
    except UnicodeDecodeError:
        return False
    return True

def is_first_byte(byte):
    """return if the UTF-8 @byte is the first byte of an encoded character"""
    o = ord(byte)
    return ((0b10111111 & o) != o)

def truncate_utf8(bytestr, maxlen):
    u"""

    >>> us = u"ウィキペディアにようこそ"
    >>> s = us.encode("UTF-8")

    >>> trunc20 = truncate_utf8(s, 20)
    >>> print trunc20.decode("UTF-8")
    ウィキペディ
    >>> len(trunc20)
    18

    >>> trunc21 = truncate_utf8(s, 21)
    >>> print trunc21.decode("UTF-8")
    ウィキペディア
    >>> len(trunc21)
    21
    """
    L = maxlen
    for x in xrange(1, 5):
        if is_first_byte(bytestr[L-x]) and not decodeok(bytestr[L-x:L]):
            return bytestr[:L-x]
    return bytestr[:L]

if __name__ == '__main__':
    # unicode doctest hack
    import sys
    reload(sys)
    sys.setdefaultencoding("UTF-8")
    import doctest
    doctest.testmod()
2 голосов
/ 27 ноября 2009

Это будет сделано для UTF8, если вы хотите сделать это в регулярном выражении.

import re

partial="\xc2\x80\xc2\x80\xc2"

re.sub("([\xf6-\xf7][\x80-\xbf]{0,2}|[\xe0-\xef][\x80-\xbf]{0,1}|[\xc0-\xdf])$","",partial)

"\xc2\x80\xc2\x80"

Его покрытие от U + 0080 (2 байта) до U + 10FFFF (4 байта) utf8 строк

Это действительно прямо как UTF8 алгоритм

От U + 0080 до U + 07FF Потребуется 2 байта 110yyyxx 10xxxxxx Это означает, что если вы видите только один байт в конце, например 110yyyxx (от 0b11000000 до 0b11011111) Это [\xc0-\xdf], оно будет частичным.

От U + 0800 до U + FFFF требуется 3 байта 1110yyyy 10yyyyxx 10xxxxxx Если вы видите только 1 или 2 байта в конце, он будет частичным. Будет соответствовать этому шаблону [\xe0-\xef][\x80-\xbf]{0,1}

Из U + 10000 – U + 10FFFF требуется 4 байта 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx Если вы видите только 1-3 байта в конце, он будет частичным Будет соответствовать этому шаблону [\xf6-\xf7][\x80-\xbf]{0,2}

Обновление:

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

re.sub("([\xe0-\xef][\x80-\xbf]{0,1}|[\xc0-\xdf])$","",partial)

Дайте мне знать, если есть какие-либо проблемы с этим регулярным выражением.

1 голос
/ 27 ноября 2009

Для форматирования JSON (экранирование Unicode, например, \uabcd), я использую следующий алгоритм для достижения этой цели:

  • Кодировать строку Unicode в формат с обратной косой чертой, который в конечном итоге будет в версии JSON
  • Обрезать на 3 байта больше моего окончательного лимита
  • Использование регулярного выражения для обнаружения и отключения частичной кодировки значения Unicode

Итак (в Python 2.5) с some_string и требованием сократить до 100 байтов:

# Given some_string is a long string with arbitrary Unicode data.
encoded_string = some_string.encode('unicode_escape')
partial_string = re.sub(r'([^\\])\\(u|$)[0-9a-f]{0,3}$', r'\1', encoded_string[:103])
final_string   = partial_string.decode('unicode_escape')

Теперь final_string вернулся в Unicode, но гарантированно поместится в JSON-пакет позже. Я сократил до 103, потому что сообщение в чистом Unicode будет закодировано в 102 байта.

Отказ от ответственности: тестируется только на базовой многоязычной плоскости. Да, да, я знаю.

0 голосов
/ 15 ноября 2016

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

mxlen=255        
while( toolong.encode("utf8")[mxlen-1] & 0xc0 == 0xc0 ):
    mxlen -= 1

truncated_string = toolong.encode("utf8")[0:mxlen].decode("utf8")
...