Installare e usare Python su Windows (parte 9/10).

Attenzione!

Queste guide non sono più aggiornate, e in alcuni punti sono proprio superate.
Il mio libro Python in Windows tratta questi temi, e molto altro, in modo molto più completo e aggiornato. 

Python, Windows e Unicode.

Dedichiamo questa sezione all’esame di alcuni problemi legati all’uso di Unicode in Windows. Ricordiamo che questa non è una guida introduttiva a Unicode. Le note che seguono presuppongono una certa conoscenza operativa su Unicode, gli encoding Unicode e gli encoding regionali, gli strumenti Unicode di Python e il loro uso.

Unicode nella shell.

Se usate Python per costruire programmi a interfaccia grafica (GUI), in genere non dovete preoccuparvi troppo di Unicode. I moderni GUI framework supportano Unicode in maniera trasparente, e in pratica per usarli basta sapere come funziona una stringa in Python. Se invece la parte I/O del vostro programma si appoggia alla shell (come avviene sempre, tra l’altro, nei programmi dei principianti), allora è molto facile che finirete per inciampare in qualche problema legato a Unicode. In genere il principiante non si preoccupa se ogni tanto nei suoi esercizi qualche carattere “strano” viene fuori “sbagliato”. Tuttavia esistono anche programmi professionali a interfaccia testuale (CLI), e se intendete percorrere questa strada dovete considerare attentamente Unicode.
Per cominciare, tenente presente che il problema spesso non riguarda Python. Se il font usato dalla shell non ha un certo glifo, non può visualizzare quel carattere. I font disponibili in cmd.exe sono in genere Lucida Console e Consolas, ed entrambi coprono relativamente poco dello spazio Unicode. Potete cambiare font facendo clic col pulsante destro nella barra del titolo della shell e selezionando “Proprietà”. Fate la prova con questi caratteri cirillici esotici, che Consolas copre ma Lucida Console no: “ѧ Ѩ Ѫ ѭ Ѯ”. Copiateli da questa pagina del vostro browser e provate a incollarli nella shell (clic col destro, e “incolla”). A seconda del font impostato, vedrete correttamente i glifi oppure solo dei “segnaposto”. Potete controllare esattamente quali caratteri sono supportati da un font usando tool come BabelMap. Aggiungere un font a quelli disponibili in cmd.exe è possibile, ma solo aggiungendo una chiave alla sezione HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Console\TrueTypeFont del registro di configurazione: in definitiva non conviene. Il problema di fondo è che cmd.exe non è una shell Unicode: se avete bisogno di visualizzare caratteri arbitrari, usate ConEmu (di cui abbiamo già parlato), o PowerShell ISE (già disponibile in Windows). Queste shell sono in grado di visualizzare contemporaneamente diversi font, e ricorrono di volta in volta a quelli in grado di farvi vedere i caratteri necessari (all’incirca come fa il vostro browser). Resta inteso che, se nessun font tra quelli installati sulla vostra macchina copre un certo carattere, allora in quel punto resterà sempre un “segnaposto”.
In secondo luogo, ricordate che la shell non usa di solito lo stesso encoding di default che usa Windows (detto “encoding Ansi”). Almeno da noi in Italia, “Ansi” è cp1252 (che, vale la pena di ricordarlo, non coincide con “Latin 1” come spesso si crede) mentre cmd.exe usa l’encoding “OEM” che per noi in Italia è cp850. Anche se Python in pratica può ignorare la cosa (ma solo a partire dalla versione 3.6 compresa!), quando usate strumenti della shell come type dovete ricordarvi della discrepanza. Per esempio, aprite un file con il Blocco Note, scriveteci dei caratteri accentati qualsiasi, e salvate. Se adesso dalla shell provate a vedere il contenuto del file con type test.txt, vedrete caratteri sbagliati perché Blocco Note di default salva in cp1252. All’inverso, se inviate il contenuto della shell a un file di testo, per esempio con copy con test.txt, quando aprite il file con il Blocco Note vedrete di nuovo caratteri sbagliati perché la shell lavora in cp850 e Blocco Note apre il file in cp1252. Potete in ogni caso cambiare l’encoding della shell, impostando per esempio quello di default di Windows, con il comando chcp 1252: dopo di che, queste operazioni funzioneranno senza problemi (ricordiamo che chcp senza argomenti vi mostra l’encoding attualmente attivo).
Per fortuna in Python, almeno a partire dalla versione 3.6, questo non è un problema. Operazioni come print e input riescono senza problemi, grazie al fatto che la Pep 528 impone ormai a Python di comunicare con la shell usando le versioni Unicode delle API di Windows. Questo vuol dire che voi potete ignorare i dettagli dell’encoding della shell, Windows fa tutto il lavoro sporco dietro le quinte e voi dal lato Python vi ritrovate sempre con le stringhe Unicode “giuste”. Provate a eseguire questo semplice script:
# test.py
print('test Latin 1: à á â ã ä å æ ç è é ê')
print('test Greek: ά έ ή ί ΰ α β γ δ ε ζ η θ')
print('test Cyrillic: й к л м н о п р с т у ф\n')
s = input('scrivi qualcosa: ')
print('\nquello che hai scritto:', s)
Potete provare questo script sia con cp850 sia con cp1252 nella shell, ma il risultato è sempre buono (almeno finché non cercate di stampare un carattere non supportato dal font in uso!), e non dovete preoccuparvi di fare manualmente operazioni di decode e di encode nel codice Python.
La situazione precedente a Python 3.6 (compreso quindi Python 2.7) è… non buona. Prima della Pep 528 Python usava le vecchie API Ansi di Windows, deprecate da tempo, e questo comportava parecchi problemi. Se vi serve un’analisi approfondita della situazione precedente, potete consultare la Issue 1602 nel bug tracker di Python, o leggervi il Readme della Windows Unicode Console che implementa molti dei fix che poi sono stati inseriti nella Pep 528. Anche se non avete Python 3.5 installato, potete ripristinare il vecchio comportamento settando la variabile d’ambiente PYTHONLEGACYWINDOWSSTDIO (a qualsiasi valore diverso da zero). Se provate a eseguire il nostro script con questo “legacy mode” attivo e l’encoding di default cp850 nella shell, vedrete che prima di tutto la stampa delle righe di test non va a buon fine perchè Python prova a encodarle in cp1252. Anche l’eco dei caratteri inseriti può essere sbagliato: provate per esempio con il simbolo dell’euro… D’altra parte, se impostate l’encoding della shell a cp1252 prima di eseguire lo script, le cose torneranno a funzionare. In alternativa, potete forzare l’encoding giusto direttamente nel codice Python.
Se siete in dubbio, sys.stdout.encoding vi restituisce l’encoding della shell: in Python 3.6+ dovrebbe essere UTF8, altrimenti cp850 o qualsiasi code page abbiate attivato nella shell prima di avviare Python.

Unicode nei nomi dei file.

In Windows il file system è “Unicode” (più precisamente, le path sono in utf16-le), ma Python ha sempre usato le API Ansi di Windows per colloquiare con il file system, con effetti collaterali sgradevoli. La situazione è analoga a quella appena vista per la shell, ed è stata risolta in modo identico: la Pep 529, gemella della Pep 528 vista sopra, impone a Python 3.6+ di usare le API Unicode anche per le chiamate al file system.
Vi rimandiamo alla lettura di questa Pep per i dettagli, ma il succo è questo. Nella situazione precedente, non c’erano comunque problemi almeno finché le path venivano rappresentate come stringhe (in Python 3) ovvero stringhe Unicode (in Python 2). Quando però le path erano rappresentate come bytes (Python 3) ovvero come stringhe “semplici” (Python 2), allora le API Ansi sottostanti usavano naturamente l’encoding Ansi regionale per convertire avanti e indietro da utf16-le. E quando un carattere non è coperto dall’encoding Ansi, finisce sostituito da un segnaposto. Di conseguenza non è garantito che sia possibile “fare il giro completo”, ovvero leggere il nome di un file, conservarlo in una variabile, e usare quella variabile per aprire il file.
Per capire il problema, create un file con qualche carattere cirillico nel nome (ricordate: usiamo i caratteri cirillici perché non sono coperti da cp1252 ma sono comunque visualizzabili dal font Consolas, che vi conviene usare nella shell cmd.exe. Se però usate una shell Unicode come ConEmu o PowerShell ISE, o se semplicemente non vi interessa visualizzare l’output del vostro script nella shell, allora potete sbizzarrirvi con qualsiasi carattere esotico). Diciamo che il file si chiama йклмн.txt. Fate prima una prova con Python 2 (ovvero, situazione pre-Pep 529):
> py -2
[...]
>>> import os
>>> d = os.listdir(u'.') # nota bene! se passo unicode... 
>>> d                    # ... ottengo unicode
[u'\u0439\u043a\u043b\u043c\u043d.txt']
>>> open(d[0]).close()   # a questo punto aprire il file va a buon fine

>>> d = os.listdir('.')  # se invece passo bytes (stringhe normali in py2)... 
>>> d                    # ... ottengo bytes 
['?????.txt']
>>> # ma adesso nella conversione ho perso il nome vero del file...
Il risultato di os.listdir, come di molte altre funzioni analoghe in Python, mantiene il tipo dell’argomento che passate: se passate una path in Unicode, ottenete una (lista di) path in Unicode; se passate bytes, avrete bytes. Il problema però è che dietro le quinte Python usa le API Ansi di Windows, e pertanto quando passate bytes l’encoding è quello locale cp1252, che non è in grado di coprire i caratteri della nostra path, che quindi ritorna mutilata.
La stessa situazione si verifica in Python 3, ricordando che le stringhe “normali” equivalgono alle stringhe Unicode di Python 2. Se avete solo Python 3.6+ installato, potete comunque ripristinare la vecchia situazione settando una variabile d’ambiente:
>set PYTHONLEGACYWINDOWSFSENCODING=1
>py -3
[...]
>>> import os
>>> os.listdir('.')  # usare stringhe (Unicode) funziona...
['йклмн.txt']
>>> os.listdir(b'.') # ... usare bytes (in cp1252) no
[b'?????.txt']
Invece, in Python 3.6+ senza “legacy mode” attivo, finalmente non ci sono problemi anche se usate le path sotto forma di bytes:
>>> os.listdir(b'.')
[b'\xd0\xb9\xd0\xba\xd0\xbb\xd0\xbc\xd0\xbd.txt']
Tutto questo è interessante, ma forse vi state chiedendo: perché uno vorrebbe esprimere le path sotto forma di bytes invece che come stringhe (Unicode)? E anzi, perché Python dovrebbe permettere che le path siano bytes? Non sarebbe più facile impedire di usare bytes per esprimere le path? La risposta a queste domande è: perché, piaccia o no, nel mondo Posix le path sono bytes. La Pep 529 chiarisce anche questi aspetti di compatibilità.

Abilitare i nomi lunghi dei file.

Ancora a proposito di path, ricordiamo qui che a partire dalla versione 3.6 Python supporta i “nomi lunghi” del file system NTFS in funzioni come open, o in quelle del modulo os etc. Questo è un effetto collaterale di usare le API Unicode di Windows (che appunto supportano i nomi lunghi). Attenzione però: questo vale solo se esprimete le path come stringhe (Unicode), e non funziona se le esprimete come bytes (vedi sopra per i dettagli).
La questione dei “nomi lunghi” non riguarda Python in senso stretto, ed è facile documentarsi in rete in proposito.

Unicode nei file.

Qui Python, come è ovvio, non ha alcun potere. In generale, quando il testo è “salvato” da qualche parte, si trova in forma di bytes e quindi con un determinato encoding. Quando “leggete” il testo recuperandolo dalla sua sorgente (e simmetricamente quando lo “salvate”) dovete fare un’operazione di decoding da (o di encoding in) quell’encoding per convertire i bytes in/da oggetti Unicode che Python (3) usa internamente. Per esempio,
with open('test.txt', 'r', encoding='cp1252') as f: # "f" e' uno stream di bytes
    s = f.read() # "s" e' una stringa (ovvero un oggetto Unicode)
Come potete sapere qual è l’encoding di una sorgente di testo esterna? Beh, ecco il problema: nel caso più generale, non c’è modo di esserne sicuri. Certamente alcune sorgenti sono più “trattabili” di altre: un database ha un encoding dichiarato, e il driver Python che usate per connettervi in genere è in grado di capirlo; un file html ha un “meta-tag” di encoding, se è correttamente formato; e così via. Certo anche in questi casi nessuno può proteggervi da dati malformati e dichiarazioni di encoding non rispettate, ma tutto sommato non sono casi frequenti. Un semplice, generico file di testo, d’altra parte, non ha un modo ovvio di dichiarare l’encoding. Supporre che il file sia in UTF8 è una scommessa buona come un’altra: in realtà molto dipende da chi, e dove, ha generato il file di testo.
Un file di testo “puro” (.txt, per intenderci) generato in ambiente Windows di solito non è in UTF8, a meno che non sia stato creato da un utente più esperto per esempio con un editor da programmatore, o non sia l’output di un sofware che dichiara di produrre file UTF8. Ma per quanto riguarda l’utente comune, il tool di default per aprire e modificare questi file è il Blocco Note (notepad.exe) che, in condizioni normali, salva il file con l’encoding regionale variabile Ansi: come abbiamo detto, da noi in Europa occidentale, “Ansi” vuol dire cp1252. Blocco Note può salvare in UTF8, ma occorre specificarlo al momento del salvataggio: e di solito l’utente normale non se ne cura. Fa eccezione il caso in cui il testo contiene caratteri non coperti da cp1252: allora Blocco Note, al momento del salvataggio, avvisa l’utente e propone di salvare in un formato diverso. Le scelte possibili, abbastanza criptiche, sono “Unicode” (ovvero utf16-le), “Unicode Big Endian” (utf16-be), e finalmente “UTF8”.
E questo introduce il problema aggiuntivo del BOM: il modo in cui Blocco Note riconosce l’encoding UTF8/16 è appunto controllando la presenza del BOM all’inizio del file: se non lo trova, assume una codifica Ansi. In particolare, quindi, Blocco Note usa anche il BOM UTF8 che come è noto non è obbligatorio e molte applicazioni preferiscono non usare. Si tratta dei tre bytes EF BB BF che corrispondono ai caratteri stampabili  in cp1252. Naturalmente Blocco Note non visualizza l’eventuale BOM iniziale, e lo stesso fanno altre applicazioni (per esempio i browser) che riconoscono il BOM. Per vedere con sicurezza se il BOM è presente o meno, potete aprire il file con un editor esadecimale.
Python non attribuisce un particolare significato al BOM UTF8: seguendo le indicazioni dello standard Unicode, lo tratta come un singolo carattere invisibile (ricordiamo che in UTF8 un “carattere” può essere lungo da 1 a 4 bytes). Fate un prova: aprite un file nuovo con il Blocco Note e scriveteci dentro solo un breve testo con caratteri accentati, per esempio cìàò (4 lettere). Scegliete “Salva con nome” e salvate in formato “UTF8”. Aprite adesso questo file con Python:
>>> with open('test.txt', 'r', encoding='utf8') as f:
...    s = f.read()
>>> s # ecco il BOM 
'\ufeffcìàò'
>>> len(s) # il BOM e' lungo un carattere
5
>>> print(s) # "print" stampa il BOM come carattere invisibile
cìàò
>>> s[:3] # il BOM conta come un carattere anche per lo slicing!
'\ufeffcì'
Naturalmente, se invece aprite il file con l’encoding sbagliato cp1252, il BOM viene interpretato come tre caratteri distinti immediatamente riconoscibili:
>>> with open('test.txt', 'r', encoding='cp1252') as f:
...    s = f.read()
>>> s # il BOM sono i primi 3 caratteri: 
cìà ò
In linea di principio, il problema è che al momento di aprire un file non potete essere sicuri dell’encoding, della presenza di BOM: un file potrebbe essere UTF8 ma generato da un editor, diverso da Blocco Note, che non mette il BOM. Certo, se esiste il BOM allora è quasi certamente UTF8 (e lo stesso vale per i BOM di UTF16): ma per aprire un file occorre specificare un encoding prima di sapere se dentro c’è il BOM; e se sbagliate encoding potreste ritrovarvi con caratteri incomprensibili o con un errore di decoding. Viceversa, se il BOM non è presente, allora l’encoding potrebbe essere letteramente uno qualsiasi: UTF8 senza BOM, oppure un encoding Ansi regionale di Windows, o altro ancora.
Come abbiamo detto, il problema è senza soluzione, almeno nel caso più generale. In pratica però è spesso possibile fare delle assunzioni ragionevoli se avete qualche idea di come è stato prodotto il file. Se sapete che il file viene da un signore polacco che usa Windows e non è un esperto di Unicode, allora probabilmente l’encoding sarà cp1250 (il default Ansi in Polonia). Se però non avete informazioni, non vi resta che andare per tentativi: ma allora vi conviene piuttosto adottare un pacchetto di “encoding detection” già pronto, come per esempio Chardet.
Infine, che cosa succede con i file prodotti da Python e aperti successivamente con il Blocco Note? Se scrivete il file con l’encoding Ansi cp1252, allora il file sarà normalmente leggibile da Blocco Note. Naturalmente, se nel vostro testo ci sono caratteri non coperti da cp1252, sarà Python per primo a dirvi che non è possibile scrivere il file:
>>> with open('test.txt', 'a', encoding='cp1252') as f:
...     f.write('cìàò')
...
4
>>> with open('test.txt', 'a', encoding='cp1252') as f:
...     f.write('й к л м н о п р с т у ф')
...
Traceback (most recent call last):
[...]
UnicodeEncodeError: [...]
Se invece scrivete il file in UTF8, le cose si complicano perché Python non inserisce il BOM iniziale:
>>> with open('test.txt', 'a', encoding='utf8') as f:
...     f.write('cìàò')
...
4
>>> # notare che sono stati scritti 4 caratteri: niente BOM!
Potete verificare che il BOM non è presente aprendo per prima cosa il file con un editor esadecimale. Tuttavia, quando aprite il file con Blocco Note, sorpresa: il file si legge benissimo. In effetti Blocco Note ha l’euristica necessaria per accorgersi dell’encoding anche senza trovare il BOM, e visualizza il contenuto senza problemi. Adesso, se chiudete il file senza salvare, Blocco Note non ne modifica il contenuto. Ma se invece salvate, anche senza aver apportato nessuna modifica visibile, allora Blocco Note ne approfitta per rimettere le cose a posto e aggiungere il BOM (potete controllare di nuovo con un editor esadecimale).

Commenti

Post più popolari