如何在 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
似乎同样坏掉了。
【问题讨论】:
计算字素簇可能还不够,例如,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 字符的显示宽度?的主要内容,如果未能解决你的问题,请参考以下文章