Apuntes DAM
Volver al inicio

Ejercicios de Python resueltos

Los 68 ejercicios de Python de la web en una sola hoja: cada uno con su enunciado, los datos que necesitas (código de partida, ejemplos de entrada y salida o la base de datos) y, al final, la solución explicada.

Descargar el PDF

Lenguajes de Marcas

1. ¿Están bien anidadas las etiquetas?

Medio · Fundamentos de Lenguajes de Marcas · apuntesdam.com/subject/lenguajes-marcas/topic/fundamentos

Un documento XML bien formado tiene un único elemento raíz y todas sus etiquetas cerradas en orden inverso al de apertura. Lee el documento completo y recorre sus etiquetas (ya están extraídas con una expresión regular; se ignoran <?…?>, comentarios y DOCTYPE). Las de cierre son </nombre>, las vacías terminan en /> y el resto son de apertura (el nombre es lo que va tras < hasta un espacio, / o >). Detén la comprobación en el primer error: «Error en la línea L: </x> no tiene etiqueta de apertura», «Error en la línea L: se esperaba </a> pero aparece </b>», «Error en la línea L: <x> es un segundo elemento raíz» (si empieza un elemento cuando la raíz ya se cerró), y al final «Error: falta cerrar <x>» (la más interna) o «Error: el documento no tiene elemento raíz». Si todo es correcto: «Bien formado: N elementos, profundidad máxima D, raíz <r>» (la raíz tiene profundidad 1). Los nombres distinguen mayúsculas.

Código de partida (python)
import re, sys texto = sys.stdin.read() def linea(pos):    return texto.count("\n", 0, pos) + 1 pila = []            # etiquetas abiertaselementos = 0profundidad = 0raiz = None          # nombre de la raíz cuando ya se ha cerradofor m in re.finditer(r"<[^>]*>", texto):    etiqueta = m.group()    if etiqueta.startswith(("<?", "<!")):        continue     # declaración, comentarios y DOCTYPE no cuentan    # TODO: etiquetas de cierre, vacías y de aperturaprint("Bien formado")

Ejemplo: Catálogo correcto

Entrada

<?xml version="1.0" encoding="UTF-8"?>
<!-- Catálogo de la biblioteca -->
<biblioteca>
  <libro isbn="978-84-1">
    <titulo>El Quijote</titulo>
    <autor>Cervantes</autor>
    <portada archivo="q.jpg"/>
  </libro>
  <libro isbn="978-84-2">
    <titulo>La Regenta</titulo>
  </libro>
</biblioteca>

Salida esperada

Bien formado: 7 elementos, profundidad máxima 3, raíz <biblioteca>

Ejemplo: Cierre cruzado

Entrada

<pedido>
  <cliente><nombre>Ana</cliente></nombre>
</pedido>

Salida esperada

Error en la línea 2: se esperaba </nombre> pero aparece </cliente>

Ejemplo: Dos raíces

Entrada

<a>
</a>
<b/>

Salida esperada

Error en la línea 3: <b> es un segundo elemento raíz

Ejemplo: Sin cerrar

Entrada

<html>
  <body>
    <p>Hola
  </body>
</html>

Salida esperada

Error en la línea 4: se esperaba </p> pero aparece </body>

2. Atributos y referencias de entidad

Difícil · Fundamentos de Lenguajes de Marcas · apuntesdam.com/subject/lenguajes-marcas/topic/fundamentos

Además de anidar bien, un XML bien formado exige que los atributos tengan valor entre comillas (simples o dobles), sin repetirse en la misma etiqueta y sin «<» en el valor, y que cada «&» empiece una referencia válida: &amp; &lt; &gt; &quot; &apos;, &#número; o &#xhexadecimal;. Completa las dos funciones. revisar_etiqueta recorre los atributos de cada etiqueta de apertura o vacía: el nombre debe empezar por letra o _ y seguir con letras, dígitos, _ . : o - (si no, «Línea L, <tag>: nombre de atributo no válido «x»», donde x llega hasta un espacio o =, y se deja de revisar esa etiqueta); sin = → «… el atributo «a» no tiene valor» (y se sigue con el siguiente); sin comillas → «… el valor de «a» no está entre comillas» (se deja la etiqueta); repetido → «… el atributo «a» está repetido»; con < → «… el valor de «a» contiene «<»». Cada atributo con valor cuenta en atributos y su valor pasa por revisar_referencias. revisar_referencias, para cada &, toma el trozo desde & hasta el primer ; (incluido) sin pasar de un espacio, &, < o ;; si es válido suma una referencia y si no, «Línea L: referencia no válida «trozo»», con la línea real dentro del texto.

Código de partida (python)
import re, sys texto = sys.stdin.read()VALIDA = re.compile(r"&(amp|lt|gt|quot|apos|#[0-9]+|#x[0-9a-fA-F]+);") def linea(pos):    return texto.count("\n", 0, pos) + 1 errores = []atributos = referencias = 0 def revisar_referencias(cadena, l):    """Comprueba cada & de la cadena; l es la línea donde se informa."""    global referencias    # TODO: referencias de entidad y de carácter def revisar_etiqueta(etiqueta, l):    global atributos    nombre = re.match(r"<\s*([^\s/>]+)", etiqueta).group(1)    resto = etiqueta[etiqueta.index(nombre) + len(nombre):].rstrip(">").rstrip("/")    # TODO: atributos de la etiqueta fin = 0for m in re.finditer(r"<[^>]*>", texto):    revisar_referencias(texto[fin:m.start()], linea(fin))    fin = m.end()    etiqueta = m.group()    if not etiqueta.startswith(("</", "<?", "<!")):        revisar_etiqueta(etiqueta, linea(m.start()))revisar_referencias(texto[fin:], linea(fin)) if errores:    print("\n".join(errores))    print(f"Errores: {len(errores)}")else:    print(f"Sin errores: {atributos} atributos y {referencias} referencias correctas")

Ejemplo: Tienda con errores

Entrada

<?xml version="1.0"?>
<tienda nombre="Pan &amp; Café" web='https://pan.es/?a=1&amp;b=2'>
  <producto id="p1" precio="2,50 &#8364;" id="p2">Barra & media</producto>
  <producto id=p3 oferta>Croissant &lt;nuevo&gt;</producto>
  <producto nota="a < b" 1id="x">Tarta &copy; &#x1F370;</producto>
  <producto disponible = "sí" stock="12"/>
</tienda>

Salida esperada

Línea 3, <producto>: el atributo «id» está repetido
Línea 3: referencia no válida «&»
Línea 4, <producto>: el valor de «id» no está entre comillas
Línea 5, <producto>: el valor de «nota» contiene «<»
Línea 5, <producto>: nombre de atributo no válido «1id»
Línea 5: referencia no válida «&copy;»
Errores: 6

Ejemplo: Documento correcto

Entrada

<a x="1" y='&quot;dos&quot;'>&lt;3 &#65;&#x42;</a>

Salida esperada

Sin errores: 2 atributos y 5 referencias correctas

3. Resolver los espacios de nombres

Muy difícil · Fundamentos de Lenguajes de Marcas · apuntesdam.com/subject/lenguajes-marcas/topic/fundamentos

Un procesador XML no trabaja con prefijos sino con nombres expandidos {URI}local. Lee un documento bien formado y muestra el árbol con el nombre expandido de cada elemento, sangrado dos espacios por nivel; debajo, sangrados dos espacios más, sus atributos «@nombre = valor» (excepto las declaraciones xmlns). Reglas: xmlns="uri" declara el espacio por defecto (xmlns="" lo anula) y xmlns:p="uri" un prefijo; las declaraciones valen para el elemento que las tiene y sus descendientes. Un elemento sin prefijo está en el espacio por defecto; un atributo sin prefijo no está en ningún espacio. El prefijo xml está siempre declarado (http://www.w3.org/XML/1998/namespace). Sin espacio de nombres se muestra solo el nombre local. Si un prefijo no está declarado: «Error en la línea L: el prefijo «p» no está declarado» y se termina. Al final, «Espacios de nombres usados: N» y, por orden de primera aparición, « URI: K elemento(s)» contando solo elementos.

Código de partida (python)
import re, sys texto = sys.stdin.read()XML_NS = "http://www.w3.org/XML/1998/namespace"ATRIBUTO = re.compile(r"""([\w.:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')""") def linea(pos):    return texto.count("\n", 0, pos) + 1 ambitos = [{"xml": XML_NS, "": ""}]     # pila de ámbitos: prefijo → URI ("" es el espacio por defecto)usos = {}                              # URI → número de elementos (en orden de primera aparición)for m in re.finditer(r"<[^>]*>", texto):    etiqueta = m.group()    if etiqueta.startswith(("<?", "<!")):        continue    if etiqueta.startswith("</"):        ambitos.pop()        continue    nombre = re.match(r"<\s*([^\s/>]+)", etiqueta).group(1)    atributos = [(a.group(1), a.group(2) if a.group(2) is not None else a.group(3)) for a in ATRIBUTO.finditer(etiqueta)]    actual = dict(ambitos[-1])         # ámbito de este elemento: el del padre más sus declaraciones    # TODO: declarar, resolver y mostrar el nombre expandido    if not etiqueta.endswith("/>"):        ambitos.append(actual)print(f"Espacios de nombres usados: {len(usos)}")for uri, n in usos.items():    print(f"  {uri}: {n} elemento{'s' if n != 1 else ''}")

Ejemplo: Factura con tres vocabularios

Entrada

<?xml version="1.0"?>
<factura xmlns="urn:facturas" xmlns:cli="urn:clientes" numero="F-12">
  <cli:cliente cli:nif="12345678Z">
    <cli:nombre xml:lang="es">Ana</cli:nombre>
  </cli:cliente>
  <lineas>
    <linea xmlns="urn:productos" ref="P1">Teclado</linea>
    <linea xmlns="" ref="P2">Ratón</linea>
  </lineas>
  <total>45.90</total>
</factura>

Salida esperada

{urn:facturas}factura
  @numero = F-12
  {urn:clientes}cliente
    @{urn:clientes}nif = 12345678Z
    {urn:clientes}nombre
      @{http://www.w3.org/XML/1998/namespace}lang = es
  {urn:facturas}lineas
    {urn:productos}linea
      @ref = P1
    linea
      @ref = P2
  {urn:facturas}total
Espacios de nombres usados: 3
  urn:facturas: 3 elementos
  urn:clientes: 2 elementos
  urn:productos: 1 elemento

Ejemplo: Prefijo sin declarar

Entrada

<doc xmlns:a="urn:a">
  <a:x/>
  <b:y/>
</doc>

Salida esperada

doc
  {urn:a}x
Error en la línea 3: el prefijo «b» no está declarado

4. Genera un canal RSS 2.0 correcto

Difícil · Sindicación de contenidos: RSS y Atom · apuntesdam.com/subject/lenguajes-marcas/topic/sindicacion-contenidos

Una panadería quiere publicar sus novedades como canal RSS. La primera línea tiene «título|enlace|descripción» del canal y cada una de las siguientes una noticia «fecha ISO 8601|título|enlace|descripción|categoría». Construye el documento con xml.etree.ElementTree (que escapa solo los caracteres especiales) y escríbelo: primero la línea <?xml version="1.0" encoding="UTF-8"?> y después el <rss version="2.0"> sangrado con dos espacios (ET.indent). El <channel> contiene, en este orden, title, link, description, language (es-es), lastBuildDate (la fecha de la noticia más reciente; se omite si no hay noticias) y los item de la más reciente a la más antigua. Cada item contiene title, link, description, category, guid (igual al enlace) y pubDate. Las fechas del RSS van en formato RFC 822, como «Mon, 29 Sep 2025 09:00:00 +0200» (email.utils.format_datetime).

Código de partida (python)
import sysimport xml.etree.ElementTree as ETfrom datetime import datetimefrom email.utils import format_datetime lineas = [l for l in sys.stdin.read().splitlines() if l.strip()]titulo, enlace, descripcion = lineas[0].split("|")noticias = []          # (fecha, titulo, link, descripcion, categoria)for l in lineas[1:]:    fecha, t, link, desc, cat = l.split("|")    noticias.append((datetime.fromisoformat(fecha), t, link, desc, cat)) # TODO: construye el canal con ElementTree y escríbelo

Ejemplo: Panadería

Entrada

Panadería La Espiga|https://laespiga.es|Novedades & ofertas del obrador
2025-09-26T18:30:00+02:00|Pan de centeno|https://laespiga.es/centeno|Vuelve el pan de centeno 100 %|Productos
2025-09-29T09:00:00+02:00|Horario de otoño|https://laespiga.es/horario|Abrimos a las 7:30 de lunes a sábado|Avisos
2025-09-28T12:15:00+02:00|Taller: masa madre|https://laespiga.es/taller?dia=4&plazas=10|Aprende a hacer <b>masa madre</b>|Talleres

Salida esperada

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Panadería La Espiga</title>
    <link>https://laespiga.es</link>
    <description>Novedades &amp; ofertas del obrador</description>
    <language>es-es</language>
    <lastBuildDate>Mon, 29 Sep 2025 09:00:00 +0200</lastBuildDate>
    <item>
      <title>Horario de otoño</title>
      <link>https://laespiga.es/horario</link>
      <description>Abrimos a las 7:30 de lunes a sábado</description>
      <category>Avisos</category>
      <guid>https://laespiga.es/horario</guid>
      <pubDate>Mon, 29 Sep 2025 09:00:00 +0200</pubDate>
    </item>
    <item>
      <title>Taller: masa madre</title>
      <link>https://laespiga.es/taller?dia=4&amp;plazas=10</link>
      <description>Aprende a hacer &lt;b&gt;masa madre&lt;/b&gt;</description>
      <category>Talleres</category>
      <guid>https://laespiga.es/taller?dia=4&amp;plazas=10</guid>
      <pubDate>Sun, 28 Sep 2025 12:15:00 +0200</pubDate>
    </item>
    <item>
      <title>Pan de centeno</title>
      <link>https://laespiga.es/centeno</link>
      <description>Vuelve el pan de centeno 100 %</description>
      <category>Productos</category>
      <guid>https://laespiga.es/centeno</guid>
      <pubDate>Fri, 26 Sep 2025 18:30:00 +0200</pubDate>
    </item>
  </channel>
</rss>

5. Un agregador que mezcla RSS y Atom

Muy difícil · Sindicación de contenidos: RSS y Atom · apuntesdam.com/subject/lenguajes-marcas/topic/sindicacion-contenidos

Un agregador junta varios canales en una sola lista. La entrada empieza con «maximo N» y después vienen los documentos separados por líneas «=====»: unos son RSS 2.0 (raíz <rss>) y otros Atom 1.0 (raíz <feed> en el espacio de nombres http://www.w3.org/2005/Atom, que ElementTree escribe como {http://www.w3.org/2005/Atom}feed). De cada entrada obtén: la fecha (pubDate en RFC 822 con email.utils.parsedate_to_datetime, o updated en ISO 8601 con datetime.fromisoformat), el nombre del canal (su title), el título y un identificador (guid o, si no tiene, link en RSS; id en Atom). Descarta las repetidas por identificador conservando la primera que aparece, ordena de la más reciente a la más antigua (a igual fecha, por título) y muestra «Canales: C · Entradas: E (duplicadas descartadas: D)» y las N primeras como «AAAA-MM-DD HH:MM UTC · canal · título», con la hora pasada a UTC.

Código de partida (python)
import sysimport xml.etree.ElementTree as ETfrom datetime import datetime, timezonefrom email.utils import parsedate_to_datetime ATOM = "{http://www.w3.org/2005/Atom}"partes = sys.stdin.read().split("=====")maximo = int(partes[0].split()[1])          # «maximo N»documentos = [ET.fromstring(p.strip()) for p in partes[1:] if p.strip()] entradas = []        # (fecha UTC, canal, título, identificador)# TODO: leer RSS y Atom, quitar duplicados, ordenar y mostrar

Ejemplo: Dos canales con un duplicado

Entrada

maximo 10
=====
<rss version="2.0"><channel>
  <title>Apuntes DAM</title><link>https://apuntesdam.com</link><description>Novedades</description>
  <item><title>Nuevo tema de XSLT</title><link>https://apuntesdam.com/xslt</link><guid>tag:apuntesdam,2025:41</guid><pubDate>Mon, 29 Sep 2025 09:00:00 +0200</pubDate></item>
  <item><title>Retos SQL</title><link>https://apuntesdam.com/sql</link><pubDate>Thu, 25 Sep 2025 12:00:00 +0200</pubDate></item>
  <item><title>Ejercicios de Entornos</title><link>https://apuntesdam.com/entornos</link><guid>tag:apuntesdam,2025:40</guid><pubDate>Fri, 26 Sep 2025 18:30:00 +0200</pubDate></item>
</channel></rss>
=====
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Blog de Marta</title>
  <id>urn:uuid:blog-marta</id>
  <updated>2025-09-29T10:00:00Z</updated>
  <entry><title>Mi primer canal Atom</title><id>urn:uuid:m-3</id><updated>2025-09-29T07:30:00Z</updated><link href="https://marta.dev/atom"/></entry>
  <entry><title>XPath sin miedo</title><id>urn:uuid:m-2</id><updated>2025-09-27T16:45:00+02:00</updated><link href="https://marta.dev/xpath"/></entry>
  <entry><title>Nuevo tema de XSLT (enlace)</title><id>tag:apuntesdam,2025:41</id><updated>2025-09-29T09:05:00+02:00</updated><link href="https://apuntesdam.com/xslt"/></entry>
</feed>

Salida esperada

Canales: 2 · Entradas: 5 (duplicadas descartadas: 1)
2025-09-29 07:30 UTC · Blog de Marta · Mi primer canal Atom
2025-09-29 07:00 UTC · Apuntes DAM · Nuevo tema de XSLT
2025-09-27 14:45 UTC · Blog de Marta · XPath sin miedo
2025-09-26 16:30 UTC · Apuntes DAM · Ejercicios de Entornos
2025-09-25 10:00 UTC · Apuntes DAM · Retos SQL

6. Importación incremental con identificadores externos

Muy difícil · Sistemas de Gestión de Información Empresarial · apuntesdam.com/subject/lenguajes-marcas/topic/sistemas-gestion-informacion

Los ERP como Odoo importan datos de forma incremental gracias a un identificador externo (external_id): si ya existe, el registro se actualiza; si no, se crea. La entrada tiene una sección [erp] con los clientes actuales «external_id;nif;nombre;email;activo(si/no)» y una sección [importar] con las filas del fichero «external_id;nif;nombre;email». Procesa las filas en orden (la línea N empieza en 1): primero se valida el NIF (8 dígitos y letra, o letra, 7 dígitos y carácter alfanumérico), después el email y después que haya nombre; si falla, «ERROR línea N (id): NIF no válido «x»» (o «email no válido «x»» o «falta el nombre»). Si el NIF ya pertenece a otro registro: «DUPLICADO línea N (id): el NIF x ya es de otro_id». Si el id existe y nada cambia (y está activo): «SIN CAMBIOS id»; si cambia algo: «ACTUALIZAR id: campo antes → después, …» (en el orden nif, nombre, email), añadiendo « (reactivado)» si estaba inactivo (y «sin cambios de datos» si solo se reactiva). Si no existe: «CREAR id (nombre)». Los cambios se aplican en el momento, así que afectan a las filas siguientes. Termina con «Creados: a · Actualizados: b · Sin cambios: c · Errores: d» y, si hay clientes activos que no aparecen en el fichero, «No incluidos en el fichero: id, id» en el orden del ERP.

Código de partida (python)
import re, sys NIF = re.compile(r"[0-9]{8}[A-Z]|[A-Z][0-9]{7}[A-Z0-9]")EMAIL = re.compile(r"[^\s@]+@[^\s@]+\.[^\s@]+") erp = {}            # external_id → {"nif", "nombre", "email", "activo"} (el orden de inserción es el del ERP)filas = []          # filas del fichero a importar: (external_id, nif, nombre, email)seccion = Nonefor linea in sys.stdin.read().splitlines():    linea = linea.strip()    if not linea:        continue    if linea.startswith("["):        seccion = linea        continue    campos = [c.strip() for c in linea.split(";")]    if seccion == "[erp]":        ext, nif, nombre, email, activo = campos        erp[ext] = {"nif": nif, "nombre": nombre, "email": email, "activo": activo == "si"}    else:        filas.append(tuple(campos)) # TODO: procesar la importación y mostrar el informe

Ejemplo: Clientes de una gestoría

Entrada

[erp]
cli_001;12345678Z;Ana García;ana@correo.es;si
cli_002;B1234567X;Talleres Norte S.L.;admin@tnorte.es;si
cli_003;87654321X;Luis Pérez;luis@correo.es;no
cli_004;A7654321B;Academia Rioja;info@arioja.es;si
[importar]
cli_001;12345678Z;Ana García;ana@correo.es
cli_002;B1234567X;Talleres Norte S.L.;facturas@tnorte.es
cli_003;87654321X;Luis Pérez Gil;luis@correo.es
cli_005;C1111111D;Tienda Sur;tienda@sur.es
cli_006;1234567;Pepe Ruiz;pepe@correo.es
cli_007;A7654321B;Otra Academia;otra@academia.es
cli_008;Y7654321G;Marta Díaz;marta.correo.es
cli_005;C1111111D;Tienda Sur S.A.;tienda@sur.es

Salida esperada

SIN CAMBIOS cli_001
ACTUALIZAR cli_002: email admin@tnorte.es → facturas@tnorte.es
ACTUALIZAR cli_003: nombre Luis Pérez → Luis Pérez Gil (reactivado)
CREAR cli_005 (Tienda Sur)
ERROR línea 5 (cli_006): NIF no válido «1234567»
DUPLICADO línea 6 (cli_007): el NIF A7654321B ya es de cli_004
ERROR línea 7 (cli_008): email no válido «marta.correo.es»
ACTUALIZAR cli_005: nombre Tienda Sur → Tienda Sur S.A.
Creados: 1 · Actualizados: 3 · Sin cambios: 1 · Errores: 3
No incluidos en el fichero: cli_004

Bases de Datos

7. Fichero secuencial frente a índice

Medio · Sistemas de Almacenamiento · apuntesdam.com/subject/bases-datos/topic/almacenamiento

¿Por qué una base de datos con índice encuentra un registro entre millones casi al instante? La entrada tiene registros «id nombre» en el orden en que están en el fichero (desordenados) y búsquedas «buscar id». El índice ya está construido: una lista ordenada de (id, posición). Para cada búsqueda cuenta las lecturas de la búsqueda secuencial en el fichero (se leen registros desde el principio hasta encontrarlo; si no está, todos) y las comparaciones de una búsqueda binaria en el índice (lo = 0, hi = n−1; mientras lo ≤ hi: mid = (lo+hi)//2, se cuenta una comparación y se mira si es el buscado, si es menor o si es mayor), más una lectura del registro si se encuentra. Muestra «buscar x: encontrado (nombre) · secuencial S lecturas · índice C comparaciones + 1 lectura» (o «no existe» y sin «+ 1 lectura»; en singular «1 lectura» y «1 comparación») y al final «Total secuencial: S lecturas · índice: I accesos · ahorro P %» con P = 100 − 100·I/S redondeado.

Código de partida (python)
import sys registros = []          # (id, nombre) en el orden del fichero (sin ordenar)busquedas = []for linea in sys.stdin.read().splitlines():    p = linea.split()    if not p:        continue    if p[0] == "buscar":        busquedas.append(int(p[1]))    else:        registros.append((int(p[0]), p[1])) indice = sorted((r[0], pos) for pos, r in enumerate(registros))    # (id, posición en el fichero)# TODO: para cada búsqueda, contar lecturas secuenciales y comparaciones de la búsqueda binaria en el índice

Ejemplo: 40 registros

Entrada

431 Ana0
254 Luis1
504 Eva2
766 Juan3
149 Marta4
174 Pablo5
940 Lucía6
648 Hugo7
196 Sara8
474 Iker9
696 Ana10
159 Luis11
619 Eva12
319 Juan13
138 Marta14
188 Pablo15
544 Lucía16
528 Hugo17
171 Sara18
346 Iker19
192 Ana20
664 Luis21
534 Eva22
160 Juan23
946 Marta24
679 Pablo25
226 Lucía26
328 Hugo27
745 Sara28
742 Iker29
163 Ana30
690 Luis31
699 Eva32
506 Juan33
150 Marta34
326 Pablo35
147 Lucía36
670 Hugo37
979 Sara38
236 Iker39
buscar 431
buscar 679
buscar 236
buscar 5
buscar 474

Salida esperada

buscar 431: encontrado (Ana0) · secuencial 1 lectura · índice 1 comparación + 1 lectura
buscar 679: encontrado (Pablo25) · secuencial 26 lecturas · índice 5 comparaciones + 1 lectura
buscar 236: encontrado (Iker39) · secuencial 40 lecturas · índice 6 comparaciones + 1 lectura
buscar 5: no existe · secuencial 40 lecturas · índice 5 comparaciones
buscar 474: encontrado (Iker9) · secuencial 10 lecturas · índice 5 comparaciones + 1 lectura
Total secuencial: 117 lecturas · índice: 26 accesos · ahorro 78 %

8. Fragmentación de una base de datos distribuida

Difícil · Sistemas de Almacenamiento · apuntesdam.com/subject/bases-datos/topic/almacenamiento

En una base de datos distribuida, una tabla se reparte en fragmentos guardados en distintas sedes. La entrada define la tabla («tabla nombre col1,col2…»), sus filas («fila v1,v2,…»), fragmentos horizontales («fragmento F sede=S campo=v1,v2»: las filas cuyo campo vale uno de esos valores) y consultas. Muestra «F (sede): n filas» por fragmento («1 fila» en singular, también en las consultas); «Completitud: sí» o «Completitud: NO, sin fragmento: id X (valor), …» (las filas que no están en ningún fragmento se pierden; el valor es el del campo del primer fragmento); «Disjunción: sí» o «Disjunción: NO, id X en F1 y F4; …» (filas en varios fragmentos). Para cada consulta («campo=valor», «campo>número», «campo<número» o «todos»), decide a qué fragmentos hay que preguntar: si la condición es una igualdad sobre el campo de fragmentación, solo a los que incluyen ese valor; si no, a todos. Muestra «consulta C → sedes S1, S2 · n filas: ids» (sedes sin repetir en el orden de los fragmentos, ids ordenados y con repeticiones si una fila está en dos fragmentos; «ninguna» y sin lista si no hay).

Código de partida (python)
import sys columnas, filas, fragmentos, consultas = [], [], [], []for linea in sys.stdin.read().splitlines():    p = linea.split(" ", 1)    if p[0] == "tabla":        columnas = p[1].split(" ")[1].split(",")    elif p[0] == "fila":        valores = p[1].split(",")        filas.append(dict(zip(columnas, valores)))    elif p[0] == "fragmento":                    # fragmento F1 sede=Madrid provincia=Madrid,Toledo        nombre, sede, cond = p[1].split(" ")        campo, valores = cond.split("=")        fragmentos.append((nombre, sede.split("=")[1], campo, valores.split(",")))    elif p[0] == "consulta":        consultas.append(p[1]) # TODO: filas de cada fragmento, completitud, disjunción y consultas

Ejemplo: Clientes por provincia

Entrada

tabla clientes id,nombre,provincia,saldo
fila 1,Ana,Madrid,1200
fila 2,Luis,Sevilla,300
fila 3,Eva,Cádiz,800
fila 4,Iker,Bizkaia,50
fila 5,Marta,Madrid,90
fila 6,Pablo,Lugo,700
fila 7,Sara,Huelva,610
fila 8,Hugo,Toledo,20
fragmento F1 sede=Madrid provincia=Madrid,Toledo
fragmento F2 sede=Sevilla provincia=Sevilla,Cádiz,Huelva
fragmento F3 sede=Bilbao provincia=Bizkaia
fragmento F4 sede=Sevilla provincia=Toledo
consulta provincia=Cádiz
consulta saldo>500
consulta provincia=Lugo
consulta todos

Salida esperada

F1 (Madrid): 3 filas
F2 (Sevilla): 3 filas
F3 (Bilbao): 1 fila
F4 (Sevilla): 1 fila
Completitud: NO, sin fragmento: id 6 (Lugo)
Disjunción: NO, id 8 en F1 y F4
consulta provincia=Cádiz → sedes Sevilla · 1 fila: 3
consulta saldo>500 → sedes Madrid, Sevilla, Bilbao · 3 filas: 1, 3, 7
consulta provincia=Lugo → sedes ninguna · 0 filas
consulta todos → sedes Madrid, Sevilla, Bilbao · 8 filas: 1, 2, 3, 4, 5, 7, 8, 8

9. Anonimizar datos personales: k-anonimato

Muy difícil · Sistemas de Almacenamiento · apuntesdam.com/subject/bases-datos/topic/almacenamiento

El RGPD permite publicar o compartir datos para estudios si están anonimizados, pero quitar el nombre no basta: la edad, el código postal y el sexo juntos identifican a mucha gente. La entrada empieza con «k K» y una tabla «nombre;dni;edad;cp;sexo;diagnostico». Anonimiza: elimina nombre y dni («Identificadores eliminados: nombre, dni»); generaliza la edad a su década («34» → «30-39») y el código postal a sus tres primeras cifras («28013» → «280**»). Los cuasi-identificadores son (edad, cp, sexo): las filas con los mismos valores forman un grupo, y k es el tamaño del grupo más pequeño («k antes de suprimir: n»). Suprime las filas de los grupos con menos de K filas; si no queda ninguna, «No queda ningún dato publicable» y termina. Si quedan, muestra la cabecera «edad;cp;sexo;diagnostico», las filas en su orden original y «Grupos: G · k = k (objetivo K) · filas suprimidas: s · l-diversidad: l», donde l es el menor número de diagnósticos distintos en un grupo; si l es 1, añade «Atención: en algún grupo todos tienen el mismo diagnóstico, que se puede deducir».

Código de partida (python)
import sysfrom collections import Counter, defaultdict lineas = [l for l in sys.stdin.read().splitlines() if l.strip()]k_objetivo = int(lineas[0].split()[1])cabecera = lineas[1].split(";")                 # nombre;dni;edad;cp;sexo;diagnosticofilas = [dict(zip(cabecera, l.split(";"))) for l in lineas[2:]] # TODO: eliminar identificadores, generalizar, calcular k y l, y suprimir los grupos pequeños

Ejemplo: Estudio médico

Entrada

k 3
nombre;dni;edad;cp;sexo;diagnostico
Ana López;12345678Z;34;28013;M;asma
Luis Gil;87654321X;37;28015;H;diabetes
Eva Ruiz;11111111H;31;28019;M;migraña
Marta Sanz;22222222J;38;28011;M;asma
Pablo Díaz;33333333P;52;41004;H;hipertensión
Iker Arana;44444444A;55;41007;H;hipertensión
Hugo Vidal;55555555K;58;41002;H;hipertensión
Sara Mora;66666666Q;23;08001;M;alergia
Lucía Paz;77777777B;36;28030;M;diabetes

Salida esperada

Identificadores eliminados: nombre, dni
k antes de suprimir: 1
edad;cp;sexo;diagnostico
30-39;280**;M;asma
30-39;280**;M;migraña
30-39;280**;M;asma
50-59;410**;H;hipertensión
50-59;410**;H;hipertensión
50-59;410**;H;hipertensión
30-39;280**;M;diabetes
Grupos: 2 · k = 3 (objetivo 3) · filas suprimidas: 2 · l-diversidad: 1
Atención: en algún grupo todos tienen el mismo diagnóstico, que se puede deducir

10. Comprobar cardinalidades con datos reales

Medio · Modelo Entidad-Relación · apuntesdam.com/subject/bases-datos/topic/modelo-er

En el modelo E/R, la cardinalidad (mínimo, máximo) de una entidad en una relación dice en cuántas ocurrencias de la relación participa cada una de sus instancias. La entrada define las instancias de dos entidades («entidad Nombre i1 i2 …»), la relación («relacion nombre Entidad1(min,max) Entidad2(min,max)», donde max puede ser n) y las ocurrencias («nombre instancia1 instancia2»). Revisa las ocurrencias en orden: si alguna instancia no existe, «Ocurrencia desconocida: a b»; si el par ya apareció, «Par repetido: a b» (una relación no repite la misma pareja); ninguna de las dos cuenta. Muestra «Ocurrencias válidas de nombre: n» y, para cada instancia de la primera entidad y después de la segunda, «Entidad i participa k veces en nombre (mínimo m)» o «(máximo M)» si se sale de su cardinalidad. Termina con «Restricciones: correctas» o «Restricciones: N incumplimientos» («1 incumplimiento»; contando también desconocidas y repetidas).

Código de partida (python)
import sys instancias = {}        # entidad → lista de ocurrenciasrelacion = None        # (nombre, entidad1, (min1, max1), entidad2, (min2, max2))pares = []for linea in sys.stdin.read().splitlines():    p = linea.split()    if not p:        continue    if p[0] == "entidad":        instancias[p[1]] = p[2:]    elif p[0] == "relacion":                 # relacion cursa Alumno(1,n) Modulo(0,2)        lados = []        for t in p[2:4]:            nombre, card = t[:-1].split("(")            mn, mx = card.split(",")            lados.append((nombre, (int(mn), None if mx == "n" else int(mx))))        relacion = (p[1], lados[0][0], lados[0][1], lados[1][0], lados[1][1])    else:        pares.append((p[1], p[2]))           # cursa A1 BD # TODO: comprobar pares repetidos, instancias desconocidas y la participación mínima y máxima

Ejemplo: Alumnos y módulos

Entrada

entidad Alumno A1 A2 A3 A4
entidad Modulo BD PROG LM
relacion cursa Alumno(1,n) Modulo(0,2)
cursa A1 BD
cursa A2 BD
cursa A3 BD
cursa A1 PROG
cursa A1 BD
cursa A5 LM
cursa A2 LM

Salida esperada

Par repetido: A1 BD
Ocurrencia desconocida: A5 LM
Ocurrencias válidas de cursa: 5
Alumno A4 participa 0 veces en cursa (mínimo 1)
Modulo BD participa 3 veces en cursa (máximo 2)
Restricciones: 4 incumplimientos

11. Del diagrama E/R a las tablas

Difícil · Modelo Entidad-Relación · apuntesdam.com/subject/bases-datos/topic/modelo-er

Aplica las reglas de transformación del modelo E/R al relacional. La entrada tiene entidades «entidad Nombre *clave atributo …» (los atributos con * forman la clave primaria) y relaciones binarias «relacion nombre Entidad1 c1 Entidad2 c2 [atributos]» con c1 y c2 iguales a 1, N o M. Reglas: cada entidad es una tabla con sus atributos; en una 1:N (o N:1) la clave de la entidad del lado 1 pasa como clave ajena a la del lado N, junto con los atributos de la relación; en una 1:1 la clave de la primera pasa a la segunda como clave ajena única; una N:M crea una tabla con el nombre de la relación cuya clave primaria es la unión de las dos claves (ambas son claves ajenas), más sus atributos. Si la relación es reflexiva (la misma entidad en los dos lados), la clave ajena se llama clave_relacion; y si la tabla ya tiene una columna con el nombre de la clave ajena, se llama clave_entidad (en minúsculas). Muestra las tablas de las entidades en orden de definición (con las columnas añadidas por las relaciones en el orden de las relaciones) y después las nuevas, con el formato «Tabla(*clave, columna, fk→Tabla)» («*» delante de las columnas de la clave primaria y «fk→Tabla único» en las 1:1), y al final «Tablas: T (k por relaciones N:M)».

Código de partida (python)
import sys entidades = {}         # nombre → (claves, atributos) en ordenrelaciones = []        # (nombre, e1, card1, e2, card2, atributos)for linea in sys.stdin.read().splitlines():    p = linea.split()    if not p:        continue    if p[0] == "entidad":                    # entidad Cliente *dni nombre email        claves = [a[1:] for a in p[2:] if a.startswith("*")]        attrs = [a.lstrip("*") for a in p[2:]]        entidades[p[1]] = (claves, attrs)    elif p[0] == "relacion":                 # relacion realiza Cliente 1 Pedido N [atributos…]        relaciones.append((p[1], p[2], p[3], p[4], p[5], p[6:])) # TODO: transformar a tablas y mostrar cada una como Tabla(*clave, columna, fk→Tabla)

Ejemplo: Tienda online

Entrada

entidad Cliente *dni nombre email
entidad Pedido *num fecha
entidad Producto *codigo nombre precio
entidad Tarjeta *numero caducidad
relacion realiza Cliente 1 Pedido N
relacion contiene Pedido N Producto M cantidad precio_unidad
relacion tiene Cliente 1 Tarjeta 1

Salida esperada

Cliente(*dni, nombre, email)
Pedido(*num, fecha, dni→Cliente)
Producto(*codigo, nombre, precio)
Tarjeta(*numero, caducidad, dni→Cliente único)
contiene(*num→Pedido, *codigo→Producto, cantidad, precio_unidad)
Tablas: 5 (1 por relaciones N:M)

12. Claves candidatas y forma normal

Muy difícil · Modelo Entidad-Relación · apuntesdam.com/subject/bases-datos/topic/modelo-er

Normalizar empieza por saber qué claves tiene una relación y qué dependencias rompen cada forma normal. La primera línea es el esquema «R(A,B,C…)» y las siguientes, dependencias funcionales «X1,X2 -> Y1,Y2». Implementa cierre(x) (todos los atributos que se deducen de x aplicando las dependencias hasta que no cambie nada) y después: «Claves candidatas: {…}, {…}» (los conjuntos mínimos cuyo cierre es todo el esquema, por tamaño y en el orden de combinations sobre el esquema) y «Atributos primos: …» (los que están en alguna clave, en orden del esquema, o «ninguno»). Para cada dependencia, en orden: si la derecha está contenida en la izquierda, «X -> Y: trivial»; si X es superclave, «X -> Y: correcta (X es superclave)»; si todo lo que determina (sin contar X) es primo, «incumple FNBC (X no es superclave)»; si X es una parte propia de alguna clave, «incumple 2FN (dependencia parcial de la clave {…})» (la primera clave que la contenga); si no, «incumple 3FN (dependencia transitiva)». Termina con «Forma normal: F», la más alta que se cumple: FNBC si no hay incumplimientos; 3FN si solo se incumple FNBC; 2FN si hay alguna transitiva pero no parciales; 1FN si hay alguna parcial.

Código de partida (python)
import sysfrom itertools import combinations lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()]atributos = lineas[0][lineas[0].index("(") + 1:-1].split(",")     # R(A,B,C,D)dependencias = []for l in lineas[1:]:    izq, der = l.split("->")    dependencias.append(([a.strip() for a in izq.split(",")], [a.strip() for a in der.split(",")])) def ordenar(conjunto):    return [a for a in atributos if a in conjunto] def cierre(x):    # TODO: atributos que se deducen a partir de x con las dependencias    return set(x) # TODO: claves candidatas, atributos primos, análisis de cada dependencia y forma normal

Ejemplo: Matrícula

Entrada

R(alumno,modulo,nota,nombre,profesor,despacho)
alumno,modulo -> nota
alumno -> nombre
modulo -> profesor
profesor -> despacho

Salida esperada

Claves candidatas: {alumno,modulo}
Atributos primos: alumno, modulo
alumno,modulo -> nota: correcta (alumno,modulo es superclave)
alumno -> nombre: incumple 2FN (dependencia parcial de la clave {alumno,modulo})
modulo -> profesor: incumple 2FN (dependencia parcial de la clave {alumno,modulo})
profesor -> despacho: incumple 3FN (dependencia transitiva)
Forma normal: 1FN

Ejemplo: Transitiva

Entrada

R(dni,nombre,cp,ciudad)
dni -> nombre,cp
cp -> ciudad

Salida esperada

Claves candidatas: {dni}
Atributos primos: dni
dni -> nombre,cp: correcta (dni es superclave)
cp -> ciudad: incumple 3FN (dependencia transitiva)
Forma normal: 2FN

Ejemplo: Casi FNBC

Entrada

R(A,B,C)
A,B -> C
C -> B

Salida esperada

Claves candidatas: {A,B}, {A,C}
Atributos primos: A, B, C
A,B -> C: correcta (A,B es superclave)
C -> B: incumple FNBC (C no es superclave)
Forma normal: 3FN

13. ¿Qué columnas pueden ser clave?

Medio · Modelo Relacional · apuntesdam.com/subject/bases-datos/topic/modelo-relacional

Antes de crear una tabla a partir de datos existentes (una hoja de cálculo, un CSV) hay que decidir la clave primaria. La entrada es un CSV sencillo: la cabecera y las filas, con los valores vacíos como nulos. Muestra para cada columna «col: k valores distintos de n» (sin contar nulos), «Columnas con nulos: …» (o «ninguna»; una clave primaria no admite nulos) y «Claves candidatas según los datos: {a}, {b,c}»: las combinaciones de columnas sin nulos, de hasta 3 columnas, cuyos valores no se repiten en ninguna fila y que no contienen otra ya encontrada (mínimas), por tamaño y en el orden de las columnas; si no hay, «ninguna con hasta 3 columnas». Termina con el aviso «Aviso: los datos solo descartan claves; que hoy no haya repetidos no garantiza que no los haya mañana».

Código de partida (python)
import sysfrom itertools import combinations lineas = [l for l in sys.stdin.read().splitlines() if l.strip()]columnas = lineas[0].split(",")filas = [[v if v != "" else None for v in l.split(",")] for l in lineas[1:]] # TODO: valores distintos por columna, columnas con nulos y combinaciones únicas mínimas (hasta 3 columnas)

Ejemplo: Alumnos

Entrada

dni,nombre,email,telefono,ciudad,curso
12345678Z,Ana García,ana@correo.es,600111222,Logroño,1DAM
87654321X,Luis Pérez,luis@correo.es,,Haro,1DAM
11111111H,Ana García,ana.g@correo.es,600333444,Logroño,2DAM
22222222J,Marta Gil,marta@correo.es,600555666,Arnedo,1DAM
33333333P,Luis Pérez,lperez@correo.es,600777888,Logroño,2DAM

Salida esperada

dni: 5 valores distintos de 5
nombre: 3 valores distintos de 5
email: 5 valores distintos de 5
telefono: 4 valores distintos de 5
ciudad: 3 valores distintos de 5
curso: 2 valores distintos de 5
Columnas con nulos: telefono
Claves candidatas según los datos: {dni}, {email}, {nombre,curso}
Aviso: los datos solo descartan claves; que hoy no haya repetidos no garantiza que no los haya mañana

14. Integridad referencial al borrar e insertar

Difícil · Modelo Relacional · apuntesdam.com/subject/bases-datos/topic/modelo-relacional

Una clave ajena impide que queden filas «huérfanas», y su regla ON DELETE decide qué pasa al borrar la fila referenciada: RESTRICT (se impide el borrado), CASCADE (se borran también las filas que la referencian) o SET NULL (su clave ajena pasa a NULL). La entrada define tablas («tabla nombre id col…»; la primera columna es siempre id), claves ajenas («fk tabla.columna -> otra.id ON DELETE REGLA»), filas («fila tabla valores…», NULL para nulo) y órdenes. «borrar tabla id» funciona como una transacción: o se hace con todos sus efectos o no se hace nada; muestra «borrar t id → borradas: t#id, hija#id, …» (en el orden en que se borran, primero la fila pedida y después, recursivamente, sus dependientes) y, si hay, «; a NULL: tabla#id.columna, …», o «borrar t id → ERROR: no se puede borrar t id: lo usan n filas de hija» (o «no existe t id»). «insertar tabla valores…» comprueba que el id no exista («ERROR: ya existe t id») y que cada clave ajena no nula apunte a una fila existente («ERROR: no existe otra v»); si no, «correcto». «mostrar» lista cada tabla «nombre: v1 v2 | v1 v2» o «(vacía)».

Código de partida (python)
import sys tablas = {}            # nombre → (columnas, {id: fila como dict})claves_ajenas = []     # (tabla, columna, tabla_ref, regla)ordenes = []for linea in sys.stdin.read().splitlines():    p = linea.split()    if not p:        continue    if p[0] == "tabla":        tablas[p[1]] = (p[2:], {})    elif p[0] == "fk":                       # fk pedido.cliente_id -> cliente.id ON DELETE CASCADE        t, c = p[1].split(".")        claves_ajenas.append((t, c, p[3].split(".")[0], " ".join(p[6:])))    elif p[0] == "fila":        cols, filas = tablas[p[1]]        fila = dict(zip(cols, [None if v == "NULL" else v for v in p[2:]]))        filas[fila["id"]] = fila    else:        ordenes.append(p) # TODO: procesar borrar, insertar y mostrar respetando las claves ajenas

Ejemplo: Tienda

Entrada

tabla cliente id nombre
tabla pedido id cliente_id fecha
tabla linea id pedido_id producto
tabla factura id pedido_id importe
fk pedido.cliente_id -> cliente.id ON DELETE CASCADE
fk linea.pedido_id -> pedido.id ON DELETE CASCADE
fk factura.pedido_id -> pedido.id ON DELETE RESTRICT
fila cliente 1 Ana
fila cliente 2 Luis
fila cliente 3 Eva
fila pedido 10 1 2025-09-01
fila pedido 11 2 2025-09-03
fila pedido 12 2 2025-09-05
fila linea 100 10 Teclado
fila linea 101 11 Ratón
fila linea 102 12 Monitor
fila linea 103 12 Cable
fila factura 500 10 45.90
borrar cliente 1
borrar cliente 2
insertar pedido 13 9 2025-10-01
insertar pedido 13 3 2025-10-01
insertar pedido 13 3 2025-10-02
borrar linea 999
mostrar

Salida esperada

borrar cliente 1 → ERROR: no se puede borrar pedido 10: lo usan 1 fila de factura
borrar cliente 2 → borradas: cliente#2, pedido#11, linea#101, pedido#12, linea#102, linea#103
insertar pedido 13 → ERROR: no existe cliente 9
insertar pedido 13 → correcto
insertar pedido 13 → ERROR: ya existe pedido 13
borrar linea 999 → ERROR: no existe linea 999
cliente: 1 Ana | 3 Eva
pedido: 10 1 2025-09-01 | 13 3 2025-10-01
linea: 100 10 Teclado
factura: 500 10 45.90

15. Un intérprete de álgebra relacional

Muy difícil · Modelo Relacional · apuntesdam.com/subject/bases-datos/topic/modelo-relacional

El álgebra relacional es la base teórica de SQL: cada consulta es una composición de operaciones sobre relaciones que devuelven relaciones. Programa un intérprete. La entrada define tablas («tabla nombre c1,c2,…», sus filas con valores separados por comas y «fin») y después órdenes «R = operación …» y «mostrar R». Operaciones: «seleccion T col=valor» (también <>, > y <; compara como número si los dos lo son); «proyeccion T c1,c2» (solo esas columnas y sin tuplas repetidas, conservando la primera aparición); «join T1 T2 a=b» (combina cada tupla de T1 con las de T2 que cumplan T1.a = T2.b; las columnas de T2 que ya existen en T1 se llaman T2.col); «union», «diferencia» e «interseccion» de dos relaciones con las mismas columnas en el mismo orden (si no, «esquemas incompatibles (…) y (…)»), sin repetidos; y «renombrar T viejo=nuevo». Si falta una relación o una columna, muestra «ERROR en R: no existe X» o «ERROR en R: no existe la columna c en T» y R no se crea («mostrar» de algo inexistente: «ERROR: no existe X»). «mostrar R» imprime «R(c1, c2)», cada tupla como « v1 | v2» y « n tuplas» («1 tupla»).

Código de partida (python)
import sys relaciones = {}        # nombre → (columnas, lista de tuplas)lineas = sys.stdin.read().splitlines()i = 0while i < len(lineas):    l = lineas[i].strip()    if l.startswith("tabla "):                 # tabla nombre col1,col2 … filas … fin        nombre, cols = l.split()[1], l.split()[2].split(",")        filas = []        i += 1        while lineas[i].strip() != "fin":            filas.append(tuple(lineas[i].strip().split(",")))            i += 1        relaciones[nombre] = (cols, filas)    elif l:        pass  # TODO: asignaciones «R = operación …» y «mostrar R»    i += 1

Ejemplo: Alumnos y matrículas

Entrada

tabla alumno id,nombre,curso
1,Ana,1DAM
2,Luis,2DAM
3,Eva,1DAM
4,Iker,1DAW
fin
tabla matricula alumno,modulo,nota
1,BD,7
1,PROG,4
2,BD,9
3,PROG,6
3,BD,5
fin
R1 = seleccion alumno curso=1DAM
R2 = join R1 matricula id=alumno
R3 = proyeccion R2 nombre,modulo
mostrar R3
R4 = seleccion R2 nota<5
R5 = proyeccion R4 nombre
mostrar R5
R6 = proyeccion alumno id
R7 = proyeccion matricula alumno
R8 = diferencia R6 R7
R9 = renombrar R7 alumno=id
R10 = diferencia R6 R9
mostrar R10
R11 = proyeccion R2 modulo
mostrar R11
R12 = seleccion alumno edad>18
R13 = union alumno matricula
mostrar R13

Salida esperada

R3(nombre, modulo)
  Ana | BD
  Ana | PROG
  Eva | PROG
  Eva | BD
  4 tuplas
R5(nombre)
  Ana
  1 tupla
ERROR en R8: esquemas incompatibles (id) y (alumno)
R10(id)
  4
  1 tupla
R11(modulo)
  BD
  PROG
  2 tuplas
ERROR en R12: no existe la columna edad en alumno
ERROR en R13: esquemas incompatibles (id,nombre,curso) y (alumno,modulo,nota)
ERROR: no existe R13

Sistemas Informáticos

16. ¿Por qué mi disco de 1 TB tiene 931 GB?

Medio · Componentes Físicos · apuntesdam.com/subject/sistemas/topic/componentes-fisicos

Los fabricantes miden en unidades decimales (1 GB = 10⁹ bytes) y muchos sistemas operativos en binarias (1 GiB = 2³⁰ bytes, aunque Windows lo muestre como «GB»). Cada línea de la entrada es una cantidad y una unidad (B, KB, MB, GB, TB, KiB, MiB, GiB o TiB). Muestra «cantidad unidad = N bytes = X GB = Y GiB», con los bytes como número entero redondeado y X e Y con 2 decimales. Si la unidad no existe, «línea: unidad desconocida».

Código de partida (python)
import sys DECIMAL = {"B": 1, "KB": 10**3, "MB": 10**6, "GB": 10**9, "TB": 10**12}BINARIO = {"KiB": 2**10, "MiB": 2**20, "GiB": 2**30, "TiB": 2**40} # TODO: convierte cada tamaño a bytes, a GB y a GiB

Ejemplo: Discos y memoria

Entrada

1 TB
512 GB
16 GiB
4.7 GB

Salida esperada

1 TB = 1000000000000 bytes = 1000.00 GB = 931.32 GiB
512 GB = 512000000000 bytes = 512.00 GB = 476.84 GiB
16 GiB = 17179869184 bytes = 17.18 GB = 16.00 GiB
4.7 GB = 4700000000 bytes = 4.70 GB = 4.38 GiB

Ejemplo: Unidades pequeñas

Entrada

1536 KiB
700 MB
1 B

Salida esperada

1536 KiB = 1572864 bytes = 0.00 GB = 0.00 GiB
700 MB = 700000000 bytes = 0.70 GB = 0.65 GiB
1 B = 1 bytes = 0.00 GB = 0.00 GiB

17. Dimensionar la fuente de alimentación

Difícil · Componentes Físicos · apuntesdam.com/subject/sistemas/topic/componentes-fisicos

Las primeras líneas son los componentes con su consumo máximo, «nombre vatios» (el nombre puede tener espacios). Después vienen cuatro líneas de configuración «clave=valor»: certificacion (bronze, gold o platinum, con eficiencias 0.85, 0.90 y 0.92), uso (porcentaje medio de carga respecto al máximo), horas (de uso al día) y precio (€ por kWh). Muestra: «Consumo de los componentes: N W»; «Con un margen del 30 %: N W» (sin decimales); «Fuente recomendada: N W» (la primera del catálogo que llegue a ese valor, o «ninguna del catálogo es suficiente»); «Consumo real de la pared: X W» (consumo × uso / 100 / eficiencia, 1 decimal); y «Coste anual: X € (Y kWh)» con 365 días al año (2 y 1 decimales).

Código de partida (python)
import sys CATALOGO = [450, 550, 650, 750, 850, 1000, 1200]EFICIENCIA = {"bronze": 0.85, "gold": 0.90, "platinum": 0.92} lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO: calcula el consumo, elige la fuente y estima el coste anual

Ejemplo: Equipo de desarrollo

Entrada

Procesador Ryzen 7 105
Gráfica RTX 4060 115
Placa base 50
RAM 2 módulos 10
SSD NVMe 8
Ventiladores 12
certificacion=gold
uso=40
horas=8
precio=0.18

Salida esperada

Consumo de los componentes: 300 W
Con un margen del 30 %: 390 W
Fuente recomendada: 450 W
Consumo real de la pared: 133.3 W
Coste anual: 70.08 € (389.3 kWh)

Ejemplo: Estación de trabajo

Entrada

CPU 253
GPU RTX 4090 450
Resto 120
certificacion=platinum
uso=70
horas=10
precio=0.2

Salida esperada

Consumo de los componentes: 823 W
Con un margen del 30 %: 1070 W
Fuente recomendada: 1200 W
Consumo real de la pared: 626.2 W
Coste anual: 457.12 € (2285.6 kWh)

18. Ancho de banda y cuellos de botella

Muy difícil · Componentes Físicos · apuntesdam.com/subject/sistemas/topic/componentes-fisicos

Cada línea describe un componente. «ram DDR5-6000 2» es memoria del tipo indicado (el número son millones de transferencias por segundo, MT/s) en 2 canales: cada canal transfiere 8 bytes, así que el ancho de banda en GB/s es MT/s × 8 × canales / 1000. «ssd 7000 pcie4 x4» es un SSD que puede leer a 7000 MB/s conectado a una ranura PCIe de 4.ª generación con 4 líneas; cada línea transporta 0.985, 1.969 o 3.938 GB/s en las generaciones 3, 4 y 5. Para la memoria muestra «Memoria DDR5-6000 en 2 canales: 96.0 GB/s» («canal» en singular si es 1). Para el SSD, «SSD de V MB/s en PCIe G.0 xN (E GB/s): sin cuello de botella» o, si el enlace es más lento que el SSD, «…: limitado a E MB/s (D MB/s desaprovechados)», con el enlace en MB/s sin decimales (× 1000) y en GB/s con 1 decimal.

Código de partida (python)
import sys PCIE_GBS_POR_LINEA = {"3": 0.985, "4": 1.969, "5": 3.938} # TODO: calcula el ancho de banda y detecta cuellos de botella

Ejemplo: Equipo actual

Entrada

ram DDR5-6000 2
ssd 7000 pcie4 x4
ssd 3500 pcie3 x4

Salida esperada

Memoria DDR5-6000 en 2 canales: 96.0 GB/s
SSD de 7000 MB/s en PCIe 4.0 x4 (7.9 GB/s): sin cuello de botella
SSD de 3500 MB/s en PCIe 3.0 x4 (3.9 GB/s): sin cuello de botella

Ejemplo: SSD en ranura lenta

Entrada

ssd 7400 pcie3 x4
ssd 12000 pcie5 x4
ram DDR4-3200 1

Salida esperada

SSD de 7400 MB/s en PCIe 3.0 x4 (3.9 GB/s): limitado a 3940 MB/s (3460 MB/s desaprovechados)
SSD de 12000 MB/s en PCIe 5.0 x4 (15.8 GB/s): sin cuello de botella
Memoria DDR4-3200 en 1 canal: 25.6 GB/s

19. Planificador FCFS y SJF

Medio · Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/sistemas-operativos

La primera línea es el algoritmo: FCFS (el primero que llega, se ejecuta primero) o SJF (de los que ya han llegado, el de ráfaga más corta; a igualdad, el que llegó antes y después por nombre). Ambos son no expropiativos: un proceso se ejecuta hasta terminar. Cada línea siguiente es un proceso «nombre llegada ráfaga». Muestra el diagrama de Gantt en una línea, con los tramos «nombre inicio-fin» separados por « | » (si la CPU queda libre esperando a que llegue alguien, un tramo «ocioso inicio-fin»); después, para cada proceso en el orden de la entrada, «nombre: espera E, retorno R» (espera = inicio − llegada; retorno = fin − llegada); y por último la espera media y el retorno medio con 2 decimales.

Código de partida (python)
import sys lineas = [l.split() for l in sys.stdin.read().splitlines() if l.strip()]algoritmo = lineas[0][0]procesos = [(nombre, int(llegada), int(rafaga)) for nombre, llegada, rafaga in lineas[1:]] # TODO: planifica con FCFS o SJF y muestra el diagrama y los tiempos medios

Ejemplo: FCFS del ejemplo

Entrada

FCFS
P1 0 5
P2 1 3
P3 2 1
P4 3 2

Salida esperada

P1 0-5 | P2 5-8 | P3 8-9 | P4 9-11
P1: espera 0, retorno 5
P2: espera 4, retorno 7
P3: espera 6, retorno 7
P4: espera 6, retorno 8
Espera media: 4.00
Retorno medio: 6.75

Ejemplo: SJF del ejemplo

Entrada

SJF
P1 0 5
P2 1 3
P3 2 1
P4 3 2

Salida esperada

P1 0-5 | P3 5-6 | P4 6-8 | P2 8-11
P1: espera 0, retorno 5
P2: espera 7, retorno 10
P3: espera 3, retorno 4
P4: espera 3, retorno 5
Espera media: 3.25
Retorno medio: 6.00

Ejemplo: CPU ociosa

Entrada

FCFS
A 2 3
B 10 2
C 11 1

Salida esperada

ocioso 0-2 | A 2-5 | ocioso 5-10 | B 10-12 | C 12-13
A: espera 0, retorno 3
B: espera 0, retorno 2
C: espera 1, retorno 2
Espera media: 0.33
Retorno medio: 2.33

20. Round Robin con quantum

Difícil · Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/sistemas-operativos

La primera línea es «quantum Q»; cada línea siguiente, un proceso «nombre llegada ráfaga». Simula Round Robin: el proceso al frente de la cola se ejecuta como mucho Q unidades; si no termina, vuelve al final de la cola. Los procesos que llegan durante ese tramo (o justo cuando termina) entran en la cola antes que el proceso expulsado. Si la cola está vacía, la CPU queda ociosa hasta la siguiente llegada. Muestra el diagrama de Gantt («nombre inicio-fin» separados por « | », con tramos «ocioso inicio-fin» si hace falta), para cada proceso en el orden de la entrada «nombre: espera E, retorno R» (retorno = fin − llegada; espera = retorno − ráfaga), la espera y el retorno medios con 2 decimales y «Cambios de contexto: N» (veces que en el diagrama un tramo es de un proceso distinto al anterior).

Código de partida (python)
import sysfrom collections import deque lineas = [l.split() for l in sys.stdin.read().splitlines() if l.strip()]quantum = int(lineas[0][1])procesos = [(nombre, int(llegada), int(rafaga)) for nombre, llegada, rafaga in lineas[1:]] # TODO: simula Round Robin y muestra el diagrama y los tiempos

Ejemplo: Ejemplo con quantum 2

Entrada

quantum 2
P1 0 5
P2 1 3
P3 2 1
P4 3 2

Salida esperada

P1 0-2 | P2 2-4 | P3 4-5 | P1 5-7 | P4 7-9 | P2 9-10 | P1 10-11
P1: espera 6, retorno 11
P2: espera 6, retorno 9
P3: espera 2, retorno 3
P4: espera 4, retorno 6
Espera media: 4.50
Retorno medio: 7.25
Cambios de contexto: 6

Ejemplo: Quantum grande

Entrada

quantum 10
P1 0 5
P2 1 3
P3 2 1
P4 3 2

Salida esperada

P1 0-5 | P2 5-8 | P3 8-9 | P4 9-11
P1: espera 0, retorno 5
P2: espera 4, retorno 7
P3: espera 6, retorno 7
P4: espera 6, retorno 8
Espera media: 4.00
Retorno medio: 6.75
Cambios de contexto: 3

Ejemplo: CPU ociosa

Entrada

quantum 3
A 0 4
B 10 5
C 12 2

Salida esperada

A 0-3 | A 3-4 | ocioso 4-10 | B 10-13 | C 13-15 | B 15-17
A: espera 0, retorno 4
B: espera 2, retorno 7
C: espera 1, retorno 3
Espera media: 1.00
Retorno medio: 4.67
Cambios de contexto: 4

21. Reemplazo de páginas: FIFO, LRU y óptimo

Muy difícil · Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/sistemas-operativos

Cuando la memoria está llena y se necesita una página que no está cargada (fallo de página), el sistema operativo elige una víctima. La primera línea es «marcos N» (páginas que caben en memoria, inicialmente vacía) y la segunda, la cadena de referencias separadas por espacios. Cuenta los fallos con tres algoritmos: FIFO (sale la que lleva más tiempo cargada), LRU (sale la que hace más tiempo que no se usa) y óptimo (sale la que tardará más en volver a usarse; si varias no vuelven a usarse, la que lleva más tiempo cargada). Muestra «FIFO: N fallos (P %)», «LRU: …» y «Óptimo: …», con el porcentaje sobre el total de referencias con 1 decimal. Prueba la cadena 1 2 3 4 1 2 5 1 2 3 4 5 con 3 y con 4 marcos: verás la anomalía de Bélády.

Código de partida (python)
import sys lineas = [l.split() for l in sys.stdin.read().splitlines() if l.strip()]marcos = int(lineas[0][1])referencias = [int(x) for x in lineas[1]] # TODO: cuenta los fallos de página con FIFO, LRU y el algoritmo óptimo

Ejemplo: Cadena clásica con 3 marcos

Entrada

marcos 3
7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1

Salida esperada

FIFO: 15 fallos (75.0 %)
LRU: 12 fallos (60.0 %)
Óptimo: 9 fallos (45.0 %)

Ejemplo: Anomalía de Bélády con 3

Entrada

marcos 3
1 2 3 4 1 2 5 1 2 3 4 5

Salida esperada

FIFO: 9 fallos (75.0 %)
LRU: 10 fallos (83.3 %)
Óptimo: 7 fallos (58.3 %)

Ejemplo: Anomalía de Bélády con 4

Entrada

marcos 4
1 2 3 4 1 2 5 1 2 3 4 5

Salida esperada

FIFO: 10 fallos (83.3 %)
LRU: 8 fallos (66.7 %)
Óptimo: 6 fallos (50.0 %)

22. Un plan de particiones alineado

Medio · Instalación de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/instalacion-sistemas-operativos

La primera línea es «disco N GiB» (N entero). Cada línea siguiente es una partición «nombre tamaño», con el tamaño en MiB («512MiB»), en GiB («50GiB», puede tener decimales) o «resto» (todo lo que quede). En un disco GPT, el primer MiB queda reservado (tabla y alineación) y también el último (copia de la tabla). Coloca las particiones una detrás de otra empezando en el MiB 1 y muestra «nombre: inicio A MiB, fin B MiB, tamaño T GiB» (T con 2 decimales). Si una partición no cabe, muestra «ERROR: no cabe nombre (faltan N MiB)» y para. Si todas caben, termina con «Sin asignar: X GiB».

Código de partida (python)
import sys lineas = [l.split() for l in sys.stdin.read().splitlines() if l.strip()]disco_mib = int(lineas[0][1].replace("GiB", "")) * 1024 # TODO: coloca las particiones alineadas a 1 MiB y muestra dónde empieza y acaba cada una

Ejemplo: Ubuntu en 500 GiB

Entrada

disco 500GiB
efi 1GiB
raiz 50GiB
swap 8GiB
home resto

Salida esperada

efi: inicio 1 MiB, fin 1025 MiB, tamaño 1.00 GiB
raiz: inicio 1025 MiB, fin 52225 MiB, tamaño 50.00 GiB
swap: inicio 52225 MiB, fin 60417 MiB, tamaño 8.00 GiB
home: inicio 60417 MiB, fin 511999 MiB, tamaño 441.00 GiB
Sin asignar: 0.00 GiB

Ejemplo: Windows con hueco libre

Entrada

disco 256GiB
esp 260MiB
msr 16MiB
windows 180GiB
recuperacion 1GiB

Salida esperada

esp: inicio 1 MiB, fin 261 MiB, tamaño 0.25 GiB
msr: inicio 261 MiB, fin 277 MiB, tamaño 0.02 GiB
windows: inicio 277 MiB, fin 184597 MiB, tamaño 180.00 GiB
recuperacion: inicio 184597 MiB, fin 185621 MiB, tamaño 1.00 GiB
Sin asignar: 74.73 GiB

23. ¿Arrancará este esquema de particiones?

Difícil · Instalación de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/instalacion-sistemas-operativos

Las tres primeras líneas son «firmware uefi|bios», «tabla gpt|mbr» y «disco N» (GiB). Cada línea siguiente es una partición «nombre clase sistema tamaño punto» (clase: primaria, extendida o logica; sistema: fat32, ntfs, ext4, swap…; tamaño en GiB; punto de montaje o «-»). Comprueba estas reglas en este orden y muestra un mensaje por cada una que falle, o «Esquema válido» si ninguna falla: (1) «UEFI necesita una tabla GPT (o activar el modo CSM)»; con MBR: (2) «MBR solo puede usar 2048 GiB de los N GiB del disco» si el disco es mayor, (3) «MBR admite como máximo 4 particiones primarias o extendidas», (4) «Solo puede haber una partición extendida», (5) «Las particiones lógicas necesitan una partición extendida»; con GPT: (6) «GPT no tiene particiones extendidas ni lógicas»; (7) con UEFI, «Falta la partición EFI (FAT32 montada en /boot/efi)»; (8) «Falta la partición raíz (/)»; (9) «Las particiones ocupan T GiB y el disco tiene N GiB» (la extendida no cuenta, porque contiene a las lógicas). Los números se muestran sin decimales innecesarios.

Código de partida (python)
import sys lineas = [l.split() for l in sys.stdin.read().splitlines() if l.strip()]config = {l[0]: l[1] for l in lineas[:3]}          # firmware, tabla, discoparticiones = lineas[3:]                            # nombre clase sistema tamaño_GiB punto_de_montaje # TODO: comprueba las reglas en orden y muestra los problemas o «Esquema válido»

Ejemplo: Esquema correcto

Entrada

firmware uefi
tabla gpt
disco 500
efi primaria fat32 1 /boot/efi
raiz primaria ext4 50 /
home primaria ext4 400 /home

Salida esperada

Esquema válido

Ejemplo: UEFI con MBR y sin EFI

Entrada

firmware uefi
tabla mbr
disco 3000
sistema primaria ntfs 200 C:
datos primaria ntfs 2500 D:

Salida esperada

UEFI necesita una tabla GPT (o activar el modo CSM)
MBR solo puede usar 2048 GiB de los 3000 GiB del disco
Falta la partición EFI (FAT32 montada en /boot/efi)
Falta la partición raíz (/)

Ejemplo: MBR clásico con lógicas

Entrada

firmware bios
tabla mbr
disco 1000
win primaria ntfs 300 C:
recup primaria ntfs 1 -
ext extendida - 699 -
raiz logica ext4 100 /
swap logica swap 8 -
home logica ext4 591 /home

Salida esperada

Esquema válido

24. Leer la tabla de particiones de un MBR

Muy difícil · Instalación de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/instalacion-sistemas-operativos

El sector 0 de un disco MBR termina con la tabla de particiones: 4 entradas de 16 bytes (desde el byte 446) y la firma 55 AA. La entrada recibe esos 66 bytes en hexadecimal. En cada entrada: el byte 0 es 0x80 si la partición es la activa (arrancable); el byte 4, el tipo; los bytes 8-11, el sector inicial (LBA); y los bytes 12-15, el número de sectores, ambos enteros de 4 bytes en little endian (el byte menos significativo primero). Las entradas con tipo 00 están vacías. Para cada partición muestra «Partición N: [activa, ]tipo XX (nombre), sector inicial S, T sectores (G GiB)» con sectores de 512 bytes y G con 2 decimales (tipo en hexadecimal con 2 cifras en mayúsculas; nombre «desconocido» si no está en la tabla). Si el tipo es EE, añade la línea «El disco usa GPT: esta entrada solo protege el disco de herramientas antiguas». Al final, «Firma: correcta (55 AA)» o «Firma: incorrecta (XX XX)».

Código de partida (python)
import sys datos = bytes.fromhex("".join(sys.stdin.read().split()))   # las 4 entradas (64 bytes) y la firma (2 bytes)TIPOS = {0x07: "NTFS/exFAT", 0x0B: "FAT32", 0x0C: "FAT32 LBA", 0x83: "Linux", 0x82: "Linux swap",         0x05: "Extendida", 0x0F: "Extendida LBA", 0xEE: "GPT de protección", 0xEF: "Sistema EFI"} # TODO: interpreta cada entrada de 16 bytes y la firma del sector

Ejemplo: Windows y Linux en arranque dual

Entrada

80 00 00 00 07 00 00 00 00 08 00 00 00 00 40 06
00 00 00 00 83 00 00 00 00 08 40 06 00 00 80 0C
00 00 00 00 82 00 00 00 00 08 C0 12 00 00 00 01
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
55 AA

Salida esperada

Partición 1: activa, tipo 07 (NTFS/exFAT), sector inicial 2048, 104857600 sectores (50.00 GiB)
Partición 2: tipo 83 (Linux), sector inicial 104859648, 209715200 sectores (100.00 GiB)
Partición 3: tipo 82 (Linux swap), sector inicial 314574848, 16777216 sectores (8.00 GiB)
Firma: correcta (55 AA)

Ejemplo: Disco GPT

Entrada

00 00 00 00 EE 00 00 00 01 00 00 00 FF FF FF FF
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
55 AA

Salida esperada

Partición 1: tipo EE (GPT de protección), sector inicial 1, 4294967295 sectores (2048.00 GiB)
El disco usa GPT: esta entrada solo protege el disco de herramientas antiguas
Firma: correcta (55 AA)

25. Capacidad y tolerancia de un RAID

Medio · Sistemas de Ficheros y Copias de Seguridad · apuntesdam.com/subject/sistemas/topic/sistemas-ficheros-almacenamiento

Cada línea es «nivel discos tamaño_GB», con el nivel RAID0, RAID1, RAID5, RAID6 o RAID10. Calcula la capacidad útil (RAID0: suma de todos; RAID1: un disco, porque todos son copias; RAID5: n − 1 discos; RAID6: n − 2; RAID10: la mitad), la eficiencia (útil / total, en % sin decimales) y los fallos de disco que soporta sin perder datos (0, n − 1, 1, 2 y en RAID10 «1 fallo por pareja (hasta n/2)»). Muestra «RAID5 con 4 × 2000 GB: 6000 GB útiles (75 %), tolera 1 fallo» (y «fallos» en plural salvo cuando es exactamente 1). Si faltan discos (mínimos: 2, 2, 3, 4 y 4; RAID10 además un número par), muestra «RAID10 con 5 discos: no válido (necesita al menos 4 discos y un número par)», y para un nivel que no existe, «nivel: nivel desconocido».

Código de partida (python)
import sys MINIMO = {"RAID0": 2, "RAID1": 2, "RAID5": 3, "RAID6": 4, "RAID10": 4} # TODO: calcula la capacidad útil, la eficiencia y los fallos que soporta cada configuración

Ejemplo: Niveles habituales

Entrada

RAID0 2 1000
RAID1 2 4000
RAID5 4 2000
RAID6 6 8000
RAID10 4 1000

Salida esperada

RAID0 con 2 × 1000 GB: 2000 GB útiles (100 %), tolera 0 fallos
RAID1 con 2 × 4000 GB: 4000 GB útiles (50 %), tolera 1 fallo
RAID5 con 4 × 2000 GB: 6000 GB útiles (75 %), tolera 1 fallo
RAID6 con 6 × 8000 GB: 32000 GB útiles (67 %), tolera 2 fallos
RAID10 con 4 × 1000 GB: 2000 GB útiles (50 %), tolera 1 fallo por pareja (hasta 2)

Ejemplo: Configuraciones no válidas

Entrada

RAID5 2 1000
RAID10 5 2000
RAID7 4 1000

Salida esperada

RAID5 con 2 discos: no válido (necesita al menos 3 discos)
RAID10 con 5 discos: no válido (necesita al menos 4 discos y un número par)
RAID7: nivel desconocido

26. Simulador de estrategias de copia

Difícil · Sistemas de Ficheros y Copias de Seguridad · apuntesdam.com/subject/sistemas/topic/sistemas-ficheros-almacenamiento

La primera línea es «datos N» (GB que se copian), la segunda «cambio P» (porcentaje de los datos que se modifica cada día, siempre ficheros distintos) y la tercera «plan …» con un tipo de copia por día: C (completa: copia todo), I (incremental: lo que ha cambiado desde la copia anterior, P % de los datos) o D (diferencial: todo lo que ha cambiado desde la última completa, P % por cada día transcurrido desde ella, como máximo el 100 %). Muestra «Día N (T): X GB» (1 decimal) por cada día, «Total ocupado: X GB» y «Para restaurar el último día: día A, día B…» con las copias necesarias: la última completa, la última diferencial posterior (si la hay) y las incrementales posteriores a esa diferencial (o a la completa si no hay diferencial). Una I o una D antes de cualquier completa muestran «Día N: ERROR, una copia T necesita una completa anterior» y no ocupan nada.

Código de partida (python)
import sys lineas = [l.split() for l in sys.stdin.read().splitlines() if l.strip()]datos = float(lineas[0][1])cambio = float(lineas[1][1])plan = lineas[2][1:]          # C = completa, I = incremental, D = diferencial, un día cada una # TODO: tamaño de cada copia, total ocupado y copias necesarias para restaurar el último día

Ejemplo: Semana con incrementales

Entrada

datos 200
cambio 5
plan C I I I I I I

Salida esperada

Día 1 (C): 200.0 GB
Día 2 (I): 10.0 GB
Día 3 (I): 10.0 GB
Día 4 (I): 10.0 GB
Día 5 (I): 10.0 GB
Día 6 (I): 10.0 GB
Día 7 (I): 10.0 GB
Total ocupado: 260.0 GB
Para restaurar el último día: día 1, día 2, día 3, día 4, día 5, día 6, día 7

Ejemplo: Semana con diferenciales

Entrada

datos 200
cambio 5
plan C D D D D D D

Salida esperada

Día 1 (C): 200.0 GB
Día 2 (D): 10.0 GB
Día 3 (D): 20.0 GB
Día 4 (D): 30.0 GB
Día 5 (D): 40.0 GB
Día 6 (D): 50.0 GB
Día 7 (D): 60.0 GB
Total ocupado: 410.0 GB
Para restaurar el último día: día 1, día 7

Ejemplo: Mixta con dos completas

Entrada

datos 500
cambio 10
plan C I I D I C I D I

Salida esperada

Día 1 (C): 500.0 GB
Día 2 (I): 50.0 GB
Día 3 (I): 50.0 GB
Día 4 (D): 150.0 GB
Día 5 (I): 50.0 GB
Día 6 (C): 500.0 GB
Día 7 (I): 50.0 GB
Día 8 (D): 100.0 GB
Día 9 (I): 50.0 GB
Total ocupado: 1500.0 GB
Para restaurar el último día: día 6, día 8, día 9

27. ¿Es buena esta copia de seguridad?

Muy difícil · Sistemas de Ficheros y Copias de Seguridad · apuntesdam.com/subject/sistemas/topic/sistemas-ficheros-almacenamiento

Para comprobar una copia se compara un manifiesto del original con otro de la copia: cada línea es «ruta tamaño hash» (como lo que genera sha256sum, abreviado). La entrada tiene los dos manifiestos separados por una línea «---». Informa, en este orden: los ficheros MOVIDOS (faltan en una ruta pero aparecen en otra de la copia con el mismo hash) como «MOVIDO ruta -> nueva_ruta»; los que FALTAN; los DISTINTOS (misma ruta, distinto hash) con «(X bytes en el original, Y en la copia)»; y los que SOBRAN; cada grupo ordenado alfabéticamente. Termina con «Correctos: N de M · Datos del original: X MiB» (2 decimales) y «Copia VÁLIDA» si no falta ni difiere nada (los movidos y sobrantes no la invalidan) o «Copia NO VÁLIDA».

Código de partida (python)
import sys texto = sys.stdin.read().split("---")original = [l.split() for l in texto[0].splitlines() if l.strip()]   # ruta tamaño hashcopia = [l.split() for l in texto[1].splitlines() if l.strip()] # TODO: compara los dos manifiestos e informa de las diferencias

Ejemplo: Copia perfecta

Entrada

docs/contrato.pdf 482133 9f2a
docs/nominas/enero.xlsx 22100 77b1
docs/nominas/febrero.xlsx 22340 01ce
fotos/logo.png 150000 aa10
bd/clientes.sql 5242880 c3d4
---
docs/contrato.pdf 482133 9f2a
docs/nominas/enero.xlsx 22100 77b1
docs/nominas/febrero.xlsx 22340 01ce
fotos/logo.png 150000 aa10
bd/clientes.sql 5242880 c3d4

Salida esperada

Correctos: 5 de 5 · Datos del original: 5.65 MiB
Copia VÁLIDA

Ejemplo: Con errores

Entrada

docs/contrato.pdf 482133 9f2a
docs/nominas/enero.xlsx 22100 77b1
docs/nominas/febrero.xlsx 22340 01ce
fotos/logo.png 150000 aa10
bd/clientes.sql 5242880 c3d4
---
docs/contrato.pdf 482133 9f2a
docs/nominas/enero.xlsx 22100 0000
fotos/logo.png 150000 aa10
bd/clientes.sql 5242880 c3d4
tmp/cache.bin 10 ffff

Salida esperada

FALTA docs/nominas/febrero.xlsx
DISTINTO docs/nominas/enero.xlsx (22100 bytes en el original, 22100 en la copia)
SOBRA tmp/cache.bin
Correctos: 3 de 5 · Datos del original: 5.65 MiB
Copia NO VÁLIDA

28. ¿Llega este cable?

Medio · Redes Informáticas · apuntesdam.com/subject/sistemas/topic/redes-informaticas

Cada línea describe un tramo de cable: «medio metros velocidad», con el medio cat5e, cat6, cat6a, cat8 (par trenzado), om3, om4 (fibra multimodo) u os2 (fibra monomodo) y la velocidad en Gbit/s con una G (1G, 10G…). El diccionario MAXIMO indica la distancia máxima de cada medio a cada velocidad que admite. Muestra «medio, M m a VG: SÍ (máximo X m)» si el tramo es válido; «…: NO (máximo X m; sirve a, b…)» si es demasiado largo, con los medios que sí llegarían en orden alfabético (o «ninguno»); «…: NO (no admite VG; velocidades inferiores válidas: …)» si el medio no admite esa velocidad (o «ninguna»); y «medio: medio desconocido» si no existe.

Código de partida (python)
import sys # Distancia máxima en metros de cada medio para cada velocidad (Gbit/s)MAXIMO = {    "cat5e": {1: 100},    "cat6": {1: 100, 10: 55},    "cat6a": {1: 100, 10: 100},    "cat8": {10: 30, 25: 30, 40: 30},    "om3": {1: 550, 10: 300, 40: 100},    "om4": {1: 550, 10: 400, 40: 150},    "os2": {1: 10000, 10: 10000, 40: 10000},} # TODO: decide si cada tramo de cable es válido

Ejemplo: Oficina

Entrada

cat6 70 1G
cat6 70 10G
cat6a 90 10G
om4 350 10G

Salida esperada

cat6, 70 m a 1G: SÍ (máximo 100 m)
cat6, 70 m a 10G: NO (máximo 55 m; sirve cat6a, om3, om4, os2)
cat6a, 90 m a 10G: SÍ (máximo 100 m)
om4, 350 m a 10G: SÍ (máximo 400 m)

Ejemplo: Velocidades no admitidas

Entrada

cat5e 20 10G
cat8 10 1G
coaxial 50 1G

Salida esperada

cat5e, 20 m a 10G: NO (no admite 10G; velocidades inferiores válidas: 1G)
cat8, 10 m a 1G: NO (no admite 1G; velocidades inferiores válidas: ninguna)
coaxial: medio desconocido

29. La tabla MAC de un switch

Difícil · Redes Informáticas · apuntesdam.com/subject/sistemas/topic/redes-informaticas

Un switch aprende en qué puerto está cada equipo mirando la MAC de origen de las tramas que recibe. La primera línea es «puertos N»; cada línea siguiente, una trama recibida: «puerto MAC_origen MAC_destino». Para cada trama: si la MAC de origen no estaba en la tabla (o estaba en otro puerto, porque el equipo se ha movido), muestra «Aprende MAC en el puerto P». Después, con dos espacios delante: si el destino es FF:FF:FF:FF:FF:FF, «Difusión: inunda por a, b…» (todos los puertos menos el de entrada); si el destino no está en la tabla, «MAC desconocido: inunda por …»; si está en el mismo puerto de entrada, «MAC está en el mismo puerto: descarta la trama»; y si no, «Reenvía a MAC por el puerto P». Al final muestra «Tabla MAC:» y cada entrada « MAC → puerto P» ordenada por MAC.

Código de partida (python)
import sys lineas = [l.split() for l in sys.stdin.read().splitlines() if l.strip()]puertos = int(lineas[0][1])BROADCAST = "FF:FF:FF:FF:FF:FF" # TODO: simula cómo aprende y reenvía las tramas un switch

Ejemplo: Arranque de una red

Entrada

puertos 4
1 AA:AA:AA:00:00:01 FF:FF:FF:FF:FF:FF
2 BB:BB:BB:00:00:02 AA:AA:AA:00:00:01
1 AA:AA:AA:00:00:01 BB:BB:BB:00:00:02
3 CC:CC:CC:00:00:03 DD:DD:DD:00:00:04

Salida esperada

Aprende AA:AA:AA:00:00:01 en el puerto 1
  Difusión: inunda por 2, 3, 4
Aprende BB:BB:BB:00:00:02 en el puerto 2
  Reenvía a AA:AA:AA:00:00:01 por el puerto 1
  Reenvía a BB:BB:BB:00:00:02 por el puerto 2
Aprende CC:CC:CC:00:00:03 en el puerto 3
  DD:DD:DD:00:00:04 desconocido: inunda por 1, 2, 4
Tabla MAC:
  AA:AA:AA:00:00:01 → puerto 1
  BB:BB:BB:00:00:02 → puerto 2
  CC:CC:CC:00:00:03 → puerto 3

30. Puntos únicos de fallo de una red

Muy difícil · Redes Informáticas · apuntesdam.com/subject/sistemas/topic/redes-informaticas

La primera línea es «raiz NOMBRE»: el equipo por el que se sale a Internet (normalmente el router). Cada línea siguiente es un enlace «enlace A B» entre dos equipos (los enlaces funcionan en los dos sentidos). Para cada equipo con dos o más enlaces que no sea la raíz, en orden alfabético, calcula qué equipos dejarían de poder llegar a la raíz si ese equipo fallara: muestra «Si falla X: sin red a, b…» (en orden alfabético) o «Si falla X: nadie pierde la red (hay caminos alternativos)». Al final, «Puntos únicos de fallo: N» con los equipos cuyo fallo deja a alguien sin red.

Código de partida (python)
import sys lineas = [l.split() for l in sys.stdin.read().splitlines() if l.strip()]raiz = lineas[0][1]                       # equipo desde el que se sale a Internetenlaces = [(a, b) for _, a, b in lineas[1:]] # TODO: para cada equipo de interconexión, ¿quién se queda sin red si falla?

Ejemplo: Estrella extendida

Entrada

raiz router
enlace router sw-core
enlace sw-core sw-planta1
enlace sw-core sw-planta2
enlace sw-planta1 pc1
enlace sw-planta1 pc2
enlace sw-planta2 pc3
enlace sw-planta2 impresora

Salida esperada

Si falla sw-core: sin red impresora, pc1, pc2, pc3, sw-planta1, sw-planta2
Si falla sw-planta1: sin red pc1, pc2
Si falla sw-planta2: sin red impresora, pc3
Puntos únicos de fallo: 3

Ejemplo: Con enlace redundante

Entrada

raiz router
enlace router sw-a
enlace router sw-b
enlace sw-a sw-b
enlace sw-a pc1
enlace sw-b pc2
enlace sw-b ap
enlace ap portatil

Salida esperada

Si falla ap: sin red portatil
Si falla sw-a: sin red pc1
Si falla sw-b: sin red ap, pc2, portatil
Puntos únicos de fallo: 3

31. ¿Pueden hablar sin router?

Medio · Configuración de Redes TCP/IP · apuntesdam.com/subject/sistemas/topic/configuracion-redes

Dos equipos solo se comunican directamente si están en la misma red, es decir, si su IP AND la máscara da el mismo resultado. Cada línea tiene «IP/prefijo IP2»: el primer equipo con su prefijo y la IP del segundo (que usa la misma máscara). Muestra «IP y IP2: misma red RED/prefijo, comunicación directa» o «IP y IP2: redes distintas (RED1 y RED2), hace falta un router». Las funciones a_entero y a_texto convierten entre la notación con puntos y un entero de 32 bits.

Código de partida (python)
import sys def a_entero(ip):    a, b, c, d = (int(x) for x in ip.split("."))    return (a << 24) | (b << 16) | (c << 8) | d def a_texto(n):    return ".".join(str((n >> s) & 255) for s in (24, 16, 8, 0)) # TODO: ¿están los dos equipos en la misma red?

Ejemplo: Redes /24 y /26

Entrada

192.168.1.10/24 192.168.1.200
192.168.1.10/26 192.168.1.70
192.168.1.65/26 192.168.1.126

Salida esperada

192.168.1.10 y 192.168.1.200: misma red 192.168.1.0/24, comunicación directa
192.168.1.10 y 192.168.1.70: redes distintas (192.168.1.0 y 192.168.1.64), hace falta un router
192.168.1.65 y 192.168.1.126: misma red 192.168.1.64/26, comunicación directa

Ejemplo: Prefijos que cortan otros octetos

Entrada

172.16.37.10/20 172.16.40.1
172.16.37.10/20 172.16.48.1
10.1.2.3/8 10.200.0.1

Salida esperada

172.16.37.10 y 172.16.40.1: misma red 172.16.32.0/20, comunicación directa
172.16.37.10 y 172.16.48.1: redes distintas (172.16.32.0 y 172.16.48.0), hace falta un router
10.1.2.3 y 10.200.0.1: misma red 10.0.0.0/8, comunicación directa

32. Abreviar y expandir direcciones IPv6

Difícil · Configuración de Redes TCP/IP · apuntesdam.com/subject/sistemas/topic/configuracion-redes

Cada línea es una dirección IPv6. Si está completa (8 grupos sin «::»), abréviala: quita los ceros a la izquierda de cada grupo (un grupo todo a ceros queda como 0) y sustituye por «::» la serie más larga de dos o más grupos seguidos a cero (si hay dos igual de largas, la primera; solo se usa «::» una vez). Si está abreviada, expándela a 8 grupos de 4 cifras. Usa siempre minúsculas. Muestra «entrada → resultado».

Código de partida (python)
import sys # TODO: abrevia las direcciones IPv6 completas y expande las abreviadas

Ejemplo: Abreviar

Entrada

2001:0db8:0000:0000:0000:ff00:0042:8329
fe80:0000:0000:0000:0202:b3ff:fe1e:8329
2001:0DB8:0000:0001:0000:0000:0000:0001

Salida esperada

2001:0db8:0000:0000:0000:ff00:0042:8329 → 2001:db8::ff00:42:8329
fe80:0000:0000:0000:0202:b3ff:fe1e:8329 → fe80::202:b3ff:fe1e:8329
2001:0DB8:0000:0001:0000:0000:0000:0001 → 2001:db8:0:1::1

Ejemplo: Expandir

Entrada

2001:db8::ff00:42:8329
::1
fe80::

Salida esperada

2001:db8::ff00:42:8329 → 2001:0db8:0000:0000:0000:ff00:0042:8329
::1 → 0000:0000:0000:0000:0000:0000:0000:0001
fe80:: → fe80:0000:0000:0000:0000:0000:0000:0000

33. Calculadora VLSM

Muy difícil · Configuración de Redes TCP/IP · apuntesdam.com/subject/sistemas/topic/configuracion-redes

La primera línea es «red A.B.C.D/p», la red que hay que repartir. Cada línea siguiente es «nombre hosts»: un departamento y los equipos que necesita. Con VLSM, asigna las subredes de la que más hosts necesita a la que menos (a igualdad, por nombre), cada una del tamaño mínimo que alcance (2^bits − 2 ≥ hosts, con al menos 2 bits) y una detrás de otra desde el principio de la red. Para cada una muestra «nombre (hosts): RED/prefijo, hosts PRIMERO - ÚLTIMO, difusión DIF, sobran N»; si ya no cabe, «nombre: no hay espacio para N hosts» (y se sigue con las demás). Termina con «Libre: N direcciones desde IP» o «Libre: 0 direcciones».

Código de partida (python)
import sys def a_entero(ip):    a, b, c, d = (int(x) for x in ip.split("."))    return (a << 24) | (b << 16) | (c << 8) | d def a_texto(n):    return ".".join(str((n >> s) & 255) for s in (24, 16, 8, 0)) lineas = [l.split() for l in sys.stdin.read().splitlines() if l.strip()]base_ip, base_prefijo = lineas[0][1].split("/")departamentos = [(nombre, int(hosts)) for nombre, hosts in lineas[1:]] # TODO: reparte la red con VLSM, de la subred que más hosts necesita a la que menos

Ejemplo: Ejemplo del tema

Entrada

red 192.168.50.0/24
Desarrollo 100
Administracion 50
Direccion 10
Enlace 2

Salida esperada

Desarrollo (100): 192.168.50.0/25, hosts 192.168.50.1 - 192.168.50.126, difusión 192.168.50.127, sobran 26
Administracion (50): 192.168.50.128/26, hosts 192.168.50.129 - 192.168.50.190, difusión 192.168.50.191, sobran 12
Direccion (10): 192.168.50.192/28, hosts 192.168.50.193 - 192.168.50.206, difusión 192.168.50.207, sobran 4
Enlace (2): 192.168.50.208/30, hosts 192.168.50.209 - 192.168.50.210, difusión 192.168.50.211, sobran 0
Libre: 44 direcciones desde 192.168.50.212

Ejemplo: Instituto en una /22

Entrada

red 10.20.0.0/22
Aulas 400
Profesorado 120
Wifi 200
Servidores 20
Impresoras 12

Salida esperada

Aulas (400): 10.20.0.0/23, hosts 10.20.0.1 - 10.20.1.254, difusión 10.20.1.255, sobran 110
Wifi (200): 10.20.2.0/24, hosts 10.20.2.1 - 10.20.2.254, difusión 10.20.2.255, sobran 54
Profesorado (120): 10.20.3.0/25, hosts 10.20.3.1 - 10.20.3.126, difusión 10.20.3.127, sobran 6
Servidores (20): 10.20.3.128/27, hosts 10.20.3.129 - 10.20.3.158, difusión 10.20.3.159, sobran 10
Impresoras (12): 10.20.3.160/28, hosts 10.20.3.161 - 10.20.3.174, difusión 10.20.3.175, sobran 2
Libre: 80 direcciones desde 10.20.3.176

34. Permisos efectivos de una carpeta compartida

Medio · Servicios de Red, Recursos Compartidos y Seguridad · apuntesdam.com/subject/sistemas/topic/servicios-red-seguridad

En Windows, a una carpeta compartida por la red se le aplican dos capas de permisos: los del recurso compartido y los NTFS. Dentro de cada capa, un usuario suma los permisos de todos sus grupos (se queda con el mayor), una denegación explícita gana a cualquier permiso, y el permiso efectivo es el más restrictivo de las dos capas. Las líneas pueden ser «compartido GRUPO nivel», «ntfs GRUPO nivel», «denegar GRUPO» (denegación NTFS total) o «usuario NOMBRE grupo1,grupo2…». Los niveles, de menor a mayor, son ninguno, lectura, modificar y control. Para cada usuario, en el orden de la entrada, muestra «nombre: compartido X, NTFS Y → efectivo Z».

Código de partida (python)
import sys NIVELES = ["ninguno", "lectura", "modificar", "control"] lineas = [l.split() for l in sys.stdin.read().splitlines() if l.strip()] # TODO: calcula el permiso efectivo de cada usuario sobre la carpeta compartida

Ejemplo: Departamentos

Entrada

compartido Todos modificar
ntfs Ventas modificar
ntfs Direccion control
ntfs Todos lectura
usuario ana Ventas,Todos
usuario luis Todos
usuario marta Direccion,Todos

Salida esperada

ana: compartido modificar, NTFS modificar → efectivo modificar
luis: compartido modificar, NTFS lectura → efectivo lectura
marta: compartido modificar, NTFS control → efectivo modificar

Ejemplo: Denegación y compartido restrictivo

Entrada

compartido Todos lectura
compartido Admins control
ntfs Admins control
ntfs Todos modificar
denegar Becarios
usuario pedro Todos,Becarios
usuario eva Todos
usuario root Admins

Salida esperada

pedro: compartido lectura, NTFS ninguno → efectivo ninguno
eva: compartido lectura, NTFS modificar → efectivo lectura
root: compartido control, NTFS control → efectivo control

35. Un cortafuegos de primera coincidencia

Difícil · Servicios de Red, Recursos Compartidos y Seguridad · apuntesdam.com/subject/sistemas/topic/servicios-red-seguridad

Un cortafuegos revisa sus reglas en orden y aplica la primera que coincide; si ninguna coincide, aplica la política por defecto. La entrada tiene dos partes separadas por «---». La primera empieza con «politica allow|deny» y sigue con reglas «allow|deny protocolo puerto from red» (protocolo tcp, udp o any; puerto un número o any; red en CIDR, una IP sola o any). La segunda son paquetes entrantes «protocolo ip_origen puerto_destino». Para cada paquete muestra «proto ip → puerto: ALLOW (regla N)», «…: DENY (regla N)» o «…: ACCIÓN (política por defecto)».

Código de partida (python)
import sys def a_entero(ip):    a, b, c, d = (int(x) for x in ip.split("."))    return (a << 24) | (b << 16) | (c << 8) | d texto = sys.stdin.read().split("---")reglas = [l.split() for l in texto[0].splitlines() if l.strip()]      # la primera línea es «politica allow|deny»paquetes = [l.split() for l in texto[1].splitlines() if l.strip()] # TODO: aplica las reglas en orden a cada paquete (gana la primera que coincide)

Ejemplo: Servidor web

Entrada

politica deny
allow tcp 22 from 192.168.1.0/24
allow tcp 443 from any
allow tcp 80 from any
deny tcp 3306 from any
allow udp 53 from 10.0.0.0/8
---
tcp 192.168.1.50 22
tcp 203.0.113.7 22
tcp 203.0.113.7 443
tcp 192.168.1.50 3306
udp 10.20.30.40 53
udp 8.8.8.8 53

Salida esperada

tcp 192.168.1.50 → 22: ALLOW (regla 1)
tcp 203.0.113.7 → 22: DENY (política por defecto)
tcp 203.0.113.7 → 443: ALLOW (regla 2)
tcp 192.168.1.50 → 3306: DENY (regla 4)
udp 10.20.30.40 → 53: ALLOW (regla 5)
udp 8.8.8.8 → 53: DENY (política por defecto)

36. ¿Cuánto aguantaría esta contraseña?

Muy difícil · Servicios de Red, Recursos Compartidos y Seguridad · apuntesdam.com/subject/sistemas/topic/servicios-red-seguridad

Cada línea es una contraseña. Si en minúsculas está en el DICCIONARIO, o lo está tras quitar los dígitos, puntos y signos de exclamación del final (Barcelona2024!), muestra «clave: muy débil (está en los diccionarios de contraseñas)». Si no, calcula su entropía: longitud × log2(juego), donde el juego suma 26 si hay minúsculas, 26 si hay mayúsculas, 10 si hay dígitos y 33 si hay otros símbolos; si tiene muy pocos caracteres distintos (no más de un tercio de su longitud), divide la entropía entre 2. Clasifícala como débil (< 40 bits), media (< 60), fuerte (< 80) o muy fuerte, y estima el tiempo medio de un ataque de fuerza bruta a 10¹⁰ intentos por segundo: 2^entropía / 2 / 10¹⁰ segundos, expresado en años, días, horas o minutos (la mayor unidad con valor ≥ 1; con 1 decimal si es menor que 100, y si no, redondeado y con punto de miles, como «857.012 años»), «más de un millón de …» si llega al millón, «menos de 1 minuto» o «instantáneo» (< 1 s). Muestra «clave: nivel (X bits, TIEMPO por fuerza bruta)», con los bits con 1 decimal.

Código de partida (python)
import sysimport math DICCIONARIO = {"password", "123456", "qwerty", "admin", "iloveyou", "dragon", "monkey", "letmein",               "futbol", "barcelona", "madrid", "contraseña", "hola", "princesa", "superman"} # TODO: estima la fortaleza de cada contraseña

Ejemplo: Varias contraseñas

Entrada

password
Barcelona2024!
Gato
T3ruel!
aaaaaaaaaaaa
caballo-bateria-grapa-correcto

Salida esperada

password: muy débil (está en los diccionarios de contraseñas)
Barcelona2024!: muy débil (está en los diccionarios de contraseñas)
Gato: débil (22.8 bits, instantáneo por fuerza bruta)
T3ruel!: media (46.0 bits, 58.2 minutos por fuerza bruta)
aaaaaaaaaaaa: débil (28.2 bits, instantáneo por fuerza bruta)
caballo-bateria-grapa-correcto: muy fuerte (176.5 bits, más de un millón de años por fuerza bruta)

Ejemplo: Largas y aleatorias

Entrada

Xk9#mP2$vL8q
z7Q!r4T@w1E%y6U^

Salida esperada

Xk9#mP2$vL8q: fuerte (78.8 bits, 856.735 años por fuerza bruta)
z7Q!r4T@w1E%y6U^: muy fuerte (105.1 bits, más de un millón de años por fuerza bruta)

37. Informe del inventario

Medio · Documentación y Aplicaciones Ofimáticas · apuntesdam.com/subject/sistemas/topic/documentacion-ofimatica

La primera línea es «hoy=AAAA-MM-DD» y el resto un CSV separado por punto y coma con cabecera «codigo;tipo;aula;precio;fin_garantia». Agrupa los equipos por aula (en orden alfabético) y muestra «AULA: N equipos, X €» (2 decimales; «1 equipo» en singular), añadiendo «, sin garantía: código1, código2» con los códigos ordenados si alguno tiene la garantía vencida (fecha de fin anterior a hoy). Termina con «TOTAL: N equipos, X €, M sin garantía (P %)» con el porcentaje sin decimales. Es lo que harías con una tabla dinámica en la hoja de cálculo.

Código de partida (python)
import sysimport csvfrom datetime import date lineas = sys.stdin.read().splitlines()hoy = date.fromisoformat(lineas[0].split("=")[1])filas = list(csv.DictReader(lineas[1:], delimiter=";"))   # codigo;tipo;aula;precio;fin_garantia # TODO: informe por aula con número de equipos, valor y garantías caducadas

Ejemplo: Instituto

Entrada

hoy=2026-10-01
codigo;tipo;aula;precio;fin_garantia
PC-201;sobremesa;2.01;780;2027-05-01
PC-202;sobremesa;2.01;780;2026-03-15
IMP-01;impresora;2.01;320.5;2025-11-30
PC-110;sobremesa;1.10;650;2028-01-10
PRY-01;proyector;1.10;899.99;2026-12-31
SRV-01;servidor;CPD;3400;2026-09-30

Salida esperada

1.10: 2 equipos, 1549.99 €
2.01: 3 equipos, 1880.50 €, sin garantía: IMP-01, PC-202
CPD: 1 equipo, 3400.00 €, sin garantía: SRV-01
TOTAL: 6 equipos, 6830.49 €, 3 sin garantía (50 %)

38. Un índice automático para la documentación

Difícil · Documentación y Aplicaciones Ofimáticas · apuntesdam.com/subject/sistemas/topic/documentacion-ofimatica

La entrada es un documento en Markdown. Genera su índice con los títulos de nivel 2 (##) y 3 (###), numerados (1., 1.1., 1.2., 2.…; la numeración de nivel 3 se reinicia en cada nivel 2) y con un enlace a su ancla: «1. [Requisitos previos](#requisitos-previos)» para el nivel 2 y, con tres espacios delante, « 1.1. [Hardware](#hardware)» para el nivel 3. El ancla se obtiene del título en minúsculas, sin tildes, eliminando lo que no sea letra, número, espacio o guion, y cambiando los espacios por guiones; si un ancla se repite, la segunda vez se le añade «-1», la tercera «-2»… Ignora el título de nivel 1 y cualquier línea dentro de un bloque de código (entre líneas que empiezan por ```).

Código de partida (python)
import sysimport reimport unicodedata lineas = sys.stdin.read().splitlines() # TODO: genera el índice numerado de los títulos (## y ###) con su ancla

Ejemplo: Manual

Entrada

# Manual de instalación del servidor

## Requisitos previos
Texto.
### Hardware
### Software
## Instalación
```bash
## esto es un comentario dentro de un bloque de código, no un título
```
### Configuración de la red
### Configuración de la red
## Copias de seguridad: ¿cómo y cuándo?

Salida esperada

1. [Requisitos previos](#requisitos-previos)
   1.1. [Hardware](#hardware)
   1.2. [Software](#software)
2. [Instalación](#instalacion)
   2.1. [Configuración de la red](#configuracion-de-la-red)
   2.2. [Configuración de la red](#configuracion-de-la-red-1)
3. [Copias de seguridad: ¿cómo y cuándo?](#copias-de-seguridad-como-y-cuando)

39. Un conversor de Markdown a HTML

Muy difícil · Documentación y Aplicaciones Ofimáticas · apuntesdam.com/subject/sistemas/topic/documentacion-ofimatica

Convierte un documento Markdown a HTML con estas reglas. Bloques: los títulos (# a ######) pasan a <h1>…<h6>; las líneas que empiezan por «- » o «* » forman una lista <ul> y las que empiezan por «1. », «2. »… una <ol>, con cada elemento en un <li>; las líneas seguidas de texto normal forman un párrafo <p> (unidas con un espacio); una línea en blanco cierra el párrafo o la lista; y lo que hay entre dos líneas que empiezan por `` es un bloque <pre><code>…</code></pre> en el que no se interpreta nada. En línea, dentro de títulos, párrafos y elementos de lista: código` → <code>, negrita → <strong>, *cursiva* → <em> y [texto](url) → <a href="url">texto</a>. Antes de nada, escapa los caracteres <, > y & (html.escape con quote=False), también dentro de los bloques de código. Muestra cada bloque en su propia línea, y las etiquetas de apertura y cierre de las listas en líneas aparte.

Código de partida (python)
import sysimport reimport html lineas = sys.stdin.read().splitlines() # TODO: convierte el Markdown en HTML

Ejemplo: Procedimiento

Entrada

# Alta de un usuario

Este procedimiento lo sigue el **técnico de guardia** cuando llega una
petición *aprobada*.

## Pasos

1. Comprueba la petición en el [gestor de incidencias](https://tickets.empresa.es).
2. Crea la cuenta con `New-ADUser`.
3. Añádela a sus grupos.

Notas:
- Nunca uses contraseñas <123456>.
- Avisa a **RR. HH.** al terminar.

```powershell
New-ADUser -Name "Ana Ruiz" -Enabled $true
if ($x -lt 5) { "<ok>" }
```

Salida esperada

<h1>Alta de un usuario</h1>
<p>Este procedimiento lo sigue el <strong>técnico de guardia</strong> cuando llega una petición <em>aprobada</em>.</p>
<h2>Pasos</h2>
<ol>
<li>Comprueba la petición en el <a href="https://tickets.empresa.es">gestor de incidencias</a>.</li>
<li>Crea la cuenta con <code>New-ADUser</code>.</li>
<li>Añádela a sus grupos.</li>
</ol>
<p>Notas:</p>
<ul>
<li>Nunca uses contraseñas &lt;123456&gt;.</li>
<li>Avisa a <strong>RR. HH.</strong> al terminar.</li>
</ul>
<pre><code>New-ADUser -Name "Ana Ruiz" -Enabled $true
if ($x -lt 5) { "&lt;ok&gt;" }</code></pre>

Sistemas de Gestión Empresarial

40. El embudo de ventas del CRM

Medio · Sistemas ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/sistemas-erp-crm

Cada línea de la entrada es una oportunidad del CRM con el formato «nombre;etapa;importe». Las etapas, en orden, y su probabilidad de cierre son: Nuevo 10 %, Calificado 30 %, Propuesta 50 %, Negociación 75 %, Ganado 100 % y Perdido 0 %. Muestra, para cada etapa en ese orden (aunque no tenga oportunidades), «Etapa: N oportunidades, importe €» con el importe total con 2 decimales. Después, «Previsión ponderada: X €» (la suma de importe × probabilidad de las oportunidades abiertas, es decir, sin contar Ganado ni Perdido) y «Tasa de conversión: X %» (ganadas entre ganadas más perdidas, con 1 decimal; si no hay ninguna cerrada, «Tasa de conversión: -»).

Código de partida (python)
import sys ETAPAS = [("Nuevo", 10), ("Calificado", 30), ("Propuesta", 50), ("Negociación", 75), ("Ganado", 100), ("Perdido", 0)] lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO: recuento por etapa, previsión ponderada y tasa de conversión

Ejemplo: Embudo completo

Entrada

Bodega Ruiz;Propuesta;12000
Hotel Mar;Nuevo;3000
Clínica Sol;Ganado;8000
Taller Paco;Perdido;1500
Academia Luz;Negociación;6000
Gimnasio Up;Ganado;4500

Salida esperada

Nuevo: 1 oportunidades, 3000.00 €
Calificado: 0 oportunidades, 0.00 €
Propuesta: 1 oportunidades, 12000.00 €
Negociación: 1 oportunidades, 6000.00 €
Ganado: 2 oportunidades, 12500.00 €
Perdido: 1 oportunidades, 1500.00 €
Previsión ponderada: 10800.00 €
Tasa de conversión: 66.7 %

Ejemplo: Ninguna cerrada

Entrada

A;Calificado;1000
B;Calificado;2500.5

Salida esperada

Nuevo: 0 oportunidades, 0.00 €
Calificado: 2 oportunidades, 3500.50 €
Propuesta: 0 oportunidades, 0.00 €
Negociación: 0 oportunidades, 0.00 €
Ganado: 0 oportunidades, 0.00 €
Perdido: 0 oportunidades, 0.00 €
Previsión ponderada: 1050.15 €
Tasa de conversión: -

41. Coste total de propiedad a varios años

Difícil · Sistemas ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/sistemas-erp-crm

La primera línea es «años N». Cada línea siguiente describe una opción de ERP: «nombre;licencia_usuario_mes;usuarios;implantación;formación;hardware;mantenimiento_anual». El coste total de propiedad (TCO) es: licencia × usuarios × 12 × años + implantación + formación + hardware + mantenimiento_anual × años. Muestra las opciones ordenadas de menor a mayor TCO (a igualdad, por nombre) con el formato «nombre: X €» (2 decimales), y después «Recomendada: nombre» (la más barata) y «Ahorro frente a la más cara: X € (Y %)», con el porcentaje respecto a la más cara y 1 decimal. Si solo hay una opción, no muestres la línea de ahorro.

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO: calcula el TCO de cada opción y recomienda

Ejemplo: Tres opciones a 5 años

Entrada

años 5
Odoo Enterprise;31.1;20;18000;4000;0;2500
Business Central;70;20;25000;5000;0;3000
Odoo Community on-premise;0;20;30000;4000;6000;7000

Salida esperada

Odoo Enterprise: 71820.00 €
Odoo Community on-premise: 75000.00 €
Business Central: 129000.00 €
Recomendada: Odoo Enterprise
Ahorro frente a la más cara: 57180.00 € (44.3 %)

Ejemplo: Una sola opción

Entrada

años 3
Holded;15;4;500;200;0;0

Salida esperada

Holded: 2860.00 €
Recomendada: Holded

42. Dimensionar servidores para Odoo

Muy difícil · Sistemas ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/sistemas-erp-crm

Cada línea es un escenario: «nombre;usuarios_concurrentes;núcleos;ram_gb». Aplica estas reglas: workers = techo(concurrentes / 6), con un mínimo de 2; además hay 1 worker de cron. Núcleos necesarios = techo(workers / 2). Memoria necesaria (MB) = (workers + 1) × 250 + 2048 de PostgreSQL + 1024 del sistema. Para cada escenario muestra, en este orden: «== nombre», «workers = N», «max_cron_threads = 1», «CPU: suficiente» o «CPU: insuficiente (faltan N núcleos)», «Memoria: suficiente» o «Memoria: insuficiente (faltan N MB)» y, si algo es insuficiente, «Recomendado: X», el servidor más pequeño del catálogo que cubra ambas cosas: S (2 núcleos, 4 GB), M (4, 8), L (8, 16), XL (16, 32); si ninguno llega, «Recomendado: varios servidores».

Código de partida (python)
import sysimport math CATALOGO = [("S", 2, 4), ("M", 4, 8), ("L", 8, 16), ("XL", 16, 32)] lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO: dimensiona cada escenario

Ejemplo: Servidor justo y otro corto

Entrada

Oficina;20;4;8
Almacén;40;2;4

Salida esperada

== Oficina
workers = 4
max_cron_threads = 1
CPU: suficiente
Memoria: suficiente
== Almacén
workers = 7
max_cron_threads = 1
CPU: insuficiente (faltan 2 núcleos)
Memoria: insuficiente (faltan 976 MB)
Recomendado: M

Ejemplo: Pocos usuarios y memoria justa

Entrada

Tienda;3;1;4

Salida esperada

== Tienda
workers = 2
max_cron_threads = 1
CPU: suficiente
Memoria: suficiente

Ejemplo: Solo falta memoria

Entrada

Taller;12;2;3

Salida esperada

== Taller
workers = 2
max_cron_threads = 1
CPU: suficiente
Memoria: insuficiente (faltan 750 MB)
Recomendado: S

43. ¿Puede este usuario hacerlo?

Medio · Implantación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/implantacion-erp

En Odoo, los derechos de acceso (ir.model.access) dan a cada grupo permisos de lectura (r), escritura (w), creación (c) y borrado (d) sobre cada modelo, y un usuario tiene la suma de los permisos de todos sus grupos. La entrada tiene tres secciones. En [accesos], líneas «grupo modelo permisos» (permisos es una combinación de rwcd, o «-» si ninguno; puede haber varias líneas del mismo grupo y modelo, que se suman). En [usuarios], «usuario grupo1,grupo2…». En [consultas], «usuario acción modelo», con acción leer, escribir, crear o borrar. Para cada consulta muestra «usuario acción modelo: PERMITIDO» o «DENEGADO». Un usuario que no existe no tiene ningún permiso.

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()]LETRA = {"leer": "r", "escribir": "w", "crear": "c", "borrar": "d"} # TODO: lee las secciones y responde a cada consulta

Ejemplo: Comercial y administración

Entrada

[accesos]
ventas_usuario sale.order rwc
ventas_usuario res.partner rw
ventas_admin sale.order rwcd
contabilidad account.move rwcd
empleado res.partner r
[usuarios]
ana ventas_usuario,empleado
luis contabilidad,empleado
marta ventas_admin
[consultas]
ana crear sale.order
ana borrar sale.order
marta borrar sale.order
luis leer res.partner
luis escribir res.partner
ana escribir res.partner
marta leer account.move

Salida esperada

ana crear sale.order: PERMITIDO
ana borrar sale.order: DENEGADO
marta borrar sale.order: PERMITIDO
luis leer res.partner: PERMITIDO
luis escribir res.partner: DENEGADO
ana escribir res.partner: PERMITIDO
marta leer account.move: DENEGADO

Ejemplo: Permisos vacíos y usuario desconocido

Entrada

[accesos]
lectura product.product r
nadie product.product -
[usuarios]
pepe nadie
eva lectura,nadie
[consultas]
pepe leer product.product
eva leer product.product
eva borrar product.product
juan leer product.product

Salida esperada

pepe leer product.product: DENEGADO
eva leer product.product: PERMITIDO
eva borrar product.product: DENEGADO
juan leer product.product: DENEGADO

44. Limpiar los clientes antes de migrarlos

Difícil · Implantación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/implantacion-erp

Antes de importar los clientes del sistema antiguo hay que limpiarlos. La entrada es un CSV con cabecera «nombre;nif;email;telefono» (la línea 1). Para cada fila, normaliza: nombre sin espacios sobrantes; NIF en mayúsculas y sin guiones ni espacios; correo en minúsculas; teléfono sin espacios, puntos ni guiones y sin el prefijo +34 o 0034. Después valida, en este orden: el NIF (DNI de 8 cifras y letra, o NIE que empieza por X, Y o Z —que valen 0, 1 y 2— seguido de 7 cifras y letra; la letra es TABLA[número % 23]); el correo (algo@algo.dominio con al menos 2 letras en el dominio); y el teléfono (9 cifras empezando por 6, 7, 8 o 9). Si falla, «ERROR línea N: NIF no válido» (o «correo no válido», «teléfono no válido»). Si es válido pero su NIF ya apareció en una fila válida anterior, «DUPLICADO línea N: NIF (ya en la línea M)». Si no, «OK nif;nombre;email;telefono». Al final, «Válidos: A · Errores: B · Duplicados: C». Las líneas se numeran como en el fichero, contando la cabecera.

Código de partida (python)
import sysimport re TABLA = "TRWAGMYFPDXBNJZSQVHLCKE" lineas = [l for l in sys.stdin.read().splitlines() if l.strip()] # TODO: valida y normaliza cada línea (la primera es la cabecera)

Ejemplo: Clientes variados

Entrada

nombre;nif;email;telefono
  Lucía   Pérez Gómez ;12345678z; Lucia.Perez@Correo.ES ;+34 612 345 678
Talleres Norte;X1234567L;info@talleresnorte.es;942.123.456
Juan Ruiz;12345678A;juan@correo.es;612345678
Marta Díaz;87654321x;marta@correo;612000111
Pedro Gil;00000000T;pedro@correo.es;512345678
L. Pérez;12345678-Z;lucia@otra.es;0034 699 000 111

Salida esperada

OK 12345678Z;Lucía Pérez Gómez;lucia.perez@correo.es;612345678
OK X1234567L;Talleres Norte;info@talleresnorte.es;942123456
ERROR línea 4: NIF no válido
ERROR línea 5: correo no válido
ERROR línea 6: teléfono no válido
DUPLICADO línea 7: 12345678Z (ya en la línea 2)
Válidos: 2 · Errores: 3 · Duplicados: 1

Ejemplo: Todo correcto

Entrada

nombre;nif;email;telefono
Ana;Y1234567X;ana@a.es;700000000
Bea;Z1234567R;bea@b.com;800 00 00 00

Salida esperada

OK Y1234567X;Ana;ana@a.es;700000000
OK Z1234567R;Bea;bea@b.com;800000000
Válidos: 2 · Errores: 0 · Duplicados: 0

45. Rotación de copias abuelo-padre-hijo

Muy difícil · Implantación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/implantacion-erp

La estrategia de rotación abuelo-padre-hijo (GFS) conserva copias de seguridad a distintas escalas sin llenar el disco. La entrada son fechas de copias (AAAA-MM-DD), una por línea, desordenadas y quizá repetidas. Se conservan: las 7 copias más recientes (diaria); la copia más reciente de cada una de las 4 semanas ISO más recientes que tengan copias (semanal); y la copia más reciente de cada uno de los 12 meses más recientes que tengan copias (mensual). Muestra todas las fechas, de la más reciente a la más antigua, con «fecha conservar (motivos)» —los motivos en el orden diaria, semanal, mensual, separados por «, »— o «fecha borrar», y al final «Conservadas: N · Borradas: M».

Código de partida (python)
import sysfrom datetime import date fechas = [date.fromisoformat(l.strip()) for l in sys.stdin.read().split() if l.strip()] # TODO: decide qué copias conservar (diaria, semanal, mensual) y cuáles borrar

Ejemplo: Un mes de copias diarias

Entrada

2026-09-01
2026-09-02
2026-09-03
2026-09-04
2026-09-05
2026-09-06
2026-09-07
2026-09-08
2026-09-09
2026-09-10
2026-09-11
2026-09-12
2026-09-13
2026-09-14
2026-09-15
2026-09-16
2026-09-17
2026-09-18
2026-09-19
2026-09-20
2026-09-21
2026-09-22
2026-09-23
2026-09-24
2026-09-25
2026-09-26
2026-09-27
2026-09-28
2026-09-29
2026-09-30

Salida esperada

2026-09-30 conservar (diaria, semanal, mensual)
2026-09-29 conservar (diaria)
2026-09-28 conservar (diaria)
2026-09-27 conservar (diaria, semanal)
2026-09-26 conservar (diaria)
2026-09-25 conservar (diaria)
2026-09-24 conservar (diaria)
2026-09-23 borrar
2026-09-22 borrar
2026-09-21 borrar
2026-09-20 conservar (semanal)
2026-09-19 borrar
2026-09-18 borrar
2026-09-17 borrar
2026-09-16 borrar
2026-09-15 borrar
2026-09-14 borrar
2026-09-13 conservar (semanal)
2026-09-12 borrar
2026-09-11 borrar
2026-09-10 borrar
2026-09-09 borrar
2026-09-08 borrar
2026-09-07 borrar
2026-09-06 borrar
2026-09-05 borrar
2026-09-04 borrar
2026-09-03 borrar
2026-09-02 borrar
2026-09-01 borrar
Conservadas: 9 · Borradas: 21

Ejemplo: Copias sueltas de varios meses

Entrada

2026-01-15
2026-01-31
2026-02-28
2026-03-01
2026-03-02
2026-05-20
2026-05-21
2026-05-22

Salida esperada

2026-05-22 conservar (diaria, semanal, mensual)
2026-05-21 conservar (diaria)
2026-05-20 conservar (diaria)
2026-03-02 conservar (diaria, semanal, mensual)
2026-03-01 conservar (diaria, semanal)
2026-02-28 conservar (diaria, mensual)
2026-01-31 conservar (diaria, semanal, mensual)
2026-01-15 borrar
Conservadas: 7 · Borradas: 1

46. Encuentra el fallo: el descuento para casi todos

Fácil · Adaptación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/adaptacion-erp

A partir de 10 unidades se aplica un descuento del 5 %. Con 12 unidades funciona, pero con 9 también da el descuento. Corrígelo.

Código de partida (python)
cantidad = input()if cantidad >= "10":    print("Descuento del 5 %")else:    print("Sin descuento")

Ejemplo: 12 unidades

Entrada

12

Salida esperada

Descuento del 5 %

Ejemplo: 9 unidades

Entrada

9

Salida esperada

Sin descuento

Ejemplo: 100 unidades

Entrada

100

Salida esperada

Descuento del 5 %

47. Encuentra el fallo: el ranking que no existe

Fácil · Adaptación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/adaptacion-erp

El programa ordena los clientes por importe facturado, de mayor a menor, y muestra el ranking. En lugar del ranking da un error. Corrígelo.

Código de partida (python)
clientes = [("Carlos", 558.0), ("Ana", 1079.86), ("Javier", 165.4), ("Lucía", 297.0)]ranking = clientes.sort(key=lambda c: c[1], reverse=True)for posicion, (nombre, importe) in enumerate(ranking, start=1):    print(f"{posicion}. {nombre}: {importe:.2f} €")

Ejemplo: Ranking de facturación

Salida esperada

1. Ana: 1079.86 €
2. Carlos: 558.00 €
3. Lucía: 297.00 €
4. Javier: 165.40 €

48. Encuentra el fallo: el stock que aparece en todos los almacenes

Medio · Adaptación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/adaptacion-erp

Se crea una tabla de stock con 2 almacenes y 3 productos, todo a 0, y se anotan 5 unidades del producto 1 en el almacén 0. Al imprimirla, las 5 unidades aparecen en los dos almacenes. Corrígelo.

Código de partida (python)
almacenes, productos = 2, 3stock = [[0] * productos] * almacenesstock[0][1] = 5for i, fila in enumerate(stock):    print(f"Almacén {i}: {fila}")

Ejemplo: Tabla de stock

Salida esperada

Almacén 0: [0, 5, 0]
Almacén 1: [0, 0, 0]

49. Un evaluador de dominios de Odoo

Muy difícil · Adaptación de un ERP-CRM · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/adaptacion-erp

Las reglas de automatización, las reglas de registro y los filtros de Odoo usan dominios: listas de condiciones (campo, operador, valor) en notación prefija (polaca), donde '&' (y) y '|' (o) se aplican a las dos expresiones que les siguen, '!' (no) a la siguiente, y los términos seguidos del nivel superior se unen con Y. La primera línea de la entrada es un dominio escrito como lista de Python; cada línea siguiente es un registro en JSON con su id. Muestra los id de los registros que cumplen el dominio, separados por espacios y en el orden de la entrada, o «ninguno». Operadores: =, !=, >, >=, <, <=, in, not in e ilike (contiene, sin distinguir mayúsculas). Un campo que falta vale None: para = False cumple, y para >, <, >=, <= e ilike no cumple.

Código de partida (python)
import sysimport astimport json lineas = [l for l in sys.stdin.read().splitlines() if l.strip()]dominio = ast.literal_eval(lineas[0])registros = [json.loads(l) for l in lineas[1:]] # TODO: muestra los id de los registros que cumplen el dominio

Ejemplo: AND implícito

Entrada

[('is_company', '=', True), ('city', '=', 'Logroño')]
{"id": 1, "name": "Talleres Norte", "city": "Logroño", "is_company": true, "credit": 1200.5, "country": "ES"}
{"id": 2, "name": "Ana Ruiz", "city": "Haro", "is_company": false, "credit": 0, "country": "ES"}
{"id": 3, "name": "Clínica Sol", "city": "Logroño", "is_company": true, "credit": 0, "country": "ES"}
{"id": 4, "name": "Weinhaus GmbH", "city": "Berlín", "is_company": true, "credit": 5300, "country": "DE"}
{"id": 5, "name": "Pedro Gil", "is_company": false, "credit": 90, "country": "ES"}

Salida esperada

1 3

Ejemplo: OR en notación prefija

Entrada

['|', ('city', '=', 'Haro'), ('credit', '>', 1000)]
{"id": 1, "name": "Talleres Norte", "city": "Logroño", "is_company": true, "credit": 1200.5, "country": "ES"}
{"id": 2, "name": "Ana Ruiz", "city": "Haro", "is_company": false, "credit": 0, "country": "ES"}
{"id": 3, "name": "Clínica Sol", "city": "Logroño", "is_company": true, "credit": 0, "country": "ES"}
{"id": 4, "name": "Weinhaus GmbH", "city": "Berlín", "is_company": true, "credit": 5300, "country": "DE"}
{"id": 5, "name": "Pedro Gil", "is_company": false, "credit": 90, "country": "ES"}

Salida esperada

1 2 4

Ejemplo: Negación, in e ilike

Entrada

['!', ('country', 'in', ['ES', 'PT']), ('name', 'ilike', 'GMBH')]
{"id": 1, "name": "Talleres Norte", "city": "Logroño", "is_company": true, "credit": 1200.5, "country": "ES"}
{"id": 2, "name": "Ana Ruiz", "city": "Haro", "is_company": false, "credit": 0, "country": "ES"}
{"id": 3, "name": "Clínica Sol", "city": "Logroño", "is_company": true, "credit": 0, "country": "ES"}
{"id": 4, "name": "Weinhaus GmbH", "city": "Berlín", "is_company": true, "credit": 5300, "country": "DE"}
{"id": 5, "name": "Pedro Gil", "is_company": false, "credit": 90, "country": "ES"}

Salida esperada

4

Ejemplo: Campo ausente y False

Entrada

[('city', '=', False)]
{"id": 1, "name": "Talleres Norte", "city": "Logroño", "is_company": true, "credit": 1200.5, "country": "ES"}
{"id": 2, "name": "Ana Ruiz", "city": "Haro", "is_company": false, "credit": 0, "country": "ES"}
{"id": 3, "name": "Clínica Sol", "city": "Logroño", "is_company": true, "credit": 0, "country": "ES"}
{"id": 4, "name": "Weinhaus GmbH", "city": "Berlín", "is_company": true, "credit": 5300, "country": "DE"}
{"id": 5, "name": "Pedro Gil", "is_company": false, "credit": 90, "country": "ES"}

Salida esperada

5

50. Encuentra el fallo: el recuento de ventas que se rompe

Fácil · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

El programa cuenta las unidades vendidas de cada producto a partir de las líneas de pedido y las muestra por orden alfabético. Se detiene con un error en la primera línea. Corrígelo.

Código de partida (python)
lineas = ["Portátil", "Ratón", "Ratón", "Webcam", "Ratón", "Portátil"]ventas = {}for producto in lineas:    ventas[producto] += 1for producto, unidades in sorted(ventas.items()):    print(producto, unidades)

Ejemplo: Seis líneas

Salida esperada

Portátil 2
Ratón 3
Webcam 1

51. Un campo calculado: la amortización

Medio · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

En el módulo de equipamiento, el campo calculado valor_actual depende de la fecha de compra, la vida útil y el precio (@api.depends). La primera línea es la fecha de hoy (AAAA-MM-DD); cada línea siguiente, un equipo: «nombre;fecha_compra;vida_util_en_años;precio». Con amortización lineal, años = días transcurridos / 365 y valor = precio × (1 − años / vida útil), sin bajar de 0. Muestra «nombre: valor € (estado, N años)», con el valor con 2 decimales, el estado «amortizado» si el valor es 0 y «en uso» si no, y los años con 1 decimal. Si la fecha de compra es posterior a hoy, «nombre: fecha de compra futura».

Código de partida (python)
import sysfrom datetime import date lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()]hoy = date.fromisoformat(lineas[0]) # TODO: calcula el valor actual y el estado de cada equipo

Ejemplo: Varios equipos

Entrada

2026-10-01
Portátil Dell;2024-10-01;4;1200
Impresora HP;2019-03-15;5;450
Monitor LG;2026-04-01;6;220
Servidor;2027-01-01;5;3000

Salida esperada

Portátil Dell: 600.00 € (en uso, 2.0 años)
Impresora HP: 0.00 € (amortizado, 7.6 años)
Monitor LG: 201.62 € (en uso, 0.5 años)
Servidor: fecha de compra futura

Ejemplo: Recién comprado

Entrada

2026-10-01
Ratón;2026-10-01;3;25

Salida esperada

Ratón: 25.00 € (en uso, 0.0 años)

52. Encuentra el fallo: las líneas que se comparten

Medio · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

Cada pedido debería empezar sin líneas. Ana pide un portátil y Luis dos ratones, pero el programa dice que los dos pedidos tienen 2 líneas. Corrígelo.

Código de partida (python)
class Pedido:    def __init__(self, cliente, lineas=[]):        self.cliente = cliente        self.lineas = lineas     def anadir(self, producto, cantidad):        self.lineas.append((producto, cantidad))  p1 = Pedido("Ana")p1.anadir("Portátil", 1)p2 = Pedido("Luis")p2.anadir("Ratón", 2)print(p1.cliente, len(p1.lineas))print(p2.cliente, len(p2.lineas))

Ejemplo: Dos pedidos

Salida esperada

Ana 1
Luis 1

53. Encuentra el fallo: los agotados que se escapan

Medio · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

El programa quita del inventario los productos sin stock. Deberían quedar el ratón y el teclado, pero también queda la webcam, que tiene 0 unidades. Corrígelo.

Código de partida (python)
productos = [["Auriculares", 0], ["Webcam", 0], ["Ratón", 120], ["Altavoz", 0], ["Teclado", 45]]for p in productos:    if p[1] == 0:        productos.remove(p)print([p[0] for p in productos])

Ejemplo: Inventario

Salida esperada

['Ratón', 'Teclado']

54. Encuentra el fallo: el pedido confirmado que no se factura

Medio · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

El programa lee el estado de un pedido y, si está confirmado, genera la factura. Al escribir «confirmado» dice que todavía no se factura. Corrígelo.

Código de partida (python)
estado = input().strip()if estado is "confirmado":    print("Pedido confirmado: se genera la factura")else:    print(f"El pedido está en estado {estado}: todavía no se factura")

Ejemplo: Pedido confirmado

Entrada

confirmado

Salida esperada

Pedido confirmado: se genera la factura

Ejemplo: Pedido en borrador

Entrada

borrador

Salida esperada

El pedido está en estado borrador: todavía no se factura

55. Restricciones de un modelo

Difícil · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

En Odoo, los métodos decorados con @api.constrains lanzan un ValidationError y el registro no se guarda. Simula el modelo de mantenimientos: la primera línea es la fecha de hoy y cada línea siguiente un mantenimiento «equipo;fecha;coste;tipo». Comprueba, en este orden: el coste no es negativo; el tipo es preventivo o correctivo; la fecha no es posterior a hoy; y el equipo no tiene ya otro mantenimiento guardado esa misma fecha (una restricción única sobre equipo y fecha). Para cada error muestra «ValidationError en el registro N: mensaje» (N empieza en 1) con los mensajes «el coste no puede ser negativo», «tipo «X» no permitido», «la fecha no puede ser futura» o «EQUIPO ya tiene un mantenimiento el FECHA». Los registros con error no se guardan (no cuentan para los duplicados). Al final, «Guardados: N».

Código de partida (python)
import sysfrom datetime import date lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()]hoy = date.fromisoformat(lineas[0])TIPOS = ("preventivo", "correctivo") # TODO: valida cada mantenimiento como lo haría @api.constrains

Ejemplo: Mezcla de errores

Entrada

2026-10-01
Portátil Dell;2026-09-10;45.5;preventivo
Impresora HP;2026-09-12;-10;correctivo
Impresora HP;2026-09-12;60;revisión
Servidor;2026-11-02;0;preventivo
Portátil Dell;2026-09-10;20;correctivo
Servidor;2026-10-01;120;correctivo

Salida esperada

ValidationError en el registro 2: el coste no puede ser negativo
ValidationError en el registro 3: tipo «revisión» no permitido
ValidationError en el registro 4: la fecha no puede ser futura
ValidationError en el registro 5: Portátil Dell ya tiene un mantenimiento el 2026-09-10
Guardados: 2

Ejemplo: Todo correcto

Entrada

2026-10-01
A;2026-01-01;0;preventivo
A;2026-01-02;10;preventivo
B;2026-01-01;10;correctivo

Salida esperada

Guardados: 3

56. Secuencias de referencias

Muy difícil · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

Los módulos de Odoo numeran sus registros con secuencias (ir.sequence): un prefijo que puede incluir la fecha, un número con ceros a la izquierda y un reinicio del contador. La entrada tiene tres líneas de configuración —«prefijo=…», «relleno=N» y «reinicio=anual|mensual|nunca»— y después las fechas (AAAA-MM-DD) en que se crean los registros, en orden de creación. En el prefijo, %(year)s es el año con 4 cifras, %(y)s con 2, %(month)s el mes y %(day)s el día, con sus ceros. El contador empieza en 1 y se reinicia cuando cambia el año (anual) o el año y mes (mensual); si se vuelve a un periodo anterior, continúa su propio contador. Muestra «fecha → referencia» por cada registro.

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()]patron = lineas[0].split("=", 1)[1]relleno = int(lineas[1].split("=")[1])reinicio = lineas[2].split("=")[1] # TODO: genera la referencia de cada registro creado en las fechas siguientes

Ejemplo: Reinicio anual

Entrada

prefijo=EQ/%(year)s/
relleno=4
reinicio=anual
2025-11-03
2025-12-20
2026-01-02
2026-01-15
2026-02-01

Salida esperada

2025-11-03 → EQ/2025/0001
2025-12-20 → EQ/2025/0002
2026-01-02 → EQ/2026/0001
2026-01-15 → EQ/2026/0002
2026-02-01 → EQ/2026/0003

Ejemplo: Reinicio mensual

Entrada

prefijo=MNT%(y)s%(month)s-
relleno=3
reinicio=mensual
2026-09-29
2026-09-30
2026-10-01
2026-10-01
2026-09-30

Salida esperada

2026-09-29 → MNT2609-001
2026-09-30 → MNT2609-002
2026-10-01 → MNT2610-001
2026-10-01 → MNT2610-002
2026-09-30 → MNT2609-003

57. Encuentra el fallo: los céntimos que se pierden

Muy difícil · Desarrollo de Componentes ERP · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/desarrollo-componentes

Las facturas se redondean al céntimo y los medios céntimos van hacia arriba (1,525 € pasa a 1,53 €), como en un tique. El programa redondea 1,525 a 1,52 y 2,675 a 2,67. Corrígelo con el módulo decimal, que ya está importado.

Código de partida (python)
from decimal import Decimal, ROUND_HALF_UP lineas = [("Tornillos", 1.15), ("Arandelas", 1.525), ("Tuercas", 2.675)]for nombre, importe in lineas:    print(nombre, round(importe, 2))

Ejemplo: Tres líneas

Salida esperada

Tornillos 1.15
Arandelas 1.53
Tuercas 2.68

58. Totales de una factura

Fácil · Ejercicios de Sistemas de Gestión Empresarial · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/ejercicios-sge

Cada línea de la entrada es una línea de factura con el formato «producto;cantidad;precio;iva» (iva en %: 21, 10 o 4). Muestra la base imponible, la cuota de IVA y el total con 2 decimales, con el formato «Base: 100.00», «IVA: 21.00» y «Total: 121.00».

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO

Ejemplo: Factura con dos tipos de IVA

Entrada

Licencia ERP;3;300;21
Formación (hora);10;60;21
Libro de consulta;2;25;4

Salida esperada

Base: 1550.00
IVA: 317.00
Total: 1867.00

Ejemplo: Una sola línea

Entrada

Soporte anual;1;900;21

Salida esperada

Base: 900.00
IVA: 189.00
Total: 1089.00

59. Stock tras los movimientos

Medio · Ejercicios de Sistemas de Gestión Empresarial · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/ejercicios-sge

La entrada tiene dos partes separadas por una línea «---». Primero, el stock inicial: «producto cantidad». Después, los movimientos: «entrada producto cantidad» o «salida producto cantidad». Una salida mayor que el stock disponible no se hace y se muestra «ERROR: stock insuficiente de producto». Al final muestra el stock de cada producto ordenado alfabéticamente con el formato «producto: cantidad». Un producto que llega por una entrada sin estar en el stock inicial empieza en 0.

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO

Ejemplo: Entradas, salidas y un error

Entrada

tornillos 500
tuercas 300
arandelas 50
---
salida tornillos 120
entrada arandelas 200
salida tuercas 400
salida tuercas 100
entrada bisagras 40

Salida esperada

ERROR: stock insuficiente de tuercas
arandelas: 250
bisagras: 40
tornillos: 380
tuercas: 200

Ejemplo: Sin movimientos

Entrada

lector 3
portatil 5
---

Salida esperada

lector: 3
portatil: 5

60. Validar NIF y NIE de clientes

Medio · Ejercicios de Sistemas de Gestión Empresarial · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/ejercicios-sge

Antes de dar de alta clientes en el ERP hay que validar su documento. Cada línea es un NIF (8 cifras y una letra) o un NIE (X, Y o Z, 7 cifras y una letra), en mayúsculas o minúsculas. Muestra «DOCUMENTO válido» o «DOCUMENTO no válido», con el documento en mayúsculas. La letra de control es la posición número % 23 de la cadena TRWAGMYFPDXBNJZSQVHLCKE; en los NIE la X, Y o Z se sustituye por 0, 1 o 2 antes de calcularla.

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO

Ejemplo: NIF y NIE variados

Entrada

12345678Z
48123456A
X1234567L
Y7654321A
1234567Z
30567891v

Salida esperada

12345678Z válido
48123456A no válido
X1234567L válido
Y7654321A no válido
1234567Z no válido
30567891V válido

61. Informe de antigüedad de deuda

Difícil · Ejercicios de Sistemas de Gestión Empresarial · apuntesdam.com/subject/sistemas-gestion-empresarial/topic/ejercicios-sge

La primera línea es la fecha del informe (AAAA-MM-DD). Las siguientes son facturas pendientes: «factura;cliente;vencimiento;pendiente». Clasifica el importe pendiente por días de retraso (fecha del informe − vencimiento) en los tramos «No vencido» (0 días o menos), «1-30», «31-60», «61-90» y «Más de 90», y muestra todos los tramos en ese orden con el formato «tramo: importe» (2 decimales). Termina con «Mayor deudor vencido: cliente (importe)», el cliente con más importe vencido (retraso > 0), o «Mayor deudor vencido: ninguno».

Código de partida (python)
import sys lineas = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] # TODO

Ejemplo: Facturas en todos los tramos

Entrada

2025-06-30
F2025-010;Talleres Ebro;2025-07-15;1200
F2025-004;Calzados Cidacos;2025-06-10;850.5
F2025-002;Talleres Ebro;2025-05-20;300
F2025-001;Hotel Los Cameros;2025-04-15;2100
F2024-090;Calzados Cidacos;2025-01-31;640
F2025-011;Muebles Oja;2025-06-30;99.9

Salida esperada

No vencido: 1299.90
1-30: 850.50
31-60: 300.00
61-90: 2100.00
Más de 90: 640.00
Mayor deudor vencido: Hotel Los Cameros (2100.00)

Ejemplo: Nada vencido

Entrada

2025-03-01
F1;Academia Rioja;2025-03-15;400
F2;Farmacia Central;2025-03-01;150

Salida esperada

No vencido: 550.00
1-30: 0.00
31-60: 0.00
61-90: 0.00
Más de 90: 0.00
Mayor deudor vencido: ninguno

Ejercicios largos

62. Corrector de exámenes tipo test

Difícil · 60 minutos · apuntesdam.com/ejercicios/python/corrector-de-examenes-tipo-test

Una profesora pasa sus exámenes tipo test a un formulario y obtiene un fichero de texto con las respuestas de cada alumno. Quiere un programa que los corrija, ordene las notas y le diga qué preguntas han salido peor, para repasarlas en clase.

Cada pregunta tiene cuatro opciones (A, B, C y D) y el alumno puede dejarla en blanco. Para que no compense contestar al azar, cada fallo resta un tercio de acierto: tres fallos anulan una pregunta bien contestada. Las preguntas en blanco no suman ni restan, y la puntuación de un examen nunca puede ser negativa.

Algunas líneas del fichero llegan mal (sin el punto y coma, con respuestas de más o de menos o con letras que no existen): el programa debe avisar de cada una y corregir las demás.

Requisitos

  • La primera línea es la plantilla: CLAVE seguido de las respuestas correctas, por ejemplo CLAVE ABCDA. El número de preguntas N es la longitud de la clave.

  • Cada línea siguiente es un examen: nombre;respuestas, con una letra (A, B, C o D) o un guion (en blanco) por pregunta. Se ignoran las líneas vacías y los espacios al principio y al final de cada parte.

  • Si una línea no tiene ;, escribe Aviso: la línea L no tiene el formato nombre;respuestas, donde L es su número de línea (la de la clave es la 1). Si las respuestas no tienen exactamente N caracteres o alguno no es A, B, C, D ni guion, escribe Aviso: las respuestas de nombre no son válidas y no se corrigen. En los dos casos ese examen no cuenta.

  • Puntuación de un examen: aciertos − fallos ÷ 3, con un mínimo de 0. La nota es esa puntuación × 10 ÷ N.

  • Escribe Corrección de N preguntas y una línea por alumno, de mayor a menor nota (a igual nota, por orden alfabético): la posición en 2 caracteres alineada a la derecha, un punto y un espacio, el nombre en 22 caracteres alineado a la izquierda y la nota con dos decimales y coma, en 6 caracteres a la derecha.

  • Después: Media: X,XX, Aprobados: A de T (aprueba quien tiene un 5 o más), Pregunta más fallada: P (F fallos) (la de más respuestas incorrectas; en blanco no cuenta como fallo; si hay empate, la de número más bajo; «fallo» en singular si es 1; ninguna si nadie ha fallado nada) y Sin aciertos: … con los números de las preguntas que nadie ha acertado separados por coma y espacio, o ninguna.

  • Si no hay ningún examen válido, después de los avisos escribe solo No hay exámenes que corregir.

Formato de la entrada

  • Línea 1: CLAVE respuestas. Resto de líneas: nombre;respuestas (puede haber líneas vacías o mal escritas).

Ejemplo: Cinco exámenes de diez preguntas

Entrada

CLAVE ABCDABCDAB
Lucía Gómez;ABCDABCDAB
Mario Vega;ABCDABCD--
Nerea Paz;ABCAABCDBB
Olga Ruiz;DCBADCBADC
Pedro Sanz;-----ABCDA

Salida esperada

Corrección de 10 preguntas
 1. Lucía Gómez            10,00
 2. Mario Vega              8,00
 3. Nerea Paz               7,33
 4. Olga Ruiz               0,00
 5. Pedro Sanz              0,00
Media: 5,07
Aprobados: 3 de 5
Pregunta más fallada: 9 (3 fallos)
Sin aciertos: ninguna

Ejemplo: Con líneas mal escritas

Entrada

CLAVE CCBA

Ana Ruiz;CCBB
Bruno Díaz;CCB
Carla Mena;CCXA
sin punto y coma
Diego Sola;CA-B
Elena Toro;C-BC

Salida esperada

Aviso: las respuestas de Bruno Díaz no son válidas y no se corrigen
Aviso: las respuestas de Carla Mena no son válidas y no se corrigen
Aviso: la línea 6 no tiene el formato nombre;respuestas
Corrección de 4 preguntas
 1. Ana Ruiz                6,67
 2. Elena Toro              4,17
 3. Diego Sola              0,83
Media: 3,89
Aprobados: 1 de 3
Pregunta más fallada: 4 (3 fallos)
Sin aciertos: 4
Código de partida (python)
import sys  def formato(numero):    """Dos decimales con coma, como se escriben las notas en España."""    return f"{numero:.2f}".replace(".", ",")  def main():    lineas = sys.stdin.read().split("\n")    clave = lineas[0].split()[1]    n = len(clave)    alumnos = []                  # (nombre, nota) de cada examen válido    fallos = [0] * n              # fallos de cada pregunta    aciertos_pregunta = [0] * n   # aciertos de cada pregunta     for num, linea in enumerate(lineas[1:], start=2):        # TODO: valida la línea, corrige el examen y guarda el nombre y la nota        pass     # TODO: clasificación, media, aprobados y estadísticas de las preguntas  main()

63. Informe de ventas a partir de un CSV

Difícil · 70 minutos · apuntesdam.com/ejercicios/python/informe-de-ventas-csv

Una papelería exporta las ventas de su caja registradora a un fichero CSV, con una línea por venta: fecha, producto, categoría, unidades y precio unitario. El dueño quiere un informe rápido sin abrir una hoja de cálculo.

El fichero lo genera una caja antigua y a veces trae líneas rotas: columnas que faltan, unidades escritas con letras o negativas. Esas líneas no deben estropear el informe: se descartan y se dice cuáles eran.

Para que los totales cuadren al céntimo, el informe se calcula en céntimos (números enteros) y solo se convierte a euros al escribirlo. Sumar precios con decimales como 0.1 + 0.2 en coma flotante da errores pequeños que, con muchas ventas, acaban notándose.

Requisitos

  • La primera línea es la cabecera fecha,producto,categoria,unidades,precio y no se procesa. Las líneas vacías se ignoran sin más.

  • Una línea es válida si tiene exactamente 5 campos separados por comas, las unidades son un entero mayor que 0 y el precio es un número no negativo. El importe de la línea es unidades × precio. Las fechas tienen el formato AAAA-MM-DD.

  • Escribe Ventas totales: X € y, bajo Por categoría:, una línea por categoría de mayor a menor importe (a igualdad, alfabético): dos espacios, el nombre en 16 caracteres a la izquierda, el importe en 12 caracteres a la derecha, dos espacios y el porcentaje sobre el total con un decimal en 5 caracteres a la derecha, seguido de %.

  • Producto más vendido: producto (N unidades): el de más unidades vendidas en total (a igualdad, el primero alfabéticamente; «unidad» en singular si es 1). Mejor día: fecha (X €): el día con más importe (a igualdad, el más antiguo).

  • Bajo Por mes:, una línea por mes en orden cronológico: dos espacios, el mes (AAAA-MM), dos espacios, el importe en 12 caracteres a la derecha, un espacio y una almohadilla # por cada 50 € completos (sin espacios al final de la línea).

  • Por último, Líneas descartadas: N y, si hay alguna, sus números de línea entre paréntesis separados por coma y espacio (la cabecera es la línea 1).

  • Los importes se escriben con punto de millar, coma decimal, dos decimales y el símbolo € (por ejemplo 1.234,50 €). Si no hay ninguna venta válida, escribe solo No hay ventas válidas.

Formato de la entrada

  • Un fichero CSV: la cabecera y una venta por línea, con los campos separados por comas y el precio con punto decimal.

Ejemplo: Una semana de ventas

Entrada

fecha,producto,categoria,unidades,precio
2026-08-28,Cuaderno A4,Papelería,10,2.50
2026-08-28,Ratón inalámbrico,Informática,2,18.90
2026-08-31,Bolígrafo azul,Papelería,25,0.80
2026-09-01,Cuaderno A4,Papelería,15,2.50
2026-09-01,Memoria USB 64 GB,Informática,4,12.00
2026-09-02,Mochila,Complementos,3,34.95
2026-09-02,Bolígrafo azul,Papelería,40,0.80
2026-09-03,Ratón inalámbrico,Informática,1,18.90

Salida esperada

Ventas totales: 324,05 €
Por categoría:
  Papelería           114,50 €   35,3 %
  Complementos        104,85 €   32,4 %
  Informática         104,70 €   32,3 %
Producto más vendido: Bolígrafo azul (65 unidades)
Mejor día: 2026-09-02 (136,85 €)
Por mes:
  2026-08       82,80 € #
  2026-09      241,25 € ####
Líneas descartadas: 0

Ejemplo: Con líneas rotas

Entrada

fecha,producto,categoria,unidades,precio
2026-09-10,Cuaderno A4,Papelería,4,2.50
2026-09-10,Grapadora,Papelería,dos,6.40
2026-09-11,Auriculares,Informática,1,29.99
2026-09-11,Funda portátil,Complementos,1
2026-09-12,Bolígrafo azul,Papelería,-3,0.80
2026-09-12,Bolígrafo azul,Papelería,12,0.80

2026-09-13,Auriculares,Informática,1,29.99

Salida esperada

Ventas totales: 79,58 €
Por categoría:
  Informática          59,98 €   75,4 %
  Papelería            19,60 €   24,6 %
Producto más vendido: Bolígrafo azul (12 unidades)
Mejor día: 2026-09-11 (29,99 €)
Por mes:
  2026-09       79,58 € #
Líneas descartadas: 3 (3, 5, 6)
Código de partida (python)
import sys  def euros(centimos):    """1234.5 € → «1.234,50 €»: puntos de millar y coma decimal."""    texto = f"{centimos / 100:,.2f}"    return texto.replace(",", "X").replace(".", ",").replace("X", ".") + " €"  def leer_venta(linea):    """Devuelve (fecha, producto, categoría, unidades, importe en céntimos) o None si la línea no es válida."""    # TODO    return None  def main():    lineas = sys.stdin.read().split("\n")    # TODO: acumula por categoría, producto, día y mes, y escribe el informe  main()

64. Planificador de procesos: FCFS, SJF, SRTF y Round Robin

Muy difícil · 100 minutos · apuntesdam.com/ejercicios/python/planificador-de-procesos

Un ordenador ejecuta muchos más procesos que núcleos tiene, así que el planificador del sistema operativo decide en cada momento cuál usa la CPU. La decisión cambia mucho el tiempo que espera cada programa: es uno de los temas que más cae en los exámenes de Sistemas, siempre con el mismo tipo de ejercicio: dibujar el diagrama de Gantt y calcular tiempos.

En este ejercicio vas a escribir ese planificador. Los procesos llegan en distintos instantes con una ráfaga de CPU (lo que necesitan ejecutar), y el programa simula cuatro algoritmos clásicos: FCFS (por orden de llegada), SJF (el de ráfaga más corta entre los que esperan, sin interrumpirlo), SRTF (el de menor tiempo restante, interrumpiendo si llega uno más corto) y Round Robin (turnos de un quantum fijo).

El resultado de cada proceso se mide con dos tiempos: el de retorno (desde que llega hasta que termina) y el de espera (el retorno menos lo que de verdad ha usado la CPU). En la web tienes también el simulador visual de planificación para comprobar tus resultados.

Requisitos

  • La primera línea es el algoritmo: FCFS, SJF, SRTF o RR q (con q, el quantum, un entero mayor que 0). Si no es válida, escribe Algoritmo no válido y nada más. Cada línea siguiente es un proceso: nombre llegada ráfaga, enteros con la ráfaga mayor que 0. Una línea con otro formato o un nombre repetido escribe Aviso: línea N ignorada. Si no queda ningún proceso, No hay procesos.

  • Desempates: FCFS y Round Robin atienden por orden de llegada y, a igual llegada, por el orden de la entrada. SJF elige la menor ráfaga y SRTF el menor tiempo restante; si empatan, el que llegó antes y luego el de la entrada. En Round Robin, cuando a un proceso se le acaba el quantum, los procesos que han llegado hasta ese instante entran en la cola antes que él.

  • SRTF reevalúa cada vez que llega un proceso: si el recién llegado necesita menos que lo que le queda al que está en la CPU, lo expulsa. Si nadie está listo, la CPU queda ociosa hasta la siguiente llegada.

  • Escribe Algoritmo: … con su nombre completo (el de la tabla; en Round Robin, Round Robin con quantum q) y Diagrama: | P1 0-3 | P2 3-7 | … |: cada tramo con el proceso, el inicio y el fin, -- para la CPU ociosa, y los tramos seguidos del mismo proceso unidos en uno solo.

  • Una tabla con la cabecera Proceso, Llegada, Ráfaga, Fin, Retorno, Espera y una fila por proceso en el orden de la entrada: el nombre en 8 caracteres a la izquierda y los números alineados a la derecha en 8, 8, 6, 9 y 8 caracteres.

  • Para terminar, Media de retorno: X · Media de espera: Y y Uso de la CPU: Z % (tiempo ocupado entre el instante final), con dos decimales y coma decimal.

Formato de la entrada

  • Línea 1: FCFS, SJF, SRTF o RR q. Resto: nombre llegada ráfaga.

Nombre de cada algoritmo en la salida
EntradaAlgoritmo
FCFSFCFS (primero en llegar, primero en ser servido)
SJFSJF (el más corto primero, sin expulsión)
SRTFSRTF (menor tiempo restante, con expulsión)
RR qRound Robin con quantum q

Ejemplo: Round Robin con quantum 2

Entrada

RR 2
P1 0 5
P2 1 3
P3 2 8
P4 3 6

Salida esperada

Algoritmo: Round Robin con quantum 2
Diagrama: | P1 0-2 | P2 2-4 | P3 4-6 | P1 6-8 | P4 8-10 | P2 10-11 | P3 11-13 | P1 13-14 | P4 14-16 | P3 16-18 | P4 18-20 | P3 20-22 |
Proceso  Llegada  Ráfaga   Fin  Retorno  Espera
P1             0       5    14       14       9
P2             1       3    11       10       7
P3             2       8    22       20      12
P4             3       6    20       17      11
Media de retorno: 15,25 · Media de espera: 9,75
Uso de la CPU: 100,00 %

Ejemplo: SRTF con CPU ociosa

Entrada

SRTF
A 0 7
B 2 4
C 4 1
D 5 4
E 30 2
F x 3
A 1 1

Salida esperada

Aviso: línea 7 ignorada
Aviso: línea 8 ignorada
Algoritmo: SRTF (menor tiempo restante, con expulsión)
Diagrama: | A 0-2 | B 2-4 | C 4-5 | B 5-7 | D 7-11 | A 11-16 | -- 16-30 | E 30-32 |
Proceso  Llegada  Ráfaga   Fin  Retorno  Espera
A              0       7    16       16       9
B              2       4     7        5       1
C              4       1     5        1       0
D              5       4    11        6       2
E             30       2    32        2       0
Media de retorno: 6,00 · Media de espera: 2,40
Uso de la CPU: 56,25 %
Código de partida (python)
import sys NOMBRES = {"FCFS": "FCFS (primero en llegar, primero en ser servido)", "SJF": "SJF (el más corto primero, sin expulsión)",           "SRTF": "SRTF (menor tiempo restante, con expulsión)", "RR": "Round Robin"}  def coma(x):    return f"{x:.2f}".replace(".", ",")  def simular(algoritmo, quantum, procesos):    """Devuelve el diagrama de Gantt como lista de [nombre, inicio, fin] y el instante en que termina cada proceso."""    # TODO    return [], {}  def main():    lineas = sys.stdin.read().split("\n")    # TODO: lee el algoritmo y los procesos, simula y escribe el diagrama, la tabla y las medias  main()

65. Plan de direccionamiento con subredes VLSM

Difícil · 70 minutos · apuntesdam.com/ejercicios/python/subredes-vlsm

Una empresa tiene asignada la red 192.168.10.0/24 y quiere dividirla entre sus departamentos. Con subredes del mismo tamaño se desperdician direcciones (el enlace entre dos routers no necesita 62 direcciones), así que se usa VLSM: cada subred recibe la máscara más ajustada a los equipos que va a tener.

El método que se enseña (y el que sigue este programa) es asignar primero las subredes más grandes: así cada bloque empieza en una dirección múltiplo de su tamaño y las subredes nunca se solapan. De cada subred hay que dar la dirección de red, la máscara, la primera y la última dirección que se pueden asignar a equipos y la de difusión.

Hacerlo a mano es lento y es fácil equivocarse con la última dirección o la de difusión. Una dirección IP no es más que un número de 32 bits escrito en cuatro bytes: con ese número, las cuentas son sumas y operaciones de bits.

Requisitos

  • La primera línea es la red, IP/prefijo, con el prefijo entre 8 y 30. Si no tiene ese formato o la IP no es válida, escribe Red no válida: texto. Si la IP no es la dirección de red de ese prefijo (tiene bits de host a 1), escribe IP no es una dirección de red (sería RED/prefijo). En los dos casos no se escribe nada más.

  • Cada línea siguiente es una subred: un nombre (puede tener espacios) y el número de hosts que necesita, un entero mayor que 0. Una línea con otro formato escribe Aviso: línea N ignorada.

  • Las subredes se asignan de mayor a menor número de hosts (si empatan, por orden de la entrada), una detrás de otra desde el principio de la red. Cada una recibe el prefijo más largo que tenga sitio para sus hosts más la dirección de red y la de difusión, y como mucho /30 (un enlace de 2 hosts es un /30).

  • Para cada subred: Nombre (H hosts) → RED/p, y debajo, con dos espacios delante, máscara M · hosts útiles U y primera A · última B · difusión D. Si no cabe en lo que queda de la red, Nombre (H hosts) → no cabe: necesita un /p y quedan N direcciones y se sigue con la siguiente.

  • Antes de las subredes, Red: RED/prefijo (T direcciones). Al final, Sin asignar: N de T direcciones, desde IP con la primera dirección libre, o Sin asignar: 0 de T direcciones si no queda ninguna.

Formato de la entrada

  • Línea 1: la red, IP/prefijo. Resto: una subred por línea, nombre hosts.

Tamaño de cada prefijo
PrefijoMáscaraDireccionesHosts útiles
/30255.255.255.25242
/29255.255.255.24886
/28255.255.255.2401614
/27255.255.255.2243230
/26255.255.255.1926462
/25255.255.255.128128126
/24255.255.255.0256254

Ejemplo: Una oficina con cinco subredes

Entrada

192.168.10.0/24
Ventas 50
Administración 20
Almacén 10
Enlace router 2
Dirección 20

Salida esperada

Red: 192.168.10.0/24 (256 direcciones)
Ventas (50 hosts) → 192.168.10.0/26
  máscara 255.255.255.192 · hosts útiles 62
  primera 192.168.10.1 · última 192.168.10.62 · difusión 192.168.10.63
Administración (20 hosts) → 192.168.10.64/27
  máscara 255.255.255.224 · hosts útiles 30
  primera 192.168.10.65 · última 192.168.10.94 · difusión 192.168.10.95
Dirección (20 hosts) → 192.168.10.96/27
  máscara 255.255.255.224 · hosts útiles 30
  primera 192.168.10.97 · última 192.168.10.126 · difusión 192.168.10.127
Almacén (10 hosts) → 192.168.10.128/28
  máscara 255.255.255.240 · hosts útiles 14
  primera 192.168.10.129 · última 192.168.10.142 · difusión 192.168.10.143
Enlace router (2 hosts) → 192.168.10.144/30
  máscara 255.255.255.252 · hosts útiles 2
  primera 192.168.10.145 · última 192.168.10.146 · difusión 192.168.10.147
Sin asignar: 108 de 256 direcciones, desde 192.168.10.148

Ejemplo: Una IP que no es de red

Entrada

10.0.0.5/24
A 10

Salida esperada

10.0.0.5 no es una dirección de red (sería 10.0.0.0/24)
Código de partida (python)
import sys  def a_numero(ip):    """«192.168.10.0» → entero de 32 bits; None si no es una IPv4."""    # TODO    return None  def a_ip(n):    # TODO    return ""  def main():    lineas = sys.stdin.read().split("\n")    # TODO: valida la red, lee las subredes y asígnalas con VLSM  main()

66. Inventario de almacén con valoración FIFO y reposición

Muy difícil · 90 minutos · apuntesdam.com/ejercicios/python/inventario-con-valoracion-fifo

En un ERP como Odoo, el módulo de inventario no solo cuenta unidades: también valora el almacén. Si un tornillo se ha comprado una vez a 0,05 € y otra a 0,06 €, ¿cuánto valen los que quedan y cuánto costaron los vendidos? La respuesta depende del método de valoración, y uno de los más usados es FIFO (primero en entrar, primero en salir): cada salida se lleva primero las unidades del lote más antiguo.

Los movimientos del almacén llegan de varios terminales y no siempre en orden, así que hay que aplicarlos por fecha. Una salida de más unidades de las que hay se rechaza (es un error de la persona que la registró) y no cambia nada.

Además, el ERP propone pedidos: cada producto tiene un stock mínimo y uno máximo, y cuando queda por debajo del mínimo hay que pedir lo necesario para volver al máximo.

Requisitos

  • Cada línea es PRODUCTO código;nombre;mínimo;máximo, ENTRADA fecha;código;cantidad;coste unitario o SALIDA fecha;código;cantidad, con las fechas como AAAA-MM-DD, cantidades enteras mayores que 0 y el coste con punto decimal. Los productos se declaran antes de sus movimientos.

  • Una línea con otro formato, un número no válido, un producto que no existe (o repetido) o un mínimo mayor que el máximo escribe Aviso: línea N ignorada al leerla.

  • Los movimientos se aplican por fecha y, si coinciden, en el orden de la entrada. Una entrada crea un lote con su cantidad y su coste. Una salida con más unidades que el stock escribe Aviso: salida de C código el fecha rechazada (stock S) y no se aplica; si hay stock, consume unidades del lote más antiguo, y cuando se acaba, del siguiente, sumando su coste al coste de lo vendido.

  • Escribe Inventario: y, por cada producto en el orden en que se declararon, código nombre: stock S · valor V · coste de lo vendido C, donde el valor es la suma de las unidades que quedan por el coste de su lote.

  • Después, Reponer: y una línea por producto con stock menor que su mínimo, código nombre: pedir P (stock S, mínimo m, máximo M) con P = máximo − stock; o Reponer: nada. Para terminar, Valor del almacén: X.

  • Los importes se calculan con decimales exactos, se redondean al céntimo solo al escribirlos (las mitades suben) y se escriben con punto de millar, coma decimal y €. Si no hay ningún producto, No hay productos.

Formato de la entrada

  • Una línea por producto o movimiento: PRODUCTO …, ENTRADA … o SALIDA ….

Ejemplo: Tornillos y tuercas

Entrada

PRODUCTO T01;Tornillo M6;100;500
PRODUCTO T02;Tuerca M6;100;400
ENTRADA 2026-09-03;T01;200;0.06
ENTRADA 2026-09-01;T01;300;0.05
SALIDA 2026-09-05;T01;350
ENTRADA 2026-09-02;T02;150;0.035
SALIDA 2026-09-06;T02;80
SALIDA 2026-09-07;T02;100

Salida esperada

Aviso: salida de 100 T02 el 2026-09-07 rechazada (stock 70)
Inventario:
  T01 Tornillo M6: stock 150 · valor 9,00 € · coste de lo vendido 18,00 €
  T02 Tuerca M6: stock 70 · valor 2,45 € · coste de lo vendido 2,80 €
Reponer:
  T02 Tuerca M6: pedir 330 (stock 70, mínimo 100, máximo 400)
Valor del almacén: 11,45 €

Ejemplo: Líneas incorrectas

Entrada

PRODUCTO A;Arandela;10;50
PRODUCTO A;Repetido;1;2
PRODUCTO B;Bisagra;20;10
ENTRADA 2026-10-01;A;40;0.10
SALIDA 2026-10-01;A;15
SALIDA 2026-10-01;A;30
SALIDA 2026-10-02;Z;1
ENTRADA 2026-10-03;A;-5;0.10
AJUSTE 2026-10-03;A;3

Salida esperada

Aviso: línea 2 ignorada
Aviso: línea 3 ignorada
Aviso: línea 7 ignorada
Aviso: línea 8 ignorada
Aviso: línea 9 ignorada
Aviso: salida de 30 A el 2026-10-01 rechazada (stock 25)
Inventario:
  A Arandela: stock 25 · valor 2,50 € · coste de lo vendido 1,50 €
Reponer: nada
Valor del almacén: 2,50 €
Código de partida (python)
import sysfrom decimal import Decimal, ROUND_HALF_UP  def euros(x):    """Decimal → «1.234,56 €» redondeado al céntimo."""    x = x.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)    return f"{x:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") + " €"  def main():    productos = {}       # código → datos del producto    movimientos = []     # (fecha, orden de la entrada, tipo, código, cantidad, coste)    # TODO: lee productos y movimientos, aplícalos por fecha con FIFO y escribe el inventario  main()

67. Resumen de pedidos en XML con ElementTree

Difícil · 70 minutos · apuntesdam.com/ejercicios/python/resumen-de-pedidos-xml

La tienda online de una empresa exporta cada noche sus pedidos en XML para el sistema de gestión. El departamento comercial quiere un resumen del día, también en XML, para cargarlo en su cuadro de mando: cuántos pedidos hay en cada estado, cuánto ha comprado cada cliente y cuál es el producto que más se ha vendido.

El fichero lo genera un sistema antiguo y a veces trae pedidos incompletos: sin cliente, con un estado que no existe o con cantidades y precios mal escritos. Esos pedidos se descartan con un aviso que explique el motivo, y el resumen se hace con el resto.

Para leer y escribir XML no se trata como texto con split: se usa un analizador. Python trae xml.etree.ElementTree, que convierte el documento en un árbol de elementos con sus atributos, y que también sabe construir un árbol nuevo y escribirlo con la sangría correcta y los caracteres especiales escapados.

Requisitos

  • La entrada es un documento XML con la raíz <pedidos fecha="…"> y elementos <pedido id cliente estado> que contienen elementos <linea ref cantidad precio/>. Si el XML no está bien formado, escribe XML mal formado (línea N) con la línea del error; si la raíz no es pedidos, El elemento raíz debe ser <pedidos>. En los dos casos no se escribe nada más.

  • Un pedido se ignora, con el aviso Aviso: pedido ID ignorado: motivo (o ? si no tiene id), cuando: le falta id, cliente o estado (falta el atributo X, comprobados en ese orden); su estado no es pendiente, enviado, entregado ni cancelado (estado desconocido X); no tiene líneas (no tiene líneas); una línea no tiene ref o su cantidad no es un entero mayor que 0 (línea REF con una cantidad no válida, con ? si falta la ref); o un precio no es un número (línea REF con un precio no válido).

  • Los pedidos válidos cuentan en su estado. Los cancelados no suman importe, ni unidades, ni cuentan como pedidos del cliente. El importe de un pedido es la suma de cantidad × precio de sus líneas, con decimales exactos.

  • La salida es un XML <resumen fecha pedidos importe> (la fecha de la raíz, el número de pedidos válidos y el importe total con dos decimales y punto), con dos espacios de sangría por nivel, que contiene: <estados> con un <estado nombre pedidos/> por estado en el orden pendiente, enviado, entregado, cancelado; <clientes> con un <cliente nombre pedidos importe/> por cliente, de mayor a menor importe (a igualdad, por nombre); y <masVendido ref unidades/> con la referencia de más unidades (a igualdad, la menor alfabéticamente), solo si se ha vendido algo.

  • Los avisos se escriben antes del XML.

Formato de la entrada

  • Un documento XML de pedidos.

Ejemplo: Los pedidos de un día

Entrada

<?xml version="1.0" encoding="UTF-8"?>
<pedidos fecha="2026-09-30">
  <pedido id="P-101" cliente="Ana Gil" estado="enviado">
    <linea ref="CAB-HDMI" cantidad="2" precio="7.95"/>
    <linea ref="RATON-01" cantidad="1" precio="24.99"/>
  </pedido>
  <pedido id="P-102" cliente="Muebles Ruiz &amp; Hijos" estado="entregado">
    <linea ref="SILLA-07" cantidad="4" precio="89.90"/>
  </pedido>
  <pedido id="P-103" cliente="Ana Gil" estado="cancelado">
    <linea ref="SILLA-07" cantidad="10" precio="89.90"/>
  </pedido>
  <pedido id="P-104" cliente="Luis Mora" estado="enviado">
    <linea ref="CAB-HDMI" cantidad="3" precio="7.95"/>
    <linea ref="TECLADO" cantidad="1" precio="abc"/>
  </pedido>
  <pedido id="P-105" cliente="Ana Gil" estado="pendiente">
    <linea ref="CAB-HDMI" cantidad="3" precio="7.95"/>
  </pedido>
</pedidos>

Salida esperada

Aviso: pedido P-104 ignorado: línea TECLADO con un precio no válido
<resumen fecha="2026-09-30" pedidos="4" importe="424.34">
  <estados>
    <estado nombre="pendiente" pedidos="1" />
    <estado nombre="enviado" pedidos="1" />
    <estado nombre="entregado" pedidos="1" />
    <estado nombre="cancelado" pedidos="1" />
  </estados>
  <clientes>
    <cliente nombre="Muebles Ruiz &amp; Hijos" pedidos="1" importe="359.60" />
    <cliente nombre="Ana Gil" pedidos="2" importe="64.74" />
  </clientes>
  <masVendido ref="CAB-HDMI" unidades="5" />
</resumen>

Ejemplo: Pedidos con errores

Entrada

<pedidos fecha="2026-10-01">
  <pedido cliente="Sin id" estado="enviado"><linea ref="A" cantidad="1" precio="1"/></pedido>
  <pedido id="P-2" estado="enviado"><linea ref="A" cantidad="1" precio="1"/></pedido>
  <pedido id="P-3" cliente="Eva" estado="perdido"><linea ref="A" cantidad="1" precio="1"/></pedido>
  <pedido id="P-4" cliente="Eva" estado="enviado"/>
  <pedido id="P-5" cliente="Eva" estado="enviado"><linea cantidad="1" precio="1"/></pedido>
  <pedido id="P-6" cliente="Eva" estado="cancelado"><linea ref="B" cantidad="2" precio="5"/></pedido>
</pedidos>

Salida esperada

Aviso: pedido ? ignorado: falta el atributo id
Aviso: pedido P-2 ignorado: falta el atributo cliente
Aviso: pedido P-3 ignorado: estado desconocido perdido
Aviso: pedido P-4 ignorado: no tiene líneas
Aviso: pedido P-5 ignorado: línea ? con una cantidad no válida
<resumen fecha="2026-10-01" pedidos="1" importe="0.00">
  <estados>
    <estado nombre="pendiente" pedidos="0" />
    <estado nombre="enviado" pedidos="0" />
    <estado nombre="entregado" pedidos="0" />
    <estado nombre="cancelado" pedidos="1" />
  </estados>
  <clientes />
</resumen>
Código de partida (python)
import sysimport xml.etree.ElementTree as ETfrom decimal import Decimal, InvalidOperation ESTADOS = ("pendiente", "enviado", "entregado", "cancelado")  def leer_pedido(nodo):    """Comprueba un <pedido> y devuelve su id, cliente, estado e importe; lanza ValueError con el motivo."""    # TODO    raise ValueError("sin hacer")  def main():    # TODO: analiza el XML, valida cada pedido y escribe el resumen como XML  main()

68. Detección de ataques de fuerza bruta en el registro de SSH

Difícil · 70 minutos · apuntesdam.com/ejercicios/python/deteccion-de-ataques-ssh

Cualquier servidor con SSH abierto a Internet recibe cientos de intentos de entrada al día: programas que prueban contraseñas para root, admin o test desde máquinas comprometidas. Cada intento queda en el registro de autenticación (/var/log/auth.log en Debian y Ubuntu) con una línea como Oct 3 10:15:01 web1 sshd[2231]: Failed password for root from 203.0.113.5 port 52314 ssh2.

Herramientas como fail2ban leen ese registro y bloquean en el cortafuegos las IP que fallan demasiadas veces en poco tiempo. Vas a escribir su lógica: con la configuración típica, 5 fallos dentro de una ventana de 10 minutos bloquean la IP.

Lo más peligroso no son los fallos, sino un acceso correcto desde una IP que antes ha fallado: puede ser alguien que ha acertado la contraseña. Esos casos se marcan como alertas para revisarlos.

Requisitos

  • Cada línea empieza con la fecha de syslog en sus primeros 15 caracteres (Oct 3 10:15:01, mes en inglés y sin año: se toma 2026). Las líneas vacías o con una fecha que no se puede leer se ignoran, igual que las que no son ni un fallo ni un acceso correcto.

  • Un fallo contiene Failed password for USUARIO from IP port (o for invalid user USUARIO, si el usuario no existe en el sistema). Un acceso correcto contiene Accepted password for USUARIO from IP port o Accepted publickey for ….

  • Una IP se bloquea en el instante de un fallo si, contando ese fallo, tiene al menos 5 fallos en los últimos 10 minutos (con una diferencia de 10 minutos exactos todavía cuenta). Cada IP se bloquea una sola vez, en el primer instante en que se cumple.

  • Un acceso correcto desde una IP que ya ha tenido algún fallo (en cualquier momento anterior) genera la alerta USUARIO entró desde IP a las HH:MM:SS tras N fallos desde esa IP («fallo» si es 1).

  • Informe: Intentos fallidos: F · Accesos correctos: A; Bloquear: con una línea IP a las HH:MM:SS (5 fallos desde las HH:MM:SS) por IP, en el orden en que se bloquearon, indicando el primer fallo de la ventana (o Bloquear: ninguna IP); Usuarios inexistentes probados: a, b en orden alfabético (o ninguno); si ha habido fallos, Usuarios más atacados: root (6), admin (3), … con los tres con más fallos (a igualdad, alfabético); y Alertas: con las alertas en orden (o Alertas: ninguna).

Formato de la entrada

  • Las líneas del registro de autenticación de SSH.

Ejemplo: Una mañana en el servidor web

Entrada

Oct  3 10:15:01 web1 sshd[2231]: Failed password for root from 203.0.113.5 port 52314 ssh2
Oct  3 10:15:04 web1 sshd[2231]: Failed password for invalid user admin from 203.0.113.5 port 52320 ssh2
Oct  3 10:15:09 web1 sshd[2232]: Failed password for root from 203.0.113.5 port 52331 ssh2
Oct  3 10:16:10 web1 sshd[2240]: Accepted password for ana from 198.51.100.7 port 50022 ssh2
Oct  3 10:16:12 web1 sshd[2241]: Failed password for invalid user test from 203.0.113.5 port 52340 ssh2
Oct  3 10:16:30 web1 sshd[2242]: Accepted publickey for luis from 192.0.2.10 port 40122 ssh2
Oct  3 10:17:02 web1 sshd[2243]: Failed password for root from 203.0.113.5 port 52351 ssh2
Oct  3 10:20:45 web1 sshd[2250]: Failed password for ana from 198.51.100.99 port 50100 ssh2
Oct  3 10:21:00 web1 sshd[2251]: Accepted password for ana from 198.51.100.99 port 50101 ssh2
Oct  3 10:22:10 web1 CRON[2260]: pam_unix(cron:session): session opened for user root

Salida esperada

Intentos fallidos: 6 · Accesos correctos: 3
Bloquear:
  203.0.113.5 a las 10:17:02 (5 fallos desde las 10:15:01)
Usuarios inexistentes probados: admin, test
Usuarios más atacados: root (3), admin (1), ana (1)
Alertas:
  ana entró desde 198.51.100.99 a las 10:21:00 tras 1 fallo desde esa IP

Ejemplo: La ventana de 10 minutos

Entrada

Oct  3 22:00:00 srv sshd[1]: Failed password for root from 10.0.0.9 port 1 ssh2
Oct  3 22:03:00 srv sshd[1]: Failed password for root from 10.0.0.9 port 1 ssh2
Oct  3 22:06:00 srv sshd[1]: Failed password for root from 10.0.0.9 port 1 ssh2
Oct  3 22:09:00 srv sshd[1]: Failed password for root from 10.0.0.9 port 1 ssh2
Oct  3 22:12:00 srv sshd[1]: Failed password for root from 10.0.0.9 port 1 ssh2
Oct  3 22:13:00 srv sshd[1]: Failed password for root from 10.0.0.9 port 1 ssh2
línea basura

Salida esperada

Intentos fallidos: 6 · Accesos correctos: 0
Bloquear:
  10.0.0.9 a las 22:13:00 (5 fallos desde las 22:03:00)
Usuarios inexistentes probados: ninguno
Usuarios más atacados: root (6)
Alertas: ninguna
Código de partida (python)
import reimport sysfrom datetime import datetime, timedelta FALLO = re.compile(r"Failed password for (invalid user )?(\S+) from (\S+) port")EXITO = re.compile(r"Accepted (password|publickey) for (\S+) from (\S+) port")VENTANA = timedelta(minutes=10)MAX_FALLOS = 5  def main():    # TODO: recorre el registro, detecta los ataques y escribe el informe    pass  main()