Python 3: Desmitificando los métodos de encoding y deencoding

Digamos que tengo una cadena en Python:

>>> s = 'python' >>> len(s) 6 

Ahora encode esta cadena como esta:

 >>> b = s.encode('utf-8') >>> b16 = s.encode('utf-16') >>> b32 = s.encode('utf-32') 

Lo que obtengo de las operaciones anteriores es una matriz de bytes, es decir, b , b16 y b32 son solo matrices de bytes (cada byte tiene una longitud de 8 bits, por supuesto).

Pero nosotros codificamos la cadena. ¿Entonces, qué significa esto? ¿Cómo adjuntamos la noción de “encoding” con la matriz cruda de bytes?

La respuesta está en el hecho de que cada una de estas matrices de bytes se genera de una manera particular. Veamos estas matrices:

 >>> [hex(x) for x in b] ['0x70', '0x79', '0x74', '0x68', '0x6f', '0x6e'] >>> len(b) 6 

Esta matriz indica que para cada carácter tenemos un byte (porque todos los caracteres caen por debajo de 127). Por lo tanto, podemos decir que “codificar” la cadena a ‘utf-8’ recostack el correspondiente punto de código de cada carácter y lo coloca en la matriz. Si el punto de código no puede caber en un byte, utf-8 consume dos bytes. Por lo tanto, utf-8 consume el menor número de bytes posible.

 >>> [hex(x) for x in b16] ['0xff', '0xfe', '0x70', '0x0', '0x79', '0x0', '0x74', '0x0', '0x68', '0x0', '0x6f', '0x0', '0x6e', '0x0'] >>> len(b16) 14 # (2 + 6*2) 

Aquí podemos ver que la “encoding a utf-16” coloca primero una lista de materiales de dos bytes ( FF FE ) en la matriz de bytes, y luego, para cada carácter, coloca dos bytes en la matriz. (En nuestro caso, el segundo byte es siempre cero)

 >>> [hex(x) for x in b32] ['0xff', '0xfe', '0x0', '0x0', '0x70', '0x0', '0x0', '0x0', '0x79', '0x0', '0x0', '0x0', '0x74', '0x0', '0x0', '0x0', '0x68', '0x0', '0x0', '0x0', '0x6f', '0x0', '0x0', '0x0', '0x6e', '0x0', '0x0', '0x0'] >>> len(b32) 28 # (2+ 6*4 + 2) 

En el caso de “encoding in utf-32”, primero ponemos la lista de materiales, luego, para cada carácter, ponemos cuatro bytes y, por último, colocamos dos bytes cero en la matriz.

Por lo tanto, podemos decir que el “proceso de encoding” recostack 1 2 o 4 bytes (dependiendo del nombre de encoding) para cada carácter en la cadena y los antepone y les agrega más bytes para crear la matriz de bytes de resultado final.

Ahora, mis preguntas:

  • ¿Mi comprensión del proceso de encoding es correcta o me falta algo?
  • Podemos ver que la representación en memoria de las variables b , b16 y b32 es en realidad una lista de bytes. ¿Cuál es la representación en memoria de la cadena? ¿Exactamente qué se almacena en la memoria para una cadena?
  • Sabemos que cuando hacemos una encode() , el punto de código correspondiente a cada carácter se recostack (punto de código correspondiente al nombre de encoding) y se coloca en una matriz o bytes. ¿Qué sucede exactamente cuando hacemos una decode() ?
  • Podemos ver que en utf-16 y utf-32, se incluye una lista de materiales, pero ¿por qué se anexan dos bytes cero en la encoding de utf-32?

En primer lugar, UTF-32 es una encoding de 4 bytes, por lo que su lista de materiales es también una secuencia de cuatro bytes:

 >>> import codecs >>> codecs.BOM_UTF32 b'\xff\xfe\x00\x00' 

Y debido a que diferentes architectures de computadora tratan los pedidos de bytes de manera diferente (llamada Endianess ), hay dos variantes de la lista de materiales, Little y Big Endian:

 >>> codecs.BOM_UTF32_LE b'\xff\xfe\x00\x00' >>> codecs.BOM_UTF32_BE b'\x00\x00\xfe\xff' 

El propósito de la lista de materiales es comunicar ese orden al decodificador; lee la lista de materiales y sabes si es grande o pequeño. Por lo tanto, esos dos últimos bytes nulos en su cadena UTF-32 son parte del último carácter codificado.

La lista de materiales UTF-16 es, por lo tanto, similar, ya que hay dos variantes:

 >>> codecs.BOM_UTF16 b'\xff\xfe' >>> codecs.BOM_UTF16_LE b'\xff\xfe' >>> codecs.BOM_UTF16_BE b'\xfe\xff' 

Depende de la architecture de su computadora, la que se usa por defecto.

UTF-8 no necesita un BOM en absoluto; UTF-8 usa 1 o más bytes por carácter (agregando bytes según sea necesario para codificar valores más complejos), pero el orden de esos bytes está definido en la norma. Microsoft consideró necesario introducir una lista de materiales UTF-8 de todos modos (por lo que su aplicación Notepad podría detectar UTF-8), pero dado que el orden de la lista de materiales nunca varía, no se recomienda su uso.

En cuanto a lo que Python almacena para las cadenas Unicode; que en realidad cambió en Python 3.3. Antes de 3.3, internamente en el nivel C, Python almacenaba combinaciones de bytes UTF16 o UTF32, dependiendo de si Python se comstackba o no con un amplio soporte de caracteres (ver ¿Cómo saber si Python está comstackdo con UCS-2 o UCS-4? UCS-2 es esencialmente UTF-16 y UCS-4 es UTF-32). Entonces, cada personaje toma 2 o 4 bytes de memoria.

A partir de Python 3.3, la representación interna utiliza el número mínimo de bytes necesarios para representar todos los caracteres en la cadena. Para texto simple ASCII y codificable en latín 1 se usa 1 byte, para el rest de BMP se usan 2 bytes, y se usa texto que contiene caracteres más allá de los 4 bytes. Python cambia entre los formatos según sea necesario. Por lo tanto, el almacenamiento se ha vuelto mucho más eficiente para la mayoría de los casos. Para más detalles, vea Lo nuevo en Python 3.3 .

Puedo recomendar encarecidamente que lea sobre Unicode y Python con:

  • El Absoluto Mínimo Todo desarrollador de software Absolutamente, positivamente debe saber acerca de Unicode y los conjuntos de caracteres (¡sin excusas!)
  • El CÓMO de Python Unicode
  1. Su comprensión es esencialmente correcta en lo que va, aunque no es realmente “1, 2 o 4 bytes”. Para UTF-32 será de 4 bytes. Para UTF-16 y UTF-8, el número de bytes depende del carácter que se codifica. Para UTF-16 será de 2 o 4 bytes. Para UTF-8 puede ser de 1, 2, 3 o 4 bytes. Pero sí, básicamente la encoding toma el punto de código Unicode y lo asigna a una secuencia de bytes. Cómo se hace este mapeo depende de la encoding. Para UTF-32 es solo una representación hexagonal recta del número de punto de código. Para UTF-16 suele ser eso, pero será un poco diferente para los caracteres inusuales (fuera del plano multilingüe base). Para UTF-8, la encoding es más compleja (ver Wikipedia ). En cuanto a los bytes adicionales al principio, esos son marcadores de orden de bytes que determinan en qué orden vienen las piezas del punto de código en UTF-16 o UTF-32.
  2. Supongo que puedes mirar las partes internas, pero el punto del tipo de cadena (o tipo Unicode en Python 2) es protegerte de esa información, al igual que el punto de una lista de Python es protegerte de tener que manipular la materia prima estructura de la memoria de esa lista. El tipo de datos de cadena existe, por lo que puede trabajar con puntos de código Unicode sin preocuparse por la representación de la memoria. Si desea trabajar con los bytes en bruto, codifique la cadena.
  3. Cuando haces una deencoding, básicamente escanea la cadena, buscando trozos de bytes. Los esquemas de encoding proporcionan esencialmente “pistas” que permiten al decodificador ver cuándo termina un carácter y comienza otro. Así que el decodificador escanea y usa estas pistas para encontrar los límites entre los caracteres, luego busca cada pieza para ver qué carácter representa en esa encoding. Puede consultar las codificaciones individuales en Wikipedia o similares si desea ver los detalles de cómo cada encoding asigna los puntos de un lado a otro con bytes.
  4. Los dos bytes cero son parte del marcador de orden de bytes para UTF-32. Dado que UTF-32 siempre usa 4 bytes por punto de código, la lista de materiales también es de cuatro bytes. Básicamente, el marcador FFFE que se ve en UTF-16 se rellena con cero con dos bytes adicionales. Estos marcadores de orden de bytes indican si los números que componen el punto de código están ordenados de mayor a menor o de menor a mayor. Básicamente, es como la elección de escribir el número “mil doscientos treinta y cuatro” como 1234 o 4321. Diferentes architectures de computadora toman diferentes decisiones al respecto.

Voy a asumir que estás usando Python 3 (en Python 2 una “cadena” es realmente una matriz de bytes, lo que causa el dolor de Unicode).

Una cadena (Unicode) es conceptualmente una secuencia de puntos de código Unicode, que son entidades abstractas que corresponden a ‘caracteres’. Puede ver la implementación real de C ++ en el repository de Python. Dado que las computadoras no tienen un concepto inherente de un punto de código, una “encoding” especifica una bijección parcial entre los puntos de código y las secuencias de bytes.

Las codificaciones están configuradas para que no haya ambigüedad en las codificaciones de ancho variable. Si ve un byte, siempre sabrá si completa el punto de código actual o si necesita leer otro. Técnicamente esto se llama estar libre de prefijos. Así que cuando haces un .decode() , Python recorre la matriz de bytes, construyendo los caracteres codificados uno a la vez y generándolos.

Los dos bytes cero forman parte de la lista de materiales de utf32: big-endian UTF32 tendría 0x0 0x0 0xff 0xfe .