Apartados


9. Caso de estudio: migrar chardet a Python 3

Nivel de dificultad:5 sobre 5

``Palabras, palabras. Son todo lo que tenemos para continuar.''
--Rosencrantz y Guildenstern han muerto9.1

9.1 Inmersión

Pregunta: ¿Cuál es la primera causa de texto ininteligible en la web, en tu buzón de entrada, y en cualquier ordenador? Es la codificación de caracteres. En el capítulo 4, sobre las cadenas de caracteres, hablé sobre la historia de la codificación de caracteres y la creación de Unicode, la “codificación de caracteres para gobernarlas a todas”. Me gustaría no volver a ver ningún carácter ininteligible en una página web nunca más, porque todos los sistemas de creación de contenidos almacenaran información precisa sobre la codificación de caracteres; todos los protocolos de transferencia de información fueran conscientes de la existencia de Unicode, y todos los sistemas manejaran el texto con fidelidad al convertirlo entre diferentes codificaciones.

También me gustaría un pony.

Un pony de Unicode.

Un Unipony, podría decir.

Creo que me decantaré por la detección automática de la codificación de caracteres.

9.2 ¿En qué consiste la detección automática?

Consiste en tomar una secuencia de bytes con una codificación de caracteres desconocida, e intentar determinar cuál es con el fin de poder leer el texto representado. Es como craquear un código cuando no dispones de la clave de decodificación.

9.2.1 ¿Eso no es imposible?

En general: sí. Sin embargo, algunas codificaciones están optimizadas para idiomas específicos, y los idiomas no son aleatorios. Algunas secuencias de caracteres se repiten constantemente, mientras que otras no tienen sentido. Una persona con un inglés fluido que abre un periódico y lee “txzqJv 2!dasd0a QqdKjvz” reconoce instantáneamente que eso no es inglés (incluso aunque esté compuesto de letras ingles). Mediante el estudio de grandes cantidades de texto “típico”, un algoritmo puede simular esta clase de “lectura” fluida y proponer la codificación de caracteres en la que puede encontrar un texto.

En otras palabras, la detección de la codificación de caracteres es en realidad la detección del idioma, combinada con el conocimiento de las codificaciones de caracteres que tienden a utilizar.

9.2.2 ¿Existe tal algoritmo?

Resulta que sí. Todos los navegadores tienen autodetección de la codificación de caracteres ya que la web está llena de páginas que no tienen ninguna información al respecto. Mozilla Firefox contiene una librería de autodetección de la codificación de caracteres9.2 que es de código abierto. Yo porté dicha librería a Python 2 bajo el módulo denominado chardet. Este capítulo describe los pasos a través del proceso de conversión del módulo charffdet de Python 2 a Python 3.

9.3 Introducción al módulo chardet

La detección de la codificación de caracteres es en realidad la detección del lenguaje con dificultades.

Antes de convertir el código, ayudaría que entendieras cómo funciona. Esta es una guía breve a través del propio código. La librería chardet es demasiado larga para incluirla completa aquí, pero puedes descargarla de chardet.feedparser.org9.3.

El punto de entrada principal para el algoritmo de detección es universaldetector.py, que contiene una clase, UniversalDetector 9.4.

UniversalDetector puede manejar cinco categorías de codificaciones de caracteres:

  1. UTF-N con BOM9.5, con las variantes ``Big-Endian'' y ``Little-Endian'' de UTF-16, y todas las variantes de 4-bytes de UTF-16.

  2. Codificaciones de escape, compatibles con ASCII de 7-bits, en las que los caracteres no-ASCII comienzan con una secuencia de escape. Por ejemplo: ISO-2022-JP (japonés) y HZ-GB-2312(chino).

  3. Codificaciones multibyte, en las que cada carácter se representa por un número variable de bytes. Por ejemplo: BIG5 (chino), SHIFT_JIS (japonés), EUC-KR (coreano), y UTF-8 sin BOM.

  4. Codificaciones de un solo byte, en las que cada carácter se representa por un único byte. Por ejemplo: KOI8-R (ruso), WINDOWS-1255 (hebreo), y TIS-620 (thai).

  5. WINDOWS-1252, que se utiliza fundamentalmente en Microsoft Windows por los mandos intermedios que no distinguen una codificación de caracteres de un agujero en el suelo.

9.3.1 UTF-N con BOM

Si el texto comienza con una marca BOM, se puede asumir de forma razonable que está codificado en UTF-8, UTF-16 o UTF-32 (Precisamente el BOM sirve para indicar cuál de ellos es). Esto es manejado por la propia clase UniversalDetector, que retorna el resultado inmediatamente sin proceso adicional.

9.3.2 Codificaciones con código de escape

Si el texto contiene una cadena de caracteres de escape reconocible podría indicar que se encuentra en una de las codificaciones que se basan en ello. UniversalDetector crea un objeto EscCharSetProber (definido en el escprober.py) y le pasa el texto.

ExcCharSetProber crea una serie de máquinas de estado, basadas en los modelos definidos en escsm.py): HZ-GB-2312, ISO-2022-CN, ISO-2022-JP, y ISO-2022-KR. EscCharSetProber alimenta el texto a cada una de las máquinas de estado, byte a byte. Si alguna de ellas finaliza identificando la codificación, EscCharSetProber finaliza devolviendo el resultado a UniversalDetector, que lo retorna al llamante. Si cualquiera de las máquinas de estado alcanza una secuencia ilegal, finaliza su ejecución y se sigue la ejecución con la siguiente máquina de estados.

9.3.3 Codificaciones multibyte

Asumiendo que no existe BOM, UniversalDetector chequea si el texto contiene algún carácter con bits altos activados. Si es así, crea una serie de ``sondas'' para detectar codificaciones multibyte, de un byte y, como último recurso, windows-1252.

La sonda de codificaciones multibyte, MBCSGroupProber (definida en mbcsgroupprober.py), es en realidad un envoltorio que gestiona un grupo de sondas, una para cada tipo de codificación multibyte: BIG5, GB2313, EUC-TW, EUC-KR, EUC-JP, SHIFT_JIS, y UTF-8. MBCSGroupProber alimenta el texto a cada una de las sondas específicas y chequea el resultado. Si una sonda reporta una secuencia de bytes ilegal, se elimina de la búsqueda (cualquier llamada posterior a UniversalDetector.feed() para este texto se saltará a esta sonda). Si una sonda informa que está segura razonablemente de que ha detectado la codificación, MBCSGroupProber informa del resultado positivo a UniversalDetector, que devuelve el resultado al llamante.

La mayoría de las sondas de la codificación multibyte heredan de MultiByteCharSetProber (definida en mbcharsetprober.py, y simplemente activan la máquina de estados y analizador de distribución apropiado y dejan a la clase MultiByteCharSetProber hacer el resto del trabajo. MultiByteCharSetProber recorre el texto a traves de la máquina de estados específica byte a byte, para buscar secuencias de caracteres que pudieran indicar de forma concluyente un resultado positivo o negativo. Al mismo tiempo, MultiByteCharSetProber alimenta el texto a un analizador de distribución específico de cada codificación de caracteres.

Los analizadores de distribución (definidos en chardistribution.py) utilizan modelos específicos para cada idioma que tienen en cuenta los caracteres más frecuentes en cada uno de ellos. Cuando MultiByteCharSetProber ha alimentado suficiente texto a los analizadores, calcula el grado de confianza basándose en el número de caracteres más frecuentes, el número total de caracteres, y el ratio de distribución específico de cada lenguaje. Si el grado de confianza es suficientemente algo, MultiByteCharSetProber devuelve el resultado a MBCSGroupProber, que lo devuelve a UniversalDetector, quien, a su vez, lo devuelve al llamante.

El caso del idioma Japonés es más difícil. Un análisis de distribución monocarácter no es siempre suficiente para distinguir entre EUC-JP y SHIFT_JIS, por ello la sonda SJISProber (definida en sjisprober.py también utiliza un análisis de distribución de dos caracteres. SJISContextAnalysis y EUCJPContextAnalysis (ambos definidos en jpcntx.py) comprueban la frecuencia en el texto de los caracteres del silabario Hiragana. Cuando se ha procesado texto suficiente, devuelven el grado de confianza a SJISProber, que chequea ambos analizadores y devuelve aquél de mayor grado de confianza a MBCSGroupProber.

9.3.4 Codificaciones de un solo byte

En serio, dónde está mi pony unicode

La sonda de codificaciones de un solo byte, SBCSGroupProber (definida en sbcsgroupprober.py), también es un envoltorio que gestiona el grupo de sondas que existen para cada combinación de idioma y codificación de un solo byte: windows-1251, KOI8-R, ISO-8859-5, MacCyrillic, IBM855, y IBM866 (ruso); ISO-8859-7 y windows-1253 (griego); ISO-8859-5 y windows-1251 (búlgaro); ISO-8859-2 y windows-1250 (húngaro); TIS-620 (Thai); windows-1255 e ISO-8859-8 (Hebreo).

SBCSGroupProber alimenta el texto a cada uno de estas sondas específicas y comprueba sus resultados. Las sondas están implementadas con un única clase SingleByteCharSetProber (definida en sbcharsetprober.py, que utiliza como parámetro del constructor el modelo del idioma. Este define la frecuencia de aparición de las diferentes secuencias de dos caracteres en un texto típico. SingleByteByteCharSetProber procesa el texto contando las secuencias de dos caracteres más frecuentes. Cuando se ha procesado suficiente texto, calcula el nivel de confianza basado en el número de dichas secuencias, el número total de caracteres, y la distribución específica del idioma.

El hebreo se maneja como un caso especial. Si el texto parece hebreo, basado en el análisis de distribución de dos caracteres, HebrewProber (definido en hebrewprober.py intenta distinguir entre hebreo visual (en el que el texto está realmente almacenado ``hacia atrás'' línea a línea, y luego se muestra directamente para que pueda leerse de derecha a izquierda) y hebreo lógico (en el que el texto se almacena en el orden de lectura y luego se visualiza de derecha a izquierda por el sistema). Debido a que ciertos caracteres se codifican de forma diferente en función de que aparezcan en medio de una palabra o al final, podemos intentar adivinar de forma razonable la dirección del texto fuente, y devolver la codificación correcta (windows-1255 en el caso de hebreo lógico, o ISO-8859-8 para el hebreo visual).

9.3.5 windows-1252

Si UniversalDetector encuentra en el texto un carácter con el bit alto activado, pero ninguno de las sondas anteriores devuelve un resultado fiable, crea un objeto Latin1Prober (definido en latin1prober.py) para intentar detectar texto en inglés en una codificación windows-1252. Esta no es fiable (inherentemente), porque las letras en inglés se codifican de la misma forma en diferentes codificaciones. La única forma de distinguir windows-1252 es a través de algunos símbolos comúnmente utilizados como las comillas inteligentes, los apóstrofos, símbolos de copyright, y otros similares. Latin1Prober reduce su estimación del nivel de confianza para permitir que le ``ganen'' otras sondas más precisas, si es posible.

9.4 Ejecutando 2to3

Vamos a migrar el módulo chardet de Python 2 a Python 3. Este último trae una utilidad denominada 2to3, que toma el código fuente de Python 2 como entrada y lo convierte de forma automática a Python 3. En algunos casos es fácil --Cambiar una función de librería que fue renombrada o movida a otro módulo--, pero en otros casos puede ser muy complejo. Para entender lo que puede llegar a hacer, lee el apéndice A, Migrando código a Python 3 con 2to3. En este capítulo comenzaremos ejecutando 2to3 en el paquete chardet, verás que aún quedará bastante trabajo que hacer después de que las herramientas automatizadas hayan aplicado su magia.

El paquete chardet está dividido en varios ficheros, todos en el mismo directorio. El script 2to3 facilita la conversión de varios ficheros a la vez: basta con pasarle el nombre del directorio como parámetro y 2to3 convertirá cada uno de los ficheros que contenga.

[escapeinside=(**)]
C:> python c:30to3.py -w chardet RefactoringTool: Skipping implicit fixer: buffer
RefactoringTool: Skipping implicit fixer: idioms
RefactoringTool: Skipping implicit fixer: set_literal
RefactoringTool: Skipping implicit fixer: ws_comma
-- chardet__init__.py (original)
+++ chardet__init__.py (refactored)
@@ -18,7 +18,7 @@
 __version__ = "1.0.1"

def detect(aBuf): (* - import universaldetector *) (* + from . import universaldetector *) u = universaldetector.UniversalDetector() u.reset() u.feed(aBuf) -- chardet5prober.py (original) +++ chardet\big5prober.py (refactored) @@ -25,10 +25,10 @@ # 02110-1301 USA ######################### END LICENSE BLOCK #########################

(* -from mbcharsetprober import MultiByteCharSetProber *) (* -from codingstatemachine import CodingStateMachine *) (* -from chardistribution import Big5DistributionAnalysis *) (* -from mbcssm import Big5SMModel *) (* +from .mbcharsetprober import MultiByteCharSetProber *) (* +from .codingstatemachine import CodingStateMachine *) (* +from .chardistribution import Big5DistributionAnalysis *) (* +from .mbcssm import Big5SMModel *)

class Big5Prober(MultiByteCharSetProber): def __init__(self): -- chardet.py (original) +++ chardet.py (refactored) @@ -25,12 +25,12 @@ # 02110-1301 USA ######################### END LICENSE BLOCK #########################

(* -import constants *) (* -from euctwfreq import EUCTWCharToFreqOrder, *) (* EUCTW_TABLE_SIZE, EUCTW_TYPICAL_DISTRIBUTION_RATIO *) (* -from euckrfreq import EUCKRCharToFreqOrder, *) (* EUCKR_TABLE_SIZE, EUCKR_TYPICAL_DISTRIBUTION_RATIO *) (* -from gb2312freq import GB2312CharToFreqOrder, *) (* GB2312_TABLE_SIZE, GB2312_TYPICAL_DISTRIBUTION_RATIO *) (* -from big5freq import Big5CharToFreqOrder, *) (* BIG5_TABLE_SIZE, BIG5_TYPICAL_DISTRIBUTION_RATIO *) (* -from jisfreq import JISCharToFreqOrder, *) (* JIS_TABLE_SIZE, JIS_TYPICAL_DISTRIBUTION_RATIO *) (* +from . import constants *) (* +from .euctwfreq import EUCTWCharToFreqOrder, *) (* EUCTW_TABLE_SIZE, EUCTW_TYPICAL_DISTRIBUTION_RATIO *) (* +from .euckrfreq import EUCKRCharToFreqOrder, *) (* EUCKR_TABLE_SIZE, EUCKR_TYPICAL_DISTRIBUTION_RATIO *) (* +from .gb2312freq import GB2312CharToFreqOrder, *) (* GB2312_TABLE_SIZE, GB2312_TYPICAL_DISTRIBUTION_RATIO *) (* +from .big5freq import Big5CharToFreqOrder, *) (* BIG5_TABLE_SIZE, BIG5_TYPICAL_DISTRIBUTION_RATIO *) (* +from .jisfreq import JISCharToFreqOrder, *) (* JIS_TABLE_SIZE, JIS_TYPICAL_DISTRIBUTION_RATIO *)

ENOUGH_DATA_THRESHOLD = 1024 SURE_YES = 0.99 . . (* . (Durante un rato va sacando mensajes de este tipo) *) . . RefactoringTool: Files that were modified: RefactoringTool: chardet__init__.py RefactoringTool: chardet5prober.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet2312prober.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet1prober.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet.py RefactoringTool: chardet8prober.py

Now run the 2to3 script on the testing harness, test.py.

C:> python c:30to3.py -w test.py RefactoringTool: Skipping implicit fixer: buffer RefactoringTool: Skipping implicit fixer: idioms RefactoringTool: Skipping implicit fixer: set_literal RefactoringTool: Skipping implicit fixer: ws_comma -- test.py (original) +++ test.py (refactored) @@ -4,7 +4,7 @@ count = 0 u = UniversalDetector() for f in glob.glob(sys.argv[1]): (* - print f.ljust(60), *) (* + print(f.ljust(60), end=' ') *) u.reset() for line in file(f, 'rb'): u.feed(line) @@ -12,8 +12,8 @@ u.close() result = u.result if result['encoding']: (* - print result['encoding'], 'with confidence', result['confidence'] *) (* + print(result['encoding'], 'with confidence', result['confidence']) *) else: (* - print '******** no result' *) (* + print('******** no result') *) count += 1 (* -print count, 'tests' *) (* +print(count, 'tests') *) RefactoringTool: Files that were modified: RefactoringTool: test.py

Bueno, no fue para tanto. Solo se han convertido unos pocos imports y sentencias print. Por cierto, cuál era el problema con todas las sentencias import. Para contestar a esto, tienes que entender que el módulo chardet estaba dividido en múltiples ficheros.

9.5 Una breve disgresión sobre los módulos multifichero

chardet es un módulo multifichero. Podría haber elegido poner todo el código en uno solo (denominado chardet.py, pero no lo hice. En vez de eso, creé un directorio (denominado chardet), luego creé un fichero __init__.py en él, con ello se asume que todos los ficheros de este directorio son parte del mismo módulo. El nombre del módulo es el nombre del directorio. Los ficheros que están dentro del directorio pueden referenciar a otros ficheros dentro del mismo, o incluso en subdirectorios (más sobre esto en un minuto). Pero la colección completa de ficheros se presenta para otro código de Python como un único módulo --como si las funciones y las clases se encontrasen un único fichero de extensión .py.

¿Qué contiene el fichero __init__.py? Nada. Todo. Algo intermedio. El fichero __init__.py no necesita definir nada. Puede ser un fichero vacío. Pero también se puede utilizar para definir en él las funciones que sean punto de entrada a tu módulo. O puedes poner todas las funciones en él. O todas, salvo una...

Un directorio con un fichero __init__.py siempre se trata como un módulo multifichero. Sin un fichero __init__.py, un directorio no contiene más que un conjunto de ficheros .py sin relación alguna

Veamos como funciona esto en la práctica.

»> import chardet
»> dir(chardet)
['__builtins__', '__doc__', '__file__', '__name__',
 '__package__', '__path__', '__version__', 'detect']
»> chardet
<module 'chardet' from 'C:31-packages__init__.py'>

  1. Línea 2: aparte de los atributos de clase habituales, lo único que hay en el módulo chardet es la función detect().
  2. Línea 5: aquí aparece la primera pista de que el módulo chardet está formado por más de un fichero; el ``módulo'' se muestra como procedente del fichero __init__.py del directorio chardet/.

Veamos el contenido del fichero __init__.py.

def detect(aBuf):                              
    from . import universaldetector            
    u = universaldetector.UniversalDetector()
    u.reset()
    u.feed(aBuf)
    u.close()
    return u.result

  1. Línea 1: El fichero define la función detect(), que es el punto de entrada principal del módulo chardet.
  2. Línea 2: Esta función tiene poquísimo código. En realidad, todo lo que hace es importar el módulo universaldetector y comenzar a usarlo. ¿Pero dónde está definido universaldetector?

La respuesta se encuentra en esa extraña sentencia import.

from . import universaldetector

Traducido, significa que ``se importe el módulo universaldetector; que está en el mismo directorio en el que estoy yo (el fichero chardet/__init__.py)''. A esto se le denomina importación relativa. Es la forma en que se localizan entre sí los ficheros que se encuentran en un módulo multifichero, sin preocuparse de conflictos de denominación con otros módulos que puedas haber instalado en tu camino de búsqueda de módulos9.6. Esta sentencia import solamente buscará a universaldetector dentro del propio directorio chardet.

Estos dos conceptos --__init__.py e importación relativa-- permiten que puedas dividir un módulo en las piezas que quieras. El módulo chardet consta de 36 ficheros .py, ¡36!. Aún así, para utilizarlo únicamente necesitas usar import chardet, y luego llamar a la función chardet.detect(). Para tu código es transparente dónde está definida la función, si utiliza una importación relativa de universaldetector y que este, a su vez, utiliza cinco importaciones relativas de otros tantos ficheros, todos contenidos en el directorio chardet/.

Si alguna vez te encuentras en la necesidad de escribir una librería grande en Python (o más probablemente, cuando te des cuenta de que tu pequeña librería ha crecido hasta convertirse en grande), tómate tu tiempo en refactorizarla en un módulo multifichero. Es una de las muchas cosas en las que Python es muy bueno, así que aprovéchate de ello.

9.6 Arreglando lo que 2to3 no puede

9.6.1 False es sintaxis inválida

¿Tienes pruebas? ¿No?

Ahora vamos a las pruebas reales: ejecutar la suite de pruebas. Puesto que la suite de pruebas esta diseñada para cubrir todos los caminos posibles del código, es una buena manera de probar el código migrado para estar seguro de que no hay errores acechando en algún rincón.

C:> python test.py tests&sstarf#star;&sstarf#star;
Traceback (most recent call last):
  File "test.py", line 1, in <module>
    from chardet.universaldetector import UniversalDetector
  File "C:.py", line 51
    self.done = constants.False
                              ^
SyntaxError: invalid syntax

Vaya, un pequeño fallo. En Python 3, False es una palabra reservada, así que no la puedes utilizar como nombre de una variable. Vamos a mirar constants.py para ver dónde está definida. Aquí está la versión original del fichero antes de que 2to3 lo cambiara:

import __builtin__
if not hasattr(__builtin__, 'False'):
    False = 0
    True = 1
else:
    False = __builtin__.False
    True = __builtin__.True

Este código está diseñado para que funcione con versiones antiguas de Python 2. Antes de Python 2.3 no existía el tipo bool. El código detecta la ausencia de las constantes True y False y las define si es necesario.

Sin embargo, en Python 3 siempre existe el tipo bool, por lo que este código es innecesario. La solución más simple pasa por sustituir todas las instancias de constants.True y constants.False por True y False, respectivamente. Y borrar este fichero.

Así, esta línea del fichero universaldetector.py:

self.done = constants.False

Se convierte en:

self.done = False

¡Ah! ¿No es satisfactorio? El código queda más corto y más legible así.

9.6.2 No hay ningún módulo denominado constants

Hora de volver a ejecutar test.py y ver hasta donde llega.

C:> python test.py tests&sstarf#star;&sstarf#star;
Traceback (most recent call last):
  File "test.py", line 1, in <module>
    from chardet.universaldetector import UniversalDetector
  File "C:.py", line 29, in <module>
    import constants, sys
ImportError: No module named constants

¿Qué dices? ¿Que no hay un módulo denominado constants? Desde luego que sí. Está ahí mismo, en chardet/constants.py.

¿Recuerdas que el comando 2to3 arregló muchas sentencias import? Esta librería tiene muchas importaciones relativas --esto es, módulos que importan a otros módulos dentro de la misma librería-- pero la lógica que gobierna las mismas ha cambiado en Python 3. En Python 2, podías escribir import constants y el primer lugar en el que se busca es en el directorio chardet/. En Python 3, todas las sentencias import son absolutas por defecto. Si quieres una relativa hay que ser explícito:

from . import constants

Pero espera, ¿No se supone que 2to3 tenía que haber resuelto esto por ti? Bueno, lo hizo, pero esta sentencia en particular (import constants, sys) combina dos tipos diferentes de importación en una única línea: una relativa, el módulo constants; y una absoluta, el módulo sys que está preinstalado en la librería estándar de Python. En Python 2, podías combinar ambas en una única sentencia. En Python 3 no puedes. Además, el comando 2to3 no es lo suficientemente inteligente como para dividir esta sentencia en dos.

La solución consiste en dividir la sentencia manualmente:

import constants, sys

Se debe transformar en:

from . import constants
import sys

Hay diversas variaciones de este problema repartidas a lo largo de la librería chardet. En algunos lugares es ``import constants, sys''; en otros, es ``import constants, re''. El arreglo siempre es igual: dividir manualmente esta sentencia en dos: una para la importación relativa, la otra para la importación absoluta.

¡Sigamos!

9.6.3 El nombre ``file'' no está definido

Y aquí vamos de nuevo, ejecutamos test.py para ver qué sucede con los casos de prueba...

C:> python test.py tests&sstarf#star;&sstarf#star;
tests.diveintomark.org.xml
Traceback (most recent call last):
  File "test.py", line 9, in <module>
    for line in file(f, 'rb'):
NameError: name 'file' is not defined

open() es el nuevo file(). PapayaWhip es el nuevo negro

Esta me sorprendió, porque he estado utilizando file() desde que tengo memoria. En Python 2, la función global file() es un alias de la función open(), que es el modo estándar de abrir ficheros de lectura9.7. En Python 3, no existe la función global file().

La solución más simple de este problema es sustituir la llamada a la función file por open:

for line in open(f, 'rb'):

Y eso es todo lo que tengo que decir sobre este tema.

9.6.4 No puedes usar un patrón de cadena de texto en un objeto que representa bytes

Las cosas comienzan a ponerse interesante. Y por ``interesante'' quiero decir ``confusas como el infierno''.

C:> python test.py tests&sstarf#star;&sstarf#star;
tests.diveintomark.org.xml
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    u.feed(line)
  File "C:.py", line 98, in feed
    if self._highBitDetector.search(aBuf):
TypeError: can't use a string pattern on a bytes-like object

Para depurar esto veamos lo que es self._highBitDetector. Está definido en el método __init__ de la clase UniversalDetector.

class UniversalDetector:
    def __init__(self):
        self._highBitDetector = re.compile(r'[80-]')

Esto precompila una expresión regular diseñada para encontrar caracteres no ASCII que se encuentren en el rango 128-255 (0x80-0xFF). Espera, esto no es del todo cierto, necesito ser más preciso con mi terminología. Este patrón está diseñado para encontrar bytes no ASCII en el rango 128-255.

Y ese es el problema.

En Python 2, una cadena de texto era un array de bytes cuya codificación de caracteres se mantenía separadamente. Si querías conservar la codificación de caracteres, en su lugar tenías que utilizar una cadena de caracteres Unicode (u''). Pero en Python 3 una cadena de caracteres siempre es lo que Python 2 llamaba una cadena de caracteres Unicode --esto es, un array de caracteres Unicode (de un número de bytes posiblemente variable). Como esta expresión regular está definida por un patrón de cadena de caracteres, solo puede utilizarse para buscar en cadenas de caracteres-- es decir, un array de caracteres. Pero lo que estamos buscando no es un cadena de caracteres, es un array de bytes. Si observamos la traza del error, este sucede en universaldetector.py:

def feed(self, aBuf):
    .
    .
    .
    if self._mInputState == ePureAscii:
        if self._highBitDetector.search(aBuf):

¿Y qué es un aBuf? Vamos un poco más atrás a un lugar que llame a UniversalDetector.feed(). La prueba test.py lo hace:

u = UniversalDetector()
.
.
.
for line in open(f, 'rb'):
    u.feed(line)

Y aquí encontramos nuestra respuesta: la variable aBuf del método UniversalDetector.feed() contiene una línea leída de un fichero del disco. Observa que los parámetros utilizados para su apertura son 'rb'. 'r' es para que sea de lectura; y 'b' es para indicar 'binario'. Sin este último parámetro, este bucle for podría leer el fichero, línea por línea, y convertir cada una de ellas en una cadena de caracteres --un array de caracteres Unicode-- de acuerdo a la codificación de caracteres por defecto del sistema. Pero con el parámetro 'b', este bucle for lee el fichero, línea a línea y almacena cada una de ellas exactamente como aparecen en el fichero, como un array de bytes. Ese array de bytes se pasa a UniversalDetector.feed() y, llegado el momento, se le pasa a la expresión regular precompilada, self._highBitDetector, con el fin de buscar caracteres con el bit alto activado...Pero no tenemos caracteres, tenemos bytes. ¡Subir as!

Necesitamos que esta expresión regular busque en un array de bytes, no de caracteres.

Una vez nos damos cuenta de ello, la solución no es difícil. Las expresiones regulares definidas con cadenas de caracteres buscan en cadenas de caracteres. Las expresiones regulares definidas con un array de bytes buscan en arrays de bytes. Para definir un patrón como un array de bytes, simplemente modificamos el tipo del argumento que usamos para definir la expresión regular:

[escapeinside=(**)]
  class UniversalDetector:
      def __init__(self):
(* 
-         self._highBitDetector = re.compile(r'[\x80-\xFF]') *)
(* 
-         self._escDetector = re.compile(r'(\033| )') *)
(* 
+         self._highBitDetector = re.compile(b'[\x80-\xFF]') *)
(* 
+         self._escDetector = re.compile(b'(\033| )') *)
          self._mEscCharSetProber = None
          self._mCharSetProbers = []
          self.reset()

La búsqueda de todo el código para localizar otros usos del módulo re encuentra dos sitios más, ambos en charsetprober.py. De nuevo, el código define las expresiones regulares como cadenas de caracteres pero las está ejecutando sobre aBuf, que es un array de bytes. La solución es la misma: definir los patrones de la expresión regular como un array de bytes.

[escapeinside=(**)]
  class CharSetProber:
      .
      .
      .
      def filter_high_bit_only(self, aBuf):
(* 
-         aBuf = re.sub(r'([\x00-\x7F])+', ' ', aBuf) *)
(* 
+         aBuf = re.sub(b'([\x00-\x7F])+', b' ', aBuf) *)
          return aBuf

def filter_without_english_letters(self, aBuf): (* - aBuf = re.sub(r'([A-Za-z])+', ' ', aBuf) *) (* + aBuf = re.sub(b'([A-Za-z])+', b' ', aBuf) *) return aBuf

9.6.5 No puedo convertir un objeto 'bytes' en str implícitamente

¡Curioso y requetecurioso!

C:> python test.py tests&sstarf#star;&sstarf#star;
tests.diveintomark.org.xml
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    u.feed(line)
  File "C:.py", line 100, in feed
    elif (self._mInputState == ePureAscii) and self._escDetector.search(self._mLastChar + aBuf):
TypeError: Can't convert 'bytes' object to str implicitly

En este caso existe un desafortunado choque entre el estilo de codificación y el intérprete de Python. El TypeError podría estar en cualquier lugar de la línea, pero la traza no dice exactamente dónde está. Podría ser en la primera condición o en la segunda. Para acotarlo, deberías dividir la línea en dos, así:

elif (self._mInputState == ePureAscii) and      self._escDetector.search(self._mLastChar + aBuf):

Ejecutar de nuevo el test:

C:> python test.py tests&sstarf#star;&sstarf#star;
tests.diveintomark.org.xml
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    u.feed(line)
  File "C:.py", line 101, in feed
    self._escDetector.search(self._mLastChar + aBuf):
TypeError: Can't convert 'bytes' object to str implicitly

¡Ajá! El problema no estaba en la primera parte de la condición (self._mInputState == ePureAscii) sino en la segunda. ¿Qué ha podido causar el error? Quizás pienses que el método search() está esperando un valor de un tipo diferente, pero eso no generaría esta traza. Las funciones de Python pueden tomar cualquier valor; si pasas el número correcto de parámetros, la función se ejecutará. Puede cascar si le pasas un valor de un tipo diferente el que esté esperando, pero si eso sucediera, la traza apuntaría a algún lugar interno de la función. Esta traza, sin embargo, dice que ni siquiera a podido llamar al método search(). Así que el problema debe estar en la operación +, cuando intenta construir el valor que posteriormente deberá pasarse a search().

Sabemos por la depuración anterior que aBuf es un array de bytes. Pero ¿qué contiene self._mLastChar? Es una variable de instancia, definida en el método reset(), que se llama desde el método __init__().

class UniversalDetector:
    def __init__(self):
        self._highBitDetector = re.compile(b'[80-]')
        self._escDetector = re.compile(b'(33| )')
        self._mEscCharSetProber = None
        self._mCharSetProbers = []
        (* 
 self.reset() *)

def reset(self): self.result = 'encoding': None, 'confidence': 0.0 self.done = False self._mStart = True self._mGotData = False self._mInputState = ePureAscii (* self._mLastChar = '' *)

Y ya tenemos nuestra respuesta. ¿La ves? self._mLastChar es una cadena de caracteres, pero aBuf es un array de bytes. Y no puedes concatenar una cadena de caracteres con un array de bytes --ni siquiera con una cadena de caracteres vacía.

En cualquier caso ¿Qué es self._mLastChar? En el método feed(), mira unas líneas más abajo en el que la traza ha sucedido.

if self._mInputState == ePureAscii:
    if self._highBitDetector.search(aBuf):
        self._mInputState = eHighbyte
    elif (self._mInputState == ePureAscii) and              self._escDetector.search(self._mLastChar + aBuf):
        self._mInputState = eEscAscii

(* self._mLastChar = aBuf[-1] *)

Se llama al método feed() una y otra vez con unos pocos bytes cada vez. El método procesa los bytes (pasados en aBuf), luego almacena el último byte en self._mLastChar por si se necesita en la siguiente llamada. (En una codificación multibyte el método feed() podría ser llamado a la mitad de un carácter para volver a ser llamado después con la otra mitad). Pero ya que aBuf ahora es un array de bytes en lugar de una cadena de caracteres, self._mLastChar también necesita ser un array de bytes. Así:

  def reset(self):
      .
      .
      .
      (* 
-     self._mLastChar = '' *)
      (* 
+     self._mLastChar = b'' *)

Si buscamos a lo largo de todo el código por ``mLastChar'' encontramos un problema similar en mbcharsetprober.py, pero en lugar de mantener el último carácter, mantiene el recuerdo de los dos últimos. La clase MultiByteCharSetProber utiliza una lista de cadenas de caracteres de un solo carácter para mantener los últimos dos caracteres. En Python 3 necesita utilizar una lista de enteros ya que en realidad no está manteniendo caracteres, sino bytes (Los bytes son enteros entre 0-255).

  class MultiByteCharSetProber(CharSetProber):
      def __init__(self):
          CharSetProber.__init__(self)
          self._mDistributionAnalyzer = None
          self._mCodingSM = None
          (* 
-         self._mLastChar = ['\x00', '\x00'] *)
          (* 
+         self._mLastChar = [0, 0] *)

def reset(self): CharSetProber.reset(self) if self._mCodingSM: self._mCodingSM.reset() if self._mDistributionAnalyzer: self._mDistributionAnalyzer.reset() (* - self._mLastChar = ['\x00', '\x00'] *) (* + self._mLastChar = [0, 0] *)

9.6.6 Tipo del operando no soportado para +: 'int' y 'bytes'

Tengo buenas y malas noticias. Las buenas noticias es que estamos haciendo progresos...

C:> python test.py tests&sstarf#star;&sstarf#star;
tests.diveintomark.org.xml
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    u.feed(line)
  File "C:.py", line 101, in feed
    self._escDetector.search(self._mLastChar + aBuf):
TypeError: unsupported operand type(s) for +: 'int' and 'bytes'

...las malas noticias es que no siempre da esa sensación.

¡Pero es progreso! ¡En serio! Incluso aunque la traza da el error en la misma línea de código: es un error diferente al que era. ¡Progreso! ¿Cuál es el problema ahora? La última vez miramos, esta línea de código no intentaba concatenar un int con un array de bytes(bytes). De hecho, hemos pasado un buen rato tratando de asegurar que self._mLastChar fuera un array de bytes. Cómo se convirtió en un int.

La respuesta descansa en las líneas siguientes de código:

if self._mInputState == ePureAscii:
    if self._highBitDetector.search(aBuf):
        self._mInputState = eHighbyte
    elif (self._mInputState == ePureAscii) and              self._escDetector.search(self._mLastChar + aBuf):
        self._mInputState = eEscAscii

(* self._mLastChar = aBuf[-1] *)

Cada elemento en una cadena de caracteres es una cadena de caracteres. Cada elemento en una array de bytes es un entero

Este error no sucede la primera vez que se ejecuta el método feed(). Ocurre la segunda vez, después de que self._mLastChar se haya activado con el último byte de aBuf. Bien ¿Cuál es el problema? Al obtener un elemento de un array de bytes se genera un valor entero (int), no un array de bytes. Para ver la diferencia, sígueme en esta consola interactiva:

»> aBuf = b''
»> len(aBuf)
3
»> mLastChar = aBuf[-1]
»> mLastChar            
191
»> type(mLastChar)      
<class 'int'>
»> mLastChar + aBuf    
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'bytes'
»> mLastChar = aBuf[-1:]
»> mLastChar
b''
»> mLastChar + aBuf    
b''

  1. Línea 1: Define un array de bytes de longitud 3.
  2. Línea 5: El último elemento del array de bytes es 191.
  3. Línea 7: Es un entero.
  4. Línea 9: No se puede concatenar un entero con un array de bytes. Con esto está replicado el error que se ha encontrado en universaldetector.py.
  5. Línea 13: Esta es la forma de arreglarlo. En lugar de tomar el último elemento del array de bytes, utiliza la selección de listas para crear un nuevo array de bytes de un único elemento. Este array de bytes comienza con el último elemento y continua la selección hasta el final del array. Ahora mLastChar contiene un array de longitud 1.
  6. Línea 16: Al concatenar un array de longitud 1 con otro de longitud 3 da como resultado un nuevo array de bytes de longitud 4.

Así, para asegurar que el método feed() de universaldetector.py continúa funcionando todas las veces que sea llamado, necesitas inicializar self._mLastChar como un array de bytes de longitud cero, y luego asegurar que sigue conteniendo un array de bytes permanentemente.

              self._escDetector.search(self._mLastChar + aBuf):
          self._mInputState = eEscAscii

(* - self._mLastChar = aBuf[-1] *) (* + self._mLastChar = aBuf[-1:] *)

9.6.7 ord() esperaba una cadena de caracteres de longitud 1, pero encontró un int

¿Cansado ya? Casi hemos terminado...

C:> python test.py tests&sstarf#star;&sstarf#star;
tests.diveintomark.org.xml   ascii with confidence 1.0
tests5804.blogspot.com.xml
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    u.feed(line)
  File "C:.py", line 116, in feed
    if prober.feed(aBuf) == constants.eFoundIt:
  File "C:.py", line 60, in feed
    st = prober.feed(aBuf)
  File "C:8prober.py", line 53, in feed
    codingState = self._mCodingSM.next_state(c)
  File "C:.py", line 43, in next_state
    byteCls = self._mModel['classTable'][ord(c)]
TypeError: ord() expected string of length 1, but int found

Ok, así que c contiene un número entero, pero la función ord() estaba esperando una cadena de caracteres de un solo carácter. Es justo. ¿Dónde está definido c?

# codingstatemachine.py
def next_state(self, c):
    # for each byte we get its class
    # if it is first byte, we also get byte length
    byteCls = self._mModel['classTable'][ord(c)]

Este no es de ayuda; simplemente sabemos que es un parámetro de la función. Vamos a tirar de la traza.

# utf8prober.py
def feed(self, aBuf):
    for c in aBuf:
        codingState = self._mCodingSM.next_state(c)

¿Lo ves? En Python 2 aBuf era una cadena de caracteres, así que c era una cadena de un solo carácter (Eso es lo que recuperas cuando iteras a través de una cadena de caracteres --todos los caracteres, de uno en uno). Pero ahora aBuf es un array de bytes, así que c es un int, no una cadena de un carácter. En otras palabras, no hay necesidad de llamar a la función ord() ya que c ya es de tipo entero.

Así que:

  def next_state(self, c):
      # for each byte we get its class
      # if it is first byte, we also get byte length
      (* 
-     byteCls = self._mModel['classTable'][ord(c)] *)
      (* 
+     byteCls = self._mModel['classTable'][c] *)

Si se busca en el código por llamadas a ord(c) se descubren problemas similares en sbcharsetprober.py...

# sbcharsetprober.py
def feed(self, aBuf):
    if not self._mModel['keepEnglishLetter']:
        aBuf = self.filter_without_english_letters(aBuf)
    aLen = len(aBuf)
    if not aLen:
        return self.get_state()
    for c in aBuf:
    (* 
 order = self._mModel['charToOrderMap'][ord(c)] *)

Y en latin1prober.py...

# latin1prober.py
def feed(self, aBuf):
    aBuf = self.filter_with_english_letters(aBuf)
    for c in aBuf:
    (* 
charClass = Latin1_CharToClass[ord(c)] *)

c itera sobre aBuf, lo que significa que es un entero, no una cadena de caracteres de longitud 1. La solución es la misma: cambiar ord(c) por c.

  # sbcharsetprober.py
  def feed(self, aBuf):
      if not self._mModel['keepEnglishLetter']:
          aBuf = self.filter_without_english_letters(aBuf)
      aLen = len(aBuf)
      if not aLen:
          return self.get_state()
      for c in aBuf:
          (* 
-         order = self._mModel['charToOrderMap'][ord(c)] *)
          (* 
+         order = self._mModel['charToOrderMap'][c] *)

# latin1prober.py def feed(self, aBuf): aBuf = self.filter_with_english_letters(aBuf) for c in aBuf: (* - charClass = Latin1_CharToClass[ord(c)] *) (* + charClass = Latin1_CharToClass[c] *)

9.6.8 Tipos no ordenables: int() >= str()

Sigamos:

C:> python test.py tests&sstarf#star;&sstarf#star;
tests.diveintomark.org.xml                       ascii with confidence 1.0
tests5804.blogspot.com.xml
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    u.feed(line)
  File "C:.py", line 116, in feed
    if prober.feed(aBuf) == constants.eFoundIt:
  File "C:.py", line 60, in feed
    st = prober.feed(aBuf)
  File "C:.py", line 68, in feed
    self._mContextAnalyzer.feed(self._mLastChar[2 - charLen :], charLen)
  File "C:.py", line 145, in feed
    order, charLen = self.get_order(aBuf[i:i+2])
  File "C:.py", line 176, in get_order
    if ((aStr[0] >= '81') and (aStr[0] <= '9F')) or  TypeError: unorderable types: int() >= str()

¿Qué significa esto? ¿Tipos no ordenables? De nuevo, la diferencia entre los arrays de bytes y las cadenas de caracteres nos ataca. Echa un vistazo al código:

class SJISContextAnalysis(JapaneseContextAnalysis):
    def get_order(self, aStr):
        if not aStr: return -1, 1
        # find out current char's byte length
        (* 
if ((aStr[0] >= '\x81') and (aStr[0] <= '\x9F')) or \*)
           ((aStr[0] >= '0') and (aStr[0] <= '')):
            charLen = 2
        else:
            charLen = 1

¿Y de dónde viene aStr? Miremos la traza:

def feed(self, aBuf, aLen):
    .
    .
    .
    i = self._mNeedToSkipCharNum
    while i < aLen:
        (* 
order, charLen = self.get_order(aBuf[i:i+2]) *)

Otra vez nuestro viejo amigo aBuf. Como ya has adivinado de todos los problemas anteriores aBuf es un array de bytes. Aquí, el método feed() no lo pasa completo; pasa una sección. Pero como viste anteriormente en el capítulo, una partición de un array de bytes devuelve un array de bytes, así que el parámetro aStr que se pasa, a su vez, al método get_order() es un array de bytes.

¿Y qué intenta hacer el código con aStr? Toma el primer elemento del array de bytes y lo compara con una cadena de longitud 1. En Python 2 esto funcionaba porque aStr y aBuf eran cadenas, y aStr[0] seguía siendo una cadena de caracteres, y se pueden comparar cadenas de caracteres para ver si son distintas. Pero en Python 3 aStr y aBuf son arrays de bytes, aStr[0] es un número entero y no se pueden comparar enteros y cadenas de caracteres sin realizar la conversión de uno de ellos.

En este caso no hay necesidad de hacer el código más complicado añadiendo una conversión explícita. aStr[0] devuelve un entero; las cosas que estás comparando son constantes. Así que vamos a cambiarlas de ser cadenas de caracteres de longitud 1 a números enteros. Y mientras estamos en ello, vamos a cambiar aStr a aBuf, ya que realmente no es una cadena de caracteres.

  class SJISContextAnalysis(JapaneseContextAnalysis):
(* 
-     def get_order(self, aStr): *)
(* 
-      if not aStr: return -1, 1 *)
(* 
+     def get_order(self, aBuf): *)
(* 
+      if not aBuf: return -1, 1 *)
          # find out current char's byte length
(* 
-         if ((aStr[0] >= '\x81') and (aStr[0] <= '\x9F')) or  *)
(* 
-            ((aBuf[0] >= '\xE0') and (aBuf[0] <= '\xFC')): *)
(* 
+         if ((aBuf[0] >= 0x81) and (aBuf[0] <= 0x9F)) or \*)
(* 
+            ((aBuf[0] >= 0xE0) and (aBuf[0] <= 0xFC)): *)
              charLen = 2
          else:
              charLen = 1

# return its order if it is hiragana (* - if len(aStr) > 1: *) (* - if (aStr[0] == '\202') and *) (* - (aStr[1] >= '\x9F') and *) (* - (aStr[1] <= '\xF1'): *) (* - return ord(aStr[1]) - 0x9F, charLen *) (* + if len(aBuf) > 1: *) (* + if (aBuf[0] == 202) and *) (* + (aBuf[1] >= 0x9F) and *) (* + (aBuf[1] <= 0xF1): *) (* + return aBuf[1] - 0x9F, charLen *)

return -1, charLen

class EUCJPContextAnalysis(JapaneseContextAnalysis): (* - def get_order(self, aStr): *) (* - if not aStr: return -1, 1 *) (* + def get_order(self, aBuf): *) (* + if not aBuf: return -1, 1 *) # find out current char's byte length (* - if (aStr[0] == '\x8E') or *) (* - ((aStr[0] >= '\xA1') and (aStr[0] <= '\xFE')): *) (* + if (aBuf[0] == 0x8E) or *) (* + ((aBuf[0] >= 0xA1) and (aBuf[0] <= 0xFE)): *) charLen = 2 (* - elif aStr[0] == '\x8F': *) (* + elif aBuf[0] == 0x8F: *) charLen = 3 else: charLen = 1

# return its order if it is hiragana (* - if len(aStr) > 1: *) (* - if (aStr[0] == '\xA4') and *) (* - (aStr[1] >= '\xA1') and *) (* - (aStr[1] <= '\xF3'): *) (* - return ord(aStr[1]) - 0xA1, charLen *) (* + if len(aBuf) > 1: *) (* + if (aBuf[0] == 0xA4) and *) (* + (aBuf[1] >= 0xA1) and *) (* + (aBuf[1] <= 0xF3): *) (* + return aBuf[1] - 0xA1, charLen *)

return -1, charLen

La búsqueda de más ocurrencias de la función ord() en el código descubre el mismo problema en chardistribution.py (específicamente en EUCTWDistributionAnalysis, EUCKRDistributionAnalysis, GB2312DistributionAnalysis, Big5DistributionAnlysis, SJIDistributionAnalysis, y EUCJPDistributionAnalysis. En cada caso, la subsanación es similar a la realizada en las clases EUCJPContextAnalysis y SJISContextAnalysis de jpcntx.py.

9.6.9 El nombre global ``reduce'' no está definido

De nuevo en la brecha...

C:> python test.py tests&sstarf#star;&sstarf#star;
tests.diveintomark.org.xml                       ascii with confidence 1.0
tests5804.blogspot.com.xml
Traceback (most recent call last):
  File "test.py", line 12, in <module>
    u.close()
  File "C:.py", line 141, in close
    proberConfidence = prober.get_confidence()
  File "C:1prober.py", line 126, in get_confidence
    total = reduce(operator.add, self._mFreqCounter)
NameError: global name 'reduce' is not defined

De acuerdo a la guía oficial ``Qué es lo nuevo de Python 3.0''9.8, la función reduce() se ha movido del espacio global de nombres al módulo functools. Parafraseando a la guía: utiliza functools.reduce() si realmente lo necesitas; sin embargo, el 99% de las ocasiones, un bucle for loop resulta más legible. Puedes leer más sobre esta decisión de Guido van Rossum en su blog: La fortuna de reduce() en Python 3.009.9.

def get_confidence(self):
    if self.get_state() == constants.eNotMe:
        return 0.01

(* total = reduce(operator.add, self._mFreqCounter) *)

La función reduce() toma dos parámetros --una función y una lista (estrictamente, un objeto iterable)-- y aplica la función acumulativamente a cada elemento de la lista. En otras palabras, es una forma de sumar todos los elementos de una lista y devolver el resultado.

Esta monstruosidad era tan común que Python añadió una función sum() global.

def get_confidence(self):
      if self.get_state() == constants.eNotMe:
          return 0.01

(* - total = reduce(operator.add, self._mFreqCounter) *) (* + total = sum(self._mFreqCounter) *)

Ya que dejamos de usar el módulo operator, podemos suprimir el import correspondiente del fichero:

from .charsetprober import CharSetProber
  from . import constants
  (* 
- import operator *) (* 
 *)
 

¿Otra prueba?

C:> python test.py tests&sstarf#star;&sstarf#star;
tests.diveintomark.org.xml                       ascii with confidence 1.0
tests5804.blogspot.com.xml                             Big5 with confidence 0.99
tests5.worren.net.xml                               Big5 with confidence 0.99
tests5.blogspot.com.xml                        Big5 with confidence 0.99
tests5.blogspot.com.xml                        Big5 with confidence 0.99
tests5.org.tw.xml                               Big5 with confidence 0.99
tests5.com.xml                               Big5 with confidence 0.99
tests5.us.xml                                       Big5 with confidence 0.99
tests5.blogspot.com.xml                         Big5 with confidence 0.99
tests5.blogspot.com.xml                       Big5 with confidence 0.99
tests5207.blogspot.com.xml                            Big5 with confidence 0.99
tests5.blogspot.com.xml                         Big5 with confidence 0.99
tests5.blogspot.com.xml                       Big5 with confidence 0.99
tests5.blogspot.com.xml                         Big5 with confidence 0.99
tests5.blogspot.com.xml                        Big5 with confidence 0.99
tests5.pchome.com.tw.xml                          Big5 with confidence 0.99
tests5-design.com.xml                                Big5 with confidence 0.99
tests5.blogspot.com.xml                         Big5 with confidence 0.99
tests5.edu.tw.xml                                 Big5 with confidence 0.99
tests51976.blogspot.com.xml                       Big5 with confidence 0.99
tests5.blogspot.com.xml                           Big5 with confidence 0.99
tests5.blog.xubg.com.xml                              Big5 with confidence 0.99
tests5.com.xml                            Big5 with confidence 0.99
tests5.com.xml                                    Big5 with confidence 0.99
tests5.blogspot.com.xml                      Big5 with confidence 0.99
tests5.blogspot.com.xml                              Big5 with confidence 0.99
tests-JP.co.jp.xml                                  EUC-JP with confidence 0.99
tests-JP.main.jp.xml                             EUC-JP with confidence 0.99
tests-JP.jp.xml                                  EUC-JP with confidence 0.99
.
.
.
316 tests

¡Funciona! ¡Quiero bailar un ratito!9.10

9.7 Resumen

¿Qué hemos aprendido?

  1. Migrar una cantidad no trivial de código de Python 2 a Python 3 es un ``dolor''. No hay forma de evitarlo. Es duro.
  2. La herramienta 2to3 es útil pero solamente cubre lo más fácil: cambios de nombre de funciones, cambios de nombre de módulos, cambios de sintaxis. Es una pieza de ingeniería impresionante, pero al final es solo un robot de búsqueda y sustitución avanzada.
  3. El primer problema en esta librería fue la diferencia existente entre cadenas de caracteres y bytes. En este caso parece obvio ya que el objetivo de esta librería es convertir bytes en cadenas de caracteres. Pero esto sucede más a menudo de lo que parece. Cualquier lectura de un fichero en modo binario devuelve un array de bytes. Cuando se recupera una página web, cuando se llama a una API en la web. Todo ello devuelve un array de bytes.

  4. Necesitas conocer tu programa. Profundamente. Muchas veces lo has escrito tú, pero otras no. Hay que conocer el porqué de su código. Los errores aparecen en cualquier sitio y hay que corregirlos.
  5. Los casos de prueba son fundamentales. No migres nada sin ellos. La única razón por la que tengo confianza en que chardet funcione en Python 3 es que comencé con una suite de pruebas que recorre la mayor parte del código de esta librería. Si no dispones de pruebas, escribe algunas antes de iniciar la migración a Python 3. Si tienes algunas pruebas: escribe más. Si tienes muchas pruebas, puedes empezar la diversión.

José Miguel González Aguilera 2016-08-18