Как считать символы в строке Unicode в C - PullRequest
55 голосов
/ 04 сентября 2011

Допустим, у меня есть строка:

char theString[] = "你们好āa";

Учитывая, что моя кодировка - utf-8, эта строка имеет длину 12 байт (три символа ханци - три байта каждый, латинский символ с макроном - два байта, а 'a' - один байт:

strlen(theString) == 12

Как подсчитать количество символов? Как я могу сделать эквивалент подписки, чтобы:

theString[3] == "好"

Как мне нарезать и катать такие струны?

Ответы [ 10 ]

29 голосов
/ 04 сентября 2011

Вы учитываете только те символы, у которых два старших бита не установлены на 10 (т. Е. Все, что меньше 0x80 или больше 0xbf).

Это потому, что все символы сверхние два бита, установленные на 10, являются байтами продолжения UTF-8.

См. здесь для описания кодировки и того, как strlen может работать со строкой UTF-8.

Для нарезки и нарезания кубиками строк UTF-8 вы должны в основном следовать тем же правилам.Любой байт, начинающийся с бита 0 или последовательности 11, является началом кодовой точки UTF-8, все остальные являются символами продолжения.

Ваш лучший выбор, если вы не хотите использоватьсторонняя библиотека должна просто предоставлять функции в следующем формате:

utf8left (char *destbuff, char *srcbuff, size_t sz);
utf8mid  (char *destbuff, char *srcbuff, size_t pos, size_t sz);
utf8rest (char *destbuff, char *srcbuff, size_t pos;

для получения соответственно:

  • левого sz UTF-8 байтаstring.
  • sz UTF-8 байтов строки, начиная с pos.
  • остальные байты UTF-8 строки, начиная с pos.

Это будет достойный строительный блок, способный манипулировать струнами в достаточной степени для ваших целей.

17 голосов
/ 04 сентября 2011

Самый простой способ - использовать такую ​​библиотеку, как ICU

14 голосов
/ 04 сентября 2011

Попробуйте для размера:

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

// returns the number of utf8 code points in the buffer at s
size_t utf8len(char *s)
{
    size_t len = 0;
    for (; *s; ++s) if ((*s & 0xC0) != 0x80) ++len;
    return len;
}

// returns a pointer to the beginning of the pos'th utf8 codepoint
// in the buffer at s
char *utf8index(char *s, size_t pos)
{    
    ++pos;
    for (; *s; ++s) {
        if ((*s & 0xC0) != 0x80) --pos;
        if (pos == 0) return s;
    }
    return NULL;
}

// converts codepoint indexes start and end to byte offsets in the buffer at s
void utf8slice(char *s, ssize_t *start, ssize_t *end)
{
    char *p = utf8index(s, *start);
    *start = p ? p - s : -1;
    p = utf8index(s, *end);
    *end = p ? p - s : -1;
}

// appends the utf8 string at src to dest
char *utf8cat(char *dest, char *src)
{
    return strcat(dest, src);
}

// test program
int main(int argc, char **argv)
{
    // slurp all of stdin to p, with length len
    char *p = malloc(0);
    size_t len = 0;
    while (true) {
        p = realloc(p, len + 0x10000);
        ssize_t cnt = read(STDIN_FILENO, p + len, 0x10000);
        if (cnt == -1) {
            perror("read");
            abort();
        } else if (cnt == 0) {
            break;
        } else {
            len += cnt;
        }
    }

    // do some demo operations
    printf("utf8len=%zu\n", utf8len(p));
    ssize_t start = 2, end = 3;
    utf8slice(p, &start, &end);
    printf("utf8slice[2:3]=%.*s\n", end - start, p + start);
    start = 3; end = 4;
    utf8slice(p, &start, &end);
    printf("utf8slice[3:4]=%.*s\n", end - start, p + start);
    return 0;
}

Пример выполнения:

matt@stanley:~/Desktop$ echo -n 你们好āa | ./utf8ops 
utf8len=5
utf8slice[2:3]=好
utf8slice[3:4]=ā

Обратите внимание, что в вашем примере отключена одна ошибка.theString[2] == "好"

8 голосов
/ 04 сентября 2011

В зависимости от вашего представления о «персонаже», этот вопрос может быть более или менее запутанным.

Прежде всего, вы должны преобразовать вашу байтовую строку в строку кодовых точек Unicode. Вы можете сделать это с iconv() ICU, хотя, если это единственное, что вы делаете, iconv() намного проще, и это часть POSIX.

Ваша строка кодовых точек Юникода может быть чем-то вроде uint32_t[] с нулевым символом в конце или, если у вас есть C1x, массив char32_t. Размер этого массива (т. Е. Его количество элементов, а не его размер в байтах) равен числу кодовых точек (плюс терминатор), и это должно дать вам очень хорошее начало.

Однако понятие «печатный символ» довольно сложное, и вы можете предпочесть считать графемы , а не кодовые точки - например, можно выразить a с ударением ^ как две кодовые точки Юникода или как объединенная устаревшая кодовая точка â - обе действительны, и оба требуют, чтобы стандарт Юникода обрабатывался одинаково. Существует процесс, называемый «нормализация», который превращает вашу строку в определенную версию, но есть много графем, которые не могут быть выражены как одна кодовая точка, и в целом нет способа обойти правильную библиотеку, которая понимает это и считает графемы для вас .

Тем не менее, вам решать, насколько сложны ваши сценарии и насколько тщательно вы хотите их обработать. Преобразование в кодировку Unicode является обязательным, все, что находится за пределами вашего усмотрения.

Не стесняйтесь задавать вопросы об отделении интенсивной терапии, если вы решите, что он вам нужен, но не стесняйтесь сначала изучить гораздо более простой iconv().

2 голосов
/ 04 сентября 2011

В реальном мире theString[3]=foo; не является значимой операцией.Зачем вам когда-нибудь захотеть заменить символ в определенной позиции в строке другим символом?Конечно, нет задачи обработки текста на естественном языке, для которой эта операция имеет смысл.

Подсчет символов также вряд ли будет иметь смысл.Сколько символов (по вашему представлению о «персонаже») в «а»?Как насчет "а"?Теперь как насчет "གི"?Если вам нужна эта информация для реализации какого-либо редактирования текста, вам придется разобраться с этими сложными вопросами или просто использовать существующий набор инструментов библиотеки / графического интерфейса.Я бы порекомендовал последнее, если вы не являетесь экспертом в области мировых сценариев и языков и не думаете, что сможете добиться большего успеха.

Для всех других целей strlen сообщает вам именно ту информацию, которая действительно полезна: сколькоместо для хранения строки занимает.Это то, что нужно для объединения и разделения строк.Если все, что вы хотите сделать, это объединить строки или разделить их в определенном разделителе, snprintf (или strcat, если вы настаиваете ...) и strstr - это все, что вам нужно.

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

Опять же, большинство программ не имеют никакого применения для такого рода вещей и просто нужно собирать и анализировать текст безлюбые соображения к естественному языку.

1 голос
/ 04 сентября 2011

Из вышеприведенных ответов непонятно, почему это не так просто. Каждый символ закодирован тем или иным способом - например, необязательно должен быть UTF-8 - и каждый символ может иметь несколько кодировок с различными способами обработки сочетания ударений и т. Д. Правила действительно сложны, и варьируется в зависимости от кодировки (например, utf-8 против utf-16).

Этот вопрос имеет огромные проблемы с безопасностью, поэтому крайне важно, чтобы это было сделано правильно. Используйте предоставленную ОС библиотеку или известную стороннюю библиотеку для манипулирования строками Юникода; не катай свои собственные.

1 голос
/ 04 сентября 2011

Как правило, мы должны использовать другой тип данных для символов Юникода.

Например, вы можете использовать широкий тип данных char

wchar_t theString[] = L"你们好āa";

Обратите внимание на модификатор L, который сообщает, что строка состоит из широких символов.

Длина этой строки может быть рассчитана с помощью функции wcslen, которая ведет себя как strlen.

1 голос
/ 04 сентября 2011
while (s[i]) {
    if ((s[i] & 0xC0) != 0x80)
        j++;
    i++;
}
return (j);

Это будет считать символы в строке UTF-8 ... (Найдено в этой статье: Еще быстрее подсчет символов в UTF-8 )

Однако я все еще нахожусь в тупике нарезки и объединения?!?

0 голосов
/ 20 октября 2012

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

Итак, когда вы подсчитываете длину ИЛИ находите подстроку (безусловно, есть случаи нахождения подстрок - скажем, играете в игру палача), вам нужно продвигать слог по слогу, а не по коду за кодом. .

Таким образом, определение символа / слога и того, где вы фактически разбиваете строку на «кусочки слогов», зависит от природы языка, с которым вы имеете дело. Например, структура слогов во многих индийских языках (хинди, телугу, каннада, малаялам, непальский, тамильский, пенджабский и т. Д.) Может быть любой из следующих

V  (Vowel in their primary form appearing at the beginning of the word)
C (consonant)
C + V (consonant + vowel in their secondary form)
C + C + V
C + C + C + V

Вам необходимо проанализировать строку и найти приведенные выше шаблоны, чтобы разбить строку и найти подстроки.

Я не думаю, что возможно иметь метод общего назначения, который может волшебным образом разбивать строки описанным выше способом для любой строки Unicode (или последовательности кодовых точек) - поскольку шаблон, который работает для одного языка, может быть неприменим для другого письма;

Я предполагаю, что могут быть некоторые методы / библиотеки, которые могут принимать некоторые параметры определения / конфигурации в качестве входных данных для разбиения строк Юникода на такие слоги. Не уверен, хотя! Цените, если кто-то может поделиться тем, как они решили эту проблему, используя любые коммерчески доступные или открытые методы.

0 голосов
/ 06 сентября 2011

Я делал подобное внедрение много лет назад Но у меня нет кода со мной.

Для каждого символа Юникода первый байт описывает количество байтов, следующих за ним для построения символа Юникода. На основе первого байта вы можете определить длину каждого символа Юникода.

Я думаю, это хорошая библиотека UTF8. введите описание ссылки здесь

...