Unicode格式化

3

我正在使用字符串格式化。对于英文来说,格式化是整齐的,但对于Unicode字符来说,格式化很混乱。请问有人能告诉我原因吗? 示例:

form = u'{:<15}{:<3}({})'
a = [
 u'സി ട്രീമിം',
 u'ബി ഡോഗേറ്റ്',
 u'ജെ ഹോളണ്ട്',
 u'എം നസീർ ',
 u'എം ബസ്ചാഗൻ…',
 u'ടി ഹെഡ് ',
 u'കെ ഭാരത് ',
 u'എം സിറാജ് ',
 u'എ ഈശ്വരൻ ',
 u'സി ഹാൻഡ്‌സ്‌കോംബ് ബി',]

 for i in range(0, 10):
     print form.format(a[i][:12], 1, 2)

输出结果为 enter image description here

然而,

s = [
 u'abcdef',
 u'akash',
 u'rohit',
 u'anubhav',
 u'bhargav',
 u'achut',
 u'punnet',
 u'tom',
 u'rach',
 u'kamal'
 ]
for i in range(0, 10):
     print form.format(s[i][:12], 1, 2)

给: 在此输入图片描述

5
并非所有的 Unicode 字符都是平等的,或者至少它们不具有相等的宽度。 - Martijn Pieters
2个回答

8
你正在打印马拉雅拉姆Unicode码点,其中使用了很多元音符号来修改前一个字形。这些元音符号码点本身并不形成新的字母,而马拉雅拉姆语在终端中产生的输出宽度与ASCII字母不同。
例如,在你的第一个字符串中以U+0D38 MALAYALAM LETTER SAU+0D3F MALAYALAM VOWEL SIGN I开头。第一个字符letter SA在屏幕上占据了一个完整的位置,但第二个字符,vowel sign I,当它在SA之前时,会改变字母的打印方式。请注意,打印了2个码点后,只有一个可见的字形:
>>> print u'\u0d38'  # letter SA
സ
>>> print u'\u0d3f'  # vowel sign I
 ി
>>> print u'\u0d38\u0d3f'  # both together
സി

马拉雅拉姆语的代码点宽度也不同;如果将ASCII字母添加到SA和元音符号I下面,分别和组合起来,就像这样:
>>> print u'\u0d38\nA..\n\u0d3f\nB..\n\u0d38\u0d3f\nAB.'  # with ASCII letters for size
സ
A..
 ി
B..
സി
AB.

请注意A宽(大约宽2.5倍),而സി几乎与固定宽度的3个ASCII代码点一样宽!然而,并非所有马拉雅拉姆字母都是这么宽。在第一个示例中,下一个字母是U+0D1F MALAYALAM LETTER TTA,它要窄得多:
>>> print u'\u0d38\nA..\n\u0d1f\nB..'
സ
A..
ട
B..

在实践中,我希望这种差异并不重要,而是将代码点组合起来,以便输出的宽度大致相同。
此外,马拉雅拉姆语还有其他的组合字符;您的第一个字符串包含 U+0D4D MALAYALAM SIGN VIRAMA,它已经与前面的字母TTA组合在一起。
当与前面的字母组合时,变音符会对打印宽度造成混乱:
>>> print u'\u0d1f\nA..\n\u0d4d\nB..\n\u0d1f\u0d4d\nAB.'
ട
A..
 ്
B..
ട്
AB.

字母TTA的宽度与ASCII字母一样,添加维拉玛符号后,宽度实际上并没有改变。

您可以通过查看代码点Unicode通用类别来近似大小。 unicodedata.category()函数会将类别作为字符串返回:

>>> import unicodedata
>>> unicodedata.category(u'\u0d38')
'Lo'
>>> unicodedata.category(u'\u0d3f')
'Mc'
>>> unicodedata.category(u'\u0d4d')
'Mn'

字母SA是Lo(Letter, other),元音符号是Mc(Mark, spacing combining),维拉马符号是Mn(Mark, nonspacing)。

>>> categories = {}
>>> for c in a[0]:
...     cat = unicodedata.category(c)
...     categories[cat] = categories.get(cat, 0) + 1
... 
>>> categories
{'Lo': 4, 'Mn': 1, 'Mc': 4, 'Zs': 1}

对于第一个字符串,有4个字母、4个组合标记和一个元音符号。类别Zs(分隔符,空格)用于ASCII空格字符' '
如果我们跳过McMn字符,能否更好地预测它们的宽度?字符串a[0]的宽度将为5个字符(4次Lo和1个空格)。
>>> print a[0] + '\nABCDE.'
സി ട്രീമിം
ABCDE.

在浏览器中,这看起来不够清晰,但在我的iTerm终端窗口中,它看起来像这样:

Python 2.7 output printing the strings <code>സി ട്രീമിം</code> and <code>ABCDE.</code>, with the capital letters in the second string producing roughly the same width on the screen as the first line.

为了让你的行排列整齐,你需要计算正确的宽度来为字符串添加额外的空格,以弥补显示宽度和代码点数之间的差异。
import unicodedata

def malayalam_width(s):
    return sum(1 for c in s if unicodedata.category(c)[0] != 'M')

form = u'{:<{width}}{:<3}({})'
for line in a:
    line = line[:12]
    adjust = len(line) - malayalam_width(line)
    print form.format(line, 1, 2, width=15 + adjust)

这已经大大提高了输出的质量

Output on terminal with adjusted code; columns line up better but still too far apart

似乎那些更宽的字母确实起到了作用。你需要手动添加更多的宽度来获得更好的结果;通过将字母映射到调整后的宽度,你可以再次使其对齐得更好一些。但是,码点宽度由所使用的字体设置,我不确定是否容易找到一种字体,它对所有马拉雅拉姆字母使用相等的宽度。
我发现使用制表位要容易得多,只需使用
form = u'{:<{width}}\t{:<3}({})'
for line in a:
    line = line[:12]
    adjust = len(line) - malayalam_width(line)
    print form.format(line, 1, 2, width=12 + adjust)

现在数字排列正确:

Lined up columns with tabs

你确实需要不断调整宽度;否则,一半的时间你会停在错误的制表位。

注意:我对马拉雅拉姆文并不熟悉,我肯定会忽略各种字母、元音符号和变音符号之间相互作用的微妙差别。对于熟悉该脚本和Unicode代码点的人来说,可能能够提供比我这里介绍的更好的宽度近似函数。

我还忽略了你最后一个字符串中目前存在的2个U+200C零宽度非连接器代码点;你可能需要从数据中删除它们。正如它的名字所示,它也没有宽度。


非常感谢你的详细解释。 - Savitha Suresh

-1
你可以使用 wcwidth 模块,它可以解决在不同终端中制表符长度解释不同的问题(据我所知)。
我在这里使用了 Python 3,我猜你在用 2,所以可能会有所不同。另外,我修改了输出格式以演示一些变量的使用。

解决方案

from wcwidth import wcswidth

a = [
    u'സി ട്രീമിം',
    u'ബി ഡോഗേറ്റ്',
    u'ജെ ഹോളണ്ട്',
    u'എം നസീർ ',
    u'എം ബസ്ചാഗൻ…',
    u'ടി ഹെഡ് ',
    u'കെ ഭാരത് ',
    u'എം സിറാജ് ',
    u'എ ഈശ്വരൻ ',
    u'സി ഹാൻഡ്‌സ്‌കോംബ് ബി'
]

desired = 15
max_str = 12

for item in a:

    sub_str = item[:max_str]

    diff = len(sub_str) - wcswidth(sub_str)

    indent = desired + diff if desired - wcswidth(sub_str) > 0 else desired + diff - 1

    form = u'{:<'+ str(indent) +'} {:<3}{:<3}{:<3}'

    print (form.format(sub_str, len(sub_str), wcswidth(sub_str), indent))

结果:

enter image description here


谢谢您的回答,能否请您解释一下为什么要执行 else desired + diff - 1 这个操作? - Savitha Suresh
注意:您可以嵌套{}部分以在str.format()模板中指定宽度。不要使用字符串连接来构建模板。u'{:<{indent}} {:<3}{:<3}{indent:<3}'form.format(sub_str, len(sub_str), wcswidth(sub_str), indent=indent)会更好地工作。 - Martijn Pieters
请注意,wcwidth 在这方面并不比使用 unicodedata 更好。wcswidth 所做的一切就是给我们与使用 unicodedata.east_asian_widthunicodedata.combining 获得的完全相同的信息(函数的源代码只是复制了 Unicode 数据表格,用于组合和 EAW 字符,并根据这些表格为代码点提供 0、1 或 2)。 - Martijn Pieters
您的输出实际上显示了很多未组合的组合标记,因此可能只是无效的Unicode字体实现或过时的Unicode呈现引擎的情况(这会让人感到惊讶,因为马拉雅拉姆语已经成为1993年发布的Unicode 1.1的一部分)。 - Martijn Pieters
无论如何,wcswidth 在这里几乎没有用处,因为这些都是组合和单宽代码点,但它们的显示输出在许多字体中是可变的。 - Martijn Pieters
显示剩余4条评论

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