9. Espacio de nombres data:: — datos externos

fix:: · Índice · Siguiente: Biblioteca estándar →

8 funciones para cruzar el contenido del PDF con listas y tablas tuyas. Todo en local: ningún dato sale de la máquina.


9.1 Dónde están los archivos

Los glosarios y los conjuntos de datos aceptan una ruta relativa al directorio de ejecución:

data::load_glossary("terminos/juridicos.txt")
data::load_dataset("datos/lotes.csv")

Las bases de consulta (query_gtin, query_medicamento, query_postal_code) tienen nombre fijo y se buscan en este orden:

  1. $PDFL_DATA_DIR (variable de entorno)
  2. ./dados/
  3. ./
  4. Perfiles instalados con pdfl add (pdfl_profiles/*/dados/)
  5. Junto al PDF analizado
# Apuntando explícitamente a la carpeta de las bases
PDFL_DATA_DIR=/opt/bases pdfl run perfil.pdfl documento.pdf

Si no se encuentra la base, el mensaje de error dice dónde ponerla.

Para distribuir las bases junto con los perfiles, usa pdfl pack; consulta el capítulo 11.


9.2 Glosarios

Un glosario es un archivo de texto con un término por línea. Las líneas vacías y las que empiezan por # se ignoran.

terminos/obligatorios.txt:

# Términos que toda póliza debe contener
plazo de carencia
cobertura contratada
condiciones generales

data::load_glossary(archivo)

Carga el glosario como lista de términos.

check "Glosario cargado" {
  terminos = data::load_glossary("terminos/obligatorios.txt")
  print("términos en el glosario:", terminos.length)
  require terminos.contains("condiciones generales")
}

data::validate_against_reference(archivo)

El camino más directo: devuelve la lista de los términos del glosario que no aparecen en el documento. Una lista vacía significa que está todo.

check "Cláusulas obligatorias" {
  faltan = data::validate_against_reference("terminos/obligatorios.txt")
  assert faltan.length == 0,
    "cláusulas ausentes en la póliza: #{faltan.join("; ")}"
}

La comparación ignora mayúsculas y espaciado: «CONDICIONES GENERALES» satisface a «condiciones generales».


9.3 Conjuntos de datos (CSV)

data::load_dataset(archivo)

Carga un CSV como lista de filas; cada fila es una lista de columnas. Las comillas se tratan según el estándar (un campo entrecomillado puede contener comas).

datos/lotes.csv:

lote,descripcion,caducidad
L2026-08,Lote homologado agosto/2026,2028-08-01
L2026-09,Lote homologado septiembre/2026,2028-09-01
check "Recorriendo la tabla" {
  filas = data::load_dataset("datos/lotes.csv")

  // La primera fila es la cabecera
  print("columnas:", filas.first().join(" | "))
  print("registros:", filas.length - 1)

  // get(n) es 1-based: get(1) es la primera columna
  filas.each { |fila|
    print(fila.get(1), "->", fila.get(2))
  }
}

data::lookup_value(archivo, clave)

Busca la clave en la primera columna y devuelve el valor de la segunda. Devuelve null si no la encuentra, y como null es falso, se puede comprobar directamente.

check "Lote homologado" {
  lote = text::extract_from_region(1, region(400, 50, 150, 20)).trim()

  descripcion = data::lookup_value("datos/lotes.csv", lote)
  assert descripcion,
    "el lote #{lote} no consta en la tabla de homologados"

  print("lote reconocido:", descripcion)
}

9.4 Bases de consulta

Estas funciones buscan archivos con nombre fijo en las carpetas descritas en 9.1 y devuelven la fila entera como lista (o null).

data::query_gtin(codigo)

Consulta gtin.csv. Ignora la puntuación del código.

datos/gtin.csv:

gtin,descripcion,fabricante
7891234567895,Metamizol sódico 500mg 20 comp,Lab Ejemplo
check "Producto homologado" {
  // Cruzando con el código leído del propio envase
  codigo = codes::decode_barcode(1)
  producto = data::query_gtin(codigo)

  assert producto,
    "el GTIN #{codigo} no consta en la base de productos"

  print("producto:", producto.get(2))
  print("fabricante:", producto.get(3))
}

data::query_medicamento(registro_o_nombre)

Consulta medicamentos.csv. Acepta el número de registro (primera columna) o parte del nombre (segunda columna).

datos/medicamentos.csv:

registro,nombre,principio_activo,franja
1.0298.0123,Metamizol sódico,metamizol monohidratado,libre
1.0298.0456,Amoxicilina,amoxicilina trihidratada,roja
check "Franja correcta en el prospecto" {
  registro = text::extract_from_region(1, region(50, 780, 200, 15)).trim()
  medicamento = data::query_medicamento(registro)

  assert medicamento,
    "el registro #{registro} no se encontró en la base del regulador"

  // Si la franja es roja, el texto obligatorio tiene que estar en el arte
  franja = medicamento.get(4)
  print("medicamento:", medicamento.get(2), "| franja:", franja)

  assert franja != "roja" || text::require_text("VENDA SOB PRESCRIÇÃO"),
    "medicamento de franja roja sin el texto obligatorio"
}

data::query_postal_code(codigo_postal)

Consulta ceps.csv. Acepta el código postal brasileño con o sin guion; exige 8 dígitos.

datos/ceps.csv:

cep,logradouro,bairro,cidade,uf
01310100,Avenida Paulista,Bela Vista,Sao Paulo,SP
check "Dirección del fabricante" {
  direccion = data::query_postal_code("01310-100")
  assert direccion, "código postal no encontrado en la base"

  print("vía:", direccion.get(2))
  print("ciudad:", direccion.get(4), "-", direccion.get(5))
}

data::validate_address(codigo_postal, "fragmento")

Comprueba si el fragmento indicado aparece en la dirección de ese código postal.

check "La dirección impresa coincide con el código postal" {
  // La dirección del envase tiene que coincidir con el código postal declarado
  assert data::validate_address("01310100", "Avenida Paulista"),
    "la dirección impresa no corresponde al código postal indicado"
}

9.5 Ejemplo completo

// prospecto_con_bases.pdfl — validación cruzando el PDF con bases locales
// Uso: PDFL_DATA_DIR=./bases pdfl run prospecto_con_bases.pdfl prospecto.pdf
profile "prospecto-con-referencias"  {

  check "Términos obligatorios del regulador" tags: ["glosario"] {
    faltan = data::validate_against_reference("bases/terminos_regulador.txt")
    assert faltan.length == 0,
      "textos obligatorios ausentes: #{faltan.join("; ")}"
  }

  check "Producto en la base" tags: ["datos", "critico"] {
    codigo = codes::decode_barcode(1)
    producto = data::query_gtin(codigo)
    assert producto, "GTIN #{codigo} no homologado"

    // El nombre de la base tiene que aparecer impreso en el prospecto
    nombre = producto.get(2)
    assert text::require_text(nombre),
      "el nombre '#{nombre}' de la base no aparece en el prospecto"
    print("producto comprobado:", nombre)
  }

  check "Registro y franja" tags: ["regulador"] {
    registro = text::extract_from_region(1, region(50, 780, 200, 15)).trim()
    med = data::query_medicamento(registro)
    assert med, "registro #{registro} no encontrado"

    assert med.get(4) != "roja" || text::require_text("VENDA SOB PRESCRIÇÃO"),
      "la franja roja exige el texto de prescripción"
  }

  check "Dirección del fabricante" tags: ["datos"] {
    assert data::validate_address("01310100", "Avenida Paulista"),
      "la dirección del fabricante no coincide con el código postal"
  }
}

fix:: · Índice · Siguiente: Biblioteca estándar →