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


145

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

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


2
它确实返回5。当文件编码为UTF-8时可能不会。
Moshe Revah,2012年

7
是的,它适用于这种情况,但我想使其通用化,例如阿拉伯语等其他UTF-8字符,但不会转换为1个字节。
阿马尔(Ammar)2012年

Answers:


177

您可以RuneCountInString从utf8包中尝试。

返回p中的符文数

该脚本所示:“ World”的长度可能是6(当用中文写成“ World”时),但其符文数为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))自动检测模式,并将其替换为r:= range调用。

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

RuneCount/lenruneslice/ASCII                  27.8ns ± 2%  14.5ns ± 3%  -47.70%  (p=0.000 n=10+10)
RuneCount/lenruneslice/Japanese                126ns ± 2%    60ns ± 2%  -52.03%  (p=0.000 n=10+10)
RuneCount/lenruneslice/MixedLength             104ns ± 2%    50ns ± 1%  -51.71%  (p=0.000 n=10+9)

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

什么是角色?

正如字符串博客文章中提到的那样,字符可以跨越多个符文
例如,一个' e'和'◌́◌́'(急性“ \ u0301”)可以组合成一个'é'(e\u0301在NFD中为“ ”)。这两个符文合在一起是一个角色

字符的定义可能会因应用程序而异。
为了规范化,我们将其定义为:

  • 以起动器开始的一系列符文,
  • 不会修改或向后组合任何其他符文的符文,
  • 随后是可能为空的非启动程序序列,即具有此功能的符文(通常为重音符号)。

归一化算法一次处理一个字符。

使用该程序包及其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文本分段

将实际计数“ 字形 ”,其中多个码点可被组合成一个用户感知的字符。

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种符文:


4
你可以看到它在这个字符串逆转功能作用stackoverflow.com/a/1758098/6309
VonC

5
这只会告诉您符文数量,而不是字形数量。许多字形由多个符文组成。
史蒂芬·温伯格

5
实际上,您可以通过键入强制转换来对len()进行操作... len([] rune(“世界”))将打印2。在Go 1.3中,不知道已经持续了多长时间。
Phrozen 2014年

3
@VonC:实际上,一个字符(字形的口语术语)有时可能会跨越多个符文,因此,使用准确的技术术语WRONG就是这个答案。您需要的是Grapheme / GraphemeCluster计数,而不是符文计数。例如,“ e”和“◌”(急性“ \ u0301”)可以组合形成“é”(在NFD中为“ e \ u0301”)。但是人类会(正确地)认为é 作为一个字符。显然,它在泰卢固语中有所作为。但也可能是法语,具体取决于您使用的键盘/语言环境。 blog.golang.org/normalization
Stefan Steiger

1
@JustinJohnson同意。我已经编辑了答案,以更好地参考我先前推荐的Oliver。
VonC

43

有一种方法可以通过将字符串转换为[] 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


5

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


什么时候不会将符文视为角色?Go规范将符文定义为Unicode代码点:golang.org/ref/spec#Rune_literals
Thomas Kappler

另外,为避免加倍解码工作,我只需要执行[] rune(str)并对其进行处理,然后在完成后转换回字符串即可。我认为这比遍历字符串时跟踪代码点更容易。
Thomas Kappler

4
@ThomasKappler:什么时候?好吧,当符文不是角色时,通常不是。只有一些符文等于角色,而不是全部。假设“符文==字符”仅对Unicode字符的子集有效。例如:en.wikipedia.org/wiki/…–
zzzz

@ThomasKappler:但如果你看它这样,那么如Java的String.length()方法不返回字符数任。也不对可可的NSString-length方法。这些仅返回UTF-16实体的数量。但是很少使用真正的代码点数,因为它需要线性的时间来进行计数。
newacct 2012年

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) 
}

谢谢你 我尝试了您的代码,但不适用于以下几种表情符号字素:🖖🏿🇸🇴。关于如何准确计数的任何想法?
比约恩·罗氏

应将已编译的正则表达式提取为var函数外部。
支石墓

5

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

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)

}

3

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

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

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

与使用Normalization包相同:

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

规范化与计数字符实际上并不相同,并且许多字符无法规范化为一个代码点等效项。

masakielastic的答案很接近,但只能处理修饰符(rainbow标志包含修饰符,因此该修饰符不算作自己的代码点):

fmt.Println(GraphemeCountInString("🏳️‍🌈🇩🇪"))  // Outputs "5".
fmt.Println(GraphemeCountInString2("🏳️‍🌈🇩🇪")) // Outputs "5".

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

fmt.Println(uniseg.GraphemeClusterCount("🏳️‍🌈🇩🇪")) // Outputs "2".

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
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.