175

如何在 Go 中获取字符串的字符数?

例如,如果我有一个字符串"hello",该方法应该返回5. 我看到它len(str)返回字节数而不是字符数,所以len("£")返回 2 而不是 1,因为 £ 在 UTF-8 中用两个字节编码。

4

7 回答 7

213

RuneCountInString您可以从 utf8 包中尝试。

返回 p 中的符文数

即,如本脚本所示:“世界”的长度可能为 6(用中文书写时:“世界”),但其符文计数为 2:

package main
    
import "fmt"
import "unicode/utf8"
    
func main() {
    fmt.Println("Hello, 世界", len("世界"), utf8.RuneCountInString("世界"))
}

Phrozen在评论中补充道:

实际上你可以len()通过类型转换来完成符文。
len([]rune("世界"))将打印2。至少在 Go 1.3 中。


并且对于CL 108985(2018 年 5 月,针对 Go 1.11),len([]rune(string))现在进行了优化。(修复问题 24923

编译器len([]rune(string))自动检测模式,并将其替换为 for r := range s 调用。

添加一个新的运行时函数来计算字符串中的符文。修改编译器以检测模式len([]rune(string)) 并将其替换为新的符文计数运行时函数。

RuneCount/lenruneslice/ASCII        27.8ns ± 2%  14.5ns ± 3%  -47.70%
RuneCount/lenruneslice/Japanese     126ns ± 2%   60  ns ± 2%  -52.03%
RuneCount/lenruneslice/MixedLength  104ns ± 2%   50  ns ± 1%  -51.71%

Stefan Steiger指向博文“ Go 中的文本规范化

什么是性格?

正如字符串博客文章中提到的,字符可以跨越多个符文
例如,一个 ' e' 和 '◌́◌́' (acute "\u0301") 可以组合成 'é'(e\u0301NFD 中的 " ")。这两个符文加在一起就是一个字符

字符的定义可能因应用而异。
对于标准化,我们将其定义为:

  • 以启动器开头的一系列符文,
  • 不修改或向后组合任何其他符文的符文,
  • 随后是可能为空的非首发序列,即能做的符文(通常是重音符号)。

规范化算法一次处理一个字符。

使用该包及其Iter类型,“字符”的实际数量将是:

package main
    
import "fmt"
import "golang.org/x/text/unicode/norm"
    
func main() {
    var ia norm.Iter
    ia.InitString(norm.NFKD, "école")
    nc := 0
    for !ia.Done() {
        nc = nc + 1
        ia.Next()
    }
    fmt.Printf("Number of chars: %d\n", nc)
}

在这里,这使用了Unicode 规范化形式NFKD “兼容性分解”


Oliver回答指出,UNICODE TEXT SEGMENTATION是可靠确定某些重要文本元素之间默认边界的唯一方法:用户感知的字符、单词和句子。

为此,您需要一个像rivo/uniseg这样的外部库,它可以执行Unicode Text Segmentation

这实际上将计算“字素”,其中多个代码点可以组合成一个用户感知的字符。

package uniseg
    
import (
    "fmt"
    
    "github.com/rivo/uniseg"
)
    
func main() {
    gr := uniseg.NewGraphemes("!")
    for gr.Next() {
        fmt.Printf("%x ", gr.Runes())
    }
    // Output: [1f44d 1f3fc] [21]
}

两个字素,即使有三个符文(Unicode 代码点)。

您可以在“如何在 GO 中操作字符串以反转它们? ”中查看其他示例。

‍ 单独是一个字素,但是,从unicode 到代码点转换器,有 4 个符文:

于 2012-10-01T07:06:46.567 回答
49

有一种方法可以通过将字符串转换为 []rune 来获取没有任何包的符文计数len([]rune(YOUR_STRING))

package main

import "fmt"

func main() {
    russian := "Спутник и погром"
    english := "Sputnik & pogrom"

    fmt.Println("count of bytes:",
        len(russian),
        len(english))

    fmt.Println("count of runes:",
        len([]rune(russian)),
        len([]rune(english)))

}

字节数 30 16

符文数 16 16

于 2016-04-03T16:54:07.620 回答
7

我应该指出,到目前为止提供的答案都没有像您期望的那样为您提供字符数,尤其是当您处理表情符号时(还有一些语言,如泰语、韩语或阿拉伯语)。VonC 的建议将输出以下内容:

fmt.Println(utf8.RuneCountInString("️‍")) // Outputs "6".
fmt.Println(len([]rune("️‍"))) // Outputs "6".

那是因为这些方法只计算 Unicode 代码点。有许多字符可以由多个代码点组成。

与使用标准化包相同:

var ia norm.Iter
ia.InitString(norm.NFKD, "️‍")
nc := 0
for !ia.Done() {
    nc = nc + 1
    ia.Next()
}
fmt.Println(nc) // Outputs "6".

规范化实际上与计数字符不同,许多字符不能规范化为一个代码点等价物。

masakielastic 的答案很接近,但只处理修饰符(彩虹标志包含一个修饰符,因此不计为它自己的代码点):

fmt.Println(GraphemeCountInString("️‍"))  // Outputs "5".
fmt.Println(GraphemeCountInString2("️‍")) // Outputs "5".

将 Unicode 字符串拆分为(用户感知的)字符(即字素簇)的正确方法在Unicode 标准附件 #29中定义。这些规则可以在第 3.1.1 节中找到。github.com/rivo/uniseg包实现了这些规则,因此您可以确定字符串中正确的字符数:

fmt.Println(uniseg.GraphemeClusterCount("️‍")) // Outputs "2".
于 2019-03-13T21:21:13.340 回答
6

有几种方法可以获取字符串长度:

package main

import (
    "bytes"
    "fmt"
    "strings"
    "unicode/utf8"
)

func main() {
    b := "这是个测试"
    len1 := len([]rune(b))
    len2 := bytes.Count([]byte(b), nil) -1
    len3 := strings.Count(b, "") - 1
    len4 := utf8.RuneCountInString(b)
    fmt.Println(len1)
    fmt.Println(len2)
    fmt.Println(len3)
    fmt.Println(len4)

}

于 2019-03-17T16:00:50.237 回答
5

很大程度上取决于您对“角色”的定义。如果“符文等于一个字符”对您的任务来说是可以的(通常不是),那么 VonC 的答案对您来说是完美的。否则,应该注意的是,在少数情况下,Unicode 字符串中的符文数量是一个有趣的值。即使在这些情况下,如果可能的话,最好在处理符文时在“遍历”字符串时推断计数,以避免将 UTF-8 解码工作加倍。

于 2012-10-01T07:49:51.423 回答
5

如果您需要考虑字素簇,请使用 regexp 或 unicode 模块。由于字素簇的长度是无限的,因此验证也需要计算代码点(符文)或字节的数量。如果要消除极长的序列,请检查序列是否符合流安全文本格式

package main

import (
    "regexp"
    "unicode"
    "strings"
)

func main() {

    str := "\u0308" + "a\u0308" + "o\u0308" + "u\u0308"
    str2 := "a" + strings.Repeat("\u0308", 1000)

    println(4 == GraphemeCountInString(str))
    println(4 == GraphemeCountInString2(str))

    println(1 == GraphemeCountInString(str2))
    println(1 == GraphemeCountInString2(str2))

    println(true == IsStreamSafeString(str))
    println(false == IsStreamSafeString(str2))
}


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

func GraphemeCountInString2(str string) int {

    length := 0
    checked := false
    index := 0

    for _, c := range str {

        if !unicode.Is(unicode.M, c) {
            length++

            if checked == false {
                checked = true
            }

        } else if checked == false {
            length++
        }

        index++
    }

    return length
}

func IsStreamSafeString(str string) bool {
    re := regexp.MustCompile("\\PM\\pM{30,}") 
    return !re.MatchString(str) 
}
于 2014-11-04T05:49:30.530 回答
0

我试图更快地进行标准化:

    en, _ = glyphSmart(data)

    func glyphSmart(text string) (int, int) {
        gc := 0
        dummy := 0
        for ind, _ := range text {
            gc++
            dummy = ind
        }
        dummy = 0
        return gc, dummy
    }
于 2019-02-12T15:37:42.680 回答