Bagaimana cara mendapatkan jumlah Karakter dalam String?

145

Bagaimana saya bisa mendapatkan jumlah karakter string di Go?

Sebagai contoh, jika saya memiliki string "hello"metode harus kembali 5. Saya melihat bahwa len(str)mengembalikan jumlah byte dan bukan jumlah karakter sehingga len("£")mengembalikan 2 bukannya 1 karena £ dikodekan dengan dua byte di UTF-8.

Ammar
sumber
2
Itu mengembalikan 5 . Mungkin tidak ketika file encoding adalah UTF-8.
Moshe Revah
7
Ya itu berlaku untuk kasus ini, tapi saya ingin membuatnya umum untuk karakter UTF-8 lainnya seperti Arab, yang tidak diterjemahkan menjadi 1 byte.
Ammar

Jawaban:

177

Anda dapat mencoba RuneCountInStringdari paket utf8.

mengembalikan jumlah rune dalam hal

bahwa, seperti yang diilustrasikan dalam skrip ini : panjang "Dunia" mungkin 6 (ketika ditulis dalam bahasa Cina: "世界"), tetapi jumlah rune-nya adalah 2:

package main

import "fmt"
import "unicode/utf8"

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

Phrozen menambahkan dalam komentar :

Sebenarnya Anda bisa melakukan len()lebih dari rune hanya dengan mengetik casting.
len([]rune("世界"))akan dicetak 2. Paling tidak aktif Go 1.3.


Dan dengan CL 108985 (Mei 2018, untuk Go 1.11), len([]rune(string))sekarang dioptimalkan. ( Masalah perbaikan 24923 )

Compiler mendeteksi len([]rune(string))pola secara otomatis, dan menggantinya dengan untuk r: = range s call.

Menambahkan fungsi runtime baru untuk menghitung rune dalam sebuah string. Memodifikasi kompiler untuk mendeteksi pola len([]rune(string)) dan menggantinya dengan fungsi runtime penghitungan rune baru.

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 menunjuk ke posting blog " Normalisasi teks di Go "

Apa itu karakter?

Seperti yang disebutkan dalam posting blog string , karakter dapat menjangkau beberapa rune .
Misalnya, ' e' dan '◌́◌́' (akut "\ u0301") dapat bergabung untuk membentuk 'é' (" e\u0301" di NFD). Bersama-sama kedua rune ini adalah satu karakter .

Definisi karakter dapat bervariasi tergantung pada aplikasi.
Untuk normalisasi, kami akan mendefinisikannya sebagai:

  • urutan rune yang dimulai dengan starter,
  • Rune yang tidak mengubah atau menggabungkan mundur dengan Rune lain,
  • diikuti oleh barisan non-starter yang mungkin kosong, yaitu rune yang melakukan (biasanya aksen).

Algoritma normalisasi memproses satu karakter pada saat bersamaan.

Menggunakan paket itu dan Itertipenya , jumlah sebenarnya "karakter" adalah:

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

Di sini, ini menggunakan bentuk Normalisasi Unicode NFKD "Dekomposisi Kompatibilitas"


Oliver 's jawaban poin untuk UNICODE TEXT SEGMENTASI sebagai satu-satunya cara untuk andal menentukan batas-batas standar di antara unsur-unsur tertentu yang signifikan teks: karakter yang dirasakan pengguna, kata, dan kalimat.

Untuk itu, Anda memerlukan perpustakaan eksternal seperti rivo / uniseg , yang melakukan Segmentasi Teks Unicode .

Itu benar-benar akan menghitung " grapheme cluster ", di mana beberapa titik kode dapat digabungkan menjadi satu karakter yang dirasakan pengguna.

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

Dua grafem, meskipun ada tiga rune (titik kode Unicode).

Anda dapat melihat contoh lain di " Cara memanipulasi string di GO untuk membalikkannya? "

👩🏾‍🦰 sendiri adalah satu grapheme, tetapi, dari unicode ke konverter poin poin , 4 rune:

VONC
sumber
4
Anda dapat melihatnya beraksi dalam fungsi pengembalian string ini di stackoverflow.com/a/1758098/6309
VonC
5
Ini hanya memberitahu Anda jumlah rune, bukan jumlah mesin terbang. Banyak mesin terbang dibuat dari banyak rune.
Stephen Weinberg
5
Sebenarnya Anda dapat melakukan len () lebih dari rune dengan hanya mengetik casting ... len ([] rune ("世界")) akan mencetak 2. Pada saat leats dalam Go 1.3, tidak tahu sudah berapa lama.
Phrozen
3
@VonC: Sebenarnya, karakter (istilah bahasa sehari-hari untuk Glyph) dapat - kadang-kadang - span beberapa rune, jadi jawaban ini adalah, untuk menggunakan istilah teknis yang tepat, SALAH. Yang Anda butuhkan adalah jumlah Grapheme / GraphemeCluster, bukan jumlah rune. Misalnya, 'e' dan '◌́' (akut "\ u0301") dapat bergabung untuk membentuk 'é' ("e \ u0301" di NFD). Tetapi manusia akan (dengan benar) menganggap & eacute; sebagai SATU karakter .. Rupanya itu membuat perbedaan dalam bahasa Telugu. Tetapi mungkin juga bahasa Prancis, tergantung pada keyboard / lokal yang Anda gunakan. blog.golang.org/normalisasi
Stefan Steiger
1
@JustinJohnson Setuju. Saya telah mengedit jawaban untuk referensi yang lebih baik dari Oliver, yang sebelumnya saya angkat suara.
VonC
43

Ada cara untuk mendapatkan hitungan rune tanpa paket apa pun dengan mengonversi string ke [] rune sebagai 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)))

}

hitungan byte 30 16

hitungan rune 16 16

Denis Kreshikhin
sumber
5

Tergantung pada definisi Anda tentang apa "karakter" itu. Jika "Rune sama dengan karakter" adalah OK untuk tugas Anda (umumnya tidak) maka jawaban oleh VonC sangat cocok untuk Anda. Kalau tidak, harus dicatat, bahwa ada beberapa situasi di mana jumlah rune dalam string Unicode adalah nilai yang menarik. Dan bahkan dalam situasi-situasi itu lebih baik, jika mungkin, untuk menyimpulkan jumlah sambil "melintasi" string saat rune diproses untuk menghindari penggandaan upaya decode UTF-8.

zzzz
sumber
Kapan Anda tidak melihat rune sebagai karakter? Go spec mendefinisikan rune sebagai Unicode codepoint: golang.org/ref/spec#Rune_literals .
Thomas Kappler
Juga, untuk menghindari menggandakan upaya decode, saya hanya melakukan [] rune (str), mengerjakannya, lalu mengonversi kembali ke string ketika saya selesai. Saya pikir itu lebih mudah daripada melacak poin kode saat melintasi string.
Thomas Kappler
4
@ThomasKappler: Kapan? Nah, ketika Rune bukan karakter, yang umumnya tidak. Hanya beberapa rune yang sama dengan karakter, tidak semuanya. Dengan asumsi "rune == karakter" hanya berlaku untuk subset karakter Unicode saja. Contoh: en.wikipedia.org/wiki/…
zzzz
@ThomasKappler: tetapi jika Anda melihatnya seperti itu, maka mis String. .length()Metode Java tidak mengembalikan jumlah karakter juga. Juga tidak Kakao NSString's -lengthmetode. Mereka hanya mengembalikan jumlah entitas UTF-16. Tetapi jumlah sebenarnya dari titik-titik codep jarang digunakan, karena butuh waktu linier untuk menghitungnya.
berita baru
5

Jika Anda perlu mempertimbangkan cluster grapheme, gunakan modul regexp atau unicode. Menghitung jumlah titik kode (rune) atau byte juga diperlukan untuk validaiton karena panjang grapheme cluster tidak terbatas. Jika Anda ingin menghilangkan urutan yang sangat panjang, periksa apakah urutannya sesuai dengan format teks stream-safe .

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) 
}
masakielastik
sumber
Terima kasih untuk ini. Saya mencoba kode Anda dan itu tidak berfungsi untuk beberapa emoji grapheme seperti ini: 🖖🏿🇸🇴. Adakah pemikiran tentang cara menghitungnya secara akurat?
Bjorn Roche
Regexp yang dikompilasi harus diekstraksi sebagai di varluar fungsi.
dolmen
5

Ada beberapa cara untuk mendapatkan panjang tali:

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)

}
anak babi
sumber
3

Saya harus menunjukkan bahwa tidak ada jawaban yang diberikan sejauh ini memberi Anda jumlah karakter seperti yang Anda harapkan, terutama ketika Anda berurusan dengan emoji (tetapi juga beberapa bahasa seperti Thailand, Korea, atau Arab). Saran VonC akan menampilkan yang berikut:

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

Itu karena metode ini hanya menghitung poin kode Unicode. Ada banyak karakter yang dapat terdiri dari beberapa titik kode.

Sama untuk menggunakan paket Normalisasi :

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

Normalisasi tidak benar-benar sama dengan menghitung karakter dan banyak karakter tidak dapat dinormalisasi menjadi setara dengan satu kode-poin.

Jawaban masakielastic mendekati tetapi hanya menangani pengubah (bendera pelangi mengandung pengubah yang dengan demikian tidak dihitung sebagai titik kode sendiri):

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

Cara yang benar untuk membagi string Unicode menjadi karakter (yang dirasakan pengguna), yaitu cluster grapheme, didefinisikan dalam Unicode Standard Annex # 29 . Aturan dapat ditemukan di Bagian 3.1.1 . The github.com/rivo/uniseg paket alat aturan ini sehingga Anda dapat menentukan jumlah yang benar dari karakter dalam string:

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

Saya mencoba melakukan normalisasi sedikit lebih cepat:

    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
sumber