如何正确格式化 Unicode 字符串?
How can I format Unicode strings properly?
当我尝试使用 str.format()
的对齐功能集中 python 中的 Unicode 字符串时,我得到了错误的结果。例如我想给一个字符串加下划线然后让它居中。
underline_txt = ''.join(map(lambda x: x + '\u0332', 'AB'))
centered_txt = '{:^4}'.format(underline_txt)
print(centered_txt)
问题是 centered_txt
是 4 个字符长,但打印输出是 2 个终端单元格宽。因为 x + '\u0332'
是一个终端单元宽。
现在我的问题是:如何正确格式化 Unicode 字符串?
我可以通过手动填充字符串来解决问题,但我想知道是否有更通用的解决方案。
快速而肮脏的解决方案,如果 len(underline_txt) == 0
以及使用波浪符 ('\u0303').
等其他组合字符时会出现问题
str_len = len(underline_txt) / 4
left_pad, right_pad = ' ' * math.floor(str_len), ' ' * math.ceil(str_len)
really_centered = left_pad + centered_txt + right_hand
我通过创建自己的 string.Formatter
找到了一个解决方案,我在其中覆盖了方法 format_field(self, value, format_spec)
。在 format_field 中,我检查给定的 value
是否是 str
的实例,并且它打印的长度与其字符长度不同。然后我才做一个"Unicode alignment"。 format_spec
的解析是 format(value, format_spec)
内置的 python 实现,它试图模仿其所有边缘情况。
import wcwidth
class UnicodeFormatter(string.Formatter):
def format_field(self, value, format_spec):
if not isinstance(value, str):
# If `value` is not a string use format built-in
return format(value, format_spec)
if format_spec == '':
# If `format_spec` is empty we just return the `value` string
return value
print_length = wcwidth.wcswidth(value)
if len(value) == print_length:
return format(value, format_spec)
fill, align, width, format_spec = UnicodeFormatter.parse_align(format_spec)
if width == 0:
return value
formatted_value = format(value, format_spec)
pad_len = width - print_length
if pad_len <= 0:
return formatted_value
left_pad = ''
right_pad = ''
if align in '<=':
right_pad = fill * pad_len
elif align == '>':
left_pad = fill * pad_len
elif align == '^':
left_pad = fill * math.floor(pad_len/2)
right_pad = fill * math.ceil(pad_len/2)
return ''.join((left_pad, formatted_value, right_pad))
@staticmethod
def parse_align(format_spec):
format_chars = '=<>^'
align = '<'
fill = None
if format_spec[1] in format_chars:
align = format_spec[1]
fill = format_spec[0]
format_spec = format_spec[2:]
elif format_spec[0] in format_chars:
align = format_spec[0]
format_spec = format_spec[1:]
if align == '=':
raise ValueError("'=' alignment not allowed in string format specifier")
if format_spec[0] in '+- ':
raise ValueError('Sign not allowed in string format specifier')
if format_spec[0] == '#':
raise ValueError('Alternate form (#) not allowed in string format specifier')
if format_spec[0] == '0':
if fill is None:
fill = '0'
format_spec = format_spec[1:]
if fill is None:
fill = ' '
width_str = ''.join(itertools.takewhile(str.isdigit, format_spec))
width_len = len(width_str)
format_spec = format_spec[width_len:]
if width_len > 0:
width = int(width_str)
else:
width = 0
return fill, align, width, format_spec
当我尝试使用 str.format()
的对齐功能集中 python 中的 Unicode 字符串时,我得到了错误的结果。例如我想给一个字符串加下划线然后让它居中。
underline_txt = ''.join(map(lambda x: x + '\u0332', 'AB'))
centered_txt = '{:^4}'.format(underline_txt)
print(centered_txt)
问题是 centered_txt
是 4 个字符长,但打印输出是 2 个终端单元格宽。因为 x + '\u0332'
是一个终端单元宽。
现在我的问题是:如何正确格式化 Unicode 字符串?
我可以通过手动填充字符串来解决问题,但我想知道是否有更通用的解决方案。
快速而肮脏的解决方案,如果 len(underline_txt) == 0
以及使用波浪符 ('\u0303').
str_len = len(underline_txt) / 4
left_pad, right_pad = ' ' * math.floor(str_len), ' ' * math.ceil(str_len)
really_centered = left_pad + centered_txt + right_hand
我通过创建自己的 string.Formatter
找到了一个解决方案,我在其中覆盖了方法 format_field(self, value, format_spec)
。在 format_field 中,我检查给定的 value
是否是 str
的实例,并且它打印的长度与其字符长度不同。然后我才做一个"Unicode alignment"。 format_spec
的解析是 format(value, format_spec)
内置的 python 实现,它试图模仿其所有边缘情况。
import wcwidth
class UnicodeFormatter(string.Formatter):
def format_field(self, value, format_spec):
if not isinstance(value, str):
# If `value` is not a string use format built-in
return format(value, format_spec)
if format_spec == '':
# If `format_spec` is empty we just return the `value` string
return value
print_length = wcwidth.wcswidth(value)
if len(value) == print_length:
return format(value, format_spec)
fill, align, width, format_spec = UnicodeFormatter.parse_align(format_spec)
if width == 0:
return value
formatted_value = format(value, format_spec)
pad_len = width - print_length
if pad_len <= 0:
return formatted_value
left_pad = ''
right_pad = ''
if align in '<=':
right_pad = fill * pad_len
elif align == '>':
left_pad = fill * pad_len
elif align == '^':
left_pad = fill * math.floor(pad_len/2)
right_pad = fill * math.ceil(pad_len/2)
return ''.join((left_pad, formatted_value, right_pad))
@staticmethod
def parse_align(format_spec):
format_chars = '=<>^'
align = '<'
fill = None
if format_spec[1] in format_chars:
align = format_spec[1]
fill = format_spec[0]
format_spec = format_spec[2:]
elif format_spec[0] in format_chars:
align = format_spec[0]
format_spec = format_spec[1:]
if align == '=':
raise ValueError("'=' alignment not allowed in string format specifier")
if format_spec[0] in '+- ':
raise ValueError('Sign not allowed in string format specifier')
if format_spec[0] == '#':
raise ValueError('Alternate form (#) not allowed in string format specifier')
if format_spec[0] == '0':
if fill is None:
fill = '0'
format_spec = format_spec[1:]
if fill is None:
fill = ' '
width_str = ''.join(itertools.takewhile(str.isdigit, format_spec))
width_len = len(width_str)
format_spec = format_spec[width_len:]
if width_len > 0:
width = int(width_str)
else:
width = 0
return fill, align, width, format_spec