Codifica binaria di caratteri
Un computer sa trattare soltanto numeri, e più precisamente sequenze di bit. Per memorizzare un testo, quindi, bisogna trasformare ogni carattere in un numero: è ciò che si chiama codifica dei caratteri.
Come per i numeri interi (complemento a 2) e per i numeri reali (virgola mobile), anche qui vale lo stesso principio: i bit non hanno un significato di per sé. La sequenza 01000001 può essere il numero intero 65, oppure la lettera A: dipende solo dalla convenzione con cui la si interpreta. Una codifica dei caratteri è proprio una convenzione di questo tipo, cioè una tabella condivisa che associa a ogni carattere un numero.
Codifica ASCII
ASCII (American Standard Code for Information Interchange) è una delle prime codifiche di larga diffusione, nata negli anni Sessanta. Usa 7 bit per carattere, quindi può rappresentare 2⁷ = 128 simboli, numerati da 0 a 127. In memoria ogni carattere occupa un byte (8 bit), con il bit più significativo posto a 0.
I 128 codici si dividono in due gruppi:
- codici da 0 a 31 e il codice 127: caratteri di controllo, che non si vedono sullo schermo ma danno istruzioni (per esempio il codice 10 indica “vai a capo”, il codice 9 è la tabulazione, il codice 0 è il carattere nullo);
- codici da 32 a 126: caratteri stampabili, cioè lo spazio, le cifre, le lettere dell’alfabeto inglese (maiuscole e minuscole) e i segni di punteggiatura e di uso comune.
Un esempio: la parola “Ciao”
| Carattere | Codice decimale | Codice esadecimale | Codice binario (8 bit) |
|---|---|---|---|
C |
67 | 43 |
01000011 |
i |
105 | 69 |
01101001 |
a |
97 | 61 |
01100001 |
o |
111 | 6F |
01101111 |
Nella memoria di un computer la parola “Ciao” è quindi la sequenza di byte 43 69 61 6F.
Una tabella ben organizzata
I codici non sono assegnati a caso. Osservando la tabella si scoprono delle regolarità molto utili:
| Gruppo | Primo e ultimo carattere | Codici (decimale) | Codici (esadecimale) |
|---|---|---|---|
| Cifre | 0 … 9 |
48 … 57 | 30 … 39 |
| Lettere maiuscole | A … Z |
65 … 90 | 41 … 5A |
| Lettere minuscole | a … z |
97 … 122 | 61 … 7A |
Da qui discendono alcune proprietà:
- Le cifre sono consecutive. Il codice del carattere
7è 55, cioè 48 + 7. Per ottenere il valore numerico di una cifra basta quindi sottrarre 48 al suo codice. Si noti anche che i 4 bit meno significativi del codice esadecimale (37) sono proprio il valore della cifra (7). - Le lettere sono in ordine alfabetico. Questo permette di ordinare le parole confrontando i codici dei loro caratteri.
- Maiuscole e minuscole differiscono di 32. Il carattere
Aè01000001e laaè01100001: cambia un solo bit (quello di valore 32). Per passare da maiuscola a minuscola basta quindi “accendere” quel bit.
Il limite dell’ASCII
L’ASCII è nato per l’inglese, e non contiene le lettere accentate (à, è, é, ì, ò, ù), né i caratteri di altri alfabeti (greco, cirillico, arabo…) o di scritture come il cinese e il giapponese. Con soli 128 codici non c’è spazio per tutto questo, e serviva una soluzione più ampia.
Qui puoi trovare la tabella ASCII completa.
Unicode
Prima di Unicode: le estensioni a 8 bit
Usando anche l’ottavo bit di ogni byte, i simboli disponibili diventano 256: i primi 128 restano quelli dell’ASCII, mentre gli altri 128 possono essere usati per lettere accentate e altri caratteri. Ne nacquero molte versioni, ciascuna pensata per un gruppo di lingue (per l’Europa occidentale, per esempio, ISO 8859-1 e Windows-1252).
Il problema è che le versioni erano incompatibili tra loro: lo stesso byte poteva significare lettere diverse a seconda della tabella usata. Un testo aperto con la tabella sbagliata mostra simboli senza senso. Un caso tipico che si incontra ancora oggi è “perché” che diventa “perché”: è l’effetto di un testo scritto con una codifica e letto con un’altra.
Unicode: un unico catalogo di caratteri
Unicode è nato per risolvere il problema alla radice: un unico catalogo universale, che contiene i caratteri di tutte le lingue del mondo, i simboli matematici, le emoji e molto altro. Ogni carattere riceve un numero, detto code point, che si scrive in esadecimale preceduto da U+:
| Carattere | Code point |
|---|---|
A |
U+0041 |
è |
U+00E8 |
€ |
U+20AC |
| 😀 | U+1F600 |
I code point vanno da U+0000 a U+10FFFF, quindi più di un milione di posizioni possibili. I primi 128 coincidono con l’ASCII: A è 65 (0x41) sia in ASCII sia in Unicode.
Unicode non è una codifica in bit. Il code point è un numero astratto: dice quale carattere è, ma non dice come scriverlo in memoria. Serve una seconda regola, ed è qui che entrano in gioco le codifiche, di cui la più diffusa è UTF-8.
Unicode e UTF-8: due livelli diversi
Unicode decide quale numero corrisponde a ogni carattere. UTF-8 decide quali byte usare per scrivere quel numero.
Un paragone con quanto già studiato può aiutare: il numero 232 è sempre lo stesso numero, ma può essere scritto in binario, in esadecimale (E8) e così via. Allo stesso modo il code point U+00E8 è sempre la lettera è, ma può essere scritto in byte in modi diversi. Le codifiche più usate sono UTF-8, UTF-16 e UTF-32:
| Codifica | Byte per è (U+00E8) |
Byte per € (U+20AC) |
|---|---|---|
| UTF-8 | C3 A8 |
E2 82 AC |
| UTF-16 | 00 E8 |
20 AC |
| UTF-32 | 00 00 00 E8 |
00 00 20 AC |
| ISO 8859-1 | E8 |
non rappresentabile |
L’ultima riga mostra il limite delle vecchie codifiche: la ISO 8859-1 non ha spazio per il simbolo dell’euro. Unicode ha separato il catalogo (i code point) dalla codifica (i byte), e questo gli permette di avere più modi di scrivere gli stessi caratteri.
Come funziona UTF-8
UTF-8 è la codifica più usata al mondo (è quella della maggior parte delle pagine web). Ha lunghezza variabile: ogni carattere occupa da 1 a 4 byte, a seconda del suo code point. I caratteri più comuni occupano meno spazio.
Il primo byte dichiara quanti byte compongono il carattere, e i bit rimanenti contengono il code point. Lo schema è il seguente (le x sono i posti in cui vanno scritti i bit del code point):
| Intervallo di code point | Numero di byte | Schema dei byte | Bit disponibili per il code point |
|---|---|---|---|
U+0000 – U+007F |
1 | 0xxxxxxx |
7 |
U+0080 – U+07FF |
2 | 110xxxxx 10xxxxxx |
11 |
U+0800 – U+FFFF |
3 | 1110xxxx 10xxxxxx 10xxxxxx |
16 |
U+10000 – U+10FFFF |
4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
21 |
Esistono due tipi di byte:
- byte iniziale: il suo prefisso indica la lunghezza. Se inizia con
0il carattere occupa 1 byte; con110ne occupa 2; con1110ne occupa 3; con11110ne occupa 4 (in pratica, il numero di1iniziali è il numero di byte); - byte di continuazione: inizia sempre con
10.
Come si codifica un carattere
- Si scrive il code point in binario.
- Si guarda in quale riga della tabella ricade, per sapere quanti byte servono.
- Si aggiungono zeri a sinistra fino a riempire tutti i bit disponibili.
- Si distribuiscono i bit sulle
x, da sinistra a destra, e si aggiungono i prefissi.
Esempio 1: A (U+0041), 1 byte.
0x41 in binario è 1000001, che sta in 7 bit. Con lo schema 0xxxxxxx si ottiene 01000001, cioè 41. È identico al codice ASCII.
Esempio 2: è (U+00E8), 2 byte.
0xE8 in binario è 11101000: non entra nei 7 bit di un solo byte, quindi servono 2 byte (11 bit disponibili). Si completa con zeri a sinistra: 00011101000, e si divide in due gruppi, di 5 e di 6 bit: 00011 e 101000.
- primo byte:
110+00011=11000011=C3 - secondo byte:
10+101000=10101000=A8
Il risultato è C3 A8.
Esempio 3: € (U+20AC), 3 byte.
0x20AC in binario è 0010 0000 1010 1100 (16 bit). Si divide in gruppi di 4, 6 e 6 bit: 0010, 000010, 101100.
- primo byte:
1110+0010=11100010=E2 - secondo byte:
10+000010=10000010=82 - terzo byte:
10+101100=10101100=AC
Il risultato è E2 82 AC.
Esempio 4: 😀 (U+1F600), 4 byte.
0x1F600 in binario è 11111011000000000 (17 bit). Si completa a 21 bit: 000011111011000000000, e si divide in gruppi di 3, 6, 6 e 6 bit: 000, 011111, 011000, 000000.
- primo byte:
11110+000=11110000=F0 - secondo byte:
10+011111=10011111=9F - terzo byte:
10+011000=10011000=98 - quarto byte:
10+000000=10000000=80
Il risultato è F0 9F 98 80.
Come si decodifica. Si legge il primo byte e se ne guarda il prefisso, che indica quanti byte compongono il carattere. Poi si tolgono i prefissi e si uniscono i bit rimasti, ottenendo il code point. Per esempio, E2 82 AC diventa 0010, 000010, 101100, cioè 0010000010101100 = 0x20AC, quindi €.
Perché UTF-8 si è affermato
Lo schema può sembrare complicato, ma ognuna delle sue caratteristiche ha una ragione precisa:
- È compatibile con l’ASCII. I caratteri ASCII occupano un solo byte, identico a quello ASCII. Quindi ogni testo ASCII è già un testo UTF-8 valido, senza bisogno di conversioni. Questo ha reso molto facile la transizione.
- Non è ambiguo. I byte di un carattere multibyte hanno sempre il bit più significativo a 1, perciò non possono essere confusi con un carattere ASCII.
- Resiste agli errori. I byte di continuazione iniziano sempre con
10, mentre i byte iniziali no. Se si finisce a metà di un carattere (o un byte si danneggia) basta scorrere fino al primo byte che non inizia per10, per capire dove comincia il carattere successivo. Un errore rovina al più un carattere, non tutto il testo. - È economico per le lingue europee. Le lettere più frequenti occupano 1 byte, e le lettere accentate ne occupano 2.
- Rispetta l’ordine. Confrontare due testi UTF-8 byte per byte dà lo stesso risultato che si avrebbe confrontando i loro code point.
Un’ultima regola: ogni carattere va scritto con il minor numero di byte possibile. La lettera A potrebbe in teoria essere scritta in 2 byte (C1 81), ma è vietato: ammettere più modi di scrivere lo stesso carattere ha causato in passato vulnerabilità di sicurezza.
Da ricordare
- Una codifica è una convenzione: associa numeri ai caratteri.
- ASCII usa 7 bit e rappresenta 128 simboli, ma non contiene le lettere accentate.
- Unicode è il catalogo universale dei caratteri: assegna a ciascuno un code point (
U+XXXX). - UTF-8 è la codifica che trasforma i code point in byte, usando da 1 a 4 byte per carattere.
- Unicode e UTF-8 non sono la stessa cosa: il primo dice quale carattere, il secondo dice come scriverlo in memoria.