格式化包含非 ascii 字符的列

Formatting columns containing non-ascii characters

所以我想对齐包含非 ascii 字符的字段。以下似乎不起作用:

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

hello                world
こんにちは      世界

有解决办法吗?

您正在格式化 multi-byte 编码字符串。您似乎正在使用 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)

你的下一个问题是这些字符也比大多数;你有 double-wide 个代码点:

>>> 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 标准中注册为更宽的字符数手动调整列宽。

棘手,因为有不止一种宽度可用。见 East Asian Width Unicode standard annex;有 narrowwideambigious 宽度;窄是大多数其他字符打印的宽度,宽是我终端上的两倍。模棱两可的是......关于它实际显示的宽度模棱两可:

Ambiguous characters require additional information not contained in the character code to further resolve their width.

它们的显示方式取决于上下文;例如,希腊字符在西方文本中显示为窄字符,但在东亚文本中显示为宽字符。我的终端将它们显示为窄,但其他终端(例如,配置为 east-asian 区域设置)可能将它们显示为宽。我不确定是否有任何 fool-proof 方法可以弄清楚它是如何工作的。

在大多数情况下,您需要将 'W''F' 值的字符计算为 unicodedata.east_asian_width() 占据 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
こんにちは           世界

您在上面可能看到的轻微错位是您的浏览器或字体对宽代码点使用了不同的宽度比(不是两倍)。

所有这些都有一个警告:并非所有终端都支持 East-Asian 宽度 Unicode 属性,并且仅以 一个 宽度显示所有代码点。

这不是一件容易的事 - 这不仅仅是 "non-ascii" - 它们是 wide-unicode 个字符,它们的显示非常棘手 - 从根本上讲,更多取决于您使用的终端类型而不是您在其中放置的空格数。

首先,您必须使用 UNICODE 字符串。由于您在 Python 2,这意味着您应该在 text-quotes 前加上 "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 中也是 multi-byte(如 ç

现在,一旦我们谈论 unicode,Python 确实包含一些实用程序 - 包括一个函数调用以了解每个 unicode-character 的显示单位 - 它是 unicode.east_asian_width - 这允许您有一种方法来计算每个字符串的宽度,然后有适当的间距数字:

"{的auto-calculation:

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)

这在我的终端上对我有用:

hello              world
こんにちは          世界

但正如 Martijn 所说,它比这更复杂。存在不明确的字符和终端类型。 如果你真的需要这个文本在文本终端中对齐,那么你应该使用 terminal-library,比如 curses,它允许你指定一个显示坐标来打印一个字符串。这样,您可以在打印每个单词之前将光标明确定位在适当的列上,并避免所有 display-width 计算。