Усечение строки до байтовой длины в Python
У меня есть функция здесь, чтобы усечь заданную строку до заданной длины байта:
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_string_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
Это, казалось, отлично работало, пока не был задан вопрос о Emoji:
string = u"\ud83d\ude14"
trunc = cut_string_to_bytes_length(string, 100)
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "<console>", line 5, in cut_string_to_bytes_length
File "<console>", line 7, in codepoint_length
AssertionError: Invalid byte 152
Кто-нибудь может объяснить, что здесь происходит, и каково возможное решение?
Изменить: У меня есть еще один фрагмент кода, который не генерирует исключение, но иногда имеет странное поведение:
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]
>>> string = u"\ud83d\ude14\ud83d\ude14\ud83d\ude14\ud83d\ude14\ud83d\ude14"
>>> print string
(prints a set of 5 Apple Emoji...)😔😔😔😔😔
>>> len(string)
10
>>> trunc = utf8_byte_truncate(string, 4)
>>> print trunc
???
>>> len(trunc)
1
Итак, с этим вторым примером, у меня есть строка из 10 байтов, усекаем ее до 4, но происходит что-то странное, а результат - строка размером 1 байт.
Ответы
Ответ 1
Если число f таково, что f & 0xF0 == 0xF0
, то также имеет место, что f & 0xC0 == 0xC0
, потому что 0xF0 имеет все биты, которые имеет 0xC0, а затем некоторые. То есть среди других проблем ваша функция codepoint_length()
вернет шаг 2, когда ему будет 4. Если вы отмените список LENGTH_BY_PREFIX, функция будет работать в порядке с первым примером.
LENGTH_BY_PREFIX = [
(0xFC, 6),
(0xF8, 5),
(0xF0, 4),
(0xE0, 3),
(0xC0, 2), # first byte mask, total codepoint length
]
Ответ 2
Алгоритм ошибочен, так как указано @jwpat7. Более простой алгоритм:
# s = u'\ud83d\ude14\ud83d\ude14\ud83d\ude14\ud83d\ude14\ud83d\ude14'
# Same as above
s = u'\U0001f614' * 5 # Unicode character U+1F614
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]
# test for various max_bytes:
for m in range(len(s.encode('utf8'))+1):
b = utf8_byte_truncate(s,m)
print m,len(b),b.decode('utf8')
Выход
0 0
1 0
2 0
3 0
4 4 😔
5 4 😔
6 4 😔
7 4 😔
8 8 😔😔
9 8 😔😔
10 8 😔😔
11 8 😔😔
12 12 😔😔😔
13 12 😔😔😔
14 12 😔😔😔
15 12 😔😔😔
16 16 😔😔😔😔
17 16 😔😔😔😔
18 16 😔😔😔😔
19 16 😔😔😔😔
20 20 😔😔😔😔😔
Ответ 3
Версия кода Mark для Python 3:
@staticmethod
def utf8_lead_byte(b):
"""A UTF-8 intermediate byte starts with the bits 10xxxxxx."""
return (b & 0xC0) != 0x80
@staticmethod
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('utf-8')
if len(utf8) <= max_bytes:
return text
i = max_bytes
while i > 0 and not utf8_lead_byte("".join(map(chr, [utf8[i]]))):
i -= 1
return utf8[:i].decode('utf-8')
Примечания:
-
utf8[i]
возвращает байт, но ord()
ожидает строковый аргумент. (См. fooobar.com/info/11686/...)
- возвращаемое значение
utf8_byte_truncate
возвращается к строке перед возвратом в исходный входной аргумент (text
).