lunes, 13 de abril de 2026

Asusta pensar que pueda haber código en sistemas críticos generado con IA

Está claro que estas herramientas ya han cambiado la forma en la que muchos trabajamos, pero da un poco de miedo pensar que estemos usándolas para lo que no están pensadas o para lo que no estén capacitadas.

Esto era cladude.ai con Sonet 4.6

Seguro que otros modelos no fallan en estas cosas, de acuerdo, pero el asunto no es qué tan bueno o no es un modelo.

El asunto es que son herramientas falibles para algunas tareas pero que aportan una sensación de seguridad que no se corresponde con la realidad.

Como el candado verde en los navegadores cuando visitas una página con un certificado SSL "de los buenos". Ah, bueno, tiene candado, es una página segura. Y resulta que la página tenía una vulnerabilidad SQL injection que le habían robado toda la base de datos. 

Pues con estas herramientas va empezando a calar la sensación y el discurso de "si lo ha escrito Gemini/Claude/ChatGPT/... estará bien".




domingo, 29 de diciembre de 2024

Euler #37 - "Truncatable Primes" - los algoritmos todavía importan

Retomando problemas del proyecto Euler, este en principio es bastante fácil.

https://projecteuler.net/problem=37



Es relativamente fácil, por ejemplo, con Python, convertir el número a string, ir quitando caracteres por la derecha y comprobando cada vez si el número que queda es primo. Luego, lo mismo por la izquierda. 

Mi primera implementación no tuvo en cuenta que no se considera "truncatable" si terminas con un 1. Lo mismo deberían decirlo en el enunciado, lo mismo es falta de cultura matemática por mi parte.

La función is_truncable, que se muestra a continuación, utiliza una función is_prime para trabajar. Va probando casos para probar que los números que van quedando son primos o no. Si después de analizar por la derecha y la izquierda todos han resultado ser primos, asume que es "truncable".


def is_truncable(number):
str_number = str(number)
# 2, 3, 5, 7 no son truncables
if number < 11:
return False
# si empieza o acaba en 1, no es truncable
elif str_number[0] == '1' or str_number[-1] == '1':
return False
else:
# Por la derecha
str_number = str(number)
while len(str_number) >= 1:
if not is_prime(int(str_number)):
return False
str_number = str_number[:-1]
# Por la izquierda
str_number = str(number)
while len(str_number) >= 1:
if not is_prime(int(str_number)):
return False
str_number = str_number[1:]

return True

Antes de eso, hemos hecho una pequeña función para generar una lista de números primos:


def next_prime(start):
next = start + 1
while not is_prime(next):
next += 1
return next

Y una función main() que monta todo el proceso:


def main():
truncables = []
next = 10;
# nos piden los 11 primeros truncables
while len(truncables) < 11:
next = next_prime(next, is_prime)
if (is_truncable(next, is_prime)):
truncables.append(next)
print("Truncables:", truncables)
print("Sum. truncables =", sum(truncables))

1ª versión malísima

En la primera implementación de is_prime() que hice, sin ninguna optimización, propia de un estudiante de ESO, simplemente iba dividiendo el número en cuestión por todos los números más pequeños que él mismo, si encontraba algún divisor, entonces no era primo.


def is_prime(number):
if number in (2,3):
return True
if number % 2 == 0:
return False
number_test = number - 1;
while number_test > 1:
resto = number % number_test
if resto == 0:
return False
number_test -= 1;
return True


Los problemas que plantean en proyecto Euler están muy bien pensados. Usando esta implementación ingenua, al llegar al 10º número truncable de la lista, se quedaba el problema clavado. 

4ª versión, usando librerías externas


Nos vamos al extremo contrario, usamos alguna librería existente (que seguramente esté bien optimizada y programada) para saber si un número es primo o no. El módulo sympy define una función isprime().


import sympy
def is_prime(number):
return sympy.isprime(number)


Con esta implementación, el problema se resuelve en un tiempo más que razonable, pero no vale, estamos haciendo trampa. 

2ª versión: mejor, pero sigue siendo mala


Esta implementación trata de ser un poco mejor: solo comprueba divisores hasta la mitad del número y hace una comprobación previa para los pares:


def is_prime(number):
if number in (2,3):
return True
if number % 2 == 0:
return False
for n in range(2, int(number/2)):
if number % n == 0:
return False
return True


Tampoco consigue resolver el problema en un tiempo razonable.

3ª versión: razonablemente buena


La siguente implementación de is_prime(), sin ser un prodigio, ya es bastante más óptima:
  • Hace una primera prueba contra los divisores más pequeños que ya sabemos que son primos.
  • Si es par, no es primo.
  • Para números mayores que 5, solo hace las comprobaciones hasta la raíz cuadrada + 1 del número + 1 en cuestión, y sólo comprueba con divisores impares.

def is_prime(number):
if number <= 3 or number == 5:
return True
if number % 2 == 0 or number % 3 == 0 or number % 5 == 0:
return False;
for n in range(5, int(math.sqrt(number)) + 1, 2):
if number % n == 0:
return False
return True


Con esta implementación un poco mejorada, ya tenemos tiempos de resolución del problema razonables y del mismo orden de magnitud que usando la librería sympy.


Para poder hacer todas las diferentes pruebas, he metido todo el código común en un módulo y desde el script principal, pasamos la función is_prime correspondiente.



# Fichero euler37lib.py

def next_prime(start, is_prime):
next = start + 1
while not is_prime(next):
next += 1
return next

def is_truncable(number, is_prime):
str_number = str(number)
# 2, 3, 5, 7 no son truncables
if number < 11:
return False
# Si empieza o acaba en 1, no es truncable
elif str_number[0] == '1' or str_number[-1] == '1':
return False
else:
# Por la derecha
str_number = str(number)
while len(str_number) >= 1:
if not is_prime(int(str_number)):
return False
str_number = str_number[:-1]
# Por la izquierda
str_number = str(number)
while len(str_number) >= 1:
if not is_prime(int(str_number)):
return False
str_number = str_number[1:]

return True

def main(is_prime):
truncables = []
next = 10;
# Nos piden los 11 primeros truncables
while len(truncables) < 11:
next = next_prime(next, is_prime)
if (is_truncable(next, is_prime)):
truncables.append(next)
print("Truncables:", truncables)
print("Sum. truncables =", sum(truncables))

Y aquí están las cuatro diferentes implementaciones. Primero, haciendo trampa.

# Fichero 037-sympy.py

import sympy
import euler37lib


def is_prime(number):
return sympy.isprime(number)

if __name__ == '__main__':
euler37lib.main(is_prime)




Refinando un poco el algoritmo:

# Fichero 037-refinado.py


import math
import euler37lib

def is_prime(number):
if number <= 3 or number == 5:
return True
if number % 2 == 0 or number % 3 == 0 or number % 5 == 0:
return False;
for n in range(5, int(math.sqrt(number)) + 1, 2):
if number % n == 0:
return False
return True



if __name__ == '__main__':
euler37lib.main(is_prime)



Implementación ligeramente mejor a la ingenua, pero aún así impracticable:

# Fichero 037-pasable.py

import euler37lib

def is_prime(number):
if number in (2,3):
return True
if number % 2 == 0:
return False
for n in range(2, int(number/2)):
if number % n == 0:
return False
return True

if __name__ == '__main__':
euler37lib.main(is_prime)




Implementación ingenua:

# Fichero 037-ingenuo.py

import euler37lib

def is_prime(number):
if number in (2,3):
return True
if number % 2 == 0:
return False
number_test = number - 1;
while number_test > 1:
resto = number % number_test
if resto == 0:
return False
number_test -= 1;
return True

if __name__ == '__main__':
euler37lib.main(is_prime)




Comparativa de rendimiento

Usando librería: 


Implementación "refinada"


Nuestra implementación "refinada" tiene tiempos comparables a la implementación usando el módulo sympy.

Implementación mala:



Esta otra pasa de apenas un segundo de las anteriores a 9 minutos. Es 540 veces más lenta.

Implementación inservible:


5.100 segundos. Las dos primeras implementaciones tardaban como 1 segundo. Un algoritmo 5.000 veces más lento.

Conclusiones

  1. Los algoritmos sí que importan. Si este programa, por ejemplo, fuese código para producción, ninguna de las dos implementaciones sencillas sería aceptable.
    1. Con matices: si en el problema se nos garanzizase que, por ejemplo, siempre tendremos datos de entrada con números por debajo de 100, no hay ninguna mejora apreciable en el rendimiento por usar una u otra versión de is_prime()

  2. Si existe una librería o módulo que hace lo que necesitas y es de pago, cómpralo o convence a tu jefe/cliente para que lo compre [*]:
    1. Funcionará mejor. 
    2. Tendrá soporte.
    3. A la larga será más económico.
[*] Es una generalización un poco burda, tampoco vamos a estar metiendo librerías o código de terceros para todo, pero es un poco la idea: los "vendor" suelen hacer las cosas mejor que lo que tú puedas hacer.

viernes, 22 de diciembre de 2023

Configurando Elasticsearch para que no distinga tildes o eñes

Típico problema, indexamos un documento con el texto "Menganita conduce un camión", buscamos "camion", sin la tilde, y no aparece el documento.

Basado en este artículo: https://sacavix.com/2020/10/elasticsearch-para-espanol-acentos-y-raiz-de-palabras/


Receta rápida

1. Reconfiguramos el índice que nos interese:

curl -XPUT http://localhost:9200/_template/miindicetemplate -H "Content-Type: application/json" -d '
{
  "index_patterns": "miindice",
  "settings": {
    "number_of_shards": 2,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer":{
      "mianalizador": {
          "tokenizer": "standard",
          "filter":  [ "lowercase", "asciifolding", "default_spanish_stopwords", "default_spanish_stemmer" ]
      }
    },
    "filter" : {
        "default_spanish_stemmer" : {
            "type" : "stemmer",
            "name" : "spanish"
        },
        "default_spanish_stopwords": {
            "type":        "stop",
            "stopwords": [ "_spanish_" ]
        }
    }
   }
  },
  "mappings": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "mianalizador",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "leadIn": {
          "type": "text",
          "analyzer": "mianalizador",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "author": {
          "type": "text",
          "analyzer": "mianalizador",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "pretitle": {
          "type": "text",
          "analyzer": "mianalizador",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    }
}'

2. Borramos el índice

curl -X DELETE http://localhost:9200/miindice



3. Indexamos de nuevo el contenido. Si es un proyecto Symfony con FOSElastica:

symfony console fos:elastica:reset
symfony console fos:elastica:populate

domingo, 1 de octubre de 2023

No pasan más cosas no sé porqué...

 Supermercado en Madrid, en el vestíbulo tienen una pantalla con autopromos, etc... y una ventana de TeamViewer en la que se ve el ID del equipo.


¿A quién no se le ocurrirían varias maldades, llamando a la tienda e identificándote como del "departamento de informática"?



martes, 23 de mayo de 2023

No, no le voy a decir a ChatGTP que escriba el blog

Si ya la web estaba llena de basura pensada para el bot de Google, ahora se le va a unir la basura generada por ChatGPT 🤦🤦🤦




lunes, 26 de diciembre de 2022

Django desempolvado, reloaded (parte I)

Aunque Django no es mi herramienta ni entorno de trabajo habitual, me gusta mantenerme actualizado y guardo un especial recuerdo de este framework. Durante un tiempo fue mi medio de trabajo y fue una de las razones que afianzaron mi preferencia por Python como uno de mis lenguajes preferidos.



Como siempre, para conocer algo lo mejor es ponerse manos a la obra y hacer un mini-proyecto lo más realista posible. El tutorial de Django es una maravilla, con el paso de los años lo han ido ampliando y es una fuente de información muy valiosa cuando uno quiere recordar las bases de Django.

Nuestro mini-proyecto: un micro-cms

Desarrollemos un CMS parecido a Wordpress o la plataforma Blogger. No vamos a hacer nada especial:
  • Para clasificar los artículos tendremos secciones (estructura jerárquica) y tags (estructura horizontal)
  • En este CMS pueden escribir diferentes autores
  • Cada artículo puede tener una imagen ilustrativa. También vamos a permitir diferentes plantillas (por ejemplo, post en columnas, post con foto grande,...)
  • Dispondremos de una herramienta de administración o backend para editar artículos, categorías,..
  • La navegación podrá ser por autores, categorías y tags. La página de inicio será un listado con los últimos artículos
Estos son los requisitos de un proyecto que se hubiese planteado hace 15 años, pero no pretendemos hacer nada que no sea refrescar Django, es el objetivo.

  • Primer sprint: configurar el entorno y definir el modelo de datos
  • Segundo sprint: habilitar el backoffice de Django y ver si nos sirve como entorno de edición
  • Tercer sprint: habilitar la página final de un post
  • Cuarto sprint: habilitar las páginas de inicio, navegación por autores, secciones, tags,...
  • ... y lo que se nos vaya ocurriendo...

Primer sprint: configurar el entorno y definir el modelo de datos

En el momento de escribir esto (dic/2022), la versión de Django estable es 4.1.4 
Instalarla es trivial. Aunque se recomienda usar un "virtual env", dado que en el equipo que se va a desarrollar este mini-proyecto no hay conflictos de versiones, instalaremos Django directamente a nivel global.

$ sudo pip install Django==4.1.4

Una vez instalado, creamos el proyecto y entramos en el directorio

$ django-admin startproject microcms
$ cd microcms
$ ls microcms

Se nos lista un fichero manage.py y un directorio que se llama igual que el proyecto, microcms. Según la documentación, el nombre del directorio más externo, es indiferente. No así el interno, que contiene código y el típico __init__.py que lo identifica como un módulo.

Dentro del proyecto vivirán las apps que vayamos desarrollando, para proyectos pequeños o de ejemplo como este podría ser razonable hacerlo todo dentro de una misma app. Por ahora crearemos una app, cms, que es donde definiremos nuestro modelo de datos.
Si más adelante se nos ocurre alguna funcionalidad extra, tendría sentido desarrollarla en una app diferente.

$ ./manage.py startapp cms
$ ls 
total 12K
drwxrwxr-x 3 david david 4,0K dic 26 17:59 cms
-rwxr-xr-x 1 david david  664 dic 26 17:49 manage.py
drwxrwxr-x 3 david david 4,0K dic 26 17:59 microcms

Tenemos un nuevo directorio en el proyecto, es también un módulo en el que se definen modelos, vistas, etc.

$ ls cms/
total 24K
-rw-r--r-- 1 david david   63 dic 26 17:59 admin.py
-rw-r--r-- 1 david david  138 dic 26 17:59 apps.py
-rw-r--r-- 1 david david    0 dic 26 17:59 __init__.py
drwxrwxr-x 2 david david 4,0K dic 26 17:59 migrations
-rw-r--r-- 1 david david   57 dic 26 17:59 models.py
-rw-r--r-- 1 david david   60 dic 26 17:59 tests.py
-rw-r--r-- 1 david david   63 dic 26 17:59 views.py

En este primer sprint trabajaremos con cms/models.py

Definimos las entidades Section, Tag y Article. Por ahora utilizaremos como autores el propio objeto User que incorpora Django. Veremos si más adelante tenemos que personalizar esto.



A reseñar la autorreferencia en la entidad Section, para poder construir un árbol de secciones.

Debemos añadir esta app, cms, en el settings.py del proyecto:


Una vez hecho, esto, podemos hacer las migraciones para crear las tablas. Estamos usando una base de datos local sqlite, podemos examinarla al terminar el proceso y veremos todas las tablas de las diferentes INSTALLED_APPS:



Si ahora intentamos crear una sección, tenemos la primera en la frente: da error porque no le hemos dicho en el modelo que la sección "parent" podría ser vacía.


Si cambiamos la definición, y corremos las migraciones, ya podemos crear Sections sin un antecesor.

parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True)

jueves, 29 de septiembre de 2022

¿Hasta cuando se van a permitir estas estafas?

 Está claro que los grandes (Facebook, Twitter, Google,...) viven de la publicidad. Está claro que su único interés es generar impresiones y cobrarlas, pero cuando los anuncios que publican son estafas, tienen una responsabilidad.

Algunas plataformas empiezan a cuidar este tema y retiran anuncios de estafas, pero otras no son tan escrupulosas.

¿Hasta cuando se va a permitir esto? Muestrario de anuncios de Facebook:




















Famosos participando en productos financieros de alto riesgo...

https://cincodias.elpais.com/cincodias/2021/11/28/mercados/1638112255_177308.html




Lo más triste del asunto es que si estos anuncios se publican es porque hay un "target" potencial, gente hastiada de trabajar, criptolais, tontokens, ninis variados... la pena es cuando engañan a gente que simplemente quería rentabilizar sus ahorrillos.



viernes, 5 de agosto de 2022

Recuperando acceso a un wp-admin de wordpress

 Situación: nos pasan una instalación de wordpress en la que no se sabe la contraseña de algún usuario del wp-admin, el correo de restablecimiento es de pruebas o no tenemos acceso a dicho buzón.

Ejemplo: un usuario con un correo inventado de pruebas

Si vamos a recuperar contraseña, nos pide el correo, que puede ser inventado o no tener acceso:


Solución: si tenemos acceso a la base de datos (por línea de comandos o alguna herramienta tipo phpMyadmin, típica de los proveedores de hosting), solo hay que establecer la contraseña cifrándola con MD5.



Aunque Wordpress utiliza un sistema más robusto de contraseñas, con salt y demás, sí que reconoce las cifradas en MD5.

Una vez que ya tenemos acceso con la nueva contraseña, podemos ir al wp-admin/users.php y establecer una contraseña de nuevo para el usuario, esta vez estará convenientemente cifrada y salteada. MD5 a día de hoy es totalmente inseguro, es casi como guardar en plano.

jueves, 30 de septiembre de 2021

Regexp para email

(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])



Fuente: https://stackoverflow.com/a/201378

martes, 21 de septiembre de 2021

Kernel 5.12

Nunca me preocupo mucho por el kernel que tengo instalado, pero esta vez me interesaba poner el 5.12, que por fin soporta el Nux Mighty Plug


Autor del parche: Takashi Iwai

viernes, 20 de diciembre de 2019

Microsoft y Linux

Que Microsoft ofreciese productos "open source" era impensable hace 10 años. Que además fuese un buen software, más impensable todavía.

Y aquí estamos, a las puertas de 2020, con Microsoft contribuyendo en el kernel de Linux y ofreciendo herramientas "open source".

lunes, 15 de abril de 2019

Cuando unos leguajes tratan los arrays por referencia y otros por copia

¿Qué ocurre cuando asignamos un array a otro? En la mayor parte de los lenguajes, lo habitual es que la nueva variable sea un puntero (referencia) al primer array. Por ejemplo, en Python:

array2 = array1 = {'uno': 1, 'dos': 2}

print('array1 antes:', array1)
print('array2 antes:', array2)

array2['dos'] = 22
array1['tres'] = 3

print('array1 después:', array1)
print('array2 después:', array2)


La salida de este script sería la siguiente:

~ $ python3 testarray.py 

array1 antes: {'uno': 1, 'dos': 2}
array2 antes: {'uno': 1, 'dos': 2}
array1 después: {'uno': 1, 'dos': 22, 'tres': 3}
array2 después: {'uno': 1, 'dos': 22, 'tres': 3}


Como era previsible, al cambiar algo en array1 o en array2, el otro array se modifica, porque las dos variables son una referencia al mismo array. (En Python a este tipo de datos se le llama diccionario, pero podemos asumir que es equivalente al array asociativo de PHP)

Este sencillo ejemplo es muy adecuado para explicar a alguien lo que son las referencias. Se ve claramente que cambiar algo en una variable tiene efectos colaterales.

Sin embargo, podemos encontrarnos sorpresas en otros lenguajes. PHP, por ejemplo, copia el array en la nueva variable y a partir de ese momento, son dos referencias independientes. Mucho cuidado si nuestro array contiene muchos datos, se duplica el consumo de memoria:

<?php 
$array1 = $array2 = array("uno" => 1, "dos" => 2);

echo "array1 antes:"; var_dump($array1); echo "\n";
echo "array2 antes:"; var_dump($array2); echo "\n";

$array1["tres"] = 3;
$array2["dos"] = 22;

echo "array1 después:"; var_dump($array1); echo "\n";
echo "array2 después:"; var_dump($array2); echo "\n";


La salida de este script es la siguiente:

~ $ php testarray.php 

array1 antes:array(2) {["uno"]=> int(1), ["dos"]=> int(2)}
array2 antes:array(2) {["uno"]=> int(1), ["dos"]=> int(2)}

array1 después:array(3) {["uno"]=> int(1), ["dos"]=> int(2), ["tres"]=> int(3)}
array2 después:array(2) {["uno"]=>, int(1), ["dos"]=> int(22)}

¿Qué ha pasado en este caso? Las dos variables son independientes, los cambios en una no afectan a la otra. Cada una apunta a una región de memoria diferente. En PHP se puede conseguir el mismo comportamiento que en otros lenguajes, esto es, que asignar una variable a otra por referencia:

<?php 
$array1 = array("uno" => 1, "dos" => 2);
$array2 = &$array1;

echo "array1 antes:"; var_dump($array1); echo "\n";
echo "array2 antes:"; var_dump($array2); echo "\n";

$array1["tres"] = 3;
$array2["dos"] = 22;

echo "array1 después:"; var_dump($array1); echo "\n";
echo "array2 después:"; var_dump($array2); echo "\n";

En este caso el comportamiento es el mismo que en el primer script en Python.
La expresión $array2 = &$array1asigna por referencia la variable 1 a la variable 2.

¿Qué comportamiento es deseable o más conveniente en un lenguaje de programación? La aproximación de PHP puede parecer más ineficiente, pero si reflexionamos un momento, tiene algo a su favor: es consistente.

<?php
$a = $b = 4;
$a = 5;
var_dump($a); var_dump($b);

La salida es:

int(5)
int(4)

Cada variable es una copia.

En Python, los tipos de datos simples sí que se asignan por copia:

Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
>>> a = b = 4
>>> a = 5
>>> a
5
>>> b
4
>>>

También se ve cómo son variables independientes. 

Todos los tipos de datos, simples y compuestos, se comportan igual PHP, sin embargo, en Python los tipos de datos compuestos (diccionarios, listas,... se asignan por referencia y los tipos de datos simples (enteros,...), se asignan por copia.

Esto no es bueno ni malo... pero hay que estar atento.