web-dev-qa-db-fra.com

Comment compter les caractères d'une chaîne Unicode en C

Disons que j'ai une chaîne:

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

Étant donné que mon encodage est utf-8, cette chaîne est de 12 octets (les trois caractères hanzi font trois octets chacun, le caractère latin avec le macron est de deux octets, et le 'a' est d'un octet:

strlen(theString) == 12

Comment puis-je compter le nombre de caractères? Comment puis-je faire l'équivalent de la souscription pour que:

theString[3] == "好"

Comment puis-je couper et chatter de telles cordes?

56
jsj

Vous ne comptez que les caractères dont les deux bits supérieurs ne sont pas définis sur 10 (c'est-à-dire tout ce qui n'est pas 0x80 ou supérieur à 0xbf).

En effet, tous les caractères dont les deux bits supérieurs sont définis sur 10 sont des octets de continuation UTF-8.

Voir ici pour une description de l'encodage et comment strlen peut fonctionner sur une chaîne UTF-8.

Pour trancher et découper des chaînes UTF-8, vous devez essentiellement suivre les mêmes règles. Tout octet commençant par 0 bit ou un 11 séquence est le début d'un point de code UTF-8, tous les autres sont des caractères de continuation.

Votre meilleur pari, si vous ne souhaitez pas utiliser une bibliothèque tierce, est de simplement fournir des fonctions allant dans le sens de:

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;

pour obtenir, respectivement:

  • les sz octets UTF-8 de gauche d'une chaîne.
  • les sz octets UTF-8 d'une chaîne, commençant à pos.
  • le reste des octets UTF-8 d'une chaîne, commençant à pos.

Ce sera un bloc de construction décent pour pouvoir manipuler les cordes suffisamment pour vos besoins.

29
paxdiablo

La façon la plus simple est d'utiliser une bibliothèque comme ICU

17
Mark

Essayez ceci pour la taille:

#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;
}

Exemple d'exécution:

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

Notez que votre exemple a une erreur de désactivation d'une. theString[2] == "好"

14
Matt Joiner

Selon votre notion de "personnage", cette question peut s'impliquer plus ou moins.

Tout d'abord, vous devez transformer votre chaîne d'octets en une chaîne de points de code unicode. Vous pouvez le faire avec iconv() d'ICU, bien que si c'est la seule chose que vous faites, iconv() est beaucoup plus facile, et cela fait partie de POSIX.

Votre chaîne de points de code unicode pourrait être quelque chose comme un uint32_t[] Terminé par null, ou si vous avez C1x, un tableau de char32_t. La taille de ce tableau (c'est-à-dire son nombre d'éléments, pas sa taille en octets) est le nombre de points de code (plus le terminateur), et cela devrait vous donner un très bon début.

Cependant, la notion de "caractère imprimable" est assez complexe, et vous préférerez peut-être compter graphèmes plutôt que les points de code - par exemple, un a avec un accent ^ peut être exprimé sous la forme de deux points de code unicode ou sous la forme d'un point de code hérité combiné â - les deux sont valides et les deux sont requis par la norme unicode pour être traités de manière égale. Il existe un processus appelé "normalisation" qui transforme votre chaîne en une version définitive, mais il existe de nombreux graphèmes qui ne sont pas exprimables comme un seul codet, et en général il n'y a aucun moyen de contourner une bibliothèque appropriée qui comprend cela et compte les graphèmes pour vous. .

Cela dit, c'est à vous de décider à quel point vos scripts sont complexes et à quel point vous voulez les traiter. La transformation en points de code unicode est un must, tout ce qui est au-delà est à votre discrétion.

N'hésitez pas à poser des questions sur ICU si vous décidez que vous en avez besoin, mais n'hésitez pas à explorer d'abord la iconv() beaucoup plus simple.

8
Kerrek SB

Dans le monde réel, theString[3]=foo; n'est pas une opération significative. Pourquoi voudriez-vous jamais remplacer un caractère à une position particulière de la chaîne par un caractère différent? Il n'y a certainement aucune tâche de traitement de texte en langage naturel pour laquelle cette opération est significative.

Il est également peu probable que le comptage des caractères soit significatif. Combien de caractères (pour votre idée de "personnage") y a-t-il dans "á"? Et "á"? Et maintenant, "གི"? Si vous avez besoin de ces informations pour implémenter une sorte de modification de texte, vous devrez traiter ces questions difficiles, ou simplement utiliser une boîte à outils bibliothèque/interface graphique existante. Je recommanderais ce dernier sauf si vous êtes un expert des scripts et des langages du monde et pensez que vous pouvez faire mieux.

À toutes autres fins, strlen vous indique exactement l'élément d'information réellement utile: l'espace de stockage qu'une chaîne prend. C'est ce qui est nécessaire pour combiner et séparer des chaînes. Si tout ce que vous voulez faire est de combiner des chaînes ou de les séparer à un délimiteur particulier, snprintf (ou strcat si vous insistez ...) et strstr sont tout ce dont vous avez besoin.

Si vous souhaitez effectuer des opérations de texte en langage naturel de niveau supérieur, comme la capitalisation, la rupture de ligne, etc. ou même des opérations de niveau supérieur comme la pluralisation, les changements de temps, etc., vous aurez besoin d'une bibliothèque comme ICU ou quelque chose respectivement de niveau supérieur et de capacité linguistique (et spécifique à la ou aux langues avec lesquelles vous travaillez).

Encore une fois, la plupart des programmes n'ont aucune utilité pour ce genre de choses et ont juste besoin d'assembler et d'analyser du texte sans aucune considération pour le langage naturel.

2
R..
while (s[i]) {
    if ((s[i] & 0xC0) != 0x80)
        j++;
    i++;
}
return (j);

Cela comptera les caractères dans une chaîne UTF-8 ... (Trouvé dans cet article: Comptage de caractères UTF-8 encore plus rapide )

Cependant, je suis toujours perplexe sur le tranchage et la concaténation?!?

1
jsj

En général, nous devons utiliser un type de données différent pour les caractères unicode.

Par exemple, vous pouvez utiliser le type de données Wide Char

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

Notez le modificateur L qui indique que la chaîne est composée de caractères larges.

La longueur de cette chaîne peut être calculée à l'aide de la fonction wcslen, qui se comporte comme strlen.

1
abahgat

Une chose qui ne ressort pas clairement des réponses ci-dessus est pourquoi ce n'est pas simple. Chaque caractère est codé d'une manière ou d'une autre - il ne doit pas être UTF-8, par exemple - et chaque caractère peut avoir plusieurs codages, avec différentes manières de gérer la combinaison d'accents, etc. Les règles sont vraiment compliquées, et varient en fonction de l'encodage (par exemple, utf-8 vs utf-16).

Cette question a d'énormes problèmes de sécurité, il est donc impératif que cela soit fait correctement. Utilisez une bibliothèque fournie par le système d'exploitation ou une bibliothèque tierce bien connue pour manipuler les chaînes unicode; ne roulez pas le vôtre.

1
Steve Dispensa

J'ai fait des implémentations similaires il y a des années. Mais je n'ai pas de code avec moi.

Pour chaque caractère unicode, le premier octet décrit le nombre d'octets qui le suivent pour construire un caractère unicode. Sur la base du premier octet, vous pouvez déterminer la longueur de chaque caractère unicode.

Je pense que c'est une bonne bibliothèque UTF8. entrez la description du lien ici

0
Senthil