Ferramentas do usuário

Ferramentas do site


prog2:codificacao_de_caracteres

Codificação de caracteres

Internamente, um computador só armazena e processa bytes, números inteiros entre 0 e 255. Não é possível armazenar diretamente textos, imagens, sons ou qualquer outra informação que não sejam bytes.

Para armazenar informações mais complexas que os bytes, é necessário codificar as mesmas, ou seja, transformá-las em sequências de bytes. Esta página discute as técnicas usadas para transformar as letras e símbolos de um texto em bytes, para poder armazená-lo e tratá-lo em um computador.

A figura a seguir mostra as etapas do tratamento de uma letra A pelo computador: o hardware do teclado é responsável por converter a letra digitada em byte(s) na memória do computador; em seguida, o hardware do terminal converte esses bytes em uma representação gráfica na tela.

Codificação de caracteres

Algumas definições

  • Caractere: é um símbolo da linguagem (letra, dígito ou sinal). Exemplos: A i ç ë ¥ β ɣ 诶 😃 ✂
  • Conjunto de caracteres (charset): é o conjunto de todos os caracteres suportados por um sistema ou por um padrão de codificação. Exemplo: A-Z, a-z, 0-9, ! @ # $ % * ( ) ~ _ - + = { } [ ] | \ / < . ,
  • Codificação (encoding): é a tradução entre os caracteres e seus respectivos valores numéricos em bytes. Exemplo, A → 65.

Existem diversas codificações de caracteres; a seguir serão apresentadas as mais usuais.

A codificação ASCII

A codificação de caracteres mais antiga ainda em amplo uso é a ASCII (American Standard Code for Information Interchange), criada nos anos 1960 a partir de códigos de telegrafia. Sua última atualização ocorreu em 1986, mesmo assim é considerada uma codificação universal.

Praticamente TODOS os sistemas computacionais suportam ASCII !

A codificação ASCII abrange o conjunto de caracteres da língua inglesa, sinais gráficos e alguns caracteres de controle (nova linha, tabulação, etc), num total de 128 caracteres. Cada caractere é codificado em um byte, mas ocupa somente 7 bits; o oitavo bit de cada byte era antigamente usado para verificação de paridade.

A codificação ASCII é definida através da famosa Tabela ASCII, que é dividida em duas partes:

  • 0 - 31 e 127: caracteres de controle (newline, form feed, tab, etc), que dependem do terminal utilizado.
  • 32 - 126: caracteres imprimíveis (A, B, C, …), independentes de terminal.

A tabela ASCII

A codificação ASCII ainda é amplamente usada para codificação de textos puros em inglês, como códigos-fonte de programas, páginas HTML, arquivos de configuração, etc.

Code pages

A codificação ASCII não suporta caracteres acentuados (á é ñ ë) ou caracteres específicos de outras línguas, como ç ¥ β ɣ 诶 etc. Pode-se usar o oitavo bit de cada byte para associar caracteres aos valores acima de 127. Isso levou à criação de diversas tabelas ASCII estendidas para definir os símbolos de 128 a 255. Cada codificação é denominada uma code page; algumas das mais conhecidas são:

  • CP-437: (code page 437), codificação usada nos primeiros PCs, com caracteres acentuados e gráficos simples (▒ ╝ ╦ ┼).
  • Windows-1252: codificação usada em sistemas Windows mais antigos; é parte de um conjunto de codificações para diversas linguagens chamado Windows code pages.
  • KOI8-R: cirílico russo (Код Обмена Информацией, 8 бит).
  • BraSCII: português brasileiro, usada nos anos 1980-90.
  • ISO-8859 : codificações da ISO para diversas línguas.

Codificações ISO 8859

Nos anos 1980, para para tentar organizar a profusão de codepages ASCII estendidas, a ISO propôs o padrão ISO-8859, que define codificações ASCII estendidas para diversas linguagens, como por exemplo:

  • ISO-8859-1: Europa ocidental (francês, espanhol, italiano, alemão, etc)
  • ISO-8859-2: Europa central (Bósnio, Polonês, Croata, etc)
  • ISO-8859-6: árabe simplificado
  • ISO-8859-7: grego
  • ISO-8859-15: revisão do ISO-8859-1, contendo o € e outros símbolos

As codificações ISO-8859-* se tornaram um padrão mundial e ainda são amplamente usadas em muitos sistemas, sendo gradualmente substituída pela codificação Unicode em sistemas mais recentes. Elas são compatíveis com a codificação ASCII, pois representam cada caractere com somente um byte e respeitam as definições ASCII dos caracteres de 0 a 127.

Caracteres multibyte

O maior problema das codificações ISO-8859 é o uso de um único byte por caractere, o que limita cada code page a 256 caracteres. Essa limitação impede a representação completa de línguas asiáticas e do árabe, por exemplo.

Para representar conjuntos com mais de 256 caracteres é necessário usar caracteres multibyte, ou seja, com mais de um byte. Por exemplo, se usarmos 2 bytes por caractere é possível representar até 216 = 65.536 caracteres distintos na mesma tabela, sem precisar trocar de code page.

Vários padrões de codificação multibyte foram propostos, como ISO-2022-CJK (Chinese/Japanese/Korean), Shift-JIS (Windows, japonês), GB 18030 (padrão oficial chinês), Big5 e Unicode. Alguns destes padrões definem todos os caracteres com uma quantidade fixa de bits (16 ou 32), enquanto outros definem caracteres com 8, 16 e 32 bits.

Unicode

O padrão Unicode define um imenso conjunto de caracteres e os modos de codificação dos mesmos. Atualmente, existem cerca de 140.000 caracteres definidos em Unicode, para todas as línguas conhecidas (inclusive Klingon), além de símbolos e emojis. Eles ocupam pouco mais de 10% da capacidade total desse padrão.

Em Unicode, cada caractere possui um código numérico único, chamado code point, que pode ser representado de diversas formas. Por exemplo, o code point do emoji 😜 vale 128540 e pode ser representado como:

  • U+1f61c : em hexadecimal
  • &#x1f61c; ou &#128540; : em páginas Web (decimal ou hexadecimal)
  • \u1f61c : em algumas linguagens de programação

Caracteres em Unicode podem ser codificados (representados em bytes) de diversas formas:

  • UTF-8: 8-bit Unicode Transformation Format, usa de 1 a 4 bytes por caractere. É usado no Linux, Windows 10 e outros sistemas recentes.
  • UTF-16: usa 2 ou 4 bytes por caractere; muito usado nas APIs dos sistemas Windows, em Java, Python e PHP.
  • UTF-32: usa sempre 4 bytes por caractere. É pouco usado na prática.

A codificação UTF-8

UTF-8 é certamente a codificação multibyte mais utilizada hoje em dia, por ser plenamente compatível com a codificação ASCII e por ser econômica em espaço.

Em UTF-8, cada caractere Unicode é codificado usando de 1 a 4 bytes. O número de bytes usado para codificar um caractere é definido conforme o número de bits necessários para representar o código numérico do caractere desejado:

# de bits do caractere formato codificado bytes Uso
até 7 bits 0xxx-xxxx 1 tabela ASCII
8-11 bits 110x-xxxx 10xx-xxxx 2 caracteres estendidos
12-16 bits 1110-xxxx 10xx-xxxx 10xx-xxxx 3 caracteres estendidos
17-21 bits 1111-0xxx 10xx-xxxx 10xx-xxxx 10xx-xxxx 4 caracteres estendidos

Pode-se observar que bytes os bytes iniciando em 0… sempre representam caracteres ASCII. Então, um texto codificado em UTF-8 contendo somente caracteres com códigos entre 0 e 127 equivale a um texto codificado em ASCII padrão.

Além disso, todos os bytes iniciando em 10… são bytes de continuação da codificação de um caractere multibyte. Isso significa que é fácil localizar o início de cada caractere no texto, mesmo na presença de erros.

Dica: pode-se visualizar o conteúdo de um arquivo em hexadecimal ou binário usando o comando xxd.

O mecanismo de codificação de code points Unicode em UTF-8 funciona da seguinte forma:

  1. Dado um caractere, verifica-se quantos bits são necessários para armazenar seu código em UTF-8. Por exemplo, o caractere 😀 (code point U+1f600) precisa de 17 bits: 1F600 → 1 1111 0110 0000 0000.
  2. Para 17 bits é necessário codificar usando 4 bytes (faixa 17-21 bits)
  3. Distribui-se os bits do código numérico do caractere nos espaços disponíveis:
    Code point (hex)                     1    f    6    0    0
    Code point (bin)                     0001 1111 0110 0000 0000
    Encoding format       1111-0xxx 10xx-xxxx 10xx-xxxx 10xx-xxxx
    Code point (bin)            000   01 1111   01 1000   00 0000
    Encoded character     1111 0000 1001 1111 1001 1000 1000 0000
                          f    0    9    f    9    8    8    0
  4. Com isso, a codificação de U+1f600 em UTF-8 resulta nos 4 bytes f0 9f 98 80.
  5. A decodificação (de UTF-8 para o code point) se efetua fazendo o caminho inverso.

Alguns arquivos codificados em UTF-* podem apresentar em seus dois primeiros bytes um valor chamado BOM (Byte Order Mark), que define em que ordem os bytes de cada caractere devem ser considerados: big endian ou little endian. o campo BOM não é necessário em UTF-8, mas pode estar presente às vezes:

Bytes Encoding Form
00 00 FE FF UTF-32, big-endian
FF FE 00 00 UTF-32, little-endian
FE FF UTF-16, big-endian
FF FE UTF-16, little-endian
EF BB BF UTF-8

Dica: no Linux, pode-se digitar caracteres Unicode usando as seguintes teclas: ctrl + shift + u, código hexadecimal, enter

Comparando as codificações

O quadro a seguir compara a representação da string “equação” usando algumas das codificações estudadas. No caso da codificação ASCII, considera-se a letra sem acento ou cedilha; a representação UTF-16 usa dois bytes de cabeçalho BOM (Byte Order Mark).

Codificação e q u a ç ã o
ASCII 65 71 75 61 63 61 6f
ISO-8859-1 65 71 75 61 e7 e3 6f
UTF-8 65 71 75 61 c3 a7 c3 a3 6f
UTF-16 ff fe 65 00 71 00 75 00 61 00 e7 00 e3 00 6f 00

Conversão de codificações

O comando file do UNIX informa o tipo de codificação usado em um arquivo de texto:

~$ file exemplo.*
exemplo.c:    C source, ISO-8859 text
exemplo.html: HTML document, ASCII text
exemplo.txt:  UTF-8 Unicode text

A conversão de codificação de um arquivo de texto pode ser feita com:

  • utilitários específicos, como o iconv no Linux:
    iconv -f ISO-8859-15 -t UTF-8 < input.txt > output.txt
  • Alguns editores de texto permitem escolher a codificação ao salvar o arquivo. Por exemplo, no VI:
    :set fileencoding=utf8
    :w myfilename

Locale

O termo “locale” designa um conjunto de parâmetros que definem as preferências de linguagem de um sistema de computação, como a língua usada, a codificação de caracteres e formatos de informações usuais (números, data/hora, moeda, etc).

No sistema Linux, por exemplo, o comando locale informa os parâmetros em uso. No exemplo abaixo usa como língua o Português brasileiro e como codificação padrão o UTF-8:

~$ locale
LANG=pt_BR.utf8
LANGUAGE=pt_BR.utf8
LC_CTYPE="pt_BR.utf8"
LC_NUMERIC="pt_BR.utf8"
LC_TIME="pt_BR.utf8"
...

O parâmetro mais importante para um programa em C é LC_CTYPE (character type), pois ele afeta o comportamento de funções como printf e scanf.

Um programa em C pode consultar ou modificar os parâmetros de locale do SO através da função setlocale():

locale.c
#include <stdio.h>
#include <locale.h>
 
int main()
{
  char *locale ;
 
  // obtém o valor de LC_CTYPE do sistema
  locale = setlocale (LC_CTYPE, "") ;
 
  // imprime valor de locale obtido
  if (locale)
    printf ("Current locale is %s\n", locale) ;
  else
    fprintf(stderr, "Can't set the specified locale\n") ;
}

Caracteres e strings em C

A linguagem C manipula caracteres codificados em ASCII sem dificuldade, usando variáveis do tipo char. Em ASCII, strings são meros vetores de caracteres terminados com um caractere nulo (\0).

Como as codificações ISO-8859-* usam apenas um byte por caractere, programas em C podem manipular caracteres em ISO sem dificuldade, usando variáveis do tipo unsigned char. Entretanto, deve-se definir o locale do programa para garantir o funcionamento correto de funções como toupper, isalpha, etc. com os caracteres estendidos:

  char *locale ;
 
  locale = setlocale (LC_CTYPE, "pt_BR.ISO-8859-1") ;

Obviamente, o locale ISO-8859-1 deve estar disponível no sistema operacional (essa informação pode ser consultada com o comando locale -a).

As coisas mudam para as codificações multibyte. O tipo char ocupa exatamente 1 byte de memória, o que é insuficiente para armazenar caracteres em codificações multibyte. Por isso, alguns cuidados devem ser tomados ao definir e usar strings em UTF-8, por exemplo:

  • As funções de entrada/saída formatadas, como printf, scanf suas variantes, suportam UTF-8 sem modificações. Basta lembrar, ao alocar memória para as strings, que alguns caracteres podem ocupar mais de um byte.
  • O índice não corresponde mais necessariamente à posição de cada caractere na string. Por exemplo, nome[3] não corresponde necessariamente ao quarto caractere da string nome, caso ela esteja codificada em UTF-8.
  • A função strlen sempre informa o número de bytes da string; para obter o número de caracteres, deve-se usar a função mbstowcs (multi-byte-string-to-wide-character-string), que retorna o número de caracteres da string.

Como regra geral, deve-se sempre consultar o manual para verificar se a função desejada funciona com strings multibyte.

Wide chars

O padrão C 90 introduziu o conceito de caracteres largos, ou seja, com mais de um byte. Ao contrário dos caracteres multibyte, os caracteres largos têm sempre o mesmo tamanho, geralmente 2 ou 4 bytes (depende da plataforma). Em Linux, um caractere largo ocupa sempre 4 bytes e pode representar qualquer code point do padrão Unicode.

Caracteres largos e strings largas são definidos pelo tipo wchar_t:

#include <stdio.h>
#include <wchar.h>
 
int main ()
{
  wchar_t c ;         // um caractere largo
  wchar_t *s ;        // ponteiro para uma string larga
 
  c = L'a' ;          // caractere constante largo
  s = L"equação" ;    // string constante larga
 
  // escrita de caracteres e strings largas
  printf ("Caractere %lc e string %ls\n", c, s) ;
 
  // número de caracteres de uma string larga
  printf ("A string tem %ld caracteres\n", wcslen (s)) ;
}

Várias funções são definidas pelo padrão POSIX para manipular caracteres e strings largas. Elas geralmente estão presentes na LibC.

Algumas diferenças entre strings largas e strings multibyte UTF-8:

  • Uma string larga é terminada pelo caractere largo nulo L'\0', enquanto string comuns e UTF-8 são terminadas por um caractere nulo com um byte '\0'.
  • Em uma string larga, o número de campos equivale ao número de caracteres, por isso s[10] sempre é o 11º caractere da string, independente do conteúdo, o que não ocorre em UTF-8.
  • Uma string larga ocupa mais memória que uma string multibyte, pois todos os seus caracteres ocupam o mesmo número de bytes independente de seu code point.

Caracteres largos são empregados na implementação de aplicações que manipulam muitas strings, como editores de texto. O ambiente Python usa caracteres largos para armazenar strings.

O código abaixo exemplifica compara algumas operações usando strings largas e strings UTF-8. Ele gera diversos avisos (warnings) ao compilar, devidos às chamadas de funções inadequadas:

wide-utf8.c
#include <stdio.h>
#include <locale.h>
#include <string.h>
#include <stdlib.h>
#include <wchar.h>
 
int main()
{
  int i ;
  char *locale ;
  char    *frase1 =  "Olá ɣ 诶 😃" ;
  wchar_t *frase2 = L"Olá ɣ 诶 😃" ;
 
  locale = setlocale (LC_CTYPE, "") ;
  if (!locale)
  {
    fprintf(stderr, "Can't set the specified locale! "
            "Check LANG, LC_CTYPE, LC_ALL.\n");
    return (1);
  }
  else
    printf ("Current locale is %s\n", locale) ;
 
  printf ("Frase 1          : %s\n",  frase1) ;
  printf ("Frase 2          : %ls\n", frase2) ;
 
  printf ("sizeof (char)    : %ld\n", sizeof(char)) ;
  printf ("sizeof (wchar_t) : %ld\n", sizeof(wchar_t)) ;
 
  printf ("strlen (frase1)  : %ld\n", strlen(frase1)) ;
  printf ("strlen (frase2)  : %ld\n", strlen(frase2)) ;  // incorreto
 
  printf ("wcslen (frase1)  : %ld\n", wcslen(frase1)) ;  // incorreto
  printf ("wcslen (frase2)  : %ld\n", wcslen(frase2)) ;
 
  printf ("mbstowcs (frase1): %ld\n", mbstowcs(NULL, frase1, 0)) ;
  printf ("mbstowcs (frase2): %ld\n", mbstowcs(NULL, frase2, 0)) ; // incorreto
 
  // percurso por índice
  printf ("Frase1: ") ;
  for (i=0; i<strlen(frase1); i++)
    printf ("[%c] ", frase1[i]) ;
  printf ("\n") ;
 
  // percurso por índice
  printf ("Frase2: ") ;
  for (i=0; i<wcslen(frase2); i++)
    printf ("[%lc] ", frase2[i]) ;
  printf ("\n") ;
 
  return 0;
}

Ao executar, este programa gera:

Current locale is pt_BR.UTF-8

Frase 1          : Olá ɣ 诶 😃
Frase 2          : Olá ɣ 诶 😃

sizeof (char)    : 1
sizeof (wchar_t) : 4

strlen (frase1)  : 16
strlen (frase2)  : 1     // incorreto

wcslen (frase1)  : 4     // incorreto
wcslen (frase2)  : 9

mbstowcs (frase1): 9
mbstowcs (frase2): 1     // incorreto

Frase1: [O] [l] [�] [�] [ ] [�] [�] [ ] [�] [�] [�] [ ] [�] [�] [�] [�] 
Frase2: [O] [l] [á] [ ] [ɣ] [ ] [诶] [ ] [😃] 

Mais informações

Exercícios

  1. Use o programas file e iconv para fazer as seguintes conversões:
    1. o arquivo exemplo.c para UTF-8
    2. o arquivo exemplo.c para ASCII
  2. escreva um programa em C para converter um texto em ISO-8859-1 para ASCII, substituindo as letras acentuadas e cedilha por seus equivalentes sem acento.
  3. escreva um programa em C para converter um texto em ISO-8859-1 para UTF-8.
  4. escreva uma função char* utf8strn(char* s, int n) que devolva um ponteiro para a posição do n-ésimo caractere da string s, que está codificada em UTF-8.
  5. escreva um programa C que imprima as tabelas ASCII e ISO-8859-1.
prog2/codificacao_de_caracteres.txt · Última modificação: 2020/03/12 16:07 por maziero