30

给定一个 Unicode 字符串和这些要求:

  • 将字符串编码为某种字节序列格式(例如 UTF-8 或 JSON unicode 转义)
  • 编码字符串具有最大长度

例如,iPhone 推送服务需要 JSON 编码,最大总数据包大小为 256 字节。

截断字符串以使其重新编码为有效的 Unicode 并合理正确显示的最佳方法是什么?

(人类语言理解不是必需的——截断的版本可能看起来很奇怪,例如对于孤立的组合字符或泰语元音,只要软件在处理数据时不会崩溃。)

也可以看看:

4

5 回答 5

35
def unicode_truncate(s, length, encoding='utf-8'):
encoded = s.encode(encoding)[:length]
return encoded.decode(encoding, 'ignore')

这是一个 unicode 字符串的示例,其中每个字符在 UTF-8 中用 2 个字节表示:

>>> unicode_truncate(u'абвгд', 5)
u'\u0430\u0431'
于 2009-11-30T16:45:24.923 回答
9

UTF-8 的特性之一是它很容易重新同步,即在编码的字节流中很容易找到 unicode 字符边界。您需要做的就是以最大长度剪切编码字符串,然后从末尾向后移动,删除任何大于 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()
于 2009-11-27T16:12:30.693 回答
2

这适用于 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 to 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)

让我知道该正则表达式是否有任何问题。

于 2009-11-27T16:33:49.663 回答
1

对于 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 字节。

免责声明:仅在基本多语言平面上测试。是啊是啊,我知道。

于 2009-11-27T16:09:42.240 回答
1

检查字符串的最后一个字符。如果设置了高位,则它不是 UTF-8 字符中的最后一个字节,因此请备份并重试,直到找到一个。

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

truncated_string = toolong.encode("utf8")[0:mxlen].decode("utf8")
于 2016-11-15T17:25:03.547 回答