Buscando imperativos ambiguos o cómo hacer la cencia del humor con Python

Leticia Martín-Fuertes
13 min readNov 18, 2018

--

Hace unos meses un chiste de Vanfunfun en Twitter despertó una chispita de curiosidad en mí. Si no conocéis a Vanfunfun, conocedle, es muy majo y además una persona que se atreve a hablar de lingüística y literatura en su canal de YouTube.

Pensé: “Tiene que haber otros”. En ese momento solo se me ocurrió óvalo y cuando se lo comenté a un compa del curro sugirió búfalo. Pero… ¿habría otros? No sé por qué, pensaba en la palabra crótalo; ¿existiría el verbo crotar? Solo hay una forma de comprobarlo: ¡hagamos la cencia!

Pero ¿cómo podemos encontrar otros pétalos de flores? Es decir, otros sustantivos que coincidan con formas del imperativo con el enclítico -lo. En realidad habrá muchas formas, pero la que se me ocurrió es la siguiente. Primero, buscar formas de sustantivos candidatas a serlo también de imperativos. Luego, buscar formas de imperativos con el clítico -lo, y finalmente cruzar ambas listas para ver qué formas están presentes en ambas. Todo esto lo podemos hacer con regex, Python, la web de la RAE, el Wikcionario y un poquito de XPath.

Disclaimer: todo el código de este artículo se puede usar para ir introduciéndolo tal cual en la terminal y que funcione todo (sí, yo qué sé, no sé hasta dónde pueden llegar vuestros niveles de aburrimiento). También lo he subido a mi cuenta de GitHub con ligeras modificaciones para ir guardando las listas en archivos y que se entienda bien el orden en que deben ser ejecutados los scripts, pero por lo demás el proceso es el mismo.

1. Buscar todos los sustantivos que tengan pinta de imperativos

Mira por dónde, vamos a dar uso a la herramienta del DLE de buscar palabras con una determinada terminación. Nos interesan las que acaban en -alo (que podrán coincidir con imperativos de verbos de la 1ª conjugación, como cántalo), -elo (de la 2ª y 3ª, como léelo o pártelo), y -nlo y -zlo para los imperativos irregulares (no hay ninguna :/).

Por supuesto, la lista que nos salga puede contener no solo sustantivos, sino también adjetivos o incluso adverbios, pero no nos importa demasiado porque podemos suponer que la lista final no será muy larga.

Sencillamente vamos a copipegar los resultados uno debajo del otro, en un archivo de texto plano como pueden ser los del bloc de notas en Windows, y guardarlo con la extensión .txt. Por ejemplo, “raw_rae.txt” porque es texto directamente sacado de la web de la RAE. Tiene esta pinta:

    acantocéfalo, la
acéfalo, la
álalo, la
ámalo, la
anacalo, la
anisopétalo, la
...

Para poder trabajar con él hay que limpiarlo de restos diccionariles, y lo primero que tenemos que hacer es convertirlo en un objeto reconocible por Python. En este caso, lo que más sentido tiene es convertirlo en una lista.

Con las siguientes 3 líneas de código, abrimos el archivo en que hemos guardado la lista de sustantivos y guardamos cada línea como elemento de una lista que llamamos, por ejemplo, lista_sustantivos; por último, cerramos el archivo.

archivo_sustantivos = open("raw_rae.txt", "r")
lista_sustantivos = archivo_sustantivos.readlines()
archivo_sustantivos.close()

Para limpiar, nada mejor que nuestras amigas las expresiones regulares (o regex). Para poder usarlas en Python, vamos a importar la librería re.

import re

Ahora tenemos que declarar la lista en la que guardaremos los sustantivos una vez limpitos; necesitamos que exista antes de ir metiéndole elementos.

lista_sustantivos_limpia = []

Y ahora, en cada uno de los elementos de la lista lista_sustantivos usaremos re.sub (de substitute) para sustituir unos caracteres por otros. Vamos a aplicar las siguientes transformaciones:

  • Quitar los 4 espacios que salen al principio cuando copipegas desde la web de la RAE
for linea in lista_sustantivos:
linea = re.sub(" ", "", linea)
  • Quitar la marca de género femenino
    linea = re.sub(", la", "", linea)
  • Eliminar toda línea que contenga un guion, ya que no nos interesan sufijos ni prefijos
    linea = re.sub(".*?-.*?\n", "", linea)
  • De las entradas que tienen varias etimologías, quedarnos solo con una de las formas
    linea = re.sub("1;.*?\n", "\n", linea)

Cuando ya están todas las transformaciones hechas, metemos cada línea, que ahora ya contiene un sustantivo limpio, en la lista lista_sustantivos_limpia que hemos declarado antes.

    lista_sustantivos_limpia.append(linea)

¡Et voilà! Se nos queda una lista de 308 palabras la mar de maja para trabajar con ella.

2. Buscar todos los imperativos con el clítico -lo

Para esto no podemos hurgar en el DLE, porque los imperativos no son las formas canónicas de los verbos (sino que los infinitivos lo son) y por eso no aparecen en los diccionarios. Entonces ¿de dónde sacamos una lista de todos los imperativos del español? Pues lo que he encontrado es la lista de entradas del Wikcionario categorizadas como imperativos.

Lo malo de este recurso es que no puedo filtrar por persona o número, así que tendré que bajármelas todas y luego limpiar. Y que vienen algunas de las formas con enclíticos, pero ¡no todas! Y justamente el clítico que a mí me interesa ¡no viene! Pero no pasa nada, después de limpiar se pueden generar.

Para descargar los imperativos he echado mano de este cuaderno de scraping de mi amigo Javi Sogo, con el que estuvimos una vez trasteando en Lingwars.

Definimos una función para descargarnos todo el contenido de una página, y que nos imprima un mensaje de error si la página no existe.

import requestsdef download(url):
r = requests.get(url)
if r.status_code != 200:
print("! Error {} retrieving url {}".format(r.status_code, url))
return r

Pero antes de ponernos a bajar los imperativos como locos hay que hacer un par de cosas. Como antes, declarar la lista vacía donde voy a ir metiendo los imperativos:

lista_imperativos = []

Y definir la cadena XPath que nos interesa:

xpath_string = ‘//html/body/div[3]/div[3]/div[4]/div[2]/div/div/div/div/ul/li/a/@title’

Un momento, ¿de dónde sale ese esperpento lleno de barras y divisores? ¿Hay que estudiarse el HTML de la página para saber cuántos divs hay que poner? Afortunadamente, no; con el inspector de página de Firefox podemos copiar fácilmente la ruta que nos interesa y pegarla en nuestro código.

Para ello solo tenemos que:

  • Activar el modo Inspección, dando a F12 y pulsando sobre este botón:
  • Seleccionar el elemento que queremos descargar
  • Pulsar con el botón derecho sobre lo que se ha seleccionado en la consola de abajo y darle a Copiar > XPath

El Wikcionario nos muestra los imperativos en tandas, en URL que contienen la primera palabra de cada tanda. Como no hay forma de predecir esa palabra, vamos a navegar por ellas usando el botón de Página siguiente.

Definimos la parte de la URL que va a variar, partiendo de la URL inicial:

url = ‘/w/index.php?title=Categor%C3%ADa:ES:Imperativos&from=A’

Y abrimos un bucle con while: vamos a ejecutar una serie de cosas mientras la variable url no sea igual a None, así que cuando terminemos de ejecutarlas tenemos que igualar url a None para no crear un bucle infinito.

while url != None:

Nos bajamos cada página en la variable page con la función download que definimos al principio. ¿Os acordáis?

    page = download('https://es.wiktionary.org' + url)

Cargamos el html en una estructura de árbol XML y la guardamos en tree.

    tree = html.fromstring(page.content)

Dentro de ese árbol, miramos si hay botón de Página siguiente y, si no lo hay, cerramos el bucle (lo cual solo pasará al final).

    urls = tree.xpath('/html/body/div[3]/div[3]/div[4]/div[2]/div/a[4]/@href')
if len(urls) > 0:
url = urls[0]
else:
url = None

Hacemos la llamada al XPath para obtener los resultados, y guardamos cada uno en la lista lista_imperativos que declaramos al principio.

    results = tree.xpath(xpath_string)
for result in results:
lista_imperativos.append(result)

Ahora observemos la lista: para casi todos los verbos contamos con todos sus imperativos.

abandona
abandonad
abandonaos
abandonate
abandone
abandonemos
abandonen
abandoná
abandonémonos
abandónate
abandónense
abandónese
pulamos
pulan
pule
pulid
pulite
pulámonos
pulí
pulíos

Pero pensemos en los que nos interesan. De aquí, a priori solo abandona y pule, que son los de 2ª persona singular, para luego añadirles el -lo, ¿verdad?

Pero ¿cómo quedarnos solo con los de 2ª persona singular, si esto no va etiquetado ni na? La mejor forma que se me ha ocurrido es eliminando de la lista los elementos que acaben con todas las demás terminaciones, tanto las de la 1ª como las de la 2ª y 3ª conjugaciones.

Después, para obtener la forma con el enclítico -lo cruzable con la lista de sustantivos del paso anterior, moveremos la acentuación de la palabra a la vocal anterior a la última y añadiremos lo al final.

Tenemos suerte, porque el imperativo es muy regular en general, y este enfoque solo nos da dos “problemas”, que en realidad no son para tanto:

  • No puedo quedarme solo con la 2ª persona singular de tú en todos los verbos, sino que la de usted se me va a colar, porque en los verbos de la 1ª conjugación esta acaba en -e (llam-e), coincidiendo con la terminación de la forma de tuteo de la 2ª y 3ª conjugaciones (le-e, mient-e); y viceversa, la terminación -a se usa tanto en las formas de usted de los verbos en -er e -ir (le-a, mient-a) como en las de tú de los acabados en -ar (llam-a). Pero bueno, no me importa demasiado; trabajemos también con las formas de usted.
  • No todos los imperativos de 2ª persona singular acaban en -a o -e, sino que hay unos cuantos acabados en -n (propón) y -z (haz). Por tanto, el método de acentuación que vamos a usar va a crear formas ortográficamente incorrectas como propónlo. Pero tampoco nos preocupa, porque no hemos encontrado sustantivos acabados en -nlo o -zlo.

¡Pues manos a la obra! Como antes, nos ayudaremos de las regex, así que hay que importar re. Vamos a definir una serie de funciones con pasos que querremos efectuar en cada uno de los imperativos, para luego solo tener que llamarlas al recorrer la lista de imperativos.

Primero, filtrar.

import redef filtrar(forma):

Si encontramos en la forma ciertos patrones, haremos que la función devuelva el valor False, para que la forma no pase a la siguiente fase. Esos patrones corresponderán a:

  • plurales de 1ª persona (cantemos, comamos, arrepintamos)
    if re.search("[ea]mos\n", forma):
return False
  • plurales de 1ª persona con clítico -os (cantémonos, comámonos, arrepintámonos)
    elif re.search("[éá]monos\n", forma):
return False
  • singulares de 2ª persona voseante (cantá, comé, arrepentí)
    elif re.search("[áéí]\n", forma):
return False
  • singulares de 2ª persona con clítico -te (cántate, cómete, arrepiéntete, pero no ate, mete, por eso exigimos que haya una vocal tildada)
    elif re.search(".*[áéíóú].*[aei]te\n", forma):
return False
  • plurales de 2ª persona (cantad, comed, arrepentid, reíd)
    elif re.search("[aeií]d\n", forma):
return False
  • plurales de 2ª persona con clítico -os (cantaos, comeos, arrepentíos)
    elif re.search("[aeí]os\n", forma):
return False
  • singulares de 3ª persona con clítico -se (cántese, cómase, arrepiéntase, pero no case, cese)
elif re.search(".*[áéíóú].*[ea]se\n", forma):
return False
  • plurales de 3ª persona (canten, coman, arrepientan)
    elif re.search("[ae]n\n", forma):
return False
  • plurales de 3ª persona con clítico -se (cántense, cómanse, arrepiéntanse, pero no amanse)
    elif re.search(".*[áéíóú].*[ea]nse\n", forma):
return False

Por último, si la forma no ha coincidido con ninguno de esos patrones, devolvemos True.

    else:
return True
Sé que es duro. Pero no desfallezcas, ¡ya queda poco!

Ahora, las funciones de desacentuar y acentuar. El ordenador no sabe que acentuar una a es poner á; hay que enseñárselo, pero es muy facilito ;)

def desacentuar(forma):
if re.search('á', forma):
return forma.replace('á', 'a')
elif re.search('é', forma):
return forma.replace('é', 'e')
elif re.search('í', forma):
return forma.replace('í', 'i')
elif re.search('ó', forma):
return forma.replace('ó', 'o')
elif re.search('ú', forma):
return forma.replace('ú', 'u')
else:
return forma
def acentuar(letra):
if letra == 'a':
return 'á'
elif letra == 'e':
return 'é'
elif letra == 'i':
return 'í'
elif letra == 'o':
return 'ó'
elif letra == 'u':
return 'ú'
return letra

Fíjate en que el argumento que le pasamos a desacentuar() (y que nos devuelve) es la forma entera, mientras que el de acentuar() es solo la letra.

Otra función que vamos a necesitar es la de arreglar el desaguisado de la qu. Es lo malo de trabajar con texto 😅 Cuando acentuemos, lo haremos sobre la última vocal (excluyendo la última letra), así que obtendremos formas como achaqúe o refresqúe, que hay que arreglar.

def arreglar_qu(forma):

Buscamos la secuencia en todas las formas, y si la contienen pasamos a la siguiente línea, que reemplaza por qu.

    if re.search("qú", forma):
forma = forma.replace("qú", "qu")

En esta forma, buscamos si el carácter anterior a la q es una vocal; si es así, la acentuamos.

        if re.match('[aeiou]', forma[-5]):
forma = forma[:-5] + acentuar(forma[-5]) + forma[-4:]

Si no, buscamos si la vocal está en la posición anterior, y si es así la acentuamos.

        elif re.match('[aeiou]', forma[-6]):
forma = forma[:-6] + acentuar(forma[-6]) + forma[-5:]

Y si resulta que no hay en la forma, no hacemos nada.

    else:
return forma
return forma

Por último, solo nos queda definir una función que añada el clítico -lo a la forma. Como cada forma acaba en \n, el marcador del salto de línea, vamos a quitar cada vez ese carácter, después insertamos lo y lo volvemos a poner:

def añadir_clitico(forma):
forma = forma[:-1] + 'lo' + '\n'
return forma

¡Venga, que ya queda poco! Estamos a un paso de terminar el segundo objetivo y ya tenemos todo lo que necesitamos. Ahora vamos a definir la función que agrupe a todas las anteriores y las aplique en el orden y en los casos apropiados. La llamamos, por ejemplo, transformar().

def transformar(forma):

Como hemos dicho, a este paso solo nos llegan las formas filtradas, así que lo primero es desacentuar.

    forma = desacentuar(forma)

Ahora hay que acentuar con cabeza. Hay que pensar hasta qué posición puede salir la vocal anterior a la última letra. En castellano, el grupo consonántico más largo es de 4 letras, nstr; es decir, que si hay una forma que acabe en -nstre o -nstra (¡y la hay! Cf. demuenstra), la vocal que habría que acentuar está en la posición séptima desde el final, o forma[-7], como se escribe en Python. Así que solo hay que contemplar 5 casos, porque no va a estar ni en la última posición (que es el marcador de salto de línea) ni en la penúltima (que es la última vocal y no es esa la que queremos acentuar).

Además, hay que ir haciéndolo según la longitud de la palabra, que evaluamos con len(), porque si no Python intentará buscar, por ejemplo, la séptima letra por el final en palabras de 4 letras, y nos dará error.

    if len(forma) > 3 and re.match('[aeiou]', forma[-3]):
forma = forma[:-3] + acentuar(forma[-3]) + forma[-2:]
elif len(forma) > 4 and re.match('[aeiou]', forma[-4]):
forma = forma[:-4] + acentuar(forma[-4]) + forma[-3:]
elif len(forma) > 5 and re.match('[aeiou]', forma[-5]):
forma = forma[:-5] + acentuar(forma[-5]) + forma[-4:]
elif len(forma) > 6 and re.match('[aeiou]', forma[-6]):
forma = forma[:-6] + acentuar(forma[-6]) + forma[-5:]
elif len(forma) > 7 and re.match('[aeiou]', forma[-7]):
forma = forma[:-7] + acentuar(forma[-7]) + forma[-6:]

Ahora arreglamos el tema de la si es necesario, añadimos el clítico, y ¡ya está!

    forma = arreglar_qu(forma)
forma = añadir_clitico(forma)
return forma

Vamos a aplicar el filtro y las transformaciones a la lista de imperativos que teníamos, lista_imperativos, y el resultado lo iremos metiendo en lista_imperativos_limpia.

lista_imperativos_limpia = []
for imperativo in lista_imperativos:
if filtrar(imperativo) == True:
imperativo = transformar(imperativo)
lista_imperativos_limpia.append(imperativo)

3. Cruzar listas

Ahora queda lo más fácil y emocionante :) Solo hay que recorrer una de las dos listas limpias que tenemos, la de sustantivos y la de imperativos, nos da igual cuál, y ver si cada forma coincide con alguna de las formas de la otra lista.

for imperativo in lista_imperativos_limpia:
for sustantivo in lista_sustantivos_limpia:
if re.match(imperativo, sustantivo):
print(imperativo)

¡Qué ilusión! ¡Salen cosas con sentido por la terminal!

ámalo
búfalo
cándalo
críalo
críalo
escándalo
óvalo
págalo
pétalo
róbalo
sépalo
tápalo
tésalo
tímalo
zócalo

Ahora analicemos los resultados (14):

  • Hay un par de palabras nada interesantes porque no se puede hablar de que coincidan en la forma, sino de que precisamente el imperativo se ha lexicalizado, es decir, se ha tomado para dar nombre a algo. Es el caso de críalo (ave) y tápalo (chal o mantón). La primera sale dos veces porque tanto la forma tildada (cría) como la no tildada (cria) están aceptadas.
  • En la mayoría de los casos, el sustantivo o el verbo son tan poco frecuentes que no nos importa mucho que coincidan, vamos, que el efecto cómico sería nulo. Es el caso de ámalo (linaje godo ilustre), cándalo (rama o tronco seco), págalo (ave marina), róbalo (pez marino), sépalo (las hojitas que envuelven a las flores en la fase de capullo, al parecer es casual que se parezca tanto a pétalo), tésalo (variante de tesalio, antiguo pueblo griego; y tesar parece una variante de tensar), tímalo (pez), y zócalo (zocar es apretar). Hay que decir que si nos hubieran salido más resultados de los que podemos manejar en un rato, lo suyo habría sido buscar la frecuencia del nombre y del verbo en algún corpus y luego ordenar por esos campos. Incluso podríamos descartar los verbos instransitivos, ya que no tendría sentido que llevaran un enclítico de complemento directo.
  • Unos pocos casos sorprendentes, entre los que se encuentran, como habíamos predicho, pétalo, búfalo y óvalo, pero también otro que no se me había ocurrido (y el único de cuatro sílabas), que es escándalo. ¡MENCANTA! Es verdad que el verbo escandir no es lo más cotidiano del mundo, pero al menos todos lo conocemos de la Lengua y Literatura del bachillerato (¿verdad? ¿VERDAD?).

Así que ya sabes, escándalo no es solo alboroto, tumulto, ruido sino también lo que alguien le dice a su interlocutor, al que está tratando de usted, para que le mida un verso que no cree necesario especificar. Espero que te acuerdes la próxima vez que escuches la canción de Raphael :P

¡Y eso es todo! Gracias por leerme. Yo me lo he pasado muy bien haciendo todo esto. Si tienes dudas, comentarios o sugerencias o has hecho algo parecido (como expandirlo a otras formas verbales o a otros clíticos, por ejemplo, porque yo me he tenido que poner un límite práctico) me encantará que me lo cuentes :)

Y si te ha gustado este artículo, échale un vistazo al máster en Humanidades Digitales para el que estoy preparando la asignatura de Introducción a la programación. En él aprenderás todo lo que hace falta para hacer cosas como esta y muchas otras aplicadas a las humanidades digitales.

--

--

Leticia Martín-Fuertes

Computational linguist at Paradigma Digital, classical philologist and NLProc enthusiast. Science & arts lover.