Как узнать количество символов в строке?

145

Как я могу получить количество символов строки в Go?

Например, если у меня есть строка, "hello"метод должен вернуться 5. Я видел, что len(str)возвращает количество байтов, а не количество символов, поэтому len("£")возвращает 2 вместо 1, потому что в кодировке UTF-8 кодируется двумя байтами.

Аммар
источник
2
Это возвращает 5 . Может быть, это не так, когда кодировка файла UTF-8.
Моше Рева
7
Да, для этого случая, но я хочу сделать его общим для других символов UTF-8, таких как арабский, который не переводится в 1 байт.
Аммар

Ответы:

177

Вы можете попробовать RuneCountInStringиз пакета utf8.

возвращает количество рун в р

что, как показано в этом сценарии : длина «Мира» может быть 6 (если написано по-китайски: «世界»), но число рун равно 2:

package main

import "fmt"
import "unicode/utf8"

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

Фрозен добавляет в комментарии :

На самом деле вы можете делать len()руны, просто набирая тип.
len([]rune("世界"))напечатает 2. По латы в Go 1.3.


И с CL 108985 (май 2018, для Go 1.11), len([]rune(string))теперь оптимизирован. ( Исправляет проблему 24923 )

Компилятор len([]rune(string))автоматически обнаруживает шаблон и заменяет его на вызов r: = range s.

Добавляет новую функцию времени выполнения для подсчета рун в строке. Модифицирует компилятор для обнаружения шаблона 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)

Стефан Штайгер указывает на сообщение в блоге « Нормализация текста в 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 «s ответ указывает на UNICODE TEXT СЕГМЕНТАЦИИ как единственный способ надежно определить границы по умолчанию между некоторыми значительными элементами текста: пользовательские воспринимаемых символов, слов и фраз.

Для этого вам нужна внешняя библиотека, такая как 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, чтобы обратить их вспять? "

👩🏾‍🦰 только одна графема, но, от юникода до конвертора кодовых точек , 4 руны:

VonC
источник
4
Вы можете увидеть это в действии в этой функции возврата строк по адресу stackoverflow.com/a/1758098/6309
VonC
5
Это говорит только о количестве рун, а не о количестве глифов. Многие глифы состоят из нескольких рун.
Стивен Вайнберг
5
На самом деле, вы можете выполнить len () над рунами, просто набрав приведение ... len ([] rune ("世界")) выведет 2. При игре в Go 1.3 не знаю, сколько времени прошло.
Phrozen
3
@VonC: На самом деле, символ (разговорный языковой термин для Glyph) может - иногда - охватывать несколько рун, поэтому этот ответ, если использовать точный технический термин, НЕПРАВИЛЬНЫЙ. Что вам нужно, так это количество Grapheme / GraphemeCluster, а не число рун. Например, «e» и «◌́» (острый «\ u0301») могут объединиться в «é» («e \ u0301» в NFD). Но человек будет (правильно) рассматривать & eacute; как ОДИН персонаж .. Видимо, это имеет значение в телугу. Но, вероятно, также французский, в зависимости от используемой клавиатуры / локали. blog.golang.org/normalization
Стефан Штайгер
1
@JustinJohnson Согласен. Я отредактировал ответ, чтобы лучше ссылаться на мнение Оливера, за которое ранее проголосовал.
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.

ZZZZ
источник
Когда бы вы не увидели руну как персонажа? Спецификация Go определяет руну как кодовую точку Unicode: golang.org/ref/spec#Rune_literals .
Томас Капплер
Кроме того, чтобы избежать удвоения усилий по декодированию, я просто делаю руну [] (str), работаю над этим, а затем преобразую обратно в строку, когда я закончу. Я думаю, что это проще, чем отслеживать кодовые точки при обходе строки.
Томас Капплер
4
@ThomasKappler: когда? Хорошо, когда руна не персонаж, который обычно не является. Только некоторые руны равны персонажам, но не все. Предполагая, что «rune == символ» действителен только для подмножества символов Юникода. Пример: en.wikipedia.org/wiki/...
ZZZZ
@ThomasKappler: но если вы посмотрите на это таким образом, то , например , в Java String«s .length()метод не возвращает количество символов , либо. Как NSStringи -lengthметод Какао . Они просто возвращают количество объектов UTF-16. Но истинное количество кодовых точек используется редко, потому что для его подсчета требуется линейное время.
newacct
5

Если вам необходимо учитывать кластеры графем, используйте модуль регулярных выражений или кодировку Юникод. Подсчет количества кодовых точек (рун) или байтов также необходим для проверки, так как длина кластера графем не ограничена. Если вы хотите исключить очень длинные последовательности, проверьте, соответствуют ли последовательности потокобезопасному текстовому формату .

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
источник
Спасибо за это. Я попробовал ваш код, и он не работает для нескольких графов эмодзи, подобных этим: 🖖🏿🇸🇴. Есть мысли о том, как их точно посчитать?
Бьорн Роше
Скомпилированное регулярное выражение должно быть извлечено как 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)

}
pigletfly
источник
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 № 29 . Правила можно найти в разделе 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
    }
Marcelloh
источник