วิธีรับจำนวนอักขระในสตริง


145

ฉันจะรับจำนวนอักขระของสตริงใน Go ได้อย่างไร?

ตัวอย่างเช่นถ้าฉันมีสตริงวิธีการที่ควรจะกลับ"hello" 5ฉันเห็นว่าlen(str)คืนค่าจำนวนไบต์ไม่ใช่จำนวนตัวอักษรดังนั้นlen("£")ส่งคืน 2 แทน 1 เนื่องจาก£ถูกเข้ารหัสด้วยสองไบต์ใน UTF-8


2
มันไม่กลับ 5 อาจจะไม่เมื่อการเข้ารหัสไฟล์เป็น UTF-8
Moshe Revah

7
ใช่มันใช้กับกรณีนี้ แต่ฉันต้องการทำให้เป็นเรื่องปกติสำหรับตัวอักษร UTF-8 อื่น ๆ เช่นอารบิกซึ่งไม่ได้แปลเป็น 1 ไบต์
Ammar

คำตอบ:


177

คุณสามารถลอง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ที่ leats ใน Go 1.3


และด้วยCL 108985 (พฤษภาคม 2018 สำหรับ Go 1.11) ได้len([]rune(string))รับการปรับปรุง (แก้ไขปัญหา 24923 )

คอมไพเลอร์ตรวจพบlen([]rune(string))รูปแบบโดยอัตโนมัติและแทนที่ด้วยสำหรับการเรียก r: = range

เพิ่มฟังก์ชั่นรันไทม์ใหม่เพื่อนับอักษรรูนในสตริง แก้ไขคอมไพเลอร์เพื่อตรวจหารูปแบบlen([]rune(string)) และแทนที่ด้วยฟังก์ชันรันไทม์นับใหม่ของ rune

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) รูทั้งสองนี้เข้าด้วยกันเป็นตัวละครเดียว

คำจำกัดความของตัวละครอาจแตกต่างกันไปขึ้นอยู่กับแอพพลิเคชั่น
สำหรับการทำให้เป็นมาตรฐานเราจะนิยามมันเป็น:

  • ลำดับของอักษรรูนที่ขึ้นต้นด้วย Starter
  • rune ที่ไม่ได้ปรับเปลี่ยนหรือรวมย้อนหลังกับ rune อื่น ๆ
  • ตามด้วยอาจเป็นลำดับว่างเปล่าของ non-starters นั่นคือ runes ที่ทำ (โดยทั่วไปจะเน้นเสียง)

อัลกอริทึมการทำให้เป็นมาตรฐานจะประมวลผลอักขระหนึ่งตัวในเวลาเดียวกัน

เมื่อใช้แพ็คเกจและ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 Normalization NFKD "Decomposition ความเข้ากันได้"


คำตอบของOliverชี้ไปที่การแบ่งส่วนข้อความ UNICODEเป็นวิธีเดียวที่จะกำหนดขอบเขตเริ่มต้นที่เชื่อถือได้ระหว่างองค์ประกอบข้อความที่สำคัญบางอย่าง: อักขระที่ผู้ใช้รับรู้คำและประโยค

เพื่อที่คุณจะต้องมีห้องสมุดภายนอกเช่นRivo / unisegซึ่งจะแบ่งส่วน Unicode Text

ซึ่งจะนับเป็น " grapheme cluster " โดยที่หลาย ๆ รหัสคะแนนอาจรวมกันเป็นอักขระที่ผู้ใช้รับรู้

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 เพื่อย้อนกลับได้อย่างไร "

👩🏾‍🦰 เพียงอย่างเดียวคือหนึ่งกราฟ แต่จากยูนิโคดถึงโค้ดตัวแปลงคะแนนรูน 4:


4
คุณสามารถเห็นมันทำงานในฟังก์ชั่นการพลิกกลับสตริงนี้ได้ที่stackoverflow.com/a/1758098/6309
VonC

5
สิ่งนี้บอกเพียงจำนวนรูนไม่ใช่จำนวนร่ายมนตร์ ร่ายมนตร์จำนวนมากทำจากอักษรรูนหลายอัน
Stephen Weinberg

5
ในความเป็นจริงคุณสามารถทำ len () เหนืออักษรรูนโดยเพียงพิมพ์ตัวอักษร ... len ([] rune ("世界")) จะพิมพ์ 2 ที่ leats ใน Go 1.3 ไม่นานเท่าไหร่แล้ว
Phrozen

3
@VonC: ที่จริงแล้วตัวละคร (คำศัพท์ภาษาพูดสำหรับร่ายมนตร์) สามารถ - บางครั้ง - ขยายรูนหลาย ๆ ดังนั้นคำตอบนี้คือการใช้คำศัพท์ทางเทคนิคที่แม่นยำผิด สิ่งที่คุณต้องการคือจำนวน Grapheme / GraphemeCluster ไม่ใช่จำนวน Rune ตัวอย่างเช่น 'e' และ '◌́' (เฉียบพลัน "\ u0301") สามารถรวมกันเป็นรูปแบบ 'é' ("e \ u0301" ใน NFD) แต่มนุษย์จะเคารพ (อย่างถูกต้อง) & eacute; ในฐานะตัวละครตัวหนึ่ง .. เห็นได้ชัดว่ามันสร้างความแตกต่างในเตลูกู แต่อาจเป็นภาษาฝรั่งเศสก็ได้ขึ้นอยู่กับคีย์บอร์ด / สถานที่ที่คุณใช้ blog.golang.org/normalization
Stefan Steiger

1
@JustinJohnson เห็นด้วย ฉันได้แก้ไขคำตอบเพื่ออ้างอิงที่ดีกว่าของ Oliver ว่าฉันเคย upvoted
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

ขึ้นอยู่กับคำจำกัดความของคุณว่า "ตัวละคร" คืออะไร หาก "rune เท่ากับตัวอักษร" ก็โอเคสำหรับงานของคุณ (โดยทั่วไปไม่ใช่) คำตอบของ VonC นั้นเหมาะสำหรับคุณ ไม่เช่นนั้นควรสังเกตว่ามีบางสถานการณ์ที่จำนวนอักษรรูนในสตริง Unicode เป็นค่าที่น่าสนใจ และแม้ในสถานการณ์เหล่านั้นมันจะดีกว่าถ้าเป็นไปได้สรุปจำนวนในขณะที่ "traversing" สตริงขณะที่รูนถูกประมวลผลเพื่อหลีกเลี่ยงการเพิ่มความพยายามถอดรหัส UTF-8 เป็นสองเท่า


เมื่อไหร่ที่คุณไม่เห็นตัวละครในคาถา? The Go ข้อมูลจำเพาะกำหนดคาถาเป็นจุดโค้ด Unicode: golang.org/ref/spec#Rune_literals
โทมัส Kappler

นอกจากนี้เพื่อหลีกเลี่ยงการเพิ่มความพยายามในการถอดรหัสฉันเพียงแค่ทำ [] rune (str) แล้วทำมันแล้วแปลงกลับเป็นสตริงเมื่อฉันเสร็จแล้ว ฉันคิดว่าง่ายกว่าการติดตามจุดโค้ดเมื่อเข้าไปในสตริง
โทมัส Kappler

4
@ThomasKappler: เมื่อไหร่ ดีเมื่อคาถาไม่ได้เป็นตัวละครซึ่งโดยทั่วไปจะไม่ มีเพียงอักษรรูนบางตัวเท่านั้นที่เท่ากับอักขระไม่ใช่ทั้งหมด สมมติว่า "rune == ตัวอักษร" ถูกต้องสำหรับชุดย่อยของตัวอักษร Unicode เท่านั้น ตัวอย่าง: en.wikipedia.org/wiki/…
zzzz

@ThomasKappler แต่ถ้าคุณมองไปที่มันเป็นอย่างนั้นแล้วเช่นของ Java String's .length()วิธีไม่กลับจำนวนตัวอักษรอย่างใดอย่างหนึ่ง ไม่ไม่โกโก้NSString's -lengthวิธี เพียงแค่คืนค่าจำนวน UTF-16 แต่จำนวน codepoints ที่แท้จริงนั้นไม่ค่อยได้ใช้เพราะต้องใช้เวลาเชิงเส้นในการนับ
newacct

5

หากคุณจำเป็นต้องคำนึงถึงกลุ่มของกราฟให้ใช้ regexp หรือ unicode module การนับจำนวนจุดโค้ด (รูน) หรือไบต์ก็เป็นสิ่งจำเป็นสำหรับ validaiton เนื่องจากความยาวของกลุ่มกราฟจะไม่ จำกัด หากคุณต้องการกำจัดลำดับที่ยาวมาก ๆ ให้ตรวจสอบว่าลำดับนั้นสอดคล้องกับรูปแบบข้อความที่ปลอดภัยหรือไม่

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

ขอบคุณสำหรับสิ่งนี้. ฉันลองใช้รหัสของคุณแล้วมันใช้ไม่ได้กับกราฟิคอิโมจิบางอย่างเช่น: 🖖🏿🇸🇴 ความคิดใด ๆ เกี่ยวกับวิธีการนับอย่างถูกต้อง?
Bjorn Roche

regexp รวบรวมควรจะแยกเป็นvarฟังก์ชั่นนอก
dolmen

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 เท่านั้น มีอักขระจำนวนมากซึ่งสามารถประกอบด้วยจุดรหัสหลายจุด

เหมือนกับการใช้แพ็คเกจการทำให้เป็นมาตรฐาน :

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 ภาคผนวก # กฎระเบียบที่สามารถพบได้ในมาตรา 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
    }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.