在Golang中计算字符串的字符数

23

我试图在Go语言中计算“字符”的数量。也就是说,如果一个字符串包含一个可打印的“字形”,或者“组合字符”(或者某个人通常想到的字符),我希望它能够计算为1。例如,字符串“Hello, 世界”应该计数为11,因为有11个字符,人们会看到这个并说有11个字形。

utf8.RuneCountInString()在大多数情况下都很好用,包括ascii、重音符号、亚洲字符甚至表情符号。但是,据我所知,rune对应的是代码点,而不是字符。当我尝试使用基本表情符号时,它能正常工作,但是当我使用具有不同肤色的表情符号时,我得到了错误的计数:https://play.golang.org/p/aFIGsB6MsO

从我所读到的这里这里来看,以下应该可以正常工作,但我似乎仍然没有得到正确的结果(它过度计数):

func CountCharactersInString(str string) int {
    var ia norm.Iter
    ia.InitString(norm.NFC, str)
    nc := 0
    for !ia.Done() {
        nc = nc + 1
        ia.Next()
    }
    return nc
}

这也不起作用:

func GraphemeCountInString(str string) int {
    re := regexp.MustCompile("\\PM\\pM*|.")
    return len(re.FindAllString(str, -1))
}

我正在寻找与此类似的Objective C代码:

+ (NSInteger)countCharactersInString:(NSString *) string {
    // --- Calculate the number of characters enterd by user and update character count label
    NSInteger count = 0;
    NSUInteger index = 0;
    while (index < string.length) {
        NSRange range = [string rangeOfComposedCharacterSequenceAtIndex:index];
        count++;
        index += range.length;
    }
    return count;
 }

你正在寻找UAX#29中“字形簇边界”算法的实现。 - 一二三
我相信这是正确的。我尝试了这个答案https://dev59.com/zGcs5IYBdhLWcg3wp1z3#26728555中提供的两种图形计数实现,但我遇到了同样的问题,也许图形簇边界计数更符合我的需求? - Bjorn Roche
那个问题的答案混淆了“字形簇”和“字符规范化”(它们都有严重的错误)。 - 一二三
你解决了这个问题吗?问题在于肤色修改器被视为单独的字符,而规范不将其与手算作一个字符。 - F21
从未找到正确的解决方案,所以我不得不放宽我的要求。 - Bjorn Roche
5个回答

16

直接使用 utf8.RuneCountInString() 函数。

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    str := "Hello, 世界"
    fmt.Println("counts =", utf8.RuneCountInString(str))
}

2
甚至可以更直接地使用utf8.RuneCountInString。 - feech
1
感谢@mvndaai的修改,RuneCountInString类似于RuneCount,但其输入为字符串而不是字节。 - 0xFK
这是最佳答案,因为它使用内部的utf8库而不是外部的。 - Serhii Polishchuk
1
Go语言不需要包来理解Unicode。只需确保计算符文而不是字节; len([]rune("Hello, 世界")) - Ferdy Pruis

13

我写了一个包,可以让你做到这一点:https://github.com/rivo/uniseg。它按照Unicode标准附录#29中指定的规则来分隔字符串,这正是您要寻找的。以下是在您的情况下如何使用它:

package main

import (
    "fmt"

    "github.com/rivo/uniseg"
)

func main() {
    fmt.Println(uniseg.GraphemeClusterCount("Hello, 世界"))
}

这将打印11,正如您所期望的那样。


最佳解决方案。所有其他解决方案都会将某些表情符号计为1个字符,而将其他表情符号计为2个字符。 - darkstar
1
字节、符文和字形之间有区别,似乎很多人混淆这三者。(在大多数情况下,这也无关紧要。)例如,️‍(彩虹旗表情符号)是1个字形,4个符文和14个字节。Go标准库只提供了字节和符文的内置函数,但没有提供字形的。 - Oliver

11

您尝试过strings.Count吗?

package main

import (
     "fmt"
     "strings"
 )

 func main() {
     fmt.Println(strings.Count("Hello, 世界", "")) // Returns 2
 }

在例子“Hello, 世界”中,我希望它计算为11,因为有11个字符,而不是2个。我将编辑我的问题以澄清。 - Bjorn Roche

5

参考API文档的示例。 https://golang.org/pkg/unicode/utf8/#example_DecodeLastRuneInString

该示例演示了如何使用Go语言中的Unicode包来解码字符串中的最后一个符文。您可以在链接中找到完整的示例代码和详细说明,以及有关Unicode和UTF-8编码的更多信息。

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    str := "Hello, 世界"
    count := 0
    for len(str) > 0 {
        r, size := utf8.DecodeLastRuneInString(str)
        count++
        fmt.Printf("%c %v\n", r, size)

        str = str[:len(str)-size]
    }
    fmt.Println("count:",count)
}

2
这里计算的是符文而不是字形str := ""的计数为2,而不是1。 - 一二三
"AX"是什么,为什么它应该是1? - Jiang YD
1
它是 U+1F1E6 U+1F1FD,应该呈现为奥兰群岛的国旗。任何其他区域指示符号都将具有相同的结果(也许在您的系统上 `` 呈现得更好?)。 - 一二三
1
是的,但在区域指示符序列中,它们形成一个字符(或者如原问题所说的“一个可打印的'字形'”)。 - 一二三
@phtrivier,是的,我在我的问题中举的例子使用了unicode/norm包,但有时仍会得到错误的答案,例如对于glyph。 - Bjorn Roche
显示剩余4条评论

-2

我认为最简单的方法是这样的:

package main

import "fmt"

func main() {
    str := "Hello, 世界"
    var counter int
    for range str {
        counter++
    }
    fmt.Println(counter)
}

这个会打印11


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