如何在 Python 3 中获得组合 Unicode 字符的显示宽度?

Posted

技术标签:

【中文标题】如何在 Python 3 中获得组合 Unicode 字符的显示宽度?【英文标题】:How do you get the display width of combined Unicode characters in Python 3? 【发布时间】:2015-09-02 02:40:44 【问题描述】:

在 Python 3 中,Unicode 字符串应该给你 Unicode 字符的数量,但鉴于某些字符组合,我无法弄清楚如何获得字符串的最终显示宽度。

创世记 1:1 -- בְּרֵאשִׁית、בָּרָא אֱלֹהִים、אֵת הַשָּׁמַיִם、וְאֵת הָאָרֶץ

>>> len('בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְאֵת הָאָרֶץ')
60

但字符串只有 37 个字符宽。规范化并不能解决问题,因为元音(较大字符下方的点)是不同的字符。

>>> len(unicodedata.normalize('NFC', 'בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְאֵת הָאָרֶץ'))
60

附带说明:textwrap 模块在这方面完全被破坏了,在不应该的地方积极地包装。 str.format 似乎同样坏掉了。

被标记为重复的类似问题: Display width of unicode strings in Python 它被标记为重复的问题仅解决了规范化: Normalizing Unicode

【问题讨论】:

计算字素簇可能还不够,例如,different fonts may lead to different text sizes 即使我们保证使用等宽字体? 点击链接,尝试代码并亲自查看。 【参考方案1】:

问题在于组合字符,Python 在计算 __len__ 时将其视为不同的字符,但会合并为单个打印字符。

要判断一个字符是否是组合字符,我们可以使用unicodedata module:

unicodedata.<strong>combining</strong>(<em>unichr</em>)

以整数形式返回分配给 Unicode 字符 unichr 的规范组合类。如果没有定义组合类,则返回 0。

一个天真的解决方案是用非零组合类去除任何字符。这留下了独立存在的字符,并且应该给我们一个字符串,该字符串在可见字符和底层字符之间具有一对一的映射。 (我是 Unicode 新手,它可能比这更复杂。组合字符和字素扩展器有一些微妙之处,我不太了解,但对于这个特定的字符串似乎无关紧要。) em>

所以我想出了这个功能:

import unicodedata

def visible_length(unistr):
    '''Returns the number of printed characters in a Unicode string.'''
    return len([char for char in unistr if unicodedata.combining(char) == 0])

返回正确的字符串长度:

>>> visible_length('בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְאֵת הָאָרֶץ')
37

这可能不是所有 Unicode 字符串的完整解决方案,但取决于您使用的 Unicode 子集,这可能足以满足您的需求。

【讨论】:

如果您需要完整的 Unicode 字素簇分割算法或行分割,那就有点复杂了——请参阅 uniseg 等第三方模块。 +1。这发生在我身上,但是当我使用 unicodedata.combining 并看到它返回的值范围很广时,我非常害怕,但也许它适合我的目的。谢谢。希望有人可以提出更强大的解决方案。【参考方案2】:

@bobince 建议的使用第三方uniseg 的几个解决方案:

>>> from uniseg.graphemecluster import grapheme_cluster_breakables
>>> sum(grapheme_cluster_breakables('בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְאֵת הָאָרֶץ'))
37
>>>
>>> from uniseg.graphemecluster import grapheme_clusters
>>> list(grapheme_clusters('בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְ  הָאָרֶץ'))
['בְּ', 'רֵ', 'א', 'שִׁ', 'י', 'ת', ',', ' ', 'בָּ', 'רָ', 'א', ' ', 'אֱ', 'לֹ', 'הִ', 'י', 'ם', ',', ' ', 'אֵ', 'ת', ' ', 'הַ', 'שָּׁ', 'מַ', 'יִ', 'ם', ',', ' ', 'וְ', 'אֵ', 'ת', ' ', 'הָ', 'אָ', 'רֶ', 'ץ']
>>> len(list(grapheme_clusters('בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַי , ואֵת הָאָרֶץ')))
37

这看起来是正确的做法。

这是一个修补 textwrap 的示例。修补其他模块的解决方案应该类似。

>>> import textwrap
>>> text = 'בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשּׁמַיִם, וְאֵת הָאָרֶץ'
>>> print(textwrap.fill(text, width=40))  # bad, aggressive wrapping
בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת
הַשָּׁמַיִם, וְאֵת הָאָרֶץ
>>> import uniseg.graphemecluster
>>> def new_len(x):
...     if isinstance(x, str):
...         return sum(1 for _ in uniseg.graphemecluster.grapheme_clusters(x))
...     return len(x)
>>> textwrap.len = new_len
>>> print(textwrap.fill(text, width=40))  # Good wrapping
בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְאֵת הָאָרֶץ

【讨论】:

你也可以使用regex模块:count_user_perceived_characters = lambda text: len(regex.findall(r'\X', text)) @J.F.Sebastian 整洁!该项目表示它打算替换re。你知道它是否真的会吗?

以上是关于如何在 Python 3 中获得组合 Unicode 字符的显示宽度?的主要内容,如果未能解决你的问题,请参考以下文章

python3 unicod,utf-8,gbk的编码和解码中文显示问题

如何获得n个二进制值的所有组合? [复制]

字符编码:ASCII,Unicod和UTF-8

如何简化这个 python 迭代?

Presto SQL - 如何获得数组的所有可能组合?

如何使用python解决组合问题?