组合天城字符

29 投票
6 回答
8014 浏览
提问于 2025-04-16 22:09

我有一个像这样的东西:

a = "बिक्रम मेरो नाम हो"

我想实现一个像这样的效果:

a[0] = बि
a[1] = क्र
a[3] = म

但是因为 म 这个字符占用4个字节,而 बि 这个字符占用8个字节,所以我没法直接做到这一点。

那么我该怎么做才能实现呢?在Python中。

6 个回答

4

你可以通过一个简单的正则表达式来实现这个功能,只要你的工具支持 \X

这里有个演示

不过,Python的内置正则库 不支持 \X 这个匹配方式。

幸运的是,推荐的替代库 regex 是支持 \X 的:

>>> a = "बिक्रम मेरो नाम हो"
>>> regex.findall(r'\X', a)
['बि', 'क्', 'र', 'म', ' ', 'मे', 'रो', ' ', 'ना', 'म', ' ', 'हो']
15

所以,你想要实现的效果大概是这样的

a[0] = बि a[1] = क्र a[3] = म

我的建议是,不要认为字符串的索引和你在屏幕上看到的字符是一一对应的。像天城文(Devanagari)这样的文字,以及其他一些文字,对于习惯使用拉丁字母的程序员来说,可能会有些麻烦。我建议你看看Unicode标准的第9章(在这里可以找到)。

看起来你想做的是把一个字符串分解成“字形簇”。单靠字符串索引是无法做到这一点的。韩文也是一种在字符串索引上表现不佳的文字,虽然即使是像西班牙语这样熟悉的语言,使用组合字符时也会出现问题。

你需要一个外部库,比如ICU,来实现这个功能(除非你有很多空闲时间)。ICU有Python的接口。

>>> a = u"बिक्रम मेरो नाम हो"
>>> import icu
    # Note: This next line took a lot of guesswork.  The C, C++, and Java
    # interfaces have better documentation.
>>> b = icu.BreakIterator.createCharacterInstance(icu.Locale())
>>> b.setText(a)
>>> i = 0
>>> for j in b:
...     s = a[i:j]
...     print '|', s, len(s)
...     i = j
... 
| बि 2
| क् 2
| र 1
| म 1
|   1
| मे 2
| रो 2
|   1
| ना 2
| म 1
|   1
| हो 2

注意这些“字符”(字形簇)中,有些长度是2,有些长度是1。这就是为什么字符串索引会有问题的原因:如果我想从一个文本文件中获取第69450个字形簇,我就得逐行扫描整个文件并进行计数。所以你的选择有:

  • 建立一个索引(这有点疯狂...)
  • 意识到你不能在每个字符边界上进行分割。分割迭代器对象可以向前和向后移动,所以如果你需要提取字符串的前140个字符,你可以查看索引140,然后向迭代到上一个字形簇的分割点,这样你就不会得到奇怪的文本。(更好的是,你可以使用适合当地语言的单词分割迭代器。)使用这种抽象层次(字符迭代器等)的好处是,你使用的编码方式不再重要:你可以使用UTF-8、UTF-16、UTF-32,基本上都能正常工作。嗯,大部分情况下是这样。
29

把文本拆分成字形簇的算法在Unicode 附录 29的第3.1节中有说明。我不会在这里给你实现完整的算法,但我会大致告诉你如何处理德瓦纳加里文字的情况,然后你可以自己去阅读附录,看看还需要实现什么。

unicodedata模块包含了你需要的信息来检测字形簇。

>>> import unicodedata
>>> a = "बिक्रम मेरो नाम हो"
>>> [unicodedata.name(c) for c in a]
['DEVANAGARI LETTER BA', 'DEVANAGARI VOWEL SIGN I', 'DEVANAGARI LETTER KA', 
 'DEVANAGARI SIGN VIRAMA', 'DEVANAGARI LETTER RA', 'DEVANAGARI LETTER MA',
 'SPACE', 'DEVANAGARI LETTER MA', 'DEVANAGARI VOWEL SIGN E',
 'DEVANAGARI LETTER RA', 'DEVANAGARI VOWEL SIGN O', 'SPACE',
 'DEVANAGARI LETTER NA', 'DEVANAGARI VOWEL SIGN AA', 'DEVANAGARI LETTER MA',
 'SPACE', 'DEVANAGARI LETTER HA', 'DEVANAGARI VOWEL SIGN O']

在德瓦纳加里文字中,每个字形簇由一个初始字母、可选的“杀元音符”(virama)和字母的组合,以及一个可选的元音符号组成。用正则表达式的方式表示就是LETTER (VIRAMA LETTER)* VOWEL?。你可以通过查找每个代码点的Unicode类别来判断它们的类型:

>>> [unicodedata.category(c) for c in a]
['Lo', 'Mc', 'Lo', 'Mn', 'Lo', 'Lo', 'Zs', 'Lo', 'Mn', 'Lo', 'Mc', 'Zs',
 'Lo', 'Mc', 'Lo', 'Zs', 'Lo', 'Mc']

字母属于Lo类别(其他字母),元音符号属于Mc类别(标记,间距组合),杀元音符属于Mn类别(标记,无间距),空格属于Zs类别(分隔符,空格)。

所以这里有一个大致的方法来拆分字形簇:

def splitclusters(s):
    """Generate the grapheme clusters for the string s. (Not the full
    Unicode text segmentation algorithm, but probably good enough for
    Devanagari.)

    """
    virama = u'\N{DEVANAGARI SIGN VIRAMA}'
    cluster = u''
    last = None
    for c in s:
        cat = unicodedata.category(c)[0]
        if cat == 'M' or cat == 'L' and last == virama:
            cluster += c
        else:
            if cluster:
                yield cluster
            cluster = c
        last = c
    if cluster:
        yield cluster

>>> list(splitclusters(a))
['बि', 'क्र', 'म', ' ', 'मे', 'रो', ' ', 'ना', 'म', ' ', 'हो']

撰写回答