codecs - Codifica e Decodifica di Stringhe

Scopo Codificatori e decodificatori per convertire testo tra diverse rappresentazioni
Versione Python 2.1 e superiore

Il modulo codecs fornisce interfacce ai flussi ed ai file per trascodificare dati nel proprio programma. E' più comunemente usato per lavorare con testo Unicode, ma sono disponibili altre codifiche per altri scopi.

Nozioni di Unicode

CPython 2.x supporta due tipi di stringhe per lavorare con dati testuali. Le istanze "vecchio stile" str usano un singolo byte composto da 8 bit per rappresentare ciascun carattere della stringa utilizzando il suo codice ASCII. Di contro, le stringhe unicode sono gestite internamente come sequenze di cosiddetti code point Unicode. I valori di un code point sono salvati come sequenze di 2 o 4 byte ciascuno, a seconda delle opzioni fornite quando Python è stato compilato. Sia unicode che str sono derivate da una classe base comune e supportano una API similare.

Quando le stringhe unicode sono richieste, esse sono codificate usando uno dei parecchi schemi standard, in modo che la sequenza di byte possa essere ricostruita tale e quale successivamente. I byte del valore codificato non sono necessariamente uguali ai valori di code point e la codifica definisce un modo per trascodificare tra i due insiemi di valori. Per leggere dati Unicode occorre anche sapere la codifica in modo che i byte in arrivo possano essere convertiti nella loro rappresentazione interna usata dalla classe unicode.

Le codifiche più comuni per i linguaggi occidentali sono UTF-8 e UTF-16, che usano rispettivamente sequenze di valori di uno e due byte per rappresentare ciascun carattere. Altre codifiche possono essere più efficaci per linguaggi per i quali la maggior parte dei caratteri sono rappresentati da code point che non rientrano nei due byte.

Per maggiori informazioni introduttive circa Unicode, fare riferimento all'elenco di risorse alla fine dell'articolo. Il documento Python Unicode HOWTO è particolarmente utile.

Encoding

Il modo migliore per comprendere l'encoding è di osservare le diverse serie di byte prodotte codificando la stessa stringa di modi diversi. Gli esempi che seguono usano questa funzione per formattare la stringa di byte per facilitarne la lettura

import binascii

def to_hex(t, nbytes):
    """Formattazione del testo t come sequenza valori lunghi nbyte separati da spazi."""
    chars_per_item = nbytes * 2
    hex_version = binascii.hexlify(t)
    num_chunks = len(hex_version) / chars_per_item
    def chunkify():
        for start in xrange(0, len(hex_version), chars_per_item):
            yield hex_version[start:start + chars_per_item]
    return ' '.join(chunkify())

if __name__ == '__main__':
    print to_hex('abcdef', 1)
    print to_hex('abcdef', 2)

La funzione usa binascii per ottenere una rappresentazione esadecimale dei byte della stringa in input, quindi inserisce uno spazio tra ogni nbytes byte prima di restituire il valore.

$ python codecs_to_hex.py

61 62 63 64 65 66
6162 6364 6566

Il primo esempio di encoding inizia stampando il testo pi: π usando la rappresentazione raw della classe unicode. Il carattere π viene sostituito con l'espressione per il code point Unicode, \u03c0. Le due successive righe codificano la stringa rispettivamente come UTF-8 e UTF-16, e mostrano i valori esadecimali risultanti dall'operazione di encoding.

from codecs_to_hex import to_hex

text = u'pi: π;'

print 'Raw   :', repr(text)
print 'UTF-8 :', to_hex(text.encode('utf-8'), 1)
print 'UTF-16:', to_hex(text.encode('utf-16'), 2)

Il risultato dell'operazione di encoding di una stringa unicode è un oggetto str.

$ codecs_encodings.py
Raw   : u'pi: \u03c0'
UTF-8 : 70 69 3a 20 70
UTF-16: fffe 7000 6900 3a00 2000 7000

Data una sequenza di byte codificati come una istanza di str, il metodo decode() li trascodifica in code point e restituisce la sequenza come istanza di unicode

from codecs_to_hex import to_hex

text = u'pi: \u03c0'
encoded = text.encode('utf-8')
decoded = encoded.decode('utf-8')

print 'Originali   :', repr(text)
print 'Codificati  :', to_hex(encoded, 1), type(encoded)
print 'Decodificati:', repr(decoded), type(decoded)

La scelta della codifica usata non modifica il tipo di output.

$ codecs_decode.py
Originali   : u'pi: \u03c0'
Codificati  : 70 69 3a 20 70 
Decodificati: u'pi: \u03c0' 
L'encoding predefinito è impostato durante il processo di avvio dell'interprete, quando viene caricato site. Fare riferimento a Unicode Defaults per una descrizione delle impostazioni di encoding predefinite accessibili tramite sys

Lavorare con i File

Codificare e decodificare le stringhe è in particolar modo importante quando si ha a che fare con operazioni di I/O. Che si stia scrivendo verso un socket, un file, od altri tipi di flusso, occorre assicurarsi che i dati vengano elaborati con l'encoding appropriato. In generale, tutti i dati testuali devono essere decodificati dalla loro rappresentazione in byte in fase di lettura, e codificati dai loro valori interni verso una specifica rappresentazione quando sono scritti. Un proprio programma potrebbe codificare/decodificare i dati, ma a seconda dell'encoding usato potrebbe non essere così semplice determinare se si sono letti byte a sufficienza per la completa decodifica dei dati. codecs fornisce delle classi per gestire la codifica e la decodifica dei dati, in modo che il programmatore non debba preoccuparsi di farlo egli stesso.

L'interfaccia più semplice fornita da codecs è un rimpiazzo della funzione built-in open(). La nuova versione funziona proprio come quella built-in, ma aggiunge due ulteriori parametri per specificare l'encoding e la tecnica di gestione degli errori desiderata.

from codecs_to_hex import to_hex

import codecs
import sys

encoding = sys.argv[1]
filename = encoding + '.txt'

print 'Scrittura verso', filename
with codecs.open(filename, mode='wt', encoding=encoding) as f:
    f.write(u'pi: \u03c0')

# Determina il raggruppamento di byte da usare per to_hex()
nbytes = { 'utf-8':1,
           'utf-16':2,
           'utf-32':4,
           }.get(encoding, 1)

# Mostra i byte raw nel file
print 'Contenuto del file:'
with open(filename, mode='rt') as f:
    print to_hex(f.read(), nbytes)

Partendo da una stringa unicode con il code point per π. questo esempio salva il testo in un file usando l'encoding specificato da riga di comando:

$ codecs_open_write.py utf-8

Scrittura verso utf-8.txt
Contenuto del file:
70 69 3a 20 cf 80

$ codecs_open_write.py utf-16

Scrittura verso utf-16.txt
Contenuto del file:
fffe 7000 6900 3a00 2000 c003

$ codecs_open_write.py utf-32

Scrittura verso utf-32.txt
Contenuto del file:
fffe0000 70000000 69000000 3a000000 20000000 c0030000

Leggere i dati con open() è lineare, ma c'è un inconveniente: occorre sapere in anticipo quale è l'encoding, per potere impostare correttamente il decodificatore. Alcuni formati di dati, tipo XML, consentono di specificare l'encoding come parte del file, ma in genere la gestione spetta all'applicazione. codecs semplicemente ottiene l'encoding come parametro ed assume che sia corretto.

import codecs
import sys

encoding = sys.argv[1]
filename = encoding + '.txt'

print 'Lettura da: ', filename
with codecs.open(filename, mode='rt', encoding=encoding) as f:
    print repr(f.read())

Questo esempio legge dai file creati dall'esempio precedente, e stampa la rappresentazione dell'oggetto unicode risultante alla console.

$ codecs_open_read.py utf-8
Lettura da:  utf-8.txt
u'pi: \u03c0'

$ codecs_open_read.py utf-16
Lettura da:  utf-16.txt
u'pi: \u03c0'

$ codecs_open_read.py utf-32
Lettura da:  utf-32.txt
u'pi: \u03c0'

Ordine dei Byte

Le codifiche multi-byte tipo UTF-16 ed UTF-32 pongono un problema quando i dati vengono trasferiti tra diversi sistemi di computer, sia copiando il file direttamente che trasferendolo tramite rete. Sistemi diversi usano ordinamenti diversi rispetto all'ordine alto e basso dei byte. Questa caratteristica dei dati, nota come endianness, dipende da fattori tipo l'architettura hardware e da scelte fatte dal sistema operativo e dallo sviluppatore dell'applicazione. Non sempre vi è modo di sapere in anticipo quale ordine di byte usare per un certo insieme di dati, per questo motivo gli encoding multi-byte comprendono un marcatore dell'ordine dei byte (byte-order marker - BOM ) come primi byte dell'output codificato. Ad esempio UTF-16 viene definito in modo tale che 0xFFFE e 0xFEFF non sono caratteri validi, e possono essere usati per indicare l'ordine di byte. codecs definisce le costanti per i marcatori dell'ordine dei byte (BOM) usate da UTF-16 ed UTF-32.

import codecs
from codecs_to_hex import to_hex

for name in [ 'BOM', 'BOM_BE', 'BOM_LE',
              'BOM_UTF8',
              'BOM_UTF16', 'BOM_UTF16_BE', 'BOM_UTF16_LE',
              'BOM_UTF32', 'BOM_UTF32_BE', 'BOM_UTF32_LE',
              ]:
    print '{:12} : {}'.format(name, to_hex(getattr(codecs, name), 2))

BOM, BOM_UTF16 e BOM_UTF32 sono impostati automaticamente ai valori big-endian o little-endian appropriati a seconda del nativo ordine dei byte nel sistema corrente.

$ python codecs_bom.py

BOM          : fffe
BOM_BE       : feff
BOM_LE       : fffe
BOM_UTF8     : efbb bf
BOM_UTF16    : fffe
BOM_UTF16_BE : feff
BOM_UTF16_LE : fffe
BOM_UTF32    : fffe 0000
BOM_UTF32_BE : 0000 feff
BOM_UTF32_LE : fffe 0000

L'ordinamento dei byte è rilevato e gestito automaticamente dai decodificatori in codecs, ma si può anche scegliere un ordinamento esplicito per la codifica.

import codecs
from codecs_to_hex import to_hex

# Sceglie la versione non-nativa dell'encoding UTF-16
if codecs.BOM_UTF16 == codecs.BOM_UTF16_BE:
    bom = codecs.BOM_UTF16_LE
    encoding = 'utf_16_le'
else:
    bom = codecs.BOM_UTF16_BE
    encoding = 'utf_16_be'

print 'Ordine nativo     :', to_hex(codecs.BOM_UTF16, 2)
print 'Ordine selezionato:', to_hex(bom, 2)

# Codifica il testo.
encoded_text = u'pi: \u03c0'.encode(encoding)
print '{:14}: {:}'.format(encoding, to_hex(encoded_text, 2))

with open('non-native-encoded.txt', mode='wb') as f:
    # Scrive il byte-order marker selezionato.  Non e' incluso nel
    # testo codificato in quanto la cosa e' stata resa esplicita in fase di
    # selezione della codifica
    f.write(bom)
    # Scrive la stringa di byte per il testo codificato.
    f.write(encoded_text)

codecs_bom_create_file.py determina l'ordinamento nativo dei byte, quindi usa esplicitamente la forma alternativa in modo che il prossimo esempio possa dimestrare l'auto identificazione in fase di lettura.

$ codecs_bom_create_file.py
Ordine nativo     : fffe
Ordine selezionato: feff
utf_16_be     : 0070 0069 003a 0020 03c0

codecs_bom_detection.py non specifica un ordine di byte durante l'apertura del file, quindi il decodificatore usa il valore BOM che si trova nei primi due byte del file per determinarlo.

import codecs
from codecs_to_hex import to_hex

# Lettura dei dati grezzi
with open('non-native-encoded.txt', mode='rb') as f:
    raw_bytes = f.read()

print 'Raw    :', to_hex(raw_bytes, 2)

# Riapertura del file lasciando che codecs identifichi il  BOM
with codecs.open('non-native-encoded.txt', mode='rt', encoding='utf-16') as f:
    decoded_text = f.read()

print 'Decodificato:', repr(decoded_text)

Viso che i primi due byte del file vengono usati per l'identificazione dell'ordinamento dei byte, essi non sono compresi nei dati restituiti da read().

$ codecs_bom_detection.py
Raw    : feff 0070 0069 003a 0020 03c0
Decodificato: u'pi: \u03c0'

Gestione degli Errori

Le sezioni precedenti rimarcavano la necessità di sapere la codifica usata durante la lettura e la scrittura dei file Unicode. Impostare correttamente la codifica è importante per due ragioni. Se la codifica è configurata in modo errato durante la lettura da un file, i dati verranno interpretati in modo sbagliato e potrebbero essere corrotti oppure si verificherà semplicemente un fallimento della decodifica. Non tutti i caratteri Unicode possono essere rappresentati in tutte le codifiche, quindi se viene usata la codifica sbagliata in fase di scrittura sarà generato un errore ed i dati potrebbero andare persi.

codecs usa le stesse cinque opzioni di gestione degli errori che sono fornite dal metodo encode() di unicode e dal metodo decode() di str


Errori in Codifica

La condizione di errore più comune è ricevere un UnicodeEncodeError in fase di scrittura di dati Unicode verso un flusso in uscita ASCII, tipo un normale file oppure sys.stdout

import codecs
import sys

error_handling = sys.argv[1]

text = u'pi: \u03c0'

try:
    # Salva i dati, codificati in ASCII, usando la modalita'
    # di gestione errori specificata da riga di comando.
    with codecs.open('encode_error.txt', 'w',
                     encoding='ascii',
                     errors=error_handling) as f:
        f.write(text)

except UnicodeEncodeError, err:
    print 'ERRORE:', err

else:
    # Se non ci sono errori scrivendo sul file,
    # si mostra il suo contenuto
    with open('encode_error.txt', 'rb') as f:
        print 'Contenuto del file:', repr(f.read())

La modalità strict è la più sicura per fare sì che una propria applicazione imposti esplicitamente la corretta codifica per tutte le operazioni di I/O, tuttavia può condurre all'uscita prematura dal programma una volta che l'eccezione viene sollevata.

$ python codecs_encode_error.py strict
ERRORE: 'ascii' codec can't encode character u'\u03c0' in position 4: ordinal not in range(128)

Alcune delle altre tipologie di errore sono più flessibili. Ad esempio replace assicura che non venga sollevato alcun errore, a scapito della possibile perdita dei dati che non possono essere convertiti nella codifica richiesta. Il carattere Unicode per il pi greco non può essere codificato in ASCII, ma invece che sollevare una eccezione, viene sostituito il carattere con un ? nell'output.

$ python codecs_encode_error.py replace

Contenuto del file: 'pi: ?'

Per ignorare completamente problemi sui dati si usa ignore. Tutti i dati che non possono essere codificati vengono semplicemente ignorati.

$ python codecs_encode_error.py ignore

Contenuto del file: 'pi: '

Ci sono due opzioni di gestione errori che non causano perdita di dati, entrambe sostituiscono il carattere con una rappresentazione alternativa definita da uno standard a prescindere dalla codifica. xmlcharrefreplace usa una entità XML come sostituto (l'elenco delle entitàviene specificato nel documento presso W3C XML Entity Definitions for Characters).

$ python codecs_encode_error.py xmlcharrefreplace
Contenuto del file: 'pi: π'

L'altro schema di gestione errori senza perdita di dati è backslashreplace che produce un formato in output simile a quello che si ottiene quando si usa repr() per stampare un oggetto unicode. I caratteri Unicode sono sostituiti da \u seguito dal valore esadecimale del code point

$ python codecs_encode_error.py backslashreplace
Contenuto del file: 'pi: \\u03c0'

Errori in Decodifica

E' anche possibile che si verifichino errori durante la decodifica dei dati, specialmente se viene usata un codifica sbagliata.

import codecs
import sys

from codecs_to_hex import to_hex

error_handling = sys.argv[1]

text = u'pi: \u03c0'
print 'Originale         :', repr(text)

# Salva i dati con una codifica
with codecs.open('decode_error.txt', 'w', encoding='utf-16') as f:
    f.write(text)

# Scarica i byte dal file
with open('decode_error.txt', 'rb') as f:
    print 'Contenuto del file:', to_hex(f.read(), 1)

# Tenta di leggere i dati con la codifica errata
with codecs.open('decode_error.txt', 'r',
                 encoding='utf-8',
                 errors=error_handling) as f:
    try:
        data = f.read()
    except UnicodeDecodeError, err:
        print 'ERRORE:', err
    else:
        print 'Originale         :', repr(data)

Come per la codifica, la gestione di errori in modalità strict solleva una eccezione se il flusso di byte non può essere decodificato appropriatamente. In questo caso, si ottiene un UnicodeDecodeError tendando di convertire parte del BOM UTF-16 in un carattere usando il decodificatore UTF-8.

$ python codecs_decode_error.py strict

Originale         : u'pi: \u03c0'
Contenuto del file: ff fe 70 00 69 00 3a 00 20 00 c0 03
ERRORE: 'utf8' codec can't decode byte 0xff in position 0: unexpected code byte

Se si passa alla modalità ignore il decodificatore ignorerà i byte non validi. Il risultato non è ancora quello che ci si aspetta comunque, visto che sono inclusi i byte null incorporati.

$ python codecs_decode_error.py ignore

Originale         : u'pi: \u03c0'
Contenuto del file: ff fe 70 00 69 00 3a 00 20 00 c0 03
Letto        : u'p\x00i\x00:\x00 \x00'

Con la modalità replace i byte non validi sono sostituiti da \uFFFD, il carattere Unicode di sostituizione ufficiale, che assomiglia ad un diamante con uno sfondo nero che contiene un punto interrogativo bianco (�)

$ python codecs_decode_error.py replace

Originale         : u'pi: \u03c0'
Contenuto del file: ff fe 70 00 69 00 3a 00 20 00 c0 03
Letto        : u'\ufffd\ufffdp\x00i\x00:\x00 \x00\ufffd'

Flussi Standard di Input ed Output

La causa più comune delle eccezioni UnicodeEncodeError è del codice che tenta di stampare dati unicode alla console o verso una pipeline Unix quando sys.stdout non è configurato con una codifica.

import codecs
import sys

text = u'pi: π'

# La stampa verso stdout potrebbe causare un errore di codifica
print 'encoding predefinito:', sys.stdout.encoding
print 'TTY:', sys.stdout.isatty()
print text

I problemi con la codifica predefinita dei canali standard I/O possono essere difficili da rilevare e risolvere in quanto il programma funziona come ci si attende quando l'output va verso la console, ma provoca errori di codifica quando viene usato come parte di una pipeline e l'output include caratteri Unicode al di sopra del limite ASCII. Questa differenza di comportamento è causata dal codice di inizializzazione di Python, che imposta la codifica predefinita per ogni canale I/O standard solo se il canale è connesso ad un terminale (isatty() restituisce True). Se non esiste un terminale, Python presume che il programma configurerà esplicitamente la codifica, e lascia perdere il canale di I/O.

$ python codecs_stdout.py
encoding predefinito: cp850
TTY: True
pi: π

$ python codecs_stdout.py | cat -
encoding predefinito: cp850
TTY: False
Traceback (most recent call last):
  File "codecs_stdout.py", line 18, in 
    print text
UnicodeEncodeError: 'ascii' codec can't encode character u'\u03c0' in
 position 4: ordinal not in range(128)

Per impostare esplicitamente la codifica sul canale standard di output si usa getwriter() per ottenere una classe per la codifica di un flusso per una certo encoding. Successivamente si istanzia la classe passando sys.stdout come unico parametro.

import codecs
import sys

text = u'pi: π'

# Si inserisce sys.stdout in un writer che sa come
# gestire la codifica di dati Unicode.
wrapped_stdout = codecs.getwriter('UTF-8')(sys.stdout)
wrapped_stdout.write(u'Tramite write: ' + text + '\n')

# Sostituzione di sys.stdout con un writer
sys.stdout = wrapped_stdout

print u'Tramite print:', text

Quando si scrive alla versione incapsulata di sys.stdout il testo Unicode viene passato attraverso un codificatore prima che i byte codidificati siano poi trasmessi a stdout. Sostituire sys.stdout vuol dire che qualsiasi codice usato dalla propria applicazione che stampi sullo standard output sarà sempre in grado di usufruire del codificatore in scrittura.

$ python codecs_stdout_wrapped.py
Tramite write: pi: π
Tramite print: pi: π

Il prossimo problema da risolvere è come si possa sapere quale codifica dovrebbe essere usata. La codifica appropriata varia in base alla locazione, alla lingua ed alle configurazioni utente o di sistema, quindi scrivere direttamente nel codice un valore fisso non è una buona idea. Sarebbe inoltre poco simpatico per un utente essere costretto a passare esplicitamente dei parametri per ogni programma per impostare le codifiche per input ed output. Per fortuna esiste un metodo globale per ottenere una codifica ragionevole predefinita, tramite l'uso di locale.

import codecs
import locale
import sys

text = u'pi: π'

# Configurazione locale dalle impostazioni di ambiente dell'utente.
locale.setlocale(locale.LC_ALL, '')

# Inserisce stdout all'interno di un writer che conosce la codifica.
lang, encoding = locale.getdefaultlocale()
print 'Codifica locale    :', encoding
sys.stdout = codecs.getwriter(encoding)(sys.stdout)

print 'Con stdout incapsulato:', text

getdefaultlocale() restituisce la lingua e la codifica preferenziale in base alle impostazioni di configurazione del sistema e dell'utente in un formato che possa essere usato con getwriter().

$ python codecs_stdout_locale.py

Codifica locale    : cp1252
Con stdout incapsulato: pi: π

La codifica deve inoltre essere impostata quando si lavora con sys.stdin. Si usa getreader() per ottenere un lettore in grado di decodificare i byte in input.

import codecs
import locale
import sys

# Configurazione locale dalle impostazioni di ambiente dell'utente
locale.setlocale(locale.LC_ALL, '')

# Inserisce stdin in un lettore che conosce la codifica
lang, encoding = locale.getdefaultlocale()
sys.stdin = codecs.getreader(encoding)(sys.stdin)

print 'Da stdin:', repr(sys.stdin.read())

La lettura da un handle incapsulato restituisce degli oggetti unicode in luogo di istanze di str.

$ python codecs_stdout_locale.py | python codecs_stdin.py

Da stdin: u'Codifica locale    : cp1252\nCon stdout incapsulato: pi:  \u03c0\n'

Comunicazioni di Rete

Anche i socket di rete sono flussi di byte, quindi i dati Unicode devono essere codificati in byte prima di essere scritti verso un socket

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import SocketServer

class Echo(SocketServer.BaseRequestHandler):

    def handle(self):
        # Ottengo alcuni byte e li riverbero al client.
        data = self.request.recv(1024)
        self.request.send(data)
        return


if __name__ == '__main__':
    import codecs
    import socket
    import threading

    address = ('localhost', 0) # Lasciamo che il kernel ci fornisca una porta
    server = SocketServer.TCPServer(address, Echo)
    ip, port = server.server_address # scopriamo quale porta ci è stata assegnata

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True) #  non lo lasciamo appeso all'uscita
    t.start()

    # Connessione al server
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # Invia i dati
    text = u'pi: π'
    len_sent = s.send(text)

    # Riceve una risposta
    response = s.recv(len_sent)
    print repr(response)

    # Pulizia
    s.close()
    server.socket.close()

Si potrebbero codificare i dati esplicitamente, prima di inviarli, ma basterebbe perdere una chiamata a send() ed il proprio programma fallirebbe con un errore di encoding.

$ python codecs_socket_fail.py

Traceback (most recent call last):
  File "codecs_socket_fail.py", line 35, in 
    len_sent = s.send(text)
UnicodeEncodeError: 'ascii' codec can't encode character u'\u03c0' in position 4: ordinal not in range(128)

Se si usa makefile() per ottenere un file handle per il socket, quindi lo si incapsula in un lettore/scrittore di flusso, si riescono a passare stringhe Unicode sapendo che sono codificate in entrata ed uscita dal socket.

import sys
import SocketServer


class Echo(SocketServer.BaseRequestHandler):

    def handle(self):
        # Si ottengono alcuni byte e li si riverbera al client.
        # Non occorre decoficarli visto che non sono usati.
        data = self.request.recv(1024)
        self.request.send(data)
        return


class PassThrough(object):

    def __init__(self, other):
        self.other = other

    def write(self, data):
        print 'In scrittura :', repr(data)
        return self.other.write(data)

    def read(self, size=-1):
        print 'In lettura :',
        data = self.other.read(size)
        print repr(data)
        return data

    def flush(self):
        return self.other.flush()

    def close(self):
        return self.other.close()


if __name__ == '__main__':
    import codecs
    import socket
    import threading

    address = ('localhost', 0) # Lasciamo che il kernel ci fornisca una porta
    server = SocketServer.TCPServer(address, Echo)
    ip, port = server.server_address # scopriamo quale porta ci è stata assegnata

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True) # non lo lasciamo appeso all'uscita
    t.start()

    # Connessione al server
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # Incapsula il socket in un lettore e scrittore
    incoming = codecs.getreader('utf-8')(PassThrough(s.makefile('r')))
    outgoing = codecs.getwriter('utf-8')(PassThrough(s.makefile('w')))

    # Invia i dati
    text = u'pi: π'
    print 'Inviati :', repr(text)
    outgoing.write(text)
    outgoing.flush()

    # Riceve una risposta
    response = incoming.read()
    print 'Ricevuti:', repr(response)

    # Pulizia
    s.close()
    server.socket.close()

Questo esempio usa PassThrough per mostrare che i dati sono codificati prima di essere spediti, e la risposta viene decodificata dopo che è stata ricevuta nel client.

$ python codecs_socket.py Inviati : u'pi: \u03c0'

In scrittura : 'pi: \xcf\x80'
In lettura : 'pi: \xcf\x80'
Ricevuti: u'pi: \u03c0'

Trascodifica di Encoding

Sebbene la maggior parte delle applicazioni internamente lavorino con dati unicode, la decodifica o la codifica è parte di una operazione di I/O, ci sono volte nelle quali cambiare l'encoding di un file senza mantenere quel formato dati intermedio è utile. EncodedFile() riceve un handle di file aperto usando un encoding, quindi lo incapsula in una classe che trascodifica i dati in un altro encoding mentre è in corso l'operazione di I/O

from codecs_to_hex import to_hex

import codecs
from cStringIO import StringIO

# Versione grezza dei dati originali
data = u'pi: \u03c0'

# Codifica manuale come UTF-8
utf8 = data.encode('utf-8')
print 'Inizia come UTF-8   :', to_hex(utf8, 1)

# Imposta un buffer in uscita, quindi lo incapsula come EncodedFile.
output = StringIO()
encoded_file = codecs.EncodedFile(output, data_encoding='utf-8',
                                  file_encoding='utf-16')
encoded_file.write(utf8)

# Recupera il contenuto del buffer con codifica della stringa
# di byte in UTF-16
utf16 = output.getvalue()
print 'Codificato in UTF-16:', to_hex(utf16, 2)

# Imposta un altro buffer con i dati UTF-16 per la lettura
# e li incapsula con un altro EncodedFile.
buffer = StringIO(utf16)
encoded_file = codecs.EncodedFile(buffer, data_encoding='utf-8',
                                  file_encoding='utf-16')

# Legge la versione dati codificata come UTF-8
recoded = encoded_file.read()
print 'Ritorna ad UTF-8    :', to_hex(recoded, 1)

L'esempio mostra la lettura da e la scrittura verso handle separati restituiti da EncodedFile(). Non importa se l'handle viene usato per leggere o scrivere, file_encoding fa sempre riferimento all'encoding usato dall'handle del file aperto passato come primo parametro, ed il valore di data_encoding si riferisce alla codifica usata dai dati che passano attraverso le chiamate di read() e write().

$ python codecs_encodedfile.py
Inizia come UTF-8   : 70 69 3a 20 cf 80
Codificato in UTF-16: fffe 7000 6900 3a00 2000 c003
Ritorna ad UTF-8    : 70 69 3a 20 cf 80

Encoding Non-Unicode

Sebbene la maggior parte degli esempi precedenti usino codifiche Unicode, codecs può essere usato per molte altre trascodifiche di dati. Ad esempio, Python comprende codec per lavorare con formati base-64, bzip2, ROT-13, ZIP, ed altri.

import codecs
from cStringIO import StringIO

buffer = StringIO()
stream = codecs.getwriter('rot_13')(buffer)

text = 'abcdefghijklmnopqrstuvwxyz'

stream.write(text)
stream.flush()

print 'Originale:', text
print 'ROT-13   :', buffer.getvalue()

Qualunque trasformazione che possa essere espressa come una funzione che riceve un singolo parametro come input e restituisce un byte od una stringa Unicode può essere registrata come un codec.

$ python codecs_rot13.py

Originale: abcdefghijklmnopqrstuvwxyz
ROT-13   : nopqrstuvwxyzabcdefghijklm

L'uso di codecs per incapsulare un flusso di dati fornisce una interfaccia più semplice rispetto al lavorare direttamente con zlib.

import codecs
from cStringIO import StringIO

from codecs_to_hex import to_hex

buffer = StringIO()
stream = codecs.getwriter('zlib')(buffer)

text = 'abcdefghijklmnopqrstuvwxyz\n' * 50

stream.write(text)
stream.flush()

print 'Lunghezza originale      :', len(text)
compressed_data = buffer.getvalue()
print 'Compressione ZIP         :', len(compressed_data)

buffer = StringIO(compressed_data)
stream = codecs.getreader('zlib')(buffer)

first_line = stream.readline()
print 'Lettura della prima riga :', repr(first_line)

uncompressed_data = first_line + stream.read()
print 'Non compresso            :', len(uncompressed_data)
print 'Uguale                   :', text == uncompressed_data

Non tutti i sistemi di compressione o codifica supportano la lettura di una porzione di dati attraverso l'interfaccia al flusso usando readline() oppure read(), in quanto devono trovare la fine di un segmento compresso da espandere. Se un proprio programma non può conservare l'intero insieme di dati non compressi in memoria, si devono usare le caratteristiche di accesso incrementale della libreria di compressione invece che di codecs.

$ python codecs_zlib.py

Lunghezza originale      : 1350
Compressione ZIP         : 48
Lettura della prima riga : 'abcdefghijklmnopqrstuvwxyz\n'
Non compresso            : 1350
Uguale                   : True

Encoding Incrementale

Alcuni degli encoding forniti, specialmente bz2 e zlib, possono modificare in modo significativo la lunghezza del flusso dei dati mentre stanno lavorandoci. Per insiemi di dati molto grandi, questi encoding operano meglio in modo incrementale, lavorando su una piccola porzione di dati alla volta. Le API IncrementalEncoder ed IncrementalDecoder sono progettate per questo scopo.

import codecs
import sys

from codecs_to_hex import to_hex

text = 'abcdefghijklmnopqrstuvwxyz\n'
repetitions = 50

print 'Lung. testo   :', len(text)
print 'Ripetizioni   :', repetitions
print 'Lung. prevista:', len(text) * repetitions

# Codifica il testo parecchie volte sviluppando un grande ammontare di dati
encoder = codecs.getincrementalencoder('bz2')()
encoded = []

print
print 'Encoding :',
for i in range(repetitions):
    en_c = encoder.encode(text, final = (i==repetitions-1))
    if en_c:
        print '\nCodificati: {} byte'.format(len(en_c))
        encoded.append(en_c)
    else:
        sys.stdout.write('.')

bytes = ''.join(encoded)
print
print 'Lung. totale codificati:', len(bytes)
print

# Decodifica la stringa di byte un byte alla volta
decoder = codecs.getincrementaldecoder('bz2')()
decoded = []

print 'Decoding:',
for i, b in enumerate(bytes):
    final= (i+1) == len(text)
    c = decoder.decode(b, final)
    if c:
        print '\nDecodificati : {} caratteri'.format(len(c))
        print 'Decoding:',
        decoded.append(c)
    else:
        sys.stdout.write('.')
print

restored = u''.join(decoded)

print
print 'Lung. totale non compressi:', len(restored)

Ogni volta che i dati sono passati al codificatore od al decodificatore viene aggiornato il loro stato interno. Quando lo stato è consistente (così come definito dal coded), vengono restituiti i dati e lo stato viene reimpostato. Fino a quel punto, le chiamate a encode() o decode() non restituiranno dati. Quanto l'ultimo bit di dati è stato ricevuto in input, il parametro final dovrebbe essere impostato a True in modo che il codec sappia che deve far fuoriuscire tutti i dati che sono rimasti nel buffer.

$ python codecs_incremental_bz2.py

Lung. testo   : 27
Ripetizioni   : 50
Lung. prevista: 1350

Encoding :.................................................
Codificati: 99 byte

Lung. totale codificati: 99

Decoding:........................................................................................
Decodificati : 1350 caratteri
Decoding:..........

Lung. totale non compressi: 1350

Definire La Propria Codifica

Visto che Python è gia provvisto di un vasto numero di codec, è improbabile che si debba definire un proprio codec. Se questo è il caso, ci sono parecchie classi base in codecs che possono facilitare il processo.

Il primo passo è capire la natura della trasformazione descritta dalla codifica. Ad esempio un encoding "invertcaps" converte le lettere maiuscole in minuscole e viceversa. Ecco una semplice definizione di una funzione di encoding che eseguqe questa trasformazione in una stringa in input:

import string

def invertcaps(text):
    """Restituisce una nuova stringa con le lettere maiuscole trasformate
    in minuscole e viceversa.
    """
    return ''.join( c.upper() if c in string.ascii_lowercase
                    else c.lower() if c in string.ascii_uppercase
                    else c
                    for c in text
                    )

if __name__ == '__main__':
    print invertcaps('ABC.def')
    print invertcaps('abc.DEF')

In questo caso il codificatore ed il decodificatore sono la stessa funzione (come con ROT-13).

$ python codecs_invertcaps.py

abc.DEF
ABC.def

Sebbene sia facila da comprendere, questa implementazione non è efficiente, specialmente per stringhe di testo molto grandi. Per fortuna codecs comprende alcune funzioni di aiuto per creare codec basati su delle mappe di caratteri tipo "invertcaps". Una mappa di caratteri per l'encoding è composta da due dizionari. La mappa di codifica converte i valori dei caratteri della stringa in input in valori di byte nell'output, quindi la mappa di decodifica li converte nell'altro senso. Occorre prima creare la mappa di decodifica, qundi usare make_encoding_map() per la conversione nella mappa di codifica. Le funzioni C charmap_encode() e charmap_decode() usano le mappe per convertire i dati in input con efficacia.

import codecs
import string

# Mappatura di ogni carattere su se stesso
decoding_map = codecs.make_identity_dict(range(256))

# Genera una lista di coppie di valori ordinali per le lettere
# minunscole e maiuscole
pairs = zip([ ord(c) for c in string.ascii_lowercase],
            [ ord(c) for c in string.ascii_uppercase])

# Modifica la mappatura per convertire da maiuscolo a minuscolo e da
# minuscolo a maiuscolo.
decoding_map.update( dict( (upper, lower) for (lower, upper) in pairs) )
decoding_map.update( dict( (lower, upper) for (lower, upper) in pairs) )

# Crea una mappa di codifica a se stante.
encoding_map = codecs.make_encoding_map(decoding_map)

if __name__ == '__main__':
    print codecs.charmap_encode('abc.DEF', 'strict', encoding_map)
    print codecs.charmap_decode('abc.DEF', 'strict', decoding_map)
    print encoding_map == decoding_map

Sebbene le mappe per la codifica e la decodifica per "invertcaps" siano le stesse, questo potrebbe non sempre essere il caso. make_encoding_map() rileva le situazioni dove più di un carattere in input viene codificato con lo stesso byte in output e sostituisce il valore in codifica con None per contrassegnare la codifica come indefinita.

$ python codecs_invertcaps_charmap.py

('ABC.def', 7)
(u'ABC.def', 7)
True

Le mappe di caratteri in codifica e decodifica supportano tutti i metodi di gestione di errori standard descritti in precedenza, quindi non occorre un lavoro supplementare per completare quella parte di API.

import codecs
from codecs_invertcaps_charmap import encoding_map

text = u'pi: π'

for error in [ 'ignore', 'replace', 'strict' ]:
    try:
        encoded = codecs.charmap_encode(text, error, encoding_map)
    except UnicodeEncodeError, err:
        encoded = str(err)
    print '{:7}: {}'.format(error, encoded)

Visto che il code point Unicode per π non si trova nella mappa di codifica, la modalità di gestione errore strict solleva una eccezione.

$ python codecs_invertcaps_error.py

ignore : ('PI: ', 5)
replace: ('PI: ?', 5)
strict : 'charmap' codec can't encode character u'\u03c0' in position 4: character maps to 

Dopo che le mappe di codifica e decodifica sono state definite, occorre impostare alcune classi addizionali e registrare l'encoding. register() aggiunge una funzione di ricerca al registro in modo che quando un utente vuole usare il proprio encoding codecs è in grado di trovarlo. La funzione di ricerca deve ottenere un singola stringa come parametro con il nome dell'encoding, e restituire un oggetto CodecInfo se conosce l'encoding, oppure None se non lo conosce.

import codecs
import encodings

def search1(encoding):
    print 'ricerca1: Ricerca di :', encoding
    return None

def search2(encoding):
    print 'ricerca2: Ricerca di :', encoding
    return None

codecs.register(search1)
codecs.register(search2)

utf8 = codecs.lookup('utf-8')
print 'UTF-8:', utf8

try:
    unknown = codecs.lookup('questo-encoding-non-esiste')
except LookupError, err:
    print 'ERRORE:', err

Si possono registare molteplici funzioni di ricerca, ognuna delle quali sarà chiamata di volta in volta, fino a che viene restituito un CodecInfo oppure l'elenco viene esaurito. La funzione di ricerca interna registrata da codecs sa come caricare i codec standard tipo UTF-8 da encodings, per cui questi nomi non verranno mai passati alla propria funzione di ricerca.

$ python codecs_register.py

UTF-8: 
ricerca1: Ricerca di : questo-encoding-non-esiste
ricerca2: Ricerca di : questo-encoding-non-esiste
ERRORE: unknown encoding: questo-encoding-non-esiste

L'istanza di CodecInfo restituita dalla funzione di ricerca dice a codecs come eseguire la codifica e la decodifica usando tutti i vari meccanismi supportati: stateless, incremental e stream. codecs comprende delle classi base per facilitare l'imposatzione della mappa di caratteri di codifica. Questo esempio mette insieme tutti i pezzi per registrare una funzione che restituisca una istanza di CodecInfo configurata per il codec invertcaps.

import codecs

from codecs_invertcaps_charmap import encoding_map, decoding_map

# Stateless encoder/decoder

class InvertCapsCodec(codecs.Codec):
    def encode(self, input, errors='strict'):
        return codecs.charmap_encode(input, errors, encoding_map)

    def decode(self, input, errors='strict'):
        return codecs.charmap_decode(input, errors, decoding_map)

# Forma Incrementale

class InvertCapsIncrementalEncoder(codecs.IncrementalEncoder):
    def encode(self, input, final=False):
        return codecs.charmap_encode(input, self.errors, encoding_map)[0]

class InvertCapsIncrementalDecoder(codecs.IncrementalDecoder):
    def decode(self, input, final=False):
        return codecs.charmap_decode(input, self.errors, decoding_map)[0]

# Lettore/scrittore di flusso

class InvertCapsStreamReader(InvertCapsCodec, codecs.StreamReader):
    pass

class InvertCapsStreamWriter(InvertCapsCodec, codecs.StreamWriter):
    pass

# Registra la funzione di ricerca del codec

def find_invertcaps(encoding):
    """Riturna il codec per 'invertcaps'.
    """
    if encoding == 'invertcaps':
        return codecs.CodecInfo(
            name='invertcaps',
            encode=InvertCapsCodec().encode,
            decode=InvertCapsCodec().decode,
            incrementalencoder=InvertCapsIncrementalEncoder,
            incrementaldecoder=InvertCapsIncrementalDecoder,
            streamreader=InvertCapsStreamReader,
            streamwriter=InvertCapsStreamWriter,
            )
    return None

codecs.register(find_invertcaps)

if __name__ == '__main__':

    # Stateless encoder/decoder
    encoder = codecs.getencoder('invertcaps')
    text = 'abc.DEF'
    encoded_text, consumed = encoder(text)
    print "L'encoder ha convertito '{}' in '{}', utilizzando {} caratteri".format(
        text, encoded_text, consumed)

    # Scrittore di flusso
    import sys
    writer = codecs.getwriter('invertcaps')(sys.stdout)
    print 'Scrittore di flusso per stdout: ',
    writer.write('abc.DEF')
    print

    # Decoder Incrementale
    decoder_factory = codecs.getincrementaldecoder('invertcaps')
    decoder = decoder_factory()
    decoded_text_parts = []
    for c in encoded_text:
        decoded_text_parts.append(decoder.decode(c, final=False))
    decoded_text_parts.append(decoder.decode('', final=True))
    decoded_text = ''.join(decoded_text_parts)
    print "IncrementalDecoder ha convertito '{}' in '{}'".format(
        encoded_text, decoded_text)

La classe base per il codificatore/decoficatore stateless è Codec. Occorre sovrascrivere encode() e decode() con la propria implementazione (in questo caso, chiamando charmap_encode() e charmap_decode() rispettivamente). Ogni metodo deve restituire una tupla che contiene i dati trasformati ed il numero di byte o caratteri in input che sono stati utilizzati. Opportunamente, sia charmap_encode() che charmap_decode() restituiscono già quell'informazione.

IncrementalEncoder ed IncrementalDecoder servono da classe base per le interfacce incrementali. I metodi encode() e decode() delle classi incrementali sono definiti in modo che essi restituiscono solo gli effettivi dati trasformati. Qualsiasi informazione circa il buffering viene mantenuta come stato interno. L'encoding invertcaps non necessita di un buffer di dati (usa una mappatura uno-ad-uno). Per encoding che producono un diverso ammontare in output a seconda dei dati che sono elaborati, tipo gli algoritmi di compressione, BufferedIncrementalEncoder e BufferedIncrementalDecoder sono più appropriati come classi base, visto che esse gestiscono autonomamente la porzione di input non elaborata.

Anche StreamReader e StreamWriter hanno bisogno dei metodi encode() e decode(), e visto che ci si aspetta che restituiscano lo stesso valore della versione di Codec si può utilizzare l'ereditarietà multipla per l'implementazione.

$ python codecs_invertcaps_register.py

L'encoder ha convertito 'abc.DEF' in 'ABC.def', utilizzando 7 caratteri
StreamWriter per per stdout: ABC.def
IncrementalDecoder ha convertito 'ABC.def' in 'abc.DEF'

Vedere anche:

locale
Accedere e gestire le impostazioni ed i comportamenti di configurazione basati sulla localizzazione
io
Anche il modulo io contiene metodi per incapsulare i file e gli stream che gestiscono la codifica e la decodifica
SocketServer
Per informazioni più dettagliate di un echo server, vedere il modulo SocketServer
encodings
Pacchetto della libreria standard che contiene le implementazioni per encoder/decoder fornite da Python.
Unicode HOWTO
La guida ufficiale all'uso di Unicode con Python 2.x.
Python Unicode Objects
Un articolo di Fredrik Lundh sull'uso di insiemi di caratteri non-ASCII in Python 2.0.
How to use UTF-8 with Python
Guida rapida di Evan Jones per lavorare con Unicode, incluso i dati XML ed il Byte-Order Marker.
On the Goodness of Unicode
Introduzione alla internazionalizzazione ed Unicode, di Tim Bray.
On Character Strings
Uno sguardo alla storie della elaborazione di stringhe nei linguaggi di programmazione, di Tim Bray.
Characters vs. Bytes
Prima parte di 'essay on modern character string processing for computer programmers', di Tim Bray. Questo lavoro tratta della rappresentazione in memoria del testo in formati diversi da byte ASCII.
The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)
Una introduzione ad Unicode, di Joel Spolsky.
Ordine dei byte
All'interno dell'articolo di Wikipedia una spiegazione su endianness.