截断Unicode以适应最大传输大小

31 投票
5 回答
8464 浏览
提问于 2025-04-15 16:27

给定一个Unicode字符串和以下要求:

  • 这个字符串需要被编码成某种字节序列格式(比如UTF-8或者JSON的Unicode转义)
  • 编码后的字符串有一个最大长度限制

举个例子,iPhone的推送服务要求使用JSON编码,并且总的数据包大小不能超过256字节。

那么,最好的办法是什么呢?如何截断这个字符串,使得它重新编码后仍然是有效的Unicode,并且显示得还算正常?

(人类理解语言的能力并不是必须的——截断后的版本可能看起来有点奇怪,比如出现孤立的组合字符或者泰语元音,只要软件在处理这些数据时不会崩溃就行。)

相关内容:

5 个回答

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到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)

如果这个正则表达式有任何问题,请告诉我。

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()
36
def unicode_truncate(s, length, encoding='utf-8'):
    encoded = s.encode(encoding)[:length]
    return encoded.decode(encoding, 'ignore')
>>> unicode_truncate(u'абвгд', 5)
u'\u0430\u0431'

这里有一个例子,展示了一个Unicode字符串。在这个字符串中,每个字符在UTF-8编码中用2个字节表示。如果不忽略那些被拆分的Unicode代码点,程序就会崩溃。

撰写回答