Unicode e il problema della codifica

Unicode e il problema della codifica

Introduzione

Versione inglese

Nel corso di troppi anni, ho spesso dovuto lottare con la rappresentazione elettronica del testo. Ogni volta in cui sono stato confrontato con quello che chiamo il problema fondamentale della rappresentazione del testo (vedi oltre) ho finito per risolverlo più o meno in maniera ad hoc, istintiva, insomma a naso. Solo poco tempo fa mi sono costretto a cercare di fare un po' di chiarezza nella pletora di nozioni "pratiche" che ho nel tempo utilizzato per cavarmi d'impaccio. Alla fine, ho pensato che, magari, scrivere quello che ho imparato su questo tema potesse servire a dare una mano a qualche fratello d'arme impelagato nello stesso tipo di problema, soprattutto se l'avessi scritto in italiano, visto che d'informazioni di questo tipo, in inglese, se ne trovano tante (anche se magari parziali, poco organiche o proprio sbagliate).

Non avrei mai scritto questo articolo se l'industria informatica avesse collettivamente più buon senso di quello che dimostra quotidianamente e avesse pensato - per tempo - a definire un modo di dedurre il tipo di caratteri utilizzato da un documento di testo.

Questa è una storia di pochi standard, troppi standard e di pezze multicolori applicate su buchi così grandi che nessuno pensava si potessero turare. E' altresì una storia che fa capire quanto siamo fortunati che non sia toccato all'industria informatica decidere come siano fatte le prese e le spine elettriche, perché altrimenti nessuno potrebbe infilare una spina B-Ticino in una presa fatta da un altro produttore, o magari, dalla stessa B-Ticino, ma in un anno differente. Ci penso tutte le volte che ho in mano una spina Shucko e mi manca un adattatore....

Per chi legge

La materia di questo articolo varia da nozioni alla portata di quasi chiunque abbia una informatizzazione di base a tecniche di programmazione che sono abbastanza oscure anche per chi fa del software la propria professione. Ho cercato di esporle all'incirca in questo ordine, do modo che, se ci si accorge che da un certo punto in avanti non si capisce più nulla, si sa già che si può tranquillamente smettere di leggere, perché il livello di comprensibilità non è destinato a migliorare.

D'altra parte, chi si accorge di stare leggendo informazioni abbastanza banali, può sbirciare avanti, sperando di trovare (se ci sono) le cose che ancora non sa sull'argomento. Siccome però la (mia) maggiore difficoltà nell'arrivare a capire il tema è stata per lungo tempo la mancanza di una riflessione organica sul tema - mancanza sempre dovuta al fatto di dover risolvere un particolare problema in poco tempo, situazione che da sempre si concilia male con l'apprendimento riflessivo - io sconsiglio di leggere questo articolo saltando qua e là. Metà dell'utilità di questo articolo - nella mia opinione - sta nella definizione esplicita di alcuni termini (carattere, codepoint, codifica, decodifica) il cui significato è dato spesso per scontato, mentre non lo è affatto (tanto da confondere spesso codifica e decodifica).

Il problema fondamentale

Il problema tipico è in genere posto da un utente perplesso (e contemporaneamente irritato) che lo formula più o meno in questi termini:

"Sul sistema X il testo Y (dove ) si vede perfettamente. Sul sistema Z lo stesso testo Y si vede nella maniera scorretta Z."

Qui X è un calcolatore, programma, o qualunque altra cosa faccia parte del contesto utilizzato per mostrare un testo, Y può essere testo puro e semplice, ovvero testo mostrato da una particolare applicazione, a volte come messaggio che proviene da un programma, a volte come pagina html, Z può variare da: "alcuni caratteri sono sostituiti da altri" a "tutti i caratteri sono rimpiazzati da caratteri stranissimi, scelti apparentemente a caso".

Se si ha fortuna, il testo di partenza e la sua rappresentazione sono espressi in un alfabeto vagamente intellegibile (per chi scrive, europeo), su calcolatori accessibili. Se è una brutta giornata, il testo in oggetto è in cinese, è stato tradotto a Tokyo e il calcolatore incriminato è a Seoul (benefici della globalizzazione).

In ogni caso, il professionista d'Information Technology borbotta "Ah, sì, è un problema di codifica, adesso ci guardo, ti chiamo io.", assume un'aria meditativa e spera che l'utente se ne vada (invece di restare nei dintorni sperando che la soluzione si materializzi). Dentro di sé, il nostro professionista IT sta bestemmiando come un turco.

Per capire il perché, bisogna prenderla alla lontana.

Una questione di carattere

I caratteri, si sa, sono i componenti elementari delle parole scritte. Dico parole scritte, perché, ad esempio, i componenti elementari delle parole pronunciate si chiamano fonemi, e sono una cosa abbastanza diversa, di cui qui non parlerò.

In questa sede, bisogna distinguere il carattere come concetto (ad esempio: la lettera minuscola "a") dalla sua rappresentazione visibile, che può assumere moltissime forme, a seconda del mezzo che si usa per visualizzare il carattere (un foglio di carta, lo schermo di un calcolatore, un cartellone pubblicitario) e della forma che si sceglie di dargli (il cosiddetto tipo - o font - in tipografia, o la rappresentazione calligrafica se si sta scrivendo a mano).

In questo senso, un carattere è l'idea (astratta dalla sua rappresentazione concreta) di uno dei componenti elementari della parola scritta. La sua realizzazione, o rappresentazione, dovrebbe essere chiamata grafema, una parola che sta ad indicare il segno grafico che viene interpretato come carattere. In effetti, bisogna distinguere ulteriormente tra il grafema, inteso come "idea della forma di un carattere" - che ad esempio, non incorpora il particolare tipo/font usato nella rappresentazione - dal segno fisico vero e proprio, che viene spesso chiamato glifo. Si noti che ad uno stesso carattere possono corrispondere più grafemi: ad esempio, in greco antico, la "sigma" in finale di parola ha un grafema diverso da quella ad inizio o in mezzo ad una parola. D'altra parte, a più caratteri può corrispondere uno stesso grafema e/o glifo: ad esempio la rappresentazione tipografica della sillaba "fi" è spesso un unico simbolo in cui 'f' e 'i' sono fuse e la 'i' perde il punto.

Bisogna tenere presente che, già a questo punto, stiamo indicando come "carattere" un assieme più esteso di quello che si presenta quasi automaticamente alla nostra intuizione. Ad esempio, in questa definizione, si distingue la lettera "e" dalla lettera "e con accento acuto" ("é") dalla lettera "con accento grave" ("è"). In più, si suole dare dignità di carattere a tutta una famiglia più esotica di segni che comprendono i segni d'interpunzione (apostrofi, virgole...) segni diacritici (accenti, dieresi...) e varie forme di spazio bianco (interruzioni di linea, spazi, tabulazioni...).

La collezione di caratteri (nel senso esteso definito poc'anzi) utilizzati da una lingua si chiama alfabeto. L'inventiva umana ha fatto sì che ad ogni lingua parlata, scritta o anche solo immaginata corrispondesse almeno un alfabeto proprio (spesso, più di uno).

Fino a non molto tempo fa, questo significava semplicemente che, mentre uno imparava una lingua, ne imparava anche l'alfabeto. Se si era fortunati, si trattava di un'alfabeto affine (questo succede con le lingue europee occidentali, i cui alfabeti sono in larga parte sovrapposti). Chi si cimentava con altre lingue poteva essere meno fortunato e gli capitava di doversi cimentare con un alfabeto totalmente diverso da quello nativo, magari composto da alcune migliaia di caratteri.

Lo stupido calcolatore

Come è capitato per tante altre cose, anche per quello che riguarda il testo e la sua rappresentazione, l'avvento dei calcolatore ha posto problemi nuovi.

Alla radice del problema (che è poi quello del trattamento informatico dei simboli) sta il fatto ben noto che il calcolatore (almeno il calcolatore binario di tipo Turing/Von Neumann - quello a cui siamo abituati) è in grado di manipolare senza mediazione un alfabeto (mi si passi la semplificazione) composto da due soli simboli: 0 e 1 (un bit). Questo è un modo un po' diverso di dire che un calcolatore è in grado di trattare, senza mediazione, solo informazione di tipo numerico, anzi, solo di tipo numerico, intero, binario. In effetti, siccome operare sui bit tende a rendere le cose ancora più difficili di quello che già non siano, la totalità dei calcolatori moderni utilizza come unità di calcolo, un "pacchetto" di 8 bit universalmente chiamato byte (tranne per i francesi, che lo chiamano "octet": la poco usata versione italiana è "ottetto"). Con un byte, un calcolatore è in grado di contare da zero a 255 (o da -127 a 127, ma in questa sede non c'interessa). Per la precisione il byte è l'unità minima che un calcolatore (moderno) può trasferire da un dispositivo di memorizzazione, o di input/output, all'unità di elaborazione: in altre parole, se un calcolatore vuole operare sul bit numero 20, prima di farlo dovrà andare a prendere l'intero byte numero 3, in cui il bit numero 20 è contenuto.

Per poter svolgere qualunque operazione simbolica non-numerica, il calcolatore deve usare un qualche metodo di traduzione che converta i simboli in numeri, e viceversa. Un programma che manipoli, ad esempio, note musicali, dovrà appoggiarsi ad un metodo che permetta di convertire le note (e tutti i simboli che costituiscono la notazione musicale) in numeri, e all'inverso di questo metodo per poter fare il tragitto inverso.

In buona sostanza, si tratta di numerare (spesso in modo astratto, ma la differenza non è essenziale in questa sede) i simboli che si ha intenzione di usare (l'alfabeto, o repertorio), utilizzando poi i numeri come se fossero i simboli stessi.

In quanto segue, chiamerò un'associazione di questo tra numeri e simboli codice.

Si tratta di una denominazione non standard: normalmente quello che qui indicherò con codice viene chiamato codepage o - in maniera molto ambigua - codifica. Siccome il processo di codifica, di cui parlerò tra poco, è una cosa abbastanza diversa ,che è cruciale comprendere con precisione per non perdersi, mentre il termine "codepage" è usato in maniera poco uniforme e spesso con riferimento ad un particolare fabbricante di calcolatori o sistemi operativi, ho deciso di adottare questa variante del gergo "normale".

Poiché qui si parla solo di testo, trascurerò l'importante campo dei codici relativi ad applicazioni più specialistiche, come la già citata notazione musicale, la notazione matematica e molte altre. In questa nota mi interessano soprattutto i codici il cui scopo è numerare i caratteri. D'altra parte il più importante di questi codici (Unicode) può essere utilizzato (e viene utilizzato) anche per catalogare tutti i simboli non testuali di cui ho accennato e molti altri di interesse ancora più specialistico (come gli alfabeti e i sistemi di scrittura delle lingue morte)

Sottolineo anche che, nel concentrarmi sui codici testuali, trascuro anche quasi tutto quello che riguarda l'impaginazione del testo, fatto salvo per i suoi aspetti più elementari (spazio tra le parole, interruzioni di riga e - raramente - di pagina e di colonna). Si tratta di fattori che sono invece molto importanti per chi si occupa della rappresentazione del testo (che non può trascurare il fatto che l'arabo venga scritto da sinistra a destra, o il cinese tradizionale dall'alto in basso). Anche qui vale la pena di ricordare che si tratta di fattori che sono pienamente recepiti da Unicode.

I codici

Il primo punto fermo che abbiamo raggiunto, quindi, si può formulare come segue:

"Nell'elaborazione testuale, i calcolatori e i programmi si affidano - implicitamente o esplicitamente - ad una procedura di numerazione dei caratteri, detta codice."

Un codice è quindi un'associazione tra numeri e caratteri. Il numero che in un determinato codice viene associato ad un dato carattere è detto codepoint.

Un codice può essere associato ad un qualsivoglia insieme di caratteri, non necessariamente a quelli in uso in un particolare alfabeto; un unico codice può codificare un solo alfabeto, un insieme di alfabeti o anche solo parte di un dato alfabeto.

A questo punto vale la pena di sottolineare che l'adozione di un codice delimita, in maniera implicita o esplicita, il repertorio di caratteri che si è in grado di elaborare. Questo è specialmente importante (e può costituire un grave handicap) per i testi che contengono parti scritte in lingue diverse (ad esempio la traduzione interlineare italiana di un testo di Confucio).Se, ad esempio, il codice in uso è il venerando ASCII, il repertorio, fra le altre cose, esclude tutte le lettere accentate; se il codice in uso è Latin-1, non sarà possibile rappresentare (fra l'altro) la lettera greca "ALFA", e così via. Ancora una volta, fa eccezione Unicode, che ha l'ambizione di comprendere il repertorio dei caratteri di tutte le lingue umane, e che - al momento - ha effettivamente catalogato i caratteri di tutte le principali lingue moderne.

Tipi di rappresentazione

Per arrivare a capire completamente il problema che ho chiamato fondamentale dobbiamo complicarci ancora un po' la vita, distinguendo tra la rappresentazione esterna e quella interna di un carattere. Con rappresentazione interna intendo il modo in cui un carattere "vive" in un programma mentre viene elaborato: si tratta di una rappresentazione che può essere concepita come astratta (non lo è, ma i suoi dettagli sono inessenziali). Con rappresentazione esterna intendo la forma assunta dal carattere nel momento in cui viene trascritto su disco, o spedito ad un altro programma.

Nell'ambito della rappresentazione interna - e al livello di dettaglio che m'interessa - il codice gioca una funzione di puro riferimento. Ovvero si sa che c'è e viene usato ma, posto che ne venga fatto un utilizzo corretto, esso è praticamente invisibile.

Dove il codice gioca una funzione essenziale è nella transizione tra rappresentazione interna ed esterna (e viceversa). Infatti, passando da rappresentazione interna a rappresentazione esterna, ogni carattere deve:

i) prima essere identificato come numero (codepoint) secondo le specifiche del codice;

ii) successivamente, questo numero deve essere tradotto in una sequenza di byte (codifica);

iii) infine, la sequenza di byte deve essere trasmessa.

Per passare dalla rappresentazione esterna, il procedimento è invertito:

i) la sequenza di byte (rappresentazione esterna) è ricevuta dall'ambiente di elaborazione.

ii) la sequenza di byte dev' essere trasformata in una sequenza di numeri/codepoint (decodifica)

iii) il codice identifica ogni numero come carattere (rappresentazione interna)

Se trasformare un codepoint (numero) in una sequenza di byte (o viceversa) fosse un procedimento che può essere fatto in unico modo, quello che ho chiamato "problema fondamentale" sarebbe molto semplificato - basterebbe sapere quale codice viene usato per la rappresentazione interna. Come si vedrà, le cose sono più complicate (come se non lo fossero già abbastanza).

Il processo di decodifica e codifica sono fondamentali per l'inquadramento del nostro problema e vale la pena di metterle in bella evidenza:

Codificare (encoding) è il processo per cui un carattere, in rappresentazione interna viene prima associato ad un codepoint (attraverso l'uso di un codice) e poi convertito in una rappresentazione numerica concreta o, per dire meglio, in una sequenza di byte (rappresentazione esterna).

Decodificare (decoding) è il processo inverso della codifica: una sequenza di byte (rappresentazione esterna) viene prelevata e convertita prima in sequenza numerica (sequenza di codepoint), poi - applicando un codice - in sequenza di caratteri.

Il processo di rappresentazione elettronica del testo

A questo punto il mio lettore (ammettendo che ce ne sia almeno uno) è più che giustificato ad essere confuso.

Cosa sta succedendo? Come avviene che una sequenza di byte diventa un testo leggibile sul mio schermo (a su una pagina di stampante)? E, come avviene che la stessa sequenza di byte possa trasformarsi in pagine e pagine di porcherie illeggibili su un altro schermo?

Riassumiamo e integriamo quello che abbiamo visto finora, cercando di renderlo comprensibile.

1) Il testo viene prelevato (dal disco, da internet, o da qualche altra sorgente esterna), sotto forma di sequenza di byte (numeri da 0 a 255).

2) Attraverso il procedimento di decodifica (q.v.) la sequenza di byte viene trasformata in sequenza di codepoint

3) Applicando un codice (q.v.) la sequenza di codepoint di cui al punto precedente è identificata come sequenza di caratteri.

4) Al termine della elaborazione, ad ogni carattere della sequenza è associato un glifo che viene rappresentato sul dispositivo di visualizzazione (schermo, stampante).

Il punto (4), di cui finora non si era parlato, che è importantissimo per poter vedere e interpretare il testo, non è invece particolarmente rilevante per quello che riguarda il nostro problema. Semplificando un po' le cose, infatti, quello che accade è che ad ogni carattere, in rappresentazione interna, deve essere associata una forma del tipo/font in uso (glifo). Ora, l'associazione tra un carattere e un font è ben definita una volta che sia noto il codice, e il codice è normalmente definito in maniera univoca a livello di macchina. L'unica cosa che può andare storta (rendendo il testo illeggibile) è che per qualche motivo venga usato un font sbagliato (non alfabetico, o relativo ad un alfabeto diverso). Questo tipo di errore, oltre ad essere relativamente raro, si corregge rapidamente utilizzando uno dei font alfabetici standard (Per le lingue occidentali: Arial, Helvetica, Courier...).

Incidenti nelle prime tre fasi, invece, sono di soluzione più difficile. Infatti, ogni errore nel determinare il codice o la codifica usati nella fase di trasmissione rende il testo decodificato parzialmente (più spesso, completamente) incomprensibile.

Questo è "Il problema di codifica" di cui mugugna il professionista IT di cui si è parlato qualche paragrafo fa.

E' abbastanza naturale a questo punto chiedersi come mai possano sorgere questi equivoci: come mai non c'è un solo codice e una sola codifica? Per saperlo bisogna studiarsi un pochino di storia del calcolo elettronico.

Un milione di anni fa

...o poco meno, i calcolatori facevano una sola cosa: calcolavano, appunto. Per leggere e scrivere i risultati di procedure di calcolo numerico, non servono molti simboli (le cifre decimali, qualche lettera latina, i segni +,-,. e ",") e questi sono ben noti a quasi tutti gli esseri umani. In più i calcolatori comunicavano quasi esclusivamente tramite stampati, e quasi esclusivamente con esseri umani. Il nostro problema, in pratica, non esisteva.

Questa felice situazione durò pochi anni, sia perché un'interazione esclusivamente numerica era molto faticosa anche per gli utenti tecnici, sia perché furono presto comprese le potenzialità di elaborazione "simbolica" dei calcolatori. La necessità di rappresentare informazione testuale portò quindi alla formulazione dei primi codici. Dopo un periodo di anarchia, durante il quale ogni fabbricante utilizzava un proprio codice, rendendo estremamente problematico qualunque scambio di dati tra macchine diverse, emersero due codici destinati a costituire standard di fatto: EBCDIC (1963) e ASCII (1963).

In effetti, il solo codice ASCII era un vero e proprio standard. EBCDIC è un codice sviluppato in maniera indipendente da IBM (e adottato da alcuni altri fabbricanti di mainframe suoi concorrenti). Il suo uso, oggi, è essenzialmente relegato all'ambito IBM, ed è probabilmente destinato a cadere in disuso.

La radice di tutti i mali

O almeno la radice di molti mali, è che i primi codici (e qui ci concentriamo essenzialmente su ASCII) cercavano di realizzare di un insieme di obiettivi che oggi consideriamo estremamente ristretto: odierni):

1) creazione di un codice per un ambiente sostanzialmente monolingua (inglese)

2) avere un codice che rendesse efficiente (in termini di spazio e di tempo) le comunicazioni tra apparati.

Il primo obiettivo fece sì che ci si concentrasse sulla codifica di un set di simboli molto limitato (quelli compresi nell'alfabeto inglese più i relativi segni diacritici).

La soluzione adottata per il secondo obiettivo fece sì che - nello stesso codice - si dovesse trovare spazio anche per tutta una serie di simboli utilizzati solo per la trasmissione di dati (ad esempio Start-Of-Message, End-Of-Message...) riducendo in questo modo il numero di codepoint disponibili per i caratteri. (Questo fu dovuto al fatto che le comunicazioni tra apparati si svolgessero su linea seriale, e che quindi i caratteri di controllo fossero obbligatoriamente mescolati a quelli costituenti il messaggio stesso) Inoltre i requisiti di efficienza spingevano a creare un codice il più ridotto possibile, cioè un codice che utilizzasse il minor numero possibile di caratteri.

Fu così che il codice ASCII fu pubblicato come un codice a 7 bit, il che permette la rappresentazione di 128 caratteri, di cui i primi 32 sono caratteri di controllo. I caratteri stampabili non alfanumerici furono presi dalla tastiera standard di una macchina da scrivere (inglese).

Poiché il codice ASCII usa solo sette bit degli otto disponibili in un byte, l'ottavo bit rimase libero per essere utilizzato come bit di verifica della correttezza della trasmissione.

E il resto del mondo?

Naturalmente, 96 caratteri non bastano per rappresentare tutti gli alfabeti del mondo. Non bastano neanche per rappresentare gli alfabeti di tutte le lingue europee, né quelli delle sole lingue occidentali.

Man mano che l'uso del calcolo elettronico si espandeva, la situazione diventava sempre meno accettabile, soprattutto per gli abitanti delle nazioni il cui linguaggio utilizza un set di caratteri completamente diverso da quello previsto dall' ASCII. A dire la verità, già gli scandinavi erano abbastanza ostacolati nel rappresentare la propria lingua; tutto sommato gli italiani erano fra i più avvantaggiati, dovendo solo risolvere il problema delle lettere accentate, normalmente risolto con l'uso degli apici.

I codepage

Come è tipico del mondo dell'informatica (e forse del mondo in generale) la soluzione del problema venne tentata da più parti applicando pezze su uno standard che non poteva, per sua natura, tollerarne molte.

Il primo passo fu appropriarsi dell'ottavo bit che ASCII riservava a compiti di controllo, aggiungendo in questo modo altri 128 caratteri ai codepoint disponibili. Ogni fabbricante creò poi una serie di codici detti - prendendo a prestito un termine IBM - codepage. Ognuno di questi codici utilizzava questo nuovo spazio di 128 codepoint per associarvi altri alfabeti, o - non di rado - caratteri grafici utili per disegnare cose come tabelle, report e così via.

Questo fu l'inizio della Babele informatica. La corretta interpretazione di un testo ricevuto da un'altra macchina richiedeva che il ricevente fosse in grado di determinare il codice/codepage (dipendente dal venditore) che era stato usato. La Babele, tuttavia, era abbastanza mascherata dal fatto che gli scambi di dati tra calcolatori di norma coinvolgevano calcolatori fatti dallo stesso produttore (o al limite calcolatori che aderivano ad uno standard de facto imposto dal maggior produttore del settore - IBM, in genere), spesso chiamando in causa personaggi in camice bianco che al telefono si dicevano cose tipo "Ti mando un nastro ANSI da 0,5 pollici nel codepage IBM 443", e per loro avevano un senso.

LATIN-1 e compagnia bella

La situazione era più che matura (quasi marcia, in effetti) per fare qualche tentativo di standardizzazione, tentativo che ad un certo punto fu intrapreso dalla ISO (l'organizzazione per gli standard internazionali) verso la fine degli anni 1980.

Intanto vennero definite denominazioni uniformi per una serie di (varianti di) codifiche entrate nell'uso. Ad esempio (un esempio importante) venne definita la denominazione iso-8859-1 (anche detta Latin-1) per un codice di 256 codepoint che descrive gli alfabeti di molte lingue europee occidentali. Vennero predisposti altri codici iso-8859-x (da iso-8859-2 a iso-8859-16) per le lingue europee con altri alfabeti (ad esempio greco, cirillico...).

Le codifiche per gli alfabeti orientali (essenzialmente giapponese, cinese, coreano) vennero (in maniera simile) raggruppate sotto la famiglia di denominazioni ISO/IEC 2022.

Per quasi tutte le codifiche ISO, si fece in modo che i primi 127 codepoint corrispondessero ai codici ASCII, in modo da conservare un qualche tipo di compatibilità con quest'ultimo.

Il processo fece alcune vittime (codifiche nazionali e industriali di varia denominazione che non vennero recepite) e creò alcuni orrori (ad esempio, la codifica Latin-1 è quasi uguale, ma non identica, al codepage windows-1252, un'ambiguità che persiste ancora oggi).

Uno degli effetti di questo processo fu sottolineare la necessità di unificare i codici esistenti in un unico repertorio in grado di rappresentare tutti i caratteri usati dall'uomo. Il risultato dello studio di un catalogo di questo tipo fu la creazione di Unicode (e ne parleremo fra un po')

Le codifiche

Come abbiamo detto più sopra, i calcolatori trasmettono l'informazione in unità minime chiamate byte (mentre sono in grado di elaborarla facendo riferimento ad un'unità ancora minore detta bit: un cifra binaria che può valere 0 o 1)

Siccome un byte può rappresentare i numeri interi nell'intervallo 0-256, qualunque codice contente un massimo di 256 codepoint può essere codificato (messo in forma esterna) utilizzando un byte per carattere. Per questi codici è quindi possibile far coincidere rappresentazione interna ed esterna, facendo corrispondere ad ogni codepoint la sua rappresentazione come singolo byte. In buona sostanza, codice e codifica sono indistinguibili.

Esistono però lingue che hanno (molti) più caratteri dei 256 rappresentabili con un singolo byte: il cinese e il giapponese sono due fra le più importanti. Le codifiche dei codici/codepage creati per queste lingue presenta quindi la necessità di usare più di un byte per carattere cosa che può essere fatta in almeno due modi - ed entrambi sono stati usati in diversi codici e codifiche.

Codifiche wide-char.

La scelta apparentemente più naturale è quella di usare lo stesso numero di byte per la codifica di ogni codepoint. Ad esempio, Per un alfabeto che abbia più di 256 ma meno di 65536 simboli, questo significa che ogni carattere sarà codificato con due byte, da 00000000-00000000 a 11111111-11111111. Codifiche di questo genere si chiamano "wide-char" (caratteri larghi). Benché facilmente e immediatamente comprensibili, queste codifiche hanno un problema evidente, uno latente e uno che interessa principalmente i programmatori.

Un esempio: UCS-2 (UTF-16)

Consideriamo, come esempio tutt'altro che teorico, una codifica U così fatta (questa codifica è essenzialmente quella che, in UNICODE, è chiamata UCS-2).

1) U è wide-char, con due byte per codepoint

2) U utilizza i primi 256 codepoint nello stesso ordine e con lo stesso significato del codepage latin-1. Questo significa che tutte le lettere delle principali lingue europee occidentali sono contenute in un solo byte, il primo dei due.

Il primo problema (quello evidente) è l'inefficienza di U. U infatti contiene 511 simboli che vengono codificati in sequenze che hanno almeno un byte nullo. Tuttavia, quando U viene utilizzata per codificare testi costituiti da soli caratteri occidentali, questi risultano occupare il doppio dello spazio (e vengono trasmessi nel doppio del tempo) che sarebbe necessario, perché tutti i caratteri occidentali hanno una codifica in cui il byte più significativo è nullo.

Il secondo problema (quello meno apparente) è noto come problema dell'endianness. La parola endianness e la terminologia associata derivano dai nomi di due fazioni politiche che esistevano nelle favolose isole di Lilliput e Blefuscu (come racconta Swift ne i "Viaggi di Gulliver") i cui membri si distinguevano per l'estremità da cui iniziavano ad aprire le uova: quella grande (a Lilliput, per editto del re che una volta si era tagliato aprendo un uovo dall'estremità più sottile: big endians) o quella piccola (a Blefuscu, per protesta contro il re: little endians). Su questa differenza (e sulla sua legittimazione regale), era scoppiata tra le due isole una guerra sanguinosa in cui bravi lillipuziani e blefuscudiani si scannavano in gran numero. In campo informatico, l'endianness ha dato origine a grattacapi meno sanguinosi, ma anche più idioti di quelli provocati a Lilliput.

Ho più volte detto che, per i calcolatori moderni, l'unità basilare di trasmissione e manipolazione dei dati è il byte. Molto presto, comunque, i calcolatori cominciarono ad assegnare un posto di riguardo alle coppie di byte adiacenti (dette parole, o word) che vengono spesso trattate come un tutto unico. Ad esempio i numeri interi sono di norma rappresentati da una, due o quattro word (due, quattro o otto byte adiacenti).

Siccome una word non è, come il byte, un'unità indivisibile, essa è suscettibile di essere rappresentata esternamente (o memorizzata, o scritta, o trasmessa: in fondo è la stessa cosa) in due modi diversi:

1) scrivendo prima il byte più significativo, poi quello meno significativo (big endian)

2) scrivendo prima il byte meno significativo, poi quello più significativo (little endian)

(Per completezza, dirò che possono esistere - ma sono rare - analoghe differenze nella rappresentazione di coppie di word.)

In altre parole, se immaginiamo che i byte siano cifre decimali, e dato il numero "novantuno", una macchina big-endian lo memorizzerebbe/scriverebbe come "9" "1" e una macchina little endian come "1" "9".

Il problema dell'endianness nasce dal fatto che, per incredibile/stupido che possa sembrare, nessuno ha mai pensato di stabilire come vadano scritte le word (in rappresentazione esterna). In informatica questo comportamento ufficialmente "non definito" (o in alternativa "definito dall'implementazione") ha il significato ufficioso "ognuno può fare l'accidenti che gli pare, e l'IT pensa a raccogliere i cocci".

Cosa che infatti è puntualmente successa, inserendo anche l'endianness (o byte-ordering) tra le incognite da risolvere nello stabilire la comunicazione tra due calcolatori diversi. Questo problema divenne talmente scocciante da venire infine risolto "manu militari" da Sun che, per quello che riguarda le comunicazioni tra calcolatori in rete, che riuscì a fare accettare l'idea che esistesse un network byte order a cui tutti dovevano conformarsi nelle comunicazioni. (Il "network byte order" è il big endian, non a caso quello usato da Sun). Peccato che la stessa saggezza non abbia prevalso per quello che riguarda la memorizzazione dei dati: i file vengono tuttora scritti, da macchine diverse, con endianness diversa.

Per la nostra codifica U tutto questo significa che essa potrà essere interpretata correttamente solo dopo che chi la vuole decodificare abbia in qualche modo determinato l'endianness con cui è stata scritta. Spesso, il modo è provare entrambe le endianness e vedere quale delle due sembra giusta.

L'ultimo problema (evidente solo ai programmatori) è che, come già detto, la codifica U contiene per forza un certo numero di byte nulli (anzi, per un testo occidentale big endian, sono nulli tutti i byte pari). Ma, tradizionalmente (qui tradizionalmente significa: dall'inizio degli anni 1960 fino ad una qualche data prima del 2000) il byte nullo ha avuto il significato di "fine stringa" per una grande quantità di software - in particolare per tutto quello utilizzato per manipolare direttamente testo nei paesi occidentali (gli orientali se ne erano fatto di ad hoc per le loro codifiche o avevano messo pezze su quello usato in occidente facendo leva sulla loro proverbiale pazienza).

Quello che questo significa, per la codifica U, è che la maggior parte degli strumenti tradizionali per la manipolazione del testo non sono in grado di utilizzarla o lo fanno solo con grande difficoltà.

Codifiche multibyte

Un'altra famiglia di codifiche si ottiene se si ammette la possibilità di codificare codepoint diversi con un numero variabile di byte.

Un esempio: UTF-8

Consideriamo ad esempio una codifica F (come vedremo, questa codifica è essenzialmente quella chiamata UTF-8) così concepita:

1) I primi 127 codepoint sono gli stessi - e nello stesso ordine - di quelli utilizzati dalla codifica ASCII e vengono scritti con unico byte il cui bit più significativo è posto a zero. La codifica dei primi 127 codepoint è quindi uguale alla codifica ASCII.

2) Quando il bit più significativo di un dato byte è uguale a 1, il byte fa parte della codifica di un codepoint che viene codificato in più byte. Se uno o più bit successivi a quello più significativo sono pari a uno e seguiti da uno zero (110xyyzz, 1110yyzz, ...) si è in presenza del primo bit della codifica, e il numero di bit iniziali pari ad uno indica quanti byte sono usati per codificare il codepoint in esame. Se invece il bit successivo a quello più significativo è pari a zero (10xxyyzz) il byte in esame è il secondo, terzo... della codifica di un dato codepoint.

La codifica F risolve alcuni problemi delle codifiche "wide", introducendo comunque altri inconvenienti. Confrontiamola con la codifica U descritta nel paragrafo precedente.

1) La parte di F che riguarda i primi 127 codepoint è molto più compatta della corrispondente codifica U. Per contro F è meno compatta di U nella codifica di tutti i codepoint che richiedono più di due byte (guarda caso questa è la zona riservata alla maggior parte degli alfabeti orientali), che pagano un'inefficienza di circa il 30%.

2) F è indipendente dall'endianness: ogni codepoint è concepito come una sequenza di byte (non di word!) ordinata intrinsecamente.

3) F non contiene byte nulli, ed è compatibile con la codifica ASCII: quindi i file di testo codificati in F possono essere manipolati con strumenti "tradizionali".

4) F non è invece compatibile con la codifica latin-1 (e ne riparleremo)

5) Decodificare F è più difficile che decodificare U. In particolare, una codifica come F rende difficile fare cose come "trovare l'ottavo carattere di una parola". Usando una codifica come U posso infatti compiere questa operazione semplicemente estraendo l'ottava "word" della sequenza (in una codifica a byte singolo, questo si fa estraendo l'ottavo byte). Se invece la codifica in uso è F, per poter trovare il carattere richiesto devo prima leggere i byte della sequenza di ingresso e decodificarli fino ad arrivare all'ottavo codepoint.

6) F contiene alcune sequenze di byte che sono vietate (ad esempio: 110xyyzz-0qxxyyzz). Questo rende possibile stabilire con certezza che un sequenza contenente una sotto-sequenza proibita non usa la codifica F. Questa sembra una banalità ma è il caso di far notare che questa proprietà non è condivisa da molte codifiche a byte singolo o wide: in particolare, qualunque sequenza, anche casuale, di byte può essere interpretata come corretta per una delle codifiche ISO-8859-x. Questa circostanza fa parte integrante del problema fondamentale.

Esistono molte altre possibili codifiche multibyte di cui non parlerò: in particolare esistono codifiche di tipo "shift" in cui la comparsa di una particolare sequenza di byte (upshift) cambia il significato di tutti i byte successivi fino alla ricezione di un'altra sequenza di byte definita (downshift) che ripristina la codifica precedente. Una vasta famiglia di codifiche di questo tipo è raggruppata nello standard ISO/IEC-2022, dedicato alla codifica di varie lingue orientali.

A questo punto è necessario dire che, per la maggior parte dei codici/codepage definiti dalle specifiche ISO, la codifica è univocamente determinata. Questo significa che, se si è nella condizione di sapere quale codice è utilizzato, si sa anche quale codifica è stata utilizzata. Questo però non è più vero là dove si prende in considerazione il codice noto come UNICODE, che è l'argomento del prossimo paragrafo.

Unicode

Lo standard Unicode (specificato dallo Unicode consortium) è essenzialmente un'iniziativa il cui scopo è la creazione di un repertorio unificato di tutti i caratteri usati dall'umanità, comprendendo quelli delle lingue scritte contemporanee, quelle del passato, qualche lingua immaginaria (Unicode riserva un insieme di codepoint per l'alfabeto Klingon), e con abbastanza spazio per incorporare lingue non ancora codificate.

L'esistenza di un repertorio di questo tipo, e delle relative codifiche, può permettere - ad esempio - l'utilizzo di testo multilingua senza dover identificare e cambiare codepage. Unicode insomma sarebbe il codice dei codici: se fosse usato dappertutto porrebbe fine al "problema centrale" come enunciato più sopra, senza che si dovesse rinunciare alla rappresentazione di qualche carattere..

Sorvolando sulla storia delle varie versioni di Unicode, dirò che lo standard attuale contiene 1 114 112 (un milione centoquattordicimila centododici) codepoint, suddivisi in 17 piani, ognuno composto di 65 536 codepoint, cioè 256 righe contenenti 256 codepoint ciascuna.

Il piano 0, costituito dai primi 65536 codepoint, è chiamato Basic Multilingual Plane (BMP) e contiene la maggior parte del repertorio di caratteri oggi in uso. Per assicurare la retro-compatibilità con ASCII, è previsto che i primi 127 codepoint coincidano con quelli definiti dalle specifiche ASCII.

La più recente formulazione di UNICODE contiene gran parte di tutte le lingue in uso e del passato,i loro diacritici, simboli matematici, simboli musicali e molte altre simbologie. Inoltre più di 10 piani non sono assegnati (cioè i codepoint in essi contenuti non corrispondono ad alcun carattere) né è probabile che vengano assegnati in un futuro prossimo.

Oltre a catalogare un enorme repertorio di caratteri, Unicode definisce tutta una serie di informazioni accessorie (ordinamento dei vari set di caratteri, regole per assicurare la "multi-direzionalità" del testo...) che non hanno una diretta influenza sul problema fondamentale sopra definito.

Inoltre Unicode definisce anche ciò che chiama "Unicode transformation format" (UTF) e "Universal character set" (UCS): questi non sono altre che le codifiche necessarie per la rappresentazione esterna di Unicode.

Delle diverse codifiche definite e usate nella storia di Unicode, mi limiterò a citare le più importanti (che sono anche quelle usate in più del 90% dei casi).

UTF-8: una codifica multibyte che massimizza la compatibilità con ASCII (parzialmente descritta nel materiale precedente come codifica F). In UTF-8 ogni carattere viene codificato in una sequenza di lunghezza variabile da 1 a quattro ottetti (byte)

UTF-16 (ex UCS-2, descritta nel materiale precedente come codifica U): una codifica multibyte che permette la rappresentazione dell'intero repertorio Unicode e che rappresenta l'intero BMP (65536 codepoint) con una codifica di tipo "wide" costituita da due byte (questa era l'originale codifica UCS-2, che era in grado di rappresentare il solo BMP). Mentre UTF-16 e UCS-2 sono spesso confuse, UTF-16 è l'unica di uso corrente. In UTF-16 ogni carattere viene codificato in una sequenza di lunghezza variabile da 2 a quattro ottetti (byte), riservando le codifiche a quattro byte per codepoint rarissimi gestiti tramite "codepoint surrogati".

UTF 16 definisce anche un particolare valore (Byte-Order-Mark o BOM) che si può usare per capire l'endianness usata nella codifica del testo. Il BOM è rappresentato dal codepoint (esadecimale) U+FEFF che su una macchina big-endian viene rappresentato dalla sequenza 0xFE,0xFF e dalla sequenza 0xFF,0xFE su una macchina little endian. Poiché il codepoint U+FEFF (Zero-Width No-Break Space : Spazio di ampiezza zero che non consente interruzioni) non può mai essere il primo carattere di una sequenza codificata mentre il codepoint U+FFFE non è - né sarà - mai assegnato ad un carattere valido, l'apparire di uno di questi due codepoint all' inizio di una sequenza codificata permette di dedurre la endianness dell'intera sequenza.

In UTF-8 non esiste un BOM (per motivi già spiegati) anche se alcuni programmi (soprattutto operanti in ambiente windows) ne inseriscono uno (xEF,0xBB,0xBF) equivalente a quello usato in UTF-16. Questo è permesso, ma sconsigliato, dallo standard, e in essenza non fa che rompere le scatole.

UTF-32/UCS-4: una codifica "wide" a lunghezza fissa: ogni codepoint di Unicode è rappresentato da una sequenza di 4 byte. Si applicano le considerazioni sul BOM già viste per UTF-16. Questa codifica è usata, in pratica, molto di rado.

A causa dei vantaggi illustrati della codifica F sulla codifica U, UTF-8 è oggi la codifica più usata per la rappresentazione esterna di testi e testi multilingua. UTF-16 è per contro molto usata nella rappresentazione interna delle stringhe (in particolari è quella in uso in tutti i sistemi operativi Microsoft posteriori a Windows 2000)

Il problema fondamentale, rivisitato

Giunti praticamente alla fine del nostro esame (semplificato) dei codici e codifiche associate, siamo pronti per cercare di capire quali inconvenienti possono provocare il problema fondamentale che ho enunciato qualche paragrafo fa.

Quello che succede è che un testo (file) preparato per essere visualizzato con una data tripletta (codice, codifica, endianness) va a finire su di un sistema in cui uno dei tre componenti viene applicato in maniera erronea.

Esiste un'altra possibilità, cioè che sul sistema obiettivo - quello su cui viene visualizzato il testo - non esista il font necessario per la visualizzazione (ad esempio, mancano i caratteri Giapponesi). Questo errore si elimina semplicemente installando un set di font completi (spesso chiamati font Unicode).

Il problema fondamentale è risolto quando si riescono a ricostruire la tripletta di partenza, quella di arrivo, e a determinare la tecnica corretta di traduzione tra le due.

Il teorema di non calcolabilità della codifica

Purtroppo, quello che ho detto in precedenza è sufficiente anche per enunciare quello che io (e io solo, per quel che ne so) chiamo "il principio di non calcolabilità della transcodifica":

Non esiste un metodo algoritmico per determinare con esattezza la codifica/codepage di un dato file di testo.

La dimostrazione è semplicissima, basta osservare che una qualsiasi sequenza di byte costituisce una "corretta" sequenza nel codepage iso-8859-1 (Latin-1) - in realtà, costituisce una sequenza corretta in molte tra le codifiche non-Unicode. Quindi, da un punto di vista logico, ogni file di testo potrebbe essere stato prodotto almeno con codifica 'Latin-1', e quindi, non è possibile stabilire con certezza la codifica effettivamente usata.

Questo significa che tutte le tecniche di soluzione del problema fondamentale sono procedurali, probabilistiche, o euristiche (e sono quindi tecniche solo in senso lato).

Esaminiamo ora i casi più frequenti e le "tecniche" di soluzione che si possono adottare.

Endianness errata (per una codifica multibyte).

Questo tipo di errore in pratica si verifica di rado, e l'occorrenza più frequente si ha quando si legge un nastro prodotto su di un altro sistema (cosa che da qualche anno è abbastanza rara). Se si tratta di file codificati in UTF-16, questo errore può essere risolto esaminando il BOM all'inizio del file. Per altri encoding, se non è nota la endianness della macchina su cui il file è stato creato, è spesso necessario provare a cambiare l'ordine dei byte, cercando di inferire la correttezza del file risultante dall'esame diretto.

Codice/codifica errati

Ovvero: il codice/codifica per cui il file è stato generato non è quello atteso sulla macchina obiettivo. Questo è il caso che si verifica più di frequente.

La prima cosa da fare è accertarsi su quale tipo di (codice, codifica, endianness) stia usando il sistema obiettivo. Questo può essere sorprendentemente complicato da una serie abbastanza lunga di circostanze. Ad esempio, i browser web cercano di dedurre, speso in maniera euristica, il codice e la codifica delle pagine web, e di adattarvisi; non di rado l'euristica è errata, e questo procedimento va ricostruito a ritroso prima di iniziare l'analisi del file di partenza. Se sono assenti fattori legati alla particolare applicazione in uso, le condizioni attese sono determinate dal codepage della macchina obiettivo, che ad esempio, per un sistema windows, comprendono il codepage ANSI CP_ACP e i regional settings, per un sistema Linux il LOCALE (e le variabili d'ambiente correlate). Anche in questo caso, è difficile fare un elenco esaustivo. Fortunatamente è possibile in molti casi fare qualche deduzione di massima probabilità: ad esempio, se il linguaggio della macchina è italiano, e il sistema operativo è windows si può presumere di essere nel codepage windows-1252.

Quando si sia determinato con un certo gradi di sicurezza il codice atteso sulla macchina obiettivo, bisogna cercare di determinare quale fosse il codice utilizzato in partenza. Il teorema di non computabilità esclude che questo possa essere fatto con certezza, ma non è (ancora) il caso di disperarsi.

In ogni caso, tutte le volte che la rappresentazione del testo contiene un'elevata percentuale di caratteri grafici e/o di controllo, si può essere abbastanza sicuri che la codifica in uso sia scorretta. Può sembrare banale, ma questo vale solo se si è sicuri che il file di partenza fosse un file di testo, o assimilabile - cosa che può non sempre essere vera. Sui sistemi Linux/Unix, conviene perciò consultare l'output del comando "file <nomefile>": se il risultato è ad esempio "file PDF", oppure "file compresso", questo è un indizio che le nostre ipotesi di partenza erano errate.

Il caso "facile"

Prima di affrontare il caso più generale, vediamo qualche esempio che si presenta di frequente sui sistemi in lingua italiana, per testi italiani o europei occidentali. Per questi casi, nella mia esperienza il problema più frequente di questi tempi è quello in cui un testo Unicode viene interpretato su una macchina che si aspetta iso-8859-x, o viceversa. Per la parte Unicode, la codifica sarà al 99.99% dei casi UTF-8 o UTF-16. Per i linguaggi europei è relativamente facile distinguere il secondo caso: basta esaminare il file con un editor binario (o odump su Linux) e vedere se ci sono zone estese in cui byte nulli e non nulli si alternano: in questo caso siamo in presenza di un file Unicode con codifica UTF-16 (vale la pena ribadire che, se il file in questione contiene, ad esempio, un romanzo giapponese, questo metodo NON funziona, visto che le codifiche dei codepoint giapponesi non prevedono byte nulli).

Il caso in cui la codifica è UTF-8 si può individuare osservando le modifiche che vengono fatte alle accentate. Questo è più facile quando un file UTF-8 arriva su una macchina iso-8859-x, che in Italia è praticamente sempre una macchina iso-8859-1 o CP windows-1252. In questo caso si osserva che tutte le lettere non accentate vengono tradotte correttamente, mentre le accentate vengono tradotte con due caratteri "esotici", il primo dei quali è una A maiuscola sormontata da una tilde (Ã). Se la lingua in cui è scritto il file non è l'italiano, si osserverà lo stesso fenomeno sui diacritici tipici della lingua stessa (ad esempio, per il tedesco, le dieresi) o sui segni di interpunzione "rari" (certi tipi di virgolette) o su simboli semi-grafici (simbolo dell'euro o comunque di valuta - dollaro escluso - simbolo di copyright). Questo comportamento è dovuto al fatto che tutti questi caratteri hanno una codifica UTF-8 pari a due byte, mentre i sistemi iso-8859-x decodificano un carattere per ogni byte.

Quando ci si trova nel caso inverso, (codifica attesa UTF-8, codifica effettiva iso-8859-x) si hanno sintomi un po' più vari che dipendono dall'applicativo in uso. I casi normali sono quelli in cui non viene segnalato nessun errore, ma le accentate mancano e sono sostituite, assieme al carattere successivo, da caratteri diversi (spesso un punto interrogativo bianco in campo nero). In alternativa, il programma che si usa per visualizzare il testo segnala un errore: quando questo errore è sufficientemente esplicativo (caso più raro di quanto non si creda) è possibile risalire al carattere che lo ha provocato: tabelle alla mano, si può poi vedere a quale carattere esso dovrebbe corrispondere. Il motivo di questo comportamento è che i segni diacritici, che in ISO-8859-x occupano i codepoint 128-255, avendo il bit più significativo a 1, vengono interpretati come l'inizio di una sequenza multibyte UTF-8 e il più delle volte, la sequenza ottenuta "mangiando" il byte successivo non è una codifica UTF-8 valida.

Altro fattore rivelatore è che laddove sia possibile esaminare il testo (UTF-8 o ISO-8859-x) con un editor (magari binario) è che le parti - se ce ne sono - contenenti sequenze di caratteri occidentali anglosassoni (cioè caratteri ASCII) sono invariate.

Il caso generale

Se quanto sopra non è di aiuto, l'unica cosa che resta da fare è prepararsi ad andare per tentativi. A questo fine è utile la seguente checklist:

1) Procurarsi quante più informazioni possibili sulla provenienza del file. Se possibile bisogna individuare l'applicazione che l'ha prodotto, consultare la documentazione che può essere disponibile e/o il sito del produttore, consultare Google ed altri motori di ricerca. Spesso è possibile - ed utile - parlare con la persona che ha prodotto il file.

2) Esaminare il file con altri mezzi. Un buon editor di testo è utilissimo (io direi indispensabile). Io utilizzo emacs, che dalla release 23 offre un ottimo supporto a molti codici e codifiche: a volte mi basta aprire un file con emacs per dedurre codice e codifica.

3) Non dimenticarsi dell'ovvio. La destinazione del file (se si può determinare) spesso fornisce tutte le informazioni che servono per dedurre codice e codifica. Ad esempio i file XML (sempre riconoscibili a causa dell'intestazione che deve essere presente nella prima riga) devono dichiarare esplicitamente l'encoding usato: se non lo fanno, il loro encoding deve essere UTF-8

4) Procurarsi una cassetta degli attrezzi per la transcodifica il più munita ed agguerrita possibile e utilizzarla per provare tutte le transcodifiche plausibili in ordine di probabilità decrescente secondo quanto si è determinato nei passi precedenti (ad esempio, per un file giapponese si inizierà provando le codifiche JIS). Prima di cominciare è utile - usando un editor - isolare un piccolo segmento di testo da analizzare, sfruttando il fatto che caratteri come gli spazi sono invarianti tra le varie codifiche: idealmente si dovrebbe identificare e isolare un segmento di testo contenente anche una porzione di caratteri occidentali (ad esempio un indirizzo: si ricordi che i caratteri occidentali anglosassoni sono invarianti per la maggior parte delle codifiche). E' anche possibile (e forse consigliabile) usare strumenti che automatizzano il procedimento per tentativi - anche se sempre usando un approccio euristico/probabilistico. Ad esempio lo Universal Encoding Detector utilizza la stessa euristica utilizzata nei browser.

In appendice riporto un paio di funzioni (python) che sono abbastanza utili per la manipolazione di dati multilingua.

Parte della difficoltà di questa fase della ricerca della soluzione è avere una chiara immagine mentale di quello che si sta cercando di ottenere e interpretare correttamente quello che stanno facendo i propri attrezzi. Io personalmente trovai a suo tempo illuminanti (riguardo al linguaggio di programmazione python, che uso abbastanza di frequente) le considerazioni e i metodi esposti in questa URL: http://code.activestate.com/recipes/466341/

Piccoli temi di programmazione

Quando si arriva a cercare di risolvere il "problema fondamentale" per tentativi, si deve quasi per forza ricorrere all'uso di qualche tipo di programmazione. La frase ricorrente in questo frangente è: 'il linguaggio "X" supporta Unicode'. Cosa questo significhi in generale è tutt'altro che chiaro. Io sono arrivato ad una spiegazione di questa frase che mi pare abbastanza vicino al vero, anche se non posso garantire che questa valga per tutti i linguaggi di programmazione.

La mia interpretazione è:

"Il linguaggio 'X' è in grado di rappresentare i suoi oggetti testuali (stringhe) come sequenza di codepoint Unicode ed è - viceversa - in grado di interpretare correttamente una sequenza di codepoint Unicode come un oggetto testuale."

Quello che è egregiamente assente da questa definizione è la menzione del processo di codifica/decodifica che sposta le stringhe tra le rappresentazioni interna (al linguaggio) ed esterna (sistema operativo, resto del mondo etc.)

Su questa, infatti, ogni linguaggio ha da dire la sua, e non è detto che il coro che ne risulta sia consonante.

L'approccio duro e puro è quello del C, in cui rappresentazione interna ed esterna coincidono, il che significa che le stringhe C riflettono esattamente le sequenza di byte ricevute dal mondo esterno. Lavorare sulla loro codifica richiede l'uso di librerie esterne (IBM m pare abbia un ICU multilingual library che è gratuita). Se non mi sono perso qualcosa, il C++ adotta un approccio simile. Niente di male se avete Developer Studio o automake in esecuzione dal mattino alla sera. Se invece il vostro profilo professionale è un po' diverso, suggerirei di lasciar perdere durezza e purezza e cercare qualcosa di meglio.

Unicode e Linguaggi dinamici

Il titolo di questa sezione è abbastanza esagerato. Ho intenzione di parlare di due linguaggi dinamici (perl e python) e dare dettagli su uno solo (python).

Il motivo per cui accantonerei perl in prima battuta è che (e lo dico da programmatore perl convinto) python mi pare avere un supporto UNICODE migliore di quello di perl, se non altro dal punto di vista della terminologia (che è quello che interessa di più in questa sede). Una volta che acquisita familiarità con la terminologia, e dal punto di vista di questa trattazione, direi che la funzionalità dei due linguaggi in questo campo è simile.

Python, internamente, supporta due tipi di stringa: Unicode e stringhe ordinarie o codificate. Si può pensare che le stringhe Unicode siano composte di una sequenza di codepoint, e che le stringhe ordinarie siano composte da una sequenza di byte.

Creare una stringa Unicode è semplice:

us=u'\u00e8\u00e1'

us, così definita, rappresenta la sequenza "èá": 00e8 (232 in esadecimale) e 00e1 (225 in esadecimale) sono i codepoint relativi.

Data una stringa codificata (e vedremo dopo come ottenerla) è possibile ottenere la relativa stringa Unicode posto che si conosca l'encoding della stringa codificata. Basta infatti eseguire la decodifica:

    us=cs.decode('encoding_della_stringa')
  

Ad esempio per la consueta sequenza "èá":

    cs='\xe8\xe1'
    us=cs.decode('Latin-1')
  

Sfortunatamente (dal punto di vista della chiarezza) esiste un altro modo (che è normalmente citato per primo) per fare la stessa conversione:

    us=unicode(cs,'Latin-1') # or the string encoding
  

Per evitare confusioni, io leggo mentalmente questa istruzione come "costruisci una stringa Unicode decodificando cs dall'encoding 'Latin-1')".

Naturalmente le operazioni sopra illustrate funzionano correttamente se e solo se viene specificata la giusta codifica ('Latin-1'). Non so se ho sottolineato a sufficienza il fatto (che è importantissimo tener ben presente) che una stringa Unicode è un oggetto abbastanza astratto: in particolare non è possibile salvarla, stamparla o rappresentarla senza prima applicarle un encoding: e - fatto forse sorprendente - l'encoding da applicare non è necessariamente uno di quelli riservati alla codifica di Unicode (essenzialmente UTF8 o UTF16).

Infatti è perfettamente possibile - e in questo contesto lecito - codificare una sequenza di codepoint Unicode in (ad esempio) Latin-1, posto che il carattere corrispondente esiste in questa codifica. Ad esempio è possibile rappresentare in Latin-1 il codepoint 'U+00e8, ma non il carattere Kanji U+4e01. Allo stesso modo è possibile rappresentare entrambi i caratteri dell'esempio precedente codificandoli in shift-jis-2004 o, ovviamente, in UTF8 o UTF16. (una lista parziale di encoding supportati da una installazione standard di python è in appendice).

Ciò detto, passare da una stringa Unicode (us) ad una stringa codificata (cs) è abbastanza semplice:

    cs=us.encode(encoding_desiderato)

ad esempio:

    us=u'\u00e8'
    cs=us.encode('Latin-1') #contiene '\xe8'

    us=u'\u00e8\u4e01'      # contiene un ideogramma: è丁
    cs=us.encode('Latin-1') #errore

  UnicodeEncodeError: 'latin-1' codec can't encode character
  u'\u4e01' in position 1: ordinal not in range(256)

    cs=us.encode('shift-jis-2004') # contiene '\x85}\x92\x9a'
    cs=us.encode('utf8') #contiene '\xc3\xa8\xe4\xb8\x81'
    cs=us.encode('utf16') #contiene '\xff\xfe\xe8\x00\x01N'

Componendo le due operazioni, si può tradurre da una codifica ad un'altra (transcodifica):

    us=cs_source.decode(source_encoding)
    cs_target=us.encode(target_encoding)

questo può essere fatto se e solo se i due encoding sono compatibili (cioè target è in grado di rappresentare tutti i codepoint di source).

In particolare, è sempre possibile transcodificare in UTF-8 (se si ha a disposizione il codec per la codifica di partenza: i codec a disposizione di python sono in appendice):

    us=cs_source.decode(source_encoding)
    cs_target=us.encode('utf8')
  

Cosa succede se cerchiamo di scrivere una stringa Unicode senza codificarla?

    cs=u'u'\u00e8' f=file('/tmp/ciccio','a') f.write(cs)

    UnicodeEncodeError: 'ascii' codec can't encode characters in
    position 0-1: ordinal not in range(128)
  

La risposta è che l'interprete - quando effettua I/O e conversioni di stringhe Unicode - cerca di codificare/decodificare la stringa per noi, utilizzando un encoding di default: in questo caso codifica con l'encoding ascii (in cui le accentate non esistono, da cui l'errore).

Quindi, lavorare con Unicode in python richiede:

1) decodifica delle stringhe in ingresso 2) codifica delle stringhe in uscita

Oppure, attraverso l'uso del modulo codecs si può decorare un filehandle attraverso il codificatore desiderato:

    import codecs

    f=codecs.open('/tmp/ciccio','UTF-8','r')
    g=codecs.open('/tmp/ciccia','latin-1','w') 
    us=f.read()
    g.write(us)
  

a questo punto tutte le stringhe lette da f saranno decodificate con UTF-8 e convertite a stringhe Unicode, mentre tutte le stringhe Unicode scritte su g saranno codificate in Latin-1 (quindi sarà bene che da f non arrivino stringhe contenenti caratteri coreani, o la scrittura darà errore). Sarà inoltre bene astenersi dal cercare di scrivere stringhe codificate (byte) su g: in fatti a questo punto ogni scrittura su g di stringhe di byte è preceduta da una codifica implicita, fatta usando il default (ASCII); questo probabilmente non è quello che ci si aspetta, o che si desidera.

Naturalmente, quando non si stanno risolvendo problemi che richiedono l'uso di set di caratteri multilingua, vale a dire nella normale programmazione in python, è molto probabile che le comuni byte string vadano più che bene per ciò che ci serve fare.

Un'altra considerazione riguarda la presenza di caratteri non-ascii all'interno di un file di sorgenti python (questa è un'altra accezione di supporto Unicode). In breve: è possibile farlo, basta specificare:

#-*- coding: iso-8859-1 -*-

- o altro encoding - verso l'inizio del file. Il mio consiglio è, non fatelo: alla lunga è una cosa che romperà le scatole a voi, ai vostri colleghi e soprattutto a chiunque altro dovesse lavorare con voi sullo stesso file.

Encoding impliciti, e la loro maledizione

Lavorare con Unicode e con alfabeti multinazionali è reso più complicato dal fatto che le varie periferiche di I/O tentano di "aiutare" l'utente facendo del loro meglio per interpretare quello che gli viene dato da presentare. Questo è perfetto per l'uso interattivo (specie quando funziona). Per risolvere i problemi di cui abbiamo parlato fin qui, è atroce. Questo è il motivo per cui tutti gli esempi precedenti sono stati scritti utilizzando i caratteri in rappresentazione numerica. Le relazioni tra tipi di stringhe ed encoding sono già abbastanza confuse senza che si debba tenere conto dell' encoding che ogni dispositivo di I/O utilizza implicitamente: questa circostanza è particolarmente perniciosa se si usa un interprete interattivo.

Un esempio a questo punto può essere utile. Sul sistema che sto utilizzano ultimamente per scrivere (emacs 23.1, Fedora Core 11, IPython), la seguente interazione con l'interprete ha i risultati illustrati:

    In [270]: import sys
    In [270]: sys.stdin.encoding
    Out[271]:
  'UTF-8'
    In [272]: cs='è'
    In [273]: repr(cs)
    Out[273]:
  "'\\xc3\\xa8'"

che tradotto significa: scrivere la sequenza 'è' sulla console di questo interprete, il cui encoding implicito in input è UTF-8 dà una stringa codificata (byte string) il cui contenuto è "'\xc3\xa8'"

La stessa sequenza, su un'altro sistema, diventa:

    In [270]: import sys
    In [270]: sys.stdin.encoding
    Out[271]:
  'latin_1'
    In [272]: cs='è'
    In [273]: repr(cs)
    Out[273]:
  "'\\xe8'"

che tradotto significa: scrivere la sequenza 'è' sulla console di questo interprete, il cui encoding implicito in input è Latin-1 dà una stringa codificata (byte string) il cui contenuto è "\xe8"

Se questo pare innocuo, si rifletta sul fatto che, per ottenere una stringa Unicode sul sistema (1) bisogna ora impartire l'istruzione:

    us=cs.decode('utf-8')
  

e sul sistema 2:

    us=cs.decode('latin-1')
  

Non so a voi, ma a me fa girare la testa.

Unicode, encoding e HTML

Come XML, anche HTML è un formato che ha preso coscienza abbastanza presto (in teoria, fin dalla nascita) delle questioni relativa all'uso di alfabeti multilingua. Purtroppo, la manica larga che i browser hanno tradizionalmente usato nei confronti delle prescrizioni degli standard relativi ha reso questo campo una delle peggiori babele immaginabili.

Questa è una breve lista di fatti relativa al supporto multilingue in HTML, senza alcuna pretesa di completezza (che lascio volentieri al W3 consortium).

Entità con nome

Indipendentemente da ogni altra circostanza, è possibile specificare un ristretto numero di caratteri nazionali ricorrendo alle 'named entities' di HTML, che comprendono, fra l'altro tutte le accentate (quindi gli italiani sono - quasi - a posto) e diversi simboli di uso comune . Ad esempio l'entità &agrave; viene mostrata come "à".

Entità numeriche

Indipendentemente da ogni altra circostanza, è possibile specificare l'intero set dei codepoint di Unicode esprimendoli come entità numeriche, cioè facendo precedere il numero (decimale) del codepoint da &# e facendolo seguire da ";", così:

&#8212; visualizzato come: '—'

in esadecimale:

&#x2014; visualizzato come: '—'

Chiaramente, nessun giapponese potrà mai scrivere un romanzo così (a meno che non sia il suo word processor a fare questa traduzione in automatico). Se non bastasse , farsi un'idea del contenuto di una pagina html scritta nel formato di cui sopra è quasi impossibile.

Dichiarazione del contenuto HTML

La strada maestra per la creazione di pagine HTML multilingua corrette è dichiarare il charset del documento:

<meta http-equiv="content-type" content="text-html; charset=utf-8">

"charset" è il modo HTML di chiamare l'encoding.

Un documento che specifichi il charset nell'intestazione, e lo usi consistentemente, è al sicuro, almeno se il browser che viene usato dai visitatori supporta l'encoding specificato e se il server web non decide di appiccicare al vostro documento un charset diverso, sovrascrivendo quello da voi dichiarato. (Quest' ultimo incidente è quello che mi è accaduto quando ho pubblicato questo documento sul web.) Potendo, vale comunque la pena di specificare UTF-8, che, di questi tempi, è quello che ha maggior supporto e compatibilità. Naturalmente siamo ben lontani dalla realtà e questo per alcuni fatti storici.

1) Il charset è raramente specificato dall'autore del documento - più spesso è assegnato automaticamente dai tool di editing, che non sempre c'azzeccano. In ogni caso si tratta spesso di un charset o codepage nazionale (windows-1252, per gli italiani) e non del più portabile utf-8.

2) Esistono ancora, e presumibilmente ne vengono prodotte ogni giorno di più, pagine che non specificano il charset. In questo caso, queste pagine dovrebbero contenere solo caratteri ASCII, e tutti gli altri caratteri dovrebbero essere espressi come entità con nome o entità numeriche. Questo in realtà non avviene perché:

3) Molti browser cercano d'inferire il charset dal contenuto del documento, e poi

4) Molti server cercano di "aiutare" i browser fornendo anch'essi un charset d'appoggio.

Non ho bisogno di dire che in queste condizioni risolvere i problemi di display di HTML è un rebus non molto meno difficile (e con l'aggiunta di dover distinguere testo da markup) di quello illustrato nella resto di questo documento. Gli strumenti e i consigli che ho dato più sopra tendono ad essere comunque utili anche in questa circostanza.

Database

Per finire accenno al tema della codifica nel campo delle basi di dati solo per dichiarare la mia più completa inadeguatezza a trattarla, e per dare alcuni consigli che possono tornare utili (ma non sono disposto ad assumermi alcuna responsabilità).

Ciò che rende la questione delle codifiche particolarmente spinoso, nel caso dei database, è dato il fatto che, mentre un approccio sistematico al problema è relativamente recente, esiste una straordinaria varietà di approcci storici che risale all'alba del calcolo elettronico, approcci rigorosamente diversificati secondo il produttore del DBMS, del protocollo di comunicazione, dello strumento di reporting, del sistema operativo e così via. Inoltre nel caso delle basi di dati diventano di fondamentale importanza alcuni dei fattori che ho allegramente trascurato nel caso dei file di testo, tra cui l'ordinamento (collation) delle stringhe, le unità monetarie, i sistemi di datazione, le rappresentazione dei numeri.

Se quello che interessa è la conversione dell'output di un report, siamo nel caso delle conversioni di file di testo,che abbiamo visto in precedenza.

Se quello che serve è costruire un sistema da zero, il mio consiglio è: fate tutto in UTF-8 e provate tutte le componenti con stringhe provenienti da vari linguaggi. In questo caso "tutte" le componenti comprendono il DBMS, i sistemi e i protocolli di comunicazione, i linguaggi e le librerie usate per la programmazione, gli strumenti di reporting e gli strumenti a linea comando.

Se quello che dovete fare è convertire un sistema "legacy" ad un sistema multilingue, non so che dire: leggete la documentazione del produttore (o dei produttori) e che Dio vi aiuti.

Una facilitazione - rispetto al caso della codifica di file di testo arbitrari- è che normalmente le basi di dati documentano la codifica che utilizzano per i dati di tipo testo. Tale codifica è molto spesso una proprietà di tutto il database (o addirittura della particolare installazione). Questa facilitazione, nei casi di database legacy, è però spesso inficiata dal fatto che non di rado gli architetti della base di dati originale si sono inventati modi "originali", spesso non documentati, di accomodare altri linguaggi all'interno della codifica disponibile, soprattutto quando questa coincideva con US-7 (in pratica, ASCII). Altro ostacolo è che alcuni DBMS in passato hanno adottato nomi proprietari per le codifiche in uso, peggiorando il già babelico stato dell'arte.

Alcuni DBMS moderni (per quello che ne so, SQL Server, dalla versione 2005, è tra questi) hanno ritenuto di fare ammenda per aver in passato consentito una configurazione di codifiche estremamente spartana permettendo l'attribuzione di codifiche diverse per il database, per ogni tabella al suo interno e per ogni campo di ogni tabella. La mia opinione è che questa - come il rispondere a lettere provenienti dalla Nigeria che promettono ingenti somme di denaro - è una opportunità da non cogliere assolutamente, se si tiene alla propria salute mentale.

copyryght © Alessandro Forghieri
tutti i diritti riservati
Modena, 14 Dicembre 2009
$Id: Unicode.html,v 1.4 2009/12/30 11:18:59 alf Exp $

Appendice A

import unicodedata

def unilist(u):
    """ prints the unicde description of a string """
    for i, c in enumerate(u):
        print i, '%04x' % ord(c), unicodedata.category(c),
        print unicodedata.name(c)

def safe_unicode(obj, *args):
    """ return the unicode representation of obj """
    try:
        return unicode(obj, *args)
    except UnicodeDecodeError:
        # obj is byte string
        ascii_text = str(obj).encode('string_escape')
        return unicode(ascii_text)

def safe_str(obj):
    """ return the byte string representation of obj """
    try:
        return str(obj)
    except UnicodeEncodeError:
        # obj is unicode
        return unicode(obj).encode('unicode_escape')


Appendice B: tabella degli encoding standard per python

Codec Aliases Languages
ascii 646, us-ascii English
big5 big5-tw, csbig5 Traditional Chinese
big5hkscs big5-hkscs, hkscs Traditional Chinese
cp037 IBM037, IBM039 English
cp424 EBCDIC-CP-HE, IBM424 Hebrew
cp437 437, IBM437 English
cp500 EBCDIC-CP-BE, EBCDIC-CP-CH, IBM500 Western Europe
cp737 Greek
cp775 IBM775 Baltic languages
cp850 850, IBM850 Western Europe
cp852 852, IBM852 Central and Eastern Europe
cp855 855, IBM855 Bulgarian, Byelorussian, Macedonian, Russian, Serbian
cp856 Hebrew
cp857 857, IBM857 Turkish
cp860 860, IBM860 Portuguese
cp861 861, CP-IS, IBM861 Icelandic
cp862 862, IBM862 Hebrew
cp863 863, IBM863 Canadian
cp864 IBM864 Arabic
cp865 865, IBM865 Danish, Norwegian
cp866 866, IBM866 Russian
cp869 869, CP-GR, IBM869 Greek
cp874 Thai
cp875 Greek
cp932 932, ms932, mskanji, ms-kanji Japanese
cp949 949, ms949, uhc Korean
cp950 950, ms950 Traditional Chinese
cp1006 Urdu
cp1026 ibm1026 Turkish
cp1140 ibm1140 Western Europe
cp1250 windows-1250 Central and Eastern Europe
cp1251 windows-1251 Bulgarian, Byelorussian, Macedonian, Russian, Serbian
cp1252 windows-1252 Western Europe
cp1253 windows-1253 Greek
cp1254 windows-1254 Turkish
cp1255 windows-1255 Hebrew
cp1256 windows-1256 Arabic
cp1257 windows-1257 Baltic languages
cp1258 windows-1258 Vietnamese
euc_jp eucjp, ujis, u-jis Japanese
euc_jis_2004 jisx0213, eucjis2004 Japanese
euc_jisx0213 eucjisx0213 Japanese
euc_kr euckr, korean, ksc5601, ks_c-5601, ks_c-5601-1987, ksx1001, ks_x-1001 Korean
gb2312 chinese, csiso58gb231280, euc-cn, euccn, eucgb2312-cn, gb2312-1980, gb2312-80, iso-ir-58 Simplified Chinese
gbk 936, cp936, ms936 Unified Chinese
gb18030 gb18030-2000 Unified Chinese
hz hzgb, hz-gb, hz-gb-2312 Simplified Chinese
iso2022_jp csiso2022jp, iso2022jp, iso-2022-jp Japanese
iso2022_jp_1 iso2022jp-1, iso-2022-jp-1 Japanese
iso2022_jp_2 iso2022jp-2, iso-2022-jp-2 Japanese, Korean, Simplified Chinese, Western Europe, Greek
iso2022_jp_2004 iso2022jp-2004, iso-2022-jp-2004 Japanese
iso2022_jp_3 iso2022jp-3, iso-2022-jp-3 Japanese
iso2022_jp_ext iso2022jp-ext, iso-2022-jp-ext Japanese
iso2022_kr csiso2022kr, iso2022kr, iso-2022-kr Korean
latin_1 iso-8859-1, iso8859-1, 8859, cp819, latin, latin1, L1 West Europe
iso8859_2 iso-8859-2, latin2, L2 Central and Eastern Europe
iso8859_3 iso-8859-3, latin3, L3 Esperanto, Maltese
iso8859_4 iso-8859-4, latin4, L4 Baltic languages
iso8859_5 iso-8859-5, cyrillic Bulgarian, Byelorussian, Macedonian, Russian, Serbian
iso8859_6 iso-8859-6, arabic Arabic
iso8859_7 iso-8859-7, greek, greek8 Greek
iso8859_8 iso-8859-8, hebrew Hebrew
iso8859_9 iso-8859-9, latin5, L5 Turkish
iso8859_10 iso-8859-10, latin6, L6 Nordic languages
iso8859_13 iso-8859-13 Baltic languages
iso8859_14 iso-8859-14, latin8, L8 Celtic languages
iso8859_15 iso-8859-15 Western Europe
johab cp1361, ms1361 Korean
koi8_r Russian
koi8_u Ukrainian
mac_cyrillic maccyrillic Bulgarian, Byelorussian, Macedonian, Russian, Serbian
mac_greek macgreek Greek
mac_iceland maciceland Icelandic
mac_latin2 maclatin2, maccentraleurope Central and Eastern Europe
mac_roman macroman Western Europe
mac_turkish macturkish Turkish
ptcp154 csptcp154, pt154, cp154, cyrillic-asian Kazakh
shift_jis csshiftjis, shiftjis, sjis, s_jis Japanese
shift_jis_2004 shiftjis2004, sjis_2004, sjis2004 Japanese
shift_jisx0213 shiftjisx0213, sjisx0213, s_jisx0213 Japanese
utf_32 U32, utf32 all languages
utf_32_be UTF-32BE all languages
utf_32_le UTF-32LE all languages
utf_16 U16, utf16 all languages
utf_16_be UTF-16BE all languages (BMP only)
utf_16_le UTF-16LE all languages (BMP only)
utf_7 U7, unicode-1-1-utf-7 all languages
utf_8 U8, UTF, utf8 all languages
utf_8_sig all languages

Encoding di comodo:

Codec Aliases Operand type Purpose
base64_codec base64, base-64 byte string Convert operand to MIME base64
bz2_codec bz2 byte string Compress the operand using bz2
hex_codec hex byte string Convert operand to hexadecimal representation, with two digits per byte
idna Unicode string Implements RFC 3490, see also encodings.idna
mbcs dbcs Unicode string Windows only: Encode operand according to the ANSI codepage (CP_ACP)
palmos Unicode string Encoding of PalmOS 3.5
punycode Unicode string Implements RFC 3492
quopri_codec quopri, quoted-printable, quotedprintable Convert operand to MIME quoted printable
raw_unicode_escape Unicode string Produce a string that is suitable as raw Unicode literal in Python source code
rot_13 rot13 Unicode string Returns the Caesar-cypher encryption of the operand
string_escape byte string Produce a string that is suitable as string literal in Python source code
undefined any Raise an exception for all conversions. Can be used as the system encoding if no automatic coercion between byte and Unicode strings is desired.
unicode_escape Unicode string Produce a string that is suitable as Unicode literal in Python source code
unicode_internal Unicode string Return the internal representation of the operand
uu_codec uu byte string Convert the operand using uuencode
zlib_codec zip, zlib byte string Compress the operand using gzip

copyryght © Alessandro Forghieri
tutti i diritti riservati
Modena, 14 Dicembre 2009