包含非 ASCII 字符的列的格式化

10

因为我想对包含非ASCII字符的字段进行对齐,但以下内容似乎不起作用:

for word1, word2 in [['hello', 'world'], ['こんにちは', '世界']]:
    print "{:<20} {:<20}".format(word1, word2)

hello                world
こんにちは      世界

有解决方案吗?

2个回答

8

您正在格式化一个多字节编码的字符串。您似乎在使用UTF-8对文本进行编码,该编码每个代码点使用多个字节(根据具体字符介于1到4之间)。格式化字符串计算的是字节数而不是代码点,这也是为什么您的字符串最终会错位的原因之一:

>>> len('hello')
5
>>> len('こんにちは')
15
>>> len(u'こんにちは')
5

请使用Unicode字符串格式化文本,这样您就可以计算代码点而不是字节:

for word1, word2 in [[u'hello', u'world'], [u'こんにちは', u'世界']]:
    print u"{:<20} {:<20}".format(word1, word2)

你面临的另一个问题是这些字符也比大多数字符 更宽,你有双倍宽度的代码点:

>>> import unicodedata
>>> unicodedata.east_asian_width(u'h')
'Na'
>>> unicodedata.east_asian_width(u'世')
'W'
>>> for word1, word2 in [[u'hello', u'world'], [u'こんにちは', u'世界']]:
...     print u"{:<20} {:<20}".format(word1, word2)
...
hello                world
こんにちは                世界

str.format() 无法处理此问题; 在格式化之前,您必须根据 Unicode 标准中注册为宽字符的数量手动调整列宽。

这是很棘手的,因为有多个可用的宽度。请参见 东亚宽度Unicode 标准附录;有 不确定 的宽度;窄是大多数其他字符打印的宽度,宽是我终端上的两倍。不确定在于它实际显示的宽度取决于上下文:

不确定字符要求额外的信息,而这些信息不包含在字符代码中,以进一步解决它们的宽度。

它们的显示方式取决于上下文;例如,希腊字符在西方文本中显示为窄字符,但在东亚环境中显示为宽字符。我的终端将它们显示为窄字符,但其他终端(例如配置为东亚语言环境的终端)可能会将它们显示为宽字符。我不确定是否有任何可靠的方法来确定这将如何工作。

大多数情况下,对于具有 unicodedata.east_asian_width()'W''F' 值的字符,您需要将其计为占据 2 个位置;对于这些字符,从您的格式宽度中减去 1:

def calc_width(target, text):
    return target - sum(unicodedata.east_asian_width(c) in 'WF' for c in text)

for word1, word2 in [[u'hello', u'world'], [u'こんにちは', u'世界']]:
    print u"{0:<{1}} {2:<{3}}".format(word1, calc_width(20, word1), word2, calc_width(20,  word2))

这将在我的终端中产生所需的对齐效果:
>>> for word1, word2 in [[u'hello', u'world'], [u'こんにちは', u'世界']]:
...     print u"{0:<{1}} {2:<{3}}".format(word1, calc_width(20, word1), word2, calc_width(20,  word2))
...
hello                world
こんにちは           世界

你可能会看到上面的轻微不对齐,这是由于你的浏览器或字体使用了不完全是双倍宽度比例的宽字符编码。

所有这些都有一个警告:并非所有终端都支持东亚宽度Unicode属性,并且只显示所有代码点的一个宽度。


1
这不是一项容易的任务 - 它不仅仅是“非 ASCII” - 它们是宽 Unicode 字符,它们的显示相当棘手 - 并且根本上更取决于您使用的终端类型而不是您放置的空格数量。
首先,您必须使用 UNICODE 字符串。由于您在 Python 2 中,这意味着您应该在文本引号前加上“u”。
for word1, word2 in [[u'hello', u'world'], [u'こんにちは', u'世界']]:
    print "{:<20} {:<20}".format(word1, word2)

那样,Python 可以将字符串中的每个字符作为一个字符识别,而不是仅因为偶然而显示回来的字节集合。
>>> a = u'こんにちは'
>>> len(a)
5
>>> b = 'こんにちは'
>>> len(b)
15

乍一看,这些长度似乎可以用于计算字符宽度。不幸的是,这些UTF-8编码的字节长度与字符的实际显示宽度无关。单宽Unicode字符在UTF-8中也是多字节的(例如ç)。
现在,一旦涉及到Unicode,Python包括一些实用程序 - 包括一个函数调用,以了解每个Unicode字符的显示单位 - 它是unicode.east_asian_width - 这使您可以有一种计算每个字符串的宽度并具有适当间距数字的方法:

The auto-calculation of the " {:

import unicode

def display_len(text):
    res = 0
    for char in text:
        res += 2 if unicodedata.east_asian_width(char) == 'W' else 1
    return res

for word1, word2 in [[u'hello', u'world'], [u'こんにちは', u'世界']]:
    width_format = u"{{}}{}{{}}".format(" " * (20 - (display_len(word1))))
    print width_format.format(word1, word2)

That has worked for me on my terminal:

hello              world
こんにちは          世界

But as Martijn puts it, it si more complicated than that. There are ambiguyous characters and terminal types. If you really need this text to be aligned in a text terminal, then you should use a terminal-library, like curses, whcih allow you to specify a display coordinate to print a string at. That way, you can simply position your cursor explictly on the appropriate column before printing each word, and avoid all display-width computation.


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接