格式化包含非ascii字符的列

时间:2016-01-07 12:38:39

标签: python python-2.7 unicode string-formatting non-ascii-characters

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

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

hello                world
こんにちは      世界

有解决方案吗?

2 个答案:

答案 0 :(得分:5)

您正在格式化多字节编码的字符串。您似乎使用UTF-8对文本进行编码,并且该编码每个代码点使用多个字节(1到4之间取决于特定字符)。格式化字符串会计算 bytes ,而不是代码点,这是字符串最终未对齐的原因之一:

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

这是棘手的,因为有多个宽度可用。见East Asian Width Unicode standard annex; narrow wide ambigious 宽度;窄是大多数其他字符打印的宽度,宽度是我终端上的两倍。模棱两可的......实际上显示的范围有多么模糊:

  

不明确的字符需要字符代码中未包含的其他信息才能进一步解析其宽度。

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

在大多数情况下,您需要将'W'的{​​{1}}或'F'值的字符计为 2 位置;从每种格式的宽度中减去1:

unicodedata.east_asian_width()

然后在我的终端中生成所需的对齐

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))

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

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

答案 1 :(得分: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 - 这可以让你有办法计算每个字符串的宽度,然后有适当的间隔号:

自动计算“{:

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所说,它比这复杂得多。有模糊的字符和终端类型。 如果你真的需要在文本终端中对齐这个文本,那么你应该使用一个终端库,比如curses,允许你指定一个显示坐标来打印一个字符串。这样,您可以在打印每个单词之前将光标明确地定位在相应的列上,并避免所有显示宽度计算。