Jak uzyskać liczbę znaków w ciągu?

145

Jak uzyskać liczbę znaków ciągu w Go?

Na przykład, jeśli mam ciąg, "hello"metoda powinna zwrócić 5. Widziałem, że len(str)zwraca liczbę bajtów, a nie liczbę znaków, więc len("£")zwraca 2 zamiast 1, ponieważ £ jest kodowane za pomocą dwóch bajtów w UTF-8.

Ammar
źródło
2
Zwraca 5 . Może tak nie jest, gdy kodowanie pliku to UTF-8.
Moshe Revah
7
Tak, w tym przypadku, ale chcę uczynić go ogólnym dla innych znaków UTF-8, takich jak arabski, który nie jest tłumaczony na 1 bajt.
Ammar

Odpowiedzi:

177

Możesz spróbować RuneCountInStringz pakietu utf8.

zwraca liczbę run w p

że, jak zilustrowano w tym skrypcie : długość „Świata” może wynosić 6 (po chińsku: „世界”), ale liczba jego run to 2:

package main

import "fmt"
import "unicode/utf8"

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

Phrozen dodaje w komentarzach :

Właściwie możesz zrobić len()nad runami, po prostu rzucając typ.
len([]rune("世界"))wydrukuje 2. Co najmniej w Go 1.3.


A z CL 108985 (maj 2018, dla Go 1.11), len([]rune(string))jest teraz zoptymalizowany. (Rozwiązuje problem 24923 )

Kompilator len([]rune(string))automatycznie wykrywa wzorzec i zastępuje go wywołaniem for r: = range s.

Dodaje nową funkcję wykonawczą do liczenia run w ciągu. Modyfikuje kompilator, aby wykrywał wzorzec len([]rune(string)) i zastępuje go nową funkcją zliczania run.

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 wskazuje na post na blogu „ Normalizacja tekstu w Go

Co to jest postać?

Jak wspomniano w poście na blogu dotyczącym ciągów znaków , znaki mogą obejmować wiele run .
Na przykład ' e' i '◌́◌́' (ostry "\ u0301") mogą łączyć się tworząc 'é' (" e\u0301" w NFD). Te dwie runy razem stanowią jedną postać .

Definicja postaci może się różnić w zależności od aplikacji.
Dla normalizacji zdefiniujemy to jako:

  • sekwencja run rozpoczynająca się od startera,
  • runa, która nie modyfikuje ani nie łączy się wstecz z żadną inną runą,
  • po którym może następować prawdopodobnie pusta sekwencja nieuruchomionych znaków, czyli run, które to robią (zazwyczaj akcenty).

Algorytm normalizacji przetwarza jednocześnie jeden znak.

Używając tego pakietu i jego Itertypu , rzeczywista liczba „znaków” byłaby następująca:

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

Tutaj używa się formularza normalizacji Unicode NFKD „Dekompozycja zgodności”


Oliver „s odpowiedź wskazuje na UNICODE TEKSTU SEGMENTACJI jako jedyny sposób, aby niezawodnie wyznaczania granic pomiędzy niektórymi domyślne istotnych elementów tekstowych: odczuwanego przez użytkownika znaków, wyrazów i zdań.

W tym celu potrzebujesz zewnętrznej biblioteki, takiej jak rivo / uniseg , która obsługuje segmentację tekstu Unicode .

W rzeczywistości liczy się „ klaster grafemów ”, w którym wiele punktów kodowych można połączyć w jeden znak postrzegany przez użytkownika.

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

Dwa grafemy, mimo że są trzy runy (punkty kodowe Unicode).

Możesz zobaczyć inne przykłady w „ Jak manipulować ciągami znaków w GO, aby je odwrócić?

👩🏾‍🦰 sam to jeden grafem, ale od konwertera Unicode do punktów kodowych 4 runy:

VonC
źródło
4
Możesz to zobaczyć w akcji w funkcji odwracania ciągów na stackoverflow.com/a/1758098/6309
VonC
5
Informuje tylko o liczbie run, a nie o liczbie glifów. Wiele glifów składa się z wielu run.
Stephen Weinberg
5
Właściwie możesz wykonać len () na runach, po prostu używając casting ... len ([] rune ("世界")) wypisze 2. Co najmniej w Go 1.3, nie wiem, ile to trwało.
Phrozen
3
@VonC: Właściwie postać (termin w języku potocznym na Glyph) może - czasami - obejmować kilka run, więc ta odpowiedź brzmi, używając precyzyjnego terminu technicznego, ZŁA. To, czego potrzebujesz, to liczba Grapheme / GraphemeCluster, a nie liczba run. Na przykład „e” i „◌́” (ostry „\ u0301”) mogą łączyć się w „é” („e \ u0301” w NFD). Ale człowiek (poprawnie) uważałby & eacute; jako JEDEN znak ... Najwyraźniej robi to różnicę w telugu. Ale prawdopodobnie także francuski, w zależności od używanej klawiatury / ustawień regionalnych. blog.golang.org/normalization
Stefan Steiger
1
@JustinJohnson Zgoda. Zredagowałem odpowiedź, aby lepiej odnosić się do Olivera, na którą wcześniej głosowałem.
VonC
43

Istnieje sposób, aby uzyskać liczbę run bez żadnych pakietów, konwertując ciąg na [] run jako 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)))

}

liczba bajtów 30 16

liczba run 16 16

Denis Kreshikhin
źródło
5

Wiele zależy od twojej definicji tego, czym jest „postać”. Jeśli „runa równa się postaci” jest w porządku dla twojego zadania (generalnie tak nie jest), to odpowiedź VonC jest dla ciebie idealna. W przeciwnym razie należy prawdopodobnie zauważyć, że jest kilka sytuacji, w których liczba run w łańcuchu Unicode jest interesującą wartością. I nawet w takich sytuacjach lepiej, jeśli to możliwe, wywnioskować liczbę podczas „przechodzenia” przez ciąg, gdy runy są przetwarzane, aby uniknąć podwojenia wysiłku dekodowania UTF-8.

zzzz
źródło
Kiedy byś nie zobaczyć runę jako znak? Specyfikacja Go definiuje runę jako punkt kodowy Unicode: golang.org/ref/spec#Rune_literals .
Thomas Kappler
Ponadto, aby uniknąć podwojenia wysiłku dekodowania, po prostu wykonuję runę [] (str), pracuję nad tym, a po zakończeniu konwertuję z powrotem na ciąg. Myślę, że jest to łatwiejsze niż śledzenie punktów kodowych podczas przechodzenia przez ciąg.
Thomas Kappler
4
@ThomasKappler: Kiedy? Cóż, kiedy runa nie jest postacią, a generalnie nią nie jest. Tylko niektóre runy są równe postaciom, nie wszystkie. Zakładając, że „znak rune ==” jest poprawny tylko dla podzbioru znaków Unicode. Przykład: en.wikipedia.org/wiki/…
zzzz
@ThomasKappler: ale jeśli spojrzeć na to w ten sposób, a następnie np Java String„s .length()metoda nie zwraca liczbę znaków albo. Ani nie Cocoa NSStringjest -lengthmetoda. Te po prostu zwracają liczbę jednostek UTF-16. Jednak prawdziwa liczba punktów kodowych jest rzadko używana, ponieważ jej policzenie wymaga czasu liniowego.
newacct
5

Jeśli musisz wziąć pod uwagę klastry grafemów, użyj modułu regexp lub unicode. Zliczanie liczby punktów kodowych (run) lub bajtów jest również potrzebne do walidacji, ponieważ długość klastra grafemowego jest nieograniczona. Jeśli chcesz wyeliminować bardzo długie sekwencje, sprawdź, czy sekwencje są zgodne z formatem tekstu bezpiecznym dla strumienia .

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) 
}
masakielastic
źródło
Dzięki za to. Wypróbowałem Twój kod i nie działa w przypadku kilku grafemów emoji, takich jak te: 🖖🏿🇸🇴. Jakieś przemyślenia, jak dokładnie je policzyć?
Bjorn Roche
Skompilowane wyrażenie regularne powinno zostać wyodrębnione varpoza funkcjami.
dolmen
5

Istnieje kilka sposobów uzyskania długości łańcucha:

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)

}
prosiaczek
źródło
3

Powinienem zwrócić uwagę, że żadna z dotychczas udzielonych odpowiedzi nie daje takiej liczby znaków, jakiej można się spodziewać, zwłaszcza gdy masz do czynienia z emoji (ale także z niektórymi językami, takimi jak tajski, koreański czy arabski). Sugestie VonC spowodują, że:

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

Dzieje się tak, ponieważ te metody liczą tylko punkty kodowe Unicode. Istnieje wiele znaków, które mogą składać się z wielu punktów kodowych.

To samo dotyczy korzystania z pakietu normalizacyjnego :

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

Normalizacja to tak naprawdę nie to samo, co liczenie znaków, a wielu znaków nie można znormalizować do odpowiednika jednego punktu kodowego.

odpowiedź masakielastic jest bliska, ale obsługuje tylko modyfikatory (flaga tęczowa zawiera modyfikator, który w związku z tym nie jest liczony jako własny punkt kodowy):

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

Prawidłowy sposób dzielenia łańcuchów Unicode na (postrzegane przez użytkownika) znaki, tj. Klastry grafemów, jest zdefiniowany w Załączniku nr 29 do Standardu Unicode . Zasady można znaleźć w sekcji 3.1.1 . W github.com/rivo/uniseg wdraża pakiet tych przepisów, tak można określić prawidłową liczbę znaków w ciągu:

fmt.Println(uniseg.GraphemeClusterCount("🏳️‍🌈🇩🇪")) // Outputs "2".
Oliver
źródło
0

Próbowałem zrobić normalizację trochę szybciej:

    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
    }
Marcelloh
źródło