6

我一直在尝试调试这个太久了,我显然不知道我在做什么,所以希望有人能提供帮助。我什至不确定我应该问什么,但它是这样的:

我正在尝试发送 Apple 推送通知,它们的有效负载大小限制为 256 字节。所以减去一些开销,我剩下大约 100 个英文字符的主要消息内容。

因此,如果一条消息比最大值长,我会截断它:

MAX_PUSH_LENGTH = 100
body = (body[:MAX_PUSH_LENGTH]) if len(body) > MAX_PUSH_LENGTH else body

所以这很好,花花公子,无论我有多久的消息(英文),推送通知都会成功发送。但是,现在我有一个阿拉伯字符串:

str = "هيك بنكون 
عيش بجنون تون تون تون هيك بنكون 
عيش بجنون تون تون تون 
أوكي أ"

>>> print len(str)
109

所以应该截断。但是,我总是收到无效的有效负载大小错误!好奇的是,我一直在降低 MAX_PUSH_LENGTH 阈值,以查看它需要什么才能成功,直到我将限制设置为 60 左右,推送通知才成功。

我不确定这是否与英语以外的语言的字节大小有关。我的理解是一个英文字符占一个字节,那么一个阿拉伯字符占两个字节吗?这可能与它有关吗?

此外,字符串在发送之前是 JSON 编码的,所以它最终看起来像这样:\u0647\u064a\u0643 \u0628\u0646\u0643\u0648\u0646 \n\u0639\u064a\u0634 ...它是否被解释为原始字符串,而只是 u0647 是 5 个字节?

我应该在这里做什么?是否有任何明显的错误,或者我没有问正确的问题?

4

4 回答 4

11

如果你有一个 python unicode 值并且你想截断,下面是在 Python 中执行它的一种非常简短、通用且有效的方法。

def truncate_unicode_to_byte_limit(src, byte_limit, encoding='utf-8'):
    '''
    truncate a unicode value to fit within byte_limit when encoded in encoding

    src: a unicode
    byte_limit: a non-negative integer
    encoding: a text encoding

    returns a unicode prefix of src guaranteed to fit within byte_limit when
    encoded as encoding.
    '''
    return src.encode(encoding)[:byte_limit].decode(encoding, 'ignore')

例如:

s = u"""
    هيك بنكون
    ascii
    عيش بجنون تون تون تون هيك بنكون
    عيش بجنون تون تون تون
    أوكي أ
"""

b = truncate_unicode_to_byte_limit(s, 73)
print len(b.encode('utf-8')), b

产生输出:

73 
    هيك بنكون
    ascii
    عيش بجنون تون تون تو
于 2014-07-19T09:59:49.300 回答
4

对于 unicode string s,您需要使用类似len(s.encode('utf-8'))的方法来获取其长度(以字节为单位)。len(s)只返回(未编码)字符的数量。

更新: 经过进一步研究,我发现 Python 支持增量编码,这使得可以编写一个相当快的函数来修剪多余的字符,同时避免字符串中任何多字节编码序列的损坏。这是用于此任务的示例代码:

# -*- coding: utf-8 -*-

import encodings
_incr_encoder = encodings.search_function('utf8').incrementalencoder()

def utf8_byte_truncate(text, max_bytes):
    """ truncate utf-8 text string to no more than max_bytes long """
    byte_len = 0
    _incr_encoder.reset()
    for index,ch in enumerate(text):
        byte_len += len(_incr_encoder.encode(ch))
        if byte_len > max_bytes:
            break
    else:
        return text
    return text[:index]

s = u"""
    هيك بنكون
    ascii
    عيش بجنون تون تون تون هيك بنكون
    عيش بجنون تون تون تون
    أوكي أ
"""

print 'initial string:'
print s.encode('utf-8')
print "{} chars, {} bytes".format(len(s), len(s.encode('utf-8')))
print
s2 = utf8_byte_truncate(s, 74)  # trim string
print 'after truncation to no more than 74 bytes:'
# following will raise encoding error exception on any improper truncations
print s2.encode('utf-8')
print "{} chars, {} bytes".format(len(s2), len(s2.encode('utf-8')))

输出:

initial string:

    هيك بنكون
    ascii
    عيش بجنون تون تون تون هيك بنكون
    عيش بجنون تون تون تون
    أوكي أ

98 chars, 153 bytes

after truncation to no more than 74 bytes:

    هيك بنكون
    ascii
    عيش بجنون تون تون تو
49 chars, 73 bytes
于 2012-12-02T00:02:02.367 回答
1

您需要切割到字节长度,因此您需要首先.encode('utf-8')切割您的字符串,然后在代码点边界处切割它。

在 UTF-8 中,ASCII ( <= 127) 是 1 字节。设置了两个或多个最高有效位( >= 192) 的字节是字符起始字节;后面的字节数由设置的最高有效位的数量决定。其他任何东西都是连续字节。

如果把中间的多字节序列剪掉,可能会出现问题;如果一个字符不适合,它应该被完全剪切,直到起始字节。

这是一些工作代码:

LENGTH_BY_PREFIX = [
  (0xC0, 2), # first byte mask, total codepoint length
  (0xE0, 3), 
  (0xF0, 4),
  (0xF8, 5),
  (0xFC, 6),
]

def codepoint_length(first_byte):
    if first_byte < 128:
        return 1 # ASCII
    for mask, length in LENGTH_BY_PREFIX:
        if first_byte & mask == mask:
            return length
    assert False, 'Invalid byte %r' % first_byte

def cut_to_bytes_length(unicode_text, byte_limit):
    utf8_bytes = unicode_text.encode('UTF-8')
    cut_index = 0
    while cut_index < len(utf8_bytes):
        step = codepoint_length(ord(utf8_bytes[cut_index]))
        if cut_index + step > byte_limit:
            # can't go a whole codepoint further, time to cut
            return utf8_bytes[:cut_index]
        else:
            cut_index += step
    # length limit is longer than our bytes strung, so no cutting
    return utf8_bytes

现在测试。如果.decode()成功,我们就进行了正确的切割。

unicode_text = u"هيك بنكون" # note that the literal here is Unicode

print cut_to_bytes_length(unicode_text, 100).decode('UTF-8')
print cut_to_bytes_length(unicode_text, 10).decode('UTF-8')
print cut_to_bytes_length(unicode_text, 5).decode('UTF-8')
print cut_to_bytes_length(unicode_text, 4).decode('UTF-8')
print cut_to_bytes_length(unicode_text, 3).decode('UTF-8')
print cut_to_bytes_length(unicode_text, 2).decode('UTF-8')

# This returns empty strings, because an Arabic letter
# requires at least 2 bytes to represent in UTF-8.
print cut_to_bytes_length(unicode_text, 1).decode('UTF-8')

您可以测试代码是否也适用于 ASCII。

于 2012-12-02T01:42:23.890 回答
1

使用我在您的另一个问题上发布的算法,这将以 UTF-8 编码一个 Unicode 字符串,并仅截断整个 UTF-8 序列以达到小于或等于最大长度的编码长度:

s = u"""
    هيك بنكون
    ascii
    عيش بجنون تون تون تون هيك بنكون
    عيش بجنون تون تون تون
    أوكي أ
"""

def utf8_lead_byte(b):
    '''A UTF-8 intermediate byte starts with the bits 10xxxxxx.'''
    return (ord(b) & 0xC0) != 0x80

def utf8_byte_truncate(text,max_bytes):
    '''If text[max_bytes] is not a lead byte, back up until a lead byte is
    found and truncate before that character.'''
    utf8 = text.encode('utf8')
    if len(utf8) <= max_bytes:
        return utf8
    i = max_bytes
    while i > 0 and not utf8_lead_byte(utf8[i]):
        i -= 1
    return utf8[:i]

b = utf8_byte_truncate(s,74)
print len(b),b.decode('utf8')

输出

73 
    هيك بنكون
    ascii
    عيش بجنون تون تون تو
于 2012-12-06T07:10:14.793 回答