截断Unicode以适应最大传输大小
给定一个Unicode字符串和以下要求:
- 这个字符串需要被编码成某种字节序列格式(比如UTF-8或者JSON的Unicode转义)
- 编码后的字符串有一个最大长度限制
举个例子,iPhone的推送服务要求使用JSON编码,并且总的数据包大小不能超过256字节。
那么,最好的办法是什么呢?如何截断这个字符串,使得它重新编码后仍然是有效的Unicode,并且显示得还算正常?
(人类理解语言的能力并不是必须的——截断后的版本可能看起来有点奇怪,比如出现孤立的组合字符或者泰语元音,只要软件在处理这些数据时不会崩溃就行。)
相关内容:
- 相关的Java问题: 如何将Java字符串截断以适应UTF-8编码后的字节数限制?
- 相关的JavaScript问题: 如何使用JavaScript将文本截断到特定大小
5 个回答
这个方法适用于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)
如果这个正则表达式有任何问题,请告诉我。
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()
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代码点,程序就会崩溃。