Теоретически достаточно encode
вашей строки, а затем проверить, соответствует ли она ограничению "ширины".Если это так, то строка может быть просто возвращена.В противном случае вы можете взять первые байты "ширины" из закодированной строки (минус байты, необходимые для заполнителя).Чтобы убедиться, что он работает как textwrap.shorten
, нужно также найти последний пробел в оставшихся байтах и вернуть все до пробела + заполнитель.Если пробелов нет, нужно возвращать только местозаполнитель.
Учитывая, что вы упомянули, что вы действительно хотите ограничить его количеством байтов, функция выдает исключение, если заполнитель слишком велик.Потому что наличие заполнителя, который не помещался бы в контейнер / структуру данных с ограниченным байтом, просто не имеет смысла и позволяет избежать многих крайних случаев, которые могут привести к несовместимому «максимальному размеру байта» и «размеру байта заполнителя».
Код может выглядеть следующим образом:
def shorten_rsplit(string: str, maximum_bytes: int, normalize_spaces: bool = False, placeholder: str = "[...]") -> str:
# Make sure the placeholder satisfies the byte length requirement
encoded_placeholder = placeholder.encode().strip()
if maximum_bytes < len(encoded_placeholder):
raise ValueError('placeholder too large for max width')
# Get the UTF-8 bytes that represent the string and (optionally) normalize the spaces.
if normalize_spaces:
string = " ".join(string.split())
encoded_string = string.encode()
# If the input string is empty simply return an empty string.
if not encoded_string:
return ''
# In case we don't need to shorten anything simply return
if len(encoded_string) <= maximum_bytes:
return string
# We need to shorten the string, so we need to add the placeholder
substring = encoded_string[:maximum_bytes - len(encoded_placeholder)]
splitted = substring.rsplit(b' ', 1) # Split at last space-character
if len(splitted) == 2:
return b" ".join([splitted[0], encoded_placeholder]).decode()
else:
return '[...]'
И простой тестовый пример:
t = '☺ Ilsa, le méchant ☺ ☺ gardien ☺'
for i in range(5, 50):
shortened = shorten_rsplit(t, i)
byte_length = len(shortened.encode())
print(byte_length <= i, i, byte_length, shortened)
, который возвращает
True 5 5 [...]
True 6 5 [...]
True 7 5 [...]
True 8 5 [...]
True 9 9 ☺ [...]
True 10 9 ☺ [...]
True 11 9 ☺ [...]
True 12 9 ☺ [...]
True 13 9 ☺ [...]
True 14 9 ☺ [...]
True 15 15 ☺ Ilsa, [...]
True 16 15 ☺ Ilsa, [...]
True 17 15 ☺ Ilsa, [...]
True 18 18 ☺ Ilsa, le [...]
True 19 18 ☺ Ilsa, le [...]
True 20 18 ☺ Ilsa, le [...]
True 21 18 ☺ Ilsa, le [...]
True 22 18 ☺ Ilsa, le [...]
True 23 18 ☺ Ilsa, le [...]
True 24 18 ☺ Ilsa, le [...]
True 25 18 ☺ Ilsa, le [...]
True 26 18 ☺ Ilsa, le [...]
True 27 27 ☺ Ilsa, le méchant [...]
True 28 27 ☺ Ilsa, le méchant [...]
True 29 27 ☺ Ilsa, le méchant [...]
True 30 27 ☺ Ilsa, le méchant [...]
True 31 31 ☺ Ilsa, le méchant ☺ [...]
True 32 31 ☺ Ilsa, le méchant ☺ [...]
True 33 31 ☺ Ilsa, le méchant ☺ [...]
True 34 31 ☺ Ilsa, le méchant ☺ [...]
True 35 35 ☺ Ilsa, le méchant ☺ ☺ [...]
True 36 35 ☺ Ilsa, le méchant ☺ ☺ [...]
True 37 35 ☺ Ilsa, le méchant ☺ ☺ [...]
True 38 35 ☺ Ilsa, le méchant ☺ ☺ [...]
True 39 35 ☺ Ilsa, le méchant ☺ ☺ [...]
True 40 35 ☺ Ilsa, le méchant ☺ ☺ [...]
True 41 41 ☺ Ilsa, le méchant ☺ ☺ gardien ☺
True 42 41 ☺ Ilsa, le méchant ☺ ☺ gardien ☺
True 43 41 ☺ Ilsa, le méchant ☺ ☺ gardien ☺
True 44 41 ☺ Ilsa, le méchant ☺ ☺ gardien ☺
True 45 41 ☺ Ilsa, le méchant ☺ ☺ gardien ☺
True 46 41 ☺ Ilsa, le méchant ☺ ☺ gardien ☺
True 47 41 ☺ Ilsa, le méchant ☺ ☺ gardien ☺
True 48 41 ☺ Ilsa, le méchant ☺ ☺ gardien ☺
True 49 41 ☺ Ilsa, le méchant ☺ ☺ gardien ☺
Функция также имеетаргумент для нормализации пробелов.Это может быть полезно в том случае, если у вас есть другой тип пробелов (новые строки и т. Д.) Или несколько последовательных пробелов.Хотя это будет немного медленнее.
Производительность
Я провел быстрый тест с использованием simple_benchmark
(библиотека, которую я написал), чтобы убедиться, что она действительно быстрее.
Для теста я создаю строку, содержащую случайные символы Юникода, где (в среднем) один из 8 символов является пробелом.Я также использую половину длины строки как ширину в байтах для разделения.У обоих нет особой причины, это может повлиять на критерии, поэтому я и хотел об этом упомянуть.
Функции, используемые в бенчмарке:
def shorten_rsplit(string: str, maximum_bytes: int, normalize_spaces: bool = False, placeholder: str = "[...]") -> str:
encoded_placeholder = placeholder.encode().strip()
if maximum_bytes < len(encoded_placeholder):
raise ValueError('placeholder too large for max width')
if normalize_spaces:
string = " ".join(string.split())
encoded_string = string.encode()
if not encoded_string:
return ''
if len(encoded_string) <= maximum_bytes:
return string
substring = encoded_string[:maximum_bytes - len(encoded_placeholder)]
splitted = substring.rsplit(b' ', 1) # Split at last space-character
if len(splitted) == 2:
return b" ".join([splitted[0], encoded_placeholder]).decode()
else:
return '[...]'
import textwrap
_MIN_WIDTH = 5
def shorten_to_bytes_width(text: str, width: int) -> str:
width = max(_MIN_WIDTH, width)
text = textwrap.shorten(text, width)
while len(text.encode()) > width:
text = textwrap.shorten(text, len(text) - 1)
assert len(text.encode()) <= width
return text
def naive(text: str, width: int) -> str:
width = max(_MIN_WIDTH, width)
text = textwrap.shorten(text, width)
if len(text.encode()) <= width:
return text
current_width = _MIN_WIDTH
index = 0
slice_index = 0
endings = ' '
while True:
new_width = current_width + len(text[index].encode())
if new_width > width:
break
if text[index] in endings:
slice_index = index
index += 1
current_width = new_width
if slice_index:
slice_index += 1 # to include found space
text = text[:slice_index] + '[...]'
assert len(text.encode()) <= width
return text
MAX_BYTES_PER_CHAR = 4
def bytes_to_char_length(input, bytes, start=0, max_length=None):
if bytes <= 0 or (max_length is not None and max_length <= 0):
return 0
if max_length is None:
max_length = min(bytes, len(input) - start)
bytes_too_much = len(input[start:start + max_length].encode()) - bytes
if bytes_too_much <= 0:
return max_length
min_length = max(max_length - bytes_too_much, bytes // MAX_BYTES_PER_CHAR)
max_length -= (bytes_too_much + MAX_BYTES_PER_CHAR - 1) // MAX_BYTES_PER_CHAR
new_start = start + min_length
bytes_left = bytes - len(input[start:new_start].encode())
return min_length + bytes_to_char_length(input, bytes_left, new_start, max_length - min_length)
def shorten_to_bytes(input, bytes, placeholder=' [...]', start=0):
if len(input[start:start + bytes + 1].encode()) <= bytes:
return input
bytes -= len(placeholder.encode())
max_chars = bytes_to_char_length(input, bytes, start)
if max_chars <= 0:
return placeholder.strip() if bytes >= 0 else ''
w = input.rfind(' ', start, start + max_chars + 1)
if w > 0:
return input[start:w] + placeholder
else:
return input[start:start + max_chars] + placeholder
# Benchmark
from simple_benchmark import benchmark, MultiArgument
import random
def get_random_unicode(length): # https://stackoverflow.com/a/21666621/5393381
get_char = chr
include_ranges = [
(0x0021, 0x0021), (0x0023, 0x0026), (0x0028, 0x007E), (0x00A1, 0x00AC), (0x00AE, 0x00FF),
(0x0100, 0x017F), (0x0180, 0x024F), (0x2C60, 0x2C7F), (0x16A0, 0x16F0), (0x0370, 0x0377),
(0x037A, 0x037E), (0x0384, 0x038A), (0x038C, 0x038C)
]
alphabet = [
get_char(code_point) for current_range in include_ranges
for code_point in range(current_range[0], current_range[1] + 1)
]
# Add more whitespaces
for _ in range(len(alphabet) // 8):
alphabet.append(' ')
return ''.join(random.choice(alphabet) for i in range(length))
r = benchmark(
[shorten_rsplit, shorten_to_bytes, shorten_to_bytes_width, naive, bytes_to_char_length],
{2**exponent: MultiArgument([get_random_unicode(2**exponent), 2**exponent // 2]) for exponent in range(4, 15)},
"string length"
)
Я также сделал второй бенчмарк, исключая функцию shorten_to_bytes_width
, чтобы я мог бенчмаркировать даже более длинные строки:
r = benchmark(
[shorten_rsplit, shorten_to_bytes, naive],
{2**exponent: MultiArgument([get_random_unicode(2**exponent), 2**exponent // 2]) for exponent in range(4, 20)},
"string length"
)