Mostrando entradas con la etiqueta .update(). Mostrar todas las entradas
Mostrando entradas con la etiqueta .update(). Mostrar todas las entradas

viernes, 3 de mayo de 2024

Descifrador de códigos.

 Basado en el proyecto de la Raspberripy Org "Codebreakers".

¿Qué es lo que haremos en este post?


Analizaremos un gráfico para intentar descifrar un código, mientras aprendemos sobre listas y funciones en Python.

Que haremos:

- Definir una función que acepte una serie de parámetros y devuelva un valor.
- Usar los bucles "for" y "while" para repetir tareas.
- Crear un gráfico para mostrar la frecuencia de unos datos.

El programa que crearemos puede codificar o descodificar un mensaje secreto desde un archivo *.txt adjunto, o también podremos analizar la frecuencia de las letras en un mensaje. Estos datos se mostrarán en un gráfico de frecuencias, que mostrará con que frecuencia cada letra aparece en el mensaje secreto.

Nos basaremos en el cifrado Atbash. Este cifrado es uno de los más antiguos que se conocen en el mundo. Originariamente desarrollado en el idioma hebreo, utiliza el alfabeto y lo relaciona en orden inverso para crear el mensaje secreto. El nombre deriva de la primera, última, segunda y penúltima letras hebreas: Aleph, Taw, Bet y Shin.

Cifrando el alfabeto.


Para empezar vamos a crear un diccionario para tus letras cifradas.  El programa comenzará con dos estructuras de datos. La primera es una lista de todas las letras que existen en nuestro alfabeto y la segunda el un diccionario para el código. Para ahorrar teclear código, puedes crear la lista desde una cadena de texto usando la función list().

Empieza el programa con el siguiente código:

#Estructura de datos
#Lista creada desde una cadena de texto
ALFABETO = list(" abcdefghijklmnñopqrstuvwxyz ")
codigo = {}
La lista ALFABETO, como puedes ver, contiene un espacio al principio y al final de las letras. Esto lo hemos hecho para preservar los espacios que existan en el texto del mensaje. Sin embargo en una codificación fuerte esto no debería ser así, ya que hace que los mensajes sean más fáciles de decodificar. En este proyecto hemos mantenido los espacios para que que pueda ver más fácilmente el funcionamiento del programa.

Creando una nueva lista con el alfabeto invertido.


Necesitamos crear una nueva lista pero con el alfabeto, pero invertido. Puedes usar la funcion list() de nuevo para ayudarte con esto. Puedes usar la función reversed() para invertir el orden de una lista existente.

Crea la función crear_codigo. A continuación crea una lista que almacene el alfabeto pero al revés en una lista.

# Crear el código Atbash invirtiendo el alfabeto
def crear_codigo():
    inversa = list(reversed(ALFABETO))


Codificando el alfabeto.


"Codificar" es cuando conviertes datos de un formato a otro. Ahora tienes dos listas. Una contiene la lista del alfabeto en su orden normal y la otra en su orden inverso. Vamos a usar ambas para rellenar un diccionario. La llave almacenará almacenará el alfabeto en su orden normal y el valor correspondiente guardará el diccionario al revés.

El diccionario codigo es muy importante porque lo usaremos para hacer coincidir cada letra del mensaje usando las llaves y sus correspondientes valores.

Dentro de la función crear_codigo, rellena el diccionario usando los datos de ambas listas. Usa un bucle "for" para obtener la longitud de la lista del alfabeto y completar el diccionario con los datos. 

La función len() se usa para obtener la longitud de un objeto, como puede ser una lista. Aquí se usar para que el bucle "for" itere tantas veces como elementos tiene la lista "ALFABETO", es decir su longitud. 

def crear_codigo():
    # reversa del ALFABETO
    inversa = list(reversed(ALFABETO))
    
    for i in range(len(ALFABETO)): # Longitud de una lista
        codigo[ALFABETO[i]] = inversa[i]
        # Rellena el alfabeto con una letra y su correspondiente
        # letra códificada.

Ahora tenemos un problema a solventar. En el idioma español existen los acentos. Para usarlos en nuestro código tenemos que pasar las letras acentuadas como letras normales ya que si no no se procesarían al no estar en el diccionario. Para solventarlo añadimos el diccionario caracteres_especiales y luego añadimos sus elementos al diccionario codigo.

# Crear el código Atbash invirtiendo el alfabeto
def crear_codigo():
    # reversa del ALFABETO
    inversa = list(reversed(ALFABETO))
    
    for i in range(len(ALFABETO)): # Longitud de una lista
        codigo[ALFABETO[i]] = inversa[i]
        # Rellena el alfabeto con una letra y su correspondiente
        # letra códificada.
    
    # Lista de caracteres especiales y sus equivalentes en el alfabeto normal
    caracteres_especiales = {'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ú': 'u'}
    
    # Agregar caracteres especiales al diccionario de códigos
    codigo.update(caracteres_especiales)   

Crear una función de entrada "main.py" es útil para llamar a las funciones que se necesitan cuando comienza el programa. Creemos una función main.py que llame a la función crear_codigo. 

# Comienzo
def main():
    crear_codigo()

main()

Probar y depurar el código.


Para probar que el diccionario se ha creado correctamente, vamos a imprimir el diccionario completo. El programa hasta ahora tiene que verse de esta forma:

# Crear el código Atbash invirtiendo el alfabeto
def crear_codigo():
    # reversa del ALFABETO
    inversa = list(reversed(ALFABETO))
    
    for i in range(len(ALFABETO)): # Longitud de una lista
        codigo[ALFABETO[i]] = inversa[i]
        # Rellena el alfabeto con una letra y su correspondiente
        # letra códificada.

    # Lista de caracteres especiales y sus equivalentes en el alfabeto normal
    caracteres_especiales = {'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ú': 'u'}

    # Agregar caracteres especiales al diccionario de códigos
    codigo.update(caracteres_especiales)
    
    print(codigo)

#Estructura de datos
#Lista creada desde una cadena de texto
ALFABETO = list("abcdefghijklmnñopqrstuvwxyz")
codigo = {}

# Comienzo
def main():
    crear_codigo()

main()
Ejecuta el código para ver si el diccionario se ha creado correctamente. Deberías ver un patrón que empieza con la letra "a" emparejada con la letra "z" y la letra "b" emparejada con la letra "y" y así sucesivamente.

SALIDA:

{' ': ' ', 'a': 'z', 'b': 'y', 'c': 'x', 'd': 'w', 'e': 'v', 'f': 'u', 'g': 't',
 'h': 's', 'i': 'r', 'j': 'q', 'k': 'p', 'l': 'o', 'm': 'ñ', 'n': 'n', 'ñ': 'm',
 'o': 'l', 'p': 'k', 'q': 'j', 'r': 'i', 's': 'h', 't': 'g', 'u': 'f', 'v': 'e',
 'w': 'd', 'x': 'c', 'y': 'b', 'z': 'a', 'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o',
 'ú': 'u'}

Codificar un mensaje.


En este apartado crearemos una función que pueda tomar un texto, lo transforma y te lo devuelva cifrado. 

Empezaremos comentando el print que usamos para ver si funcionaba nuestro código.

def crear_codigo():
    # reversa del ALFABETO
    inversa = list(reversed(ALFABETO))
    
    for i in range(len(ALFABETO)): # Longitud de una lista
        codigo[ALFABETO[i]] = inversa[i]
        # Rellena el alfabeto con una letra y su correspondiente
        # letra códificada.

    # Lista de caracteres especiales y sus equivalentes en el alfabeto normal
    caracteres_especiales = {'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ú': 'u'}

    # Agregar caracteres especiales al diccionario de códigos
    codigo.update(caracteres_especiales)

    #print(codigo) 

Definiendo la función Atbash.

Vamos a crear una función para codificar un texto usando el cifrado Atbash. Empieza definiendo la función Atbash, con un parámetro llamado texto. Los parámetros te permiten pasar valores para que sean usados en las funciones. 

def atbash(texto):

Convirtiendo el texto a minúsculas.

Lo primero que necesita la función es convertir el texto que le pasemos a minúsculas. Necesitamos crear una nueva variable, la podemos llamar salida, para guardar el mensaje codificado.

def atbash(texto):
    """Codifica o decodifica una línea de texto.
    Atbash es símetrico.
    """
    texto = texto.lower() # Convierte el texto en minúsculas.
    salida = ""

Codificando el texto.

Lo próximo que haremos será codificar el texto que habremos pasado a la función. Utilizaremos un bucle for para iterar sobre cada letra del texto y convertirla en una letra codificada usando el diccionario codigo. Finalmente devolveremos el mensaje codificado.

def atbash(texto):
    """Codifica o decodifica una línea de texto.
    Atbash es símetrico.
    """
    texto = texto.lower() # Convierte el texto en minúsculas.
    salida = ""
    
    for letra in texto:
        if letra in codigo:
            salida += codigo[letra]
    
    return salida

Probar y depurar.

Ahora que tenemos la función vamos a probarla codificando algo de texto y ver que funciona. Localiza la función main() y añade una llamada a la función atbash(). Podemos pasar el texto "Codificar es fácil"

Si todo ha ido bien la salida del programa sería:

>>> %Run main.py
xlwrurxzi vh uaxro 

Creando un menú.


Ahora lo que haremos será crear un menú para que el usuario pueda elegir que es lo que quiere hacer. Empezaremos creando una función llamada "menu":

def menu():
    """Crea un menú basado en texto."""
Tu menú necesita un bucle que pregunte al usuario que es lo que quiere hacer hasta que introduzca una opción válida. Tenemos que definir una variable llamada "elección" que de momento no tendrá ningún valor.  Esto permitirá al bucle while ejecutar su primer bucle.

def menu():
    """Crea un menú basado en texto."""
    eleccion = "" # El bucle comienza con una elección no válida.

Usando un bucle while para obtener lo que teclee el usuario.

Lo que queremos es que el bucle se rompa solamente cuando se haya escogido una opción válida. Queremos que el bucle while se siga ejecutando mientras no se escoja una opción válida.

Puedes usar un bucle while para ejecutar un trozo de código mientras una condición se verdadera (True). En nuestro caso vamos a definir que mientras que el usuario no elija la letra "d" o la "f" el bucle se continuará ejecutando. Lo que pretendemos es que si se pulsa una "c" se codifique/decodifique un mensaje que habremos guardado en un archivo de texto y si pulsa una "f" se muestre un gráfico con la frecuencia de las letras en ese mensaje. 

def menu():
    """Crea un menú basado en texto."""
    eleccion = ""
    
    while elección != 'c' and eleccion != 'f':
        eleccion = input('''Por favor, introduce c to codificar/decodificar texto,
o f para realizar un análisis de frecuencias: ''')

Una vez que el usuario ha introducido una elección correcta, el bucle finaliza. A continuación crearemos una sentencia 'if' que ejecutará la función atbash si el usuario introduce una 'c'

Tu decidirás que sucederá cuando el usuario introduzca una 'f' en un paso posterior.

Debajo de la última línea (¡asegurándote de tener todavía una sangría!), escribe:

def menu():
    """Crea un menú basado en texto."""
    eleccion = ""
    
    while eleccion != 'c' and eleccion != 'f':
        eleccion = input('''Por favor, introduce c to codificar/decodificar texto,
o f para realizar un análisis de frecuencias: ''')
        
        if eleccion == 'c':
            print("Pasando tu mensaje a través del cifrado...")
            mensaje = "Mi mensaje secreto"
            codigo = atbash(mensaje)
            print(codigo)

Cambia la cadena que dice "Mi mensaje secreto" por el texto que tu quieras. Este texto será el que se codifique o decodifique.

Al final de la función main() teclea menu() para llamar a la función cuando se ejecute el programa:

# Comienzo
def main():
    crear_codigo()
    #print(atbash("Codificar es fácil"))
    menu()
Ejecuta el código. Teclea 'c' y presiona Enter para codificar tu mensaje.

SALIDA.

>>> %Run main.py
Por favor, introduce c to codificar/decodificar texto,
o f para realizar un análisis de frecuencias: 
c
Pasando tu mensaje a través del cifrado...
ñr ñvnhzqv hvxivgl
En el próximo paso usaremos la función atbash para codificar el contenido de un archivo de texto.

Codificar texto desde un archivo.


Cargar el texto desde un archivo es más eficiente que teclear o copiar un gran texto para pasárselo al programa. Vamos a crear la función obtener_texto(). Esta función tiene un argumento que es el nombre del archivo a cargar. Usamos este argumento para cargar el archivo y pasárselo a la variable texto, que una vez quitados los saltos de línea es devuelto por la función. El código quedaría tal que así:

def obtener_texto(nombre_archivo):
    """Obtiene y devuelve texto desde un archivo."""
    with open(nombre_archivo) as f:
        texto = f.read().replace('\n', " ") # tenemos que eliminar los saltos de línea.
        return texto
La función en el menú necesita codificar el mensaje secreto desde un archivo de texto. Reemplaza tu mensaje secreto con la función obtener_texto(). Así quedaría el código:

def menu():
    """Crea un menú basado en texto."""
    eleccion = ""
    
    while eleccion != 'c' and eleccion != 'f':
        eleccion = input('''Por favor, introduce c to codificar/decodificar texto,
o f para realizar un análisis de frecuencias: \n''')
        
        if eleccion == 'c':
            print("Pasando tu mensaje a través del cifrado...")
            mensaje = obtener_texto('datos.txt')
            codigo = atbash(mensaje)
            print(codigo)
Crea en el mismo directorio un archivo llamado datos.txt, edítalo e introduce en el tu propio mensaje secreto.

Ejecuta tu código para ver si se muestra el mensaje codificado después de pulsar la letra 'c' en el menú.

SALIDA.

>>> %Run main.py
Por favor, introduce c para codificar/decodificar texto,
o f para realizar un análisis de frecuencias: 
c
Pasando tu mensaje a través del cifrado...
vhgv vh fn ñvnhzqv wv kifvyz kziz evi hr vo xlwrurxzwli wv gvcglh ufnxrlnz xliivxgzñvngv

Decodificando el mensaje.


El cifrado atbash codifica el mensaje utilizando el reverso de las letras del alfabeto, como ya hemos visto. Esto significa que exactamente el mismo código que hemos utilizado para codificar el mensaje vale para decodificarlo. Puedes probarlo, usando el código encriptado. Copialo y pégalo en el archivo datos.txt y vuelve a ejecutar el código de nuevo.

>>> %Run main.py
Por favor, introduce c para codificar/decodificar texto,
o f para realizar un análisis de frecuencias: 
c
Pasando tu mensaje a través del cifrado...
este es un mensaje de prueba para ver si el codificador de textos funciona correctamente 

Se mostrará la versión descodificada, es decir tu mensaje original.


Crear un analizador de frecuencias.


En este paso vamos a crear una función que nos muestre la frecuencia con la que las letras del alfabeto aparecen en tu texto.

El análisis de frecuencias mide con que frecuencia algo aparece de esta forma podrás buscar patrones de ocurrencia en esos datos. Es posible descifrar cifrados monoalfabeticos (si conoces el idioma en el que se encuentra el mensaje) observando la frecuencia con la que aparece cada letra y comparándolo con las letras más utilizadas en ese idioma. Lo explicaremos en más detalle más adelante.

Empezaremos creando una función que tome tu texto y cuente el número de veces que cada letra del alfabeto aparece en él y luego convierta ese número de apariciones en un porcentaje para que puedas compararlo con la frecuencia de las letras en español.

Vamos al lio. Define una función llamada frecuencia que tome como argumento tu texto, luego convierta el mensaje en minúsculas y lo pase todo a una lista.

def frecuencia(texto):
    """Calcula la frecuencia de las letras en un texto"""
    texto = list(texto.lower())
Crea un diccionario llamado frec donde vamos a almacenar el número de veces que aparece cada letra en el texto. Luego recorreremos el texto a través de un bucle for y añadiremos al diccionario el número de veces que aparece cada letra. Para posteriormente calcular las frecuencias almacenamos el número total de palabras en la variable letras_totales.

def frecuencia(texto):
    """Calcula la frecuencia de las letras en un texto"""
    texto = list(texto.lower())
    
    frec = {}
    
    for letra in texto:
        frec[letra] = frec.get(letra, 0) + 1
Por ejemplo si la primera letra del texto fuese una 'a' el programa busca si existe una clave en el diccionario con la 'a'. Usamos el método get ya que si existe la clave en el diccionario nos devuelve su valor y si no es así, porque fuese la primera aparición, nos devolverá el número cero en vez de un error.
Cada vez que aparece la letra en el texto le sumamos uno a su valor.

Una vez que ya tengamos el número total de veces que cada letra aparece en el mensaje vamos a calcular su frecuencia  a través de un bucle. No queremos que se tengan en cuenta los espacios en blanco para el calculo de las frecuencias, así que añadimos el condicional if para añadir solo las letras. Añade un return a la función para que podamos usar el diccionario con las frecuencias en cualquier parte del código.

def frecuencia(texto):
    """Calcula la frecuencia de las letras en un texto"""
    texto = list(texto.lower())
    
    frec = {}
        
    for letra in texto:
        if letra != " ":
            frec[letra] = frec.get(letra, 0) + 1
    
    letras_totales = sum(frec.values()) #numero total de letras sin espacios en blanco
        
    for letra in frec:
        frec[letra] = frec[letra] / letras_totales * 100  # Convierte números en porcentajes.
    
    return frec

Extendiendo el menú para incluir la opción 'f'.


Ahora que tenemos una función que calcula la frecuencia de las letras en el mensaje, necesitamos un enlace para ejecutarla a través del menú. Hasta ahora el usuario solo podía escoger la opción 'c' para codificar o decodificar un mensaje. Si tecleaba la letra 'f' no ocurría nada. Vamos a añadir esta opción para mostrar un análisis de frecuencias con la función que hemos creado anteriormente.

Debajo del comando if que verifica si el usuario ha pulsado una 'c',  necesitamos añadir el comando elif para que el usuario pueda escoger la opción 'f' para realizar un análisis de frecuencias e imprimirlas.

Deja una línea en blanco después de la sentencia 'if ' y añade el siguiente código:

def menu():
    """Crea un menú basado en texto."""
    eleccion = ""
    
    while eleccion != 'c' and eleccion != 'f':
        eleccion = input('''Por favor, introduce c para codificar/decodificar texto,
o f para realizar un análisis de frecuencias: \n''')
        
        if eleccion == 'c':
            print("Pasando tu mensaje a través del cifrado...")
            # mensaje = "Mi mensaje secreto"
            mensaje = obtener_texto('datos.txt')
            codigo = atbash(mensaje)
            print(codigo)
            
        elif eleccion == 'f':
            mensaje = obtener_texto('datos.txt')
            codigo = atbash(mensaje)
            mensaje_frec = frecuencia(codigo)
            print(mensaje_frec)
Guarda y ejecuta el código. Presiona la tecla 'f' y deberías ver en la consola una relación de las frecuencias con la que las letras aparecen en tu mensaje ya codificado. Los valores que se muestren seguramente serán diferentes de los mostrados aquí.

>>> %Run main.py
Por favor, introduce c para codificar/decodificar texto,
o f para realizar un análisis de frecuencias: 
f
{'v': 18.91891891891892, 'h': 6.756756756756757, 'g': 6.756756756756757,
 'f': 4.054054054054054, 'n': 6.756756756756757, 'ñ': 2.7027027027027026,
 'z': 9.45945945945946, 'q': 1.3513513513513513, 'w': 5.405405405405405,
 'k': 2.7027027027027026, 'i': 8.108108108108109, 'y': 1.3513513513513513,
 'e': 1.3513513513513513, 'r': 5.405405405405405, 'o': 1.3513513513513513,
 'x': 6.756756756756757, 'l': 6.756756756756757, 'u': 2.7027027027027026,
 'c': 1.3513513513513513}


Analizando la frecuencia mediante un gráfico.


Lo primero que tenemos que hacer es instalar la librería Pygal para mostrar el gráfico de las frecuencias. 

pip3 install pygal

En todos los idiomas, cada letra de su alfabeto tiene una "personalidad" o un conjunto de rasgos cuando se usa en ese idioma. Una de las características que tiene una letra en cualquier idioma es la frecuencia con la que aparece. El análisis de frecuencia es el método para descifrar códigos observando la frecuencia con la que se utilizan las letras en el idioma del código y comparándolo con la frecuencia con la que aparecen letras codificadas en un mensaje. En español, la letra e es la letra más utilizada (aparece el 13,68% de las veces), seguida de la 'a' (el 12,53 %). La letra menos utilizada es la w.

En el menú no necesitaremos más la línea print(mensaje_frec) así que lo comentaremos para que se ignore cuando ejecutemos el código. 

 elif eleccion == 'f':
            mensaje = obtener_texto('datos.txt')
            codigo = atbash(mensaje)
            mensaje_frec = frecuencia(codigo)
            # print(mensaje_frec)

Crear el gráfico con las frecuencias.

Vamos a definir una nueva función llamada crear_grafico(). Esta función tendrá dos parámetros, texto y lenguaje. El gráfico será un gráfico de barras que tendrá por título "análisis de frecuencias" y las etiquetas para el eje-x serán las claves del diccionario. (a,b,c...y,z)

El diccionario de frecuencias lo pasaremos posteriormente a la función cuando se la llame a través del parámetro texto.

Primeramente después de haberlo instalado importamos al principio del programa el tipo de gráfico que vamos a crear.

from pygal import Bar

#...
def crear_grafico(texto, lenguaje):
    """Crea el gráfico de frecuencias"""
    grafico = Bar(width=800, height=400, title="Análisis de Frecuencias",
                  x_labels=list(lenguaje.keys()))

Etiquetaremos el gráfico usando la frecuencia con la que las letras aparecen en el mensaje codificado y las frecuencias conocidas de las letras en el idioma español. Estos datos los pasaremos a través de los parámetros texto y lenguaje. 

Para pasar la frecuencia con la que las letras aparecen en un texto genérico en el idioma español, crearemos una variable llamada "spanish" y a través de un diccionario pasaremos las frecuencias.

Tenemos que solucionar un problema. A partir de la versión 3.7 de Python los datos en un diccionario se guardan en el orden en el que se han introducido y aunque no fuera así para poder representar correctamente el gráfico tenemos que emparejar las frecuencias de las letras en el mensaje codificado, con las frecuencias de las letras en el idioma. Es decir si la primera letra de nuestro texto codificado es una 'e' y las segunda una "s" pues tendremos que ordenar todas alfabéticamente y hacer coincidir sus valores con los del diccionario del lenguaje, que están ordenados alfabéticamente.

Para ello creamos una lista llamada "valores" que recogerá las frecuencias de las letras del mensaje codificado pero ordenadas alfabéticamente desde la 'a' a la 'z'. A través de un bucle for recorremos las letras "a,b,c...z" y si la letra existe pasamos su valor a la lista y sino pues sustituimos su valor con un cero.

def crear_grafico(texto, lenguaje):
    """Crea el gráfico de frecuencias"""
    grafico = Bar(width=800, height=400, title="Análisis de Frecuencias",
                  x_labels=list(lenguaje.keys()))
    # Etiquetas de las frecuencias para el mensaje codificado.
    valores = []
    for i in lenguaje:
        valores.append(texto.get(i,0))
    grafico.add('Mensaje objetivo', valores)
    # Etiquetas de las frecuencias de las letras en el idioma español
    grafico.add('Lenguaje', list(lenguaje.values()))
    
    grafico.render_to_file('grafico.svg')


# Estructura de datos
# Lista creada desde una cadena de texto
ALFABETO = list(" abcdefghijklmnñopqrstuvwxyz ")
codigo = {}
spanish = {'a': 12.53, 'b': 1.42, 'c': 4.68, 'd': 5.86, 'e': 13.68, 'f': 0.69, 'g': 1.01,
           'h': 0.70, 'i': 6.25, 'j': 0.44, 'k': 0.02, 'l': 4.97, 'm': 3.15, 'n': 6.71,
           'ñ': 0.31, 'o': 8.68, 'p': 2.51, 'q': 0.88, 'r': 6.97, 's': 7.98, 't': 4.63,
           'u': 3.93, 'v': 0.90, 'w': 0.01, 'x': 0.22, 'y': 0.90, 'z': 0.52}

LLamando a la función que representa las frecuencias.

Busca la secuencia elif dentro de la función menu(). Añade una línea de código para utilizar las frecuencias del idioma español y otra para crear el gráfico.

elif eleccion == 'f':
            mensaje = obtener_texto('datos.txt')
            codigo = atbash(mensaje)
            mensaje_frec = frecuencia(codigo)
            # print(mensaje_frec)
            lang_freq = spanish  # Importamos la frecuencia de la letras en español
            # LLama a la función que crea el gráfico
            crear_grafico(mensaje_frec, lang_freq)

Ejecuta el código y puedes ver el archivo del gráfico usando cualquier navegador para abrirlo:

grafico de frecuencias



Analiza el gráfico de frecuencias. 

El gráfico que se ha producido muestra la frecuencia de letras en el idioma español, etiquetado como "Lenguaje". Puedes ver que la letra "e" es la letra más utilizada en el idioma español porque tiene la barra más alta para todos los valores de idioma.

El gráfico de frecuencias también lista la frecuencia de letras en tu mensaje codificado, etiquetado como "Mensaje objetivo". Para averiguar qué codificación se ha utilizado para este mensaje, puedes comparar las barras que muestran el idioma español con las barras en el mensaje codificado. La barra más alta en el texto del mensaje codificado probablemente será una "e". La segunda letra más alta probablemente será una "a", ya que es la siguiente letra más popular.

Los descifradores pueden utilizar la frecuencia de letras para determinar el tipo de codificación que se ha utilizado en el mensaje. Pueden utilizar el método de ensayo y error para predecir qué letra podría representar usando el gráfico como guía.

No obstante ten en cuenta que tu mensaje es muy pequeño lo que dificulta el descifrarlo mediante el análisis de frecuencias. Sin embargo todo se normaliza si codificas un texto lo suficientemente largo.

Puedes encontrar el código de este post en este enlace de Github.

martes, 22 de junio de 2021

Los Diccionarios en Python

imagen de un diccionario clave:valor


Un diccionario en Python es un objeto (dict) en el que los elementos se almacenan utilizando un sistema basado en el sistema clave:valor o Key:values en inglés. Es decir se asocia a cada clave un determinado valor o valores. 

Un ejemplo de diccionario podría ser el que asociara los países con sus respectivas capitales. En este caso la clave sería el país y su valor la capital. 

midiccionario = {"país":"capital"} ------> {clave : valor, .........., clave : valor}

>>> diccio = {"España":"Madrid","Francia":"Paris","Alemania":"Berlin"}
# para acceder a los valores de un diccionario usamos las claves
>>> print(diccio["España"])
Madrid

Los datos a partir de la version 3.7 de python son ordenados, se pueden modificar, pero lo que no puede ocurrir nunca en un diccionario es que haya dos claves iguales.

Otra forma de definir un dicionario en Python es utilizando un lista con tuplas de dos elementos en la que la primera será la clave y la segunda el valor.

>>> mi_diccionario = dict([("clave1","valor1"), ("clave2", "valor2"), ("clave3", "valor3")])
>>> mi_diccionario
{'clave1': 'valor1', 'clave2': 'valor2', 'clave3': 'valor3'}
E incluso si las claves son strings, es decir texto, también tenemos otra opción para definir un diccionario:

>>> mi_diccionario = dict(clave1="valor1", clave2="valor2")
>>> mi_diccionario
{'clave1': 'valor1', 'clave2': 'valor2'}


Ver los valores asociados a una clave.


Podemos acceder a los elementos de un diccionario refiriéndonos a los valores de su clave entre corchetes [ ]

print(nombre_diccionario[clave])

>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlin"}
>>> print(diccionario["España"])
Madrid

aunque también podemos utilizar el método get

>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlín"}
>>> print(diccionario.get("España"))
Madrid

La diferencia que existe entre ambos métodos es que si no existe la clave en el diccionario en el primero nos dará un error mientras que con get nos dará un None o si definimos su valor opcional nos lo mostrará.

>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlin"}
>>> print(diccionario['Rusia'])
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    print(diccionario['Rusia'])
KeyError: 'Rusia'
>>> print(diccionario.get('Rusia'))
None
>>> print(diccionario.get('Rusia', 'No existe la clave'))
No existe la clave

Otra forma de ver si existe una clave en un diccionario es usando la palabra reservada in. El valor será True si existe o False en caso contrario.

>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlin"}
>>> print("Alemania" in diccionario)
True

Para añadir un registro a un diccionario se hace de una forma muy parecida. Vamos a añadir al diccionario anterior como país Reino Unido y como capital Londres.

>>> diccionario["Reino Unido"] = "Londres"
>>> print(diccionario)
{'España': 'Madrid', 'Francia': 'Paris', 'Alemania': 'Berlín', 'Reino Unido': 'Londres'}

Y si queremos eliminar un elemento utilizamos la función del, o el método pop() o pop(item):

>>> del diccionario["Francia"]
>>> print(diccionario)
{'España': 'Madrid', 'Alemania': 'Berlín', 'Reino Unido': 'Londres'}

El método pop elimina el elemento con la clave especificada:

>>> diccionario.pop('Francia')
>>> print(diccionario)
{'España': 'Madrid', 'Alemania': 'Berlín', 'Reino Unido': 'Londres'}
El método .popitem() elimina el último elemento insertado. (En versiones anteriores a la 3.7 de Python elimina un elemento de forma aleatoria).

>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlín"}
>>> diccionario.popitem()
>>> print(diccionario)
{'España': 'Madrid', 'Francia': 'Paris'}


Longitud de un Diccionario.


Para saber cuantos elementos tiene un diccionario podemos utilizar la función len()


>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlín"}
>>> print(len(diccionario))
3


Elementos de un diccionario. Tipos de datos.


Los valores de los elementos de un diccionario pueden ser de cualquier tipo de datos, incluso otro diccionario:

>>> coches={"marca":"Seat", "electrico":False, "año":1998, "colores":["rojo","blanco","azul"]}
>>> print(coches)
{'marca': 'Seat', 'electrico': False, 'año': 1998, 'colores': ['rojo', 'blanco', 'azul']}

Desde la perspectiva de Python los diccionarios son objetos con el tipo de dato "dict"

<class 'dict'>


Listar las claves, valores o elementos de un diccionario.

El método .keys() nos proporciona una lista con todas las claves del diccionario.

>>> diccionario = {'nombre' : 'Carlos', 'edad' : 22, 'cursos': ['Python','Django','JavaScript'] }
>>> print(diccionario.keys())
dict_keys(['nombre', 'edad', 'cursos'])

y el método .values() nos dará los valores del mismo.

>>> diccionario = {'nombre' : 'Carlos', 'edad' : 22, 'cursos': ['Python','Django','JavaScript'] }
>>> print(diccionario.values())
dict_values(['Carlos', 22, ['Python', 'Django', 'JavaScript']])

Para obtener los elementos de un diccionario podemos realizar una iteración en el diccionario:

>>> diccionario = {'nombre' : 'Carlos', 'edad' : 22, 'cursos': ['Python','Django','JavaScript'] }
# Se podría también iterar sobre:
# for i in diccionario.keys():
# for i in diccionario.values():
# for i in diccionario.items():
>>> for i in diccionario:
      print(i, diccionario[i])

nombre Carlos
edad 22
cursos ['Python', 'Django', 'JavaScript']

Aunque es más eficiente usar el método .items(), que nos devuelve cada el elemento del diccionario, como una tupla dentro de una lista.


Actualizando un diccionario:

El método .update() actualizará un diccionario ya existente con los elementos que le hayamos pasado como argumento. El argumento debe ser un diccionario o un iterable con la estructura clave:valor

>>> diccionario = {'nombre' : 'Carlos', 'edad' : 22, 'cursos': ['Python','Django','JavaScript'] }
>>> diccionario.update({"apellido":"moran"})
>>> print(diccionario["apellido"])
moran

o también podemos añadir un nuevo elemento simplemente usando una nueva clave y su correspondiente valor como vimos anteriormente.


Copiando un diccionario.

No podemos copiar un diccionario simplemente diciendo que diccionario2 = diccionario1 ya que entonces el diccionario2 sería solamente una referencia al diccionario1 y cualquier cambio que hiciéramos en este, automáticamente quedaría hecho en el diccionario2.

Para copiar un diccionario podemos usar el método copy() o también la función de python dict()

>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlin"}
>>> diccionario2 = diccionario.copy()
>>> diccionario3 = dict(diccionario)


Diccionarios Anidados.

Un diccionario puede contener en su interior otros diccionarios, lo cual es bastante habitual. Es lo que se conoce como diccionarios anidados. Veamos un ejemplo:

>>> empleados = {
  "empleado1" : {
    "nombre" : "Antonio",
    "año de incorporación" : 2004
  },
  "empleado2" : {
    "nombre" : "Emilio",
    "año de incorporación" : 2007
  },
  "empleado3" : {
    "nombre" : "Ana",
    "año de incorporación" : 2011
  }
}


Biblioteca Standar Collections y su relación con los diccionarios.


Particularmente  un módulo que me ha resultado muy útil de la biblioteca estándar de Python es "collections".

En el podemos encontrar la clase "Counter". Lo que hace es contar las frecuencias de una colección de elementos que le pasemos como argumento, usualmente strings o listas aunque también le podemos pasar objetos.  Lo mejor es que veamos su funcionamiento con varios ejemplos. 

>>> from collections import Counter
>>> lista = ['pera','manzana','pera','limon','pera','manzana','limon']
>>> print(Counter(lista))
Counter({'pera': 3, 'manzana': 2, 'limon': 2})

Como ves nos devuelve un iterable que contiene un diccionario en el cual están las veces que se repite la fruta en cuestión en la lista que le he pasado. Sirve para contar las veces que se repite un elemento.

>>> from collections import Counter
>>> lista=[1,3,5,4,5,1,2,7,6,7,9,1,0,1]
>>> diccionario_frecuencias = Counter(lista)
>>> print(diccionario_frecuencias)
Counter({1: 4, 5: 2, 7: 2, 3: 1, 4: 1, 2: 1, 6: 1, 9: 1, 0: 1})
>>> diccionario_frecuencias[7]
2
En este otro ejemplo le pasamos una lista de números y luego almacenamos el objeto que nos devuelve Counter a la variable "diccionario_frecuencias" y vemos como podemos consultar cualquier valor escribiendo su clave. En el ejemplo le preguntamos por el número 7 y nos devuelve que se repite 2 veces en la lista.

También podemos encontrar la clase "defaultdict". Básicamente nos sirve para que cuando intentemos acceder a claves que no existen en un diccionario no nos de el error "Keyerror". El argumento para inicializarlo es cualquier objeto que se pueda llamar, ya sea una lista, un entero, un string etc pero sin argumentos.

Veamos un ejemplo del problema.

>>> # Creamos un diccionario vacio.
>>> mi_diccionario = {}
>>> # Añadimos una clave.
>>> mi_diccionario["uno"] = 1
>>> # Si intentamos acceder a una clave que no existe entonces:
>>> mi_diccionario["missing"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'missing'
>>> # Obtenemos un error. Lo intentamos con otra clave.
>>> mi_diccionario["another_missing"].append(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'another_missing'
Ahora vamos a ver como usando "defaultdict" se nos resuelve el problema.

>>> from collections import defaultdict
>>> mi_diccionario = defaultdict(list) # el argumento sin parentesis 
>>> mi_diccionario["uno"] = 1
>>> mi_diccionario["missing"]
[]
>>> mi_diccionario["another_missing"] = 4
>>> print(mi_diccionario)
defaultdict(<class 'list'>, {'uno': 1, 'missing': [], 'another_missing': 4})
>>> 

Encontrar la clave para el valor máximo en un diccionario.


Imagina que tienes un almacén de fruta. En ese almacén las existencias de cada una de las frutas está almacenadas en un diccionario con la siguiente estructura.

diccionario {"peras":2,"manzanas":5,"naranjas":1}
¿Cómo saber de cual de las frutas es de la que tenemos más existencias? Es sencillo simplemente tenemos que usar la función "max" sobre el diccionario de las siguiente forma:

manzanas


Ordenar un diccionario por clave o por valor.


Usando el ejemplo anterior podemos ordenar un diccionario por sus claves.

diccionario {"peras":2,"manzanas":5,"naranjas":1}
dicionario_ordenado = sorted(diccionario, key = diccionario.get) >>> ['naranjas', 'peras', 'manzanas']
o en base a sus valores. Si queremos en orden descendente usamos el parámetro reverse=True.

diccionario = {"peras":2,"manzanas":5,"naranjas":1}

diccionario_ordenado = sorted(diccionario, key = diccionario.get, reverse=True)
for w in diccionario_ordenado:
    print(w, diccionario[w])
>>>
manzanas 5
peras 2
naranjas 1

Invertir un diccionario. Intercambiar claves por valores y viceversa.

# Diccionario original
dic = {'a':1, 'b':2, 'c':3}
# Diccionario con claves intercambiadas con valores
dic_invertido = {v: k for k,v in dic.items()}
print(dic_invertido)

SALIDA
{1:'a', 2:'b', 3:'c'}


Usar un diccionario para realizar bifurcaciones:

Imagina que quieres construir un menú de opciones en el que dependiendo de lo que seleccione el usuario se haga una cosa u otra. Tendrías que construir un código similar a esto:

# Bifurcaciones usando if.
'''
if acción == 1:
    print("Pulsaste uno")
elif accion == 2:
    print("Pulsaste dos")
elif accion == 3:
    print("Pulsaste tres")
elif accion == 4:
    print("Pulsaste cuatro")
'''

# Bifurcaciones usando un diccionario.

opciones = {1:'uno', 2:'dos', 3:'tres', 4:'cuatro'}
x = int(input("introduzca un valor: "))
print(f'Pulsaste {opciones[x]}')

Pues bien usando diccionarios se nos simplifica bastante el código de la siguiente manera:


Puedes encontrar más información y ejemplos sobre diccionarios en la página web de w3schools.com



domingo, 9 de mayo de 2021

Los Conjuntos en Python





Son una colección de objetos sin ordenar no duplicados es decir no puede haber dos elementos iguales y estos no guardan orden alguno. No se pueden indexar pero si se pueden recorrer. Es una colección mutable, en la que se puede usar add() para añadir elementos. 

Se usan para almacenar múltiples elementos en una sola variable.

miconjunto = {"manzana""platano""anacardo"}

o

>>> letras = set('abracadabra')
>>> print(letras)
{'r', 'c', 'a', 'd', 'b'}

Como ves, un conjunto o set en inglés se especifica encerrando sus elementos entre llaves. 

Los conjuntos son uno de los 4 tipos de datos que se usan en Python para almacenar elementos, los otros 3 son las listas, las tuplas y los diccionarios.

Nota: al no estar ordenados, tu los guardas si, pero no puedes estar seguro en que orden se mostrarán cuando los necesites. Pueden aparecer en un orden distinto cada vez y no tienen por tanto ningún índice asociado.

Una vez que el conjunto está creado, no puedes modificar sus elementos, pero si puedes añadir nuevos con add()

Una cosa importante de los conjuntos es que no permiten elementos duplicados, lo cual nos puede ser útil. Por ejemplo imaginemos que tenemos una serie con números pero solo nos interesa saber cuales no están repetidos. Para esto podemos usar los conjuntos.

Ej:

>>> set = {1,3,6,9,4,6,3,2,1,9,7,4,3}

>>> print(set)

{1, 2, 3, 4, 6, 7, 9}

Los valores que están duplicados se ignoran.

Accediendo a los elementos.


No podemos acceder a los elementos de un conjunto usando un índice o una clave.

Pero si podemos recorre sus valores usando un bucle for, o preguntando a Python si un determinado elemento está presente en el conjunto usando la palabra clave in.


# Ejemplo de bucle en un conjunto para poder ver sus valores.
>>> conjunto = {'manzana','naranja','platano','pera'}
>>> for i in conjunto:
>>>    print(i)
naranja
manzana
platano
pera

>>> # Ejemplo ver si la 'pera' está en el conjunto
>>> conjunto = {'manzana','naranja','platano','pera'}
>>> print('pera' in conjunto)
True
{1, 2, 3, 4, 6, 7, 9}

Añadiendo Nuevos Elementos.


Como ya dijimos, una vez que un conjunto está creado no se puden cambiar sus elementos, pero si se puede añadir nuevos.

Para añadir un nuevo elemento usaremos el método add()

>>> # Ejemplo para añadir el 'melon' al conjunto
>>> conjunto = {'manzana','naranja','platano','pera'}
>>> conjunto.add('melon')
>>> print(conjunto)
{'platano', 'manzana', 'pera', 'melon', 'naranja'}
Podemos añadir elementos de otro conjunto, a nuestro conjunto actual usando el método update()

>>> # Ejemplo de añadir un conjunto a otro
>>> conjunto = {'manzana','naranja','platano','pera'}
>>> otroconjunto ={'piña', 'kiwi'}
>>> conjunto.update(otroconjunto)
>>> print(conjunto)
{'pera', 'manzana', 'kiwi', 'piña', 'platano', 'naranja'}

El objeto que le pasamos al método update() no tiene porque ser un conjunto, puede ser cualquier iterable (tupla, lista, diccionario etc)

>>> # Ejemplo de añadir cualquier iterable
>>> conjunto = {'manzana','naranja','platano','pera'}
>>> listafrutas = ['cereza', 'pera', 'sandia']
>>> conjunto.update(listafrutas)
>>> print(conjunto)
{'platano', 'naranja', 'pera', 'manzana', 'cereza', 'sandia'}

Eliminar Elementos de un Conjunto.

Para eliminar elementos de un conjunto ya creado podemos usar los métodos remove() o discard()

>>> # Ejemplo de eliminar un elemento
>>> conjunto = {'manzana','naranja','platano','pera'}
>>> conjunto.remove('naranja')
>>> print(conjunto)
{'manzana', 'pera', 'platano'}
Si intentas eliminar un elemento que no existe te dará un error si usas remove(), pero NO con discard().

Para borrar completamente un conjunto podemos usar el método clear() o la función del().

>>> # Ejemplo de eliminar todo un conjunto y dejarlo vacio
>>> conjunto = {'manzana','naranja','platano','pera'}
>>> conjunto.clear()
>>> print(conjunto)
set()
No obstante si usas del() borraras del todo la variable dejando de existir por lo que si tratas de usar print() te dará un error porque la variable, no es que esté vacía, es que no existe.

 Para crear un conjunto vacío se utiliza la siguiente expresión, ya que si utilizaras {} Python entenderá que quieres crear un diccionario vacio, que es un tipo de datos que veremos próximamente.

>>> conjunto_vacio = set()
>>> diccionario_vacio = {}
>>> print(type(conjunto_vacio))
>>> print(type(diccionario_vacio))
<class 'set'>
<class 'dict'>

Otros métodos y funciones que se pueden aplicar a conjuntos.

- Número de elementos de un conjunto. len(set)

>>> conjunto = {1,2,3,4,5,6,7,8,9}
>>> print("El conjunto tiene",len(conjunto),"elementos")
El conjunto tiene 9 elementos
- ¿Está el elemento 'x' en el conjunto?. x in s

conjunto = {1,2,3,4,5,6,7,8,9}
print("Está el número 2 en el conjunto =", 2 in conjunto)
Está el número 2 en el conjunto = True
- ¿No está "x" en el conjunto?. x not in s

>>> conjunto = {1,2,3,4,5,6,7,8,9}
>>> print("¿No está el número 15 en el conjunto =", 15 not in conjunto)
¿No está el número 15 en el conjunto? = True
>>> print("¿No está el número 2 en el conjunto? =", 2 not in conjunto)
¿No está el número 2 en el conjunto? = False
-  ¿No tienen dos conjuntos algún elemento en común? s1.isdisjoint(s2)

Devuelve True si los dos conjuntos no tienen ningún elemento en común y False en caso contrario.

>>> s1 = {1,2,3}
>>> s2 = {3,4,5}
>>> print(s1.isdisjoint(s2))
False
>>> # El resultado es false porque s1 y s2 comparten elemento 3.

-  ¿ Es s2 un subconjunto de s1? Es decir todos los elementos de s2 están en s1. 
método s2.issubset(s1) u operador s2 <= s1

Devuelve True si es así y False en caso contrario.

>>> s1 = {1,2,3,4,7,9}
>>> s2 = {3,4,9}
>>> print(s2.issubset(s1))
True
>>> print(s2<=s1)
True
- Crear un conjunto que contenga los elementos diferentes entre dos o más conjuntos.
método s1.difference(*conjuntos) u operador s1 - s2

>>> s1 = {'pera','melocoton','limon','zanahoria'}
>>> s2 = {'casa','coche','limon','pera'}
>>> print(s1.difference(s2))
>>> print(s1-s2)
>>> # elementos del conjunto s1 que no están en s2
{'zanahoria', 'melocoton'}
{'zanahoria', 'melocoton'}
- Conjunto con los elementos no comunes a s1 y s2. 
método s1.symmetric_difference(s2) u operador s1 ^ s2

>>> s1 = {'pera','melocoton','limon','zanahoria'}
>>> s2 = {'casa','coche','limon','pera'}
>>> print(s1.symmetric_difference(s2))
>>> print(s1 ^ s2)
{'zanahoria', 'melocoton', 'coche', 'casa'}
{'zanahoria', 'melocoton', 'coche', 'casa'}
- Copia superficial de un conjunto. s1.copy()

>>> s1={1,2,3,4,5}
>>> s2={2,4}
>>> print(s2)
{1, 2, 3}
- s2 es un subconjunto de s1 pero no son iguales. s2 < s1 o s1.issuperset(s2)

>>> s1={1,2,3,4,5}
>>> s2={2,4}
>>> print(s2<s1)
True
>>> print(s1.issuperset(s2))
True
Para que ocurra lo anterior también se verifica que s2 <= s1 y s2 != s1

- Unir dos o más conjuntos en uno nuevo. Método s1.union(*conjuntos) o s1 | s2

>>> s1={1,2,3}
>>> s2={4,5,6}
>>> s3={7,8,9}
>>> print(s1.union(s2,s3))
{1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> print(s1 | s2 | s3)
{1, 2, 3, 4, 5, 6, 7, 8, 9}


- Generar un nuevo conjunto que contenga SOLO los elementos comunes a todos los conjuntos implicados. Método s1.intersection(*conjuntos) o s1 & s2 & s3 ...

>>> s1={1,2,3}
>>> s2={4,5,6,2}
>>> s3={7,8,9,2}
>>> print(s1.intersection(s2,s3))
{2}
>>> print(s1 & s2 & s3)
{2}