3. Espacio de nombres text:: — texto
← Tipos · Índice · Siguiente: struct:: →
25 funciones para extraer, normalizar, buscar y validar el texto del documento.
En las funciones marcadas con
[texto], el argumento es opcional: sin él, la función trabaja sobre el texto de todo el documento; con él, sobre la cadena que le pases.
3.1 Extracción
text::extract_all()
Todo el texto del documento (páginas unidas por saltos de línea).
check "El documento tiene contenido" {
texto = text::extract_all()
assert texto.trim() != "", "PDF sin texto extraíble"
print("total de caracteres:", texto.length)
}
text::extract_from_page(pagina)
Texto de una página (1-based). Da un error claro si la página no existe.
check "Portada y contraportada" {
portada = text::extract_from_page(1)
assert portada.contains("Manual del Usuario"), "portada sin el título esperado"
ultima = text::extract_from_page(doc.page_count)
assert ultima.contains("ISBN"), "la última página no lleva ISBN"
}
text::extract_from_region(pagina, region)
Texto contenido en un área concreta. Devuelve una cadena vacía si la región no tiene texto (no es un error).
check "El pie técnico no puede quedarse" {
// Los pies de producción (nombre del archivo .indd, fecha de exportación)
// a veces se cuelan en el archivo final
pie = region(0, 0, 467, 40, "pie")
doc.pages.each { |page|
contenido = text::extract_from_region(page.number, pie)
assert !contenido.contains(".indd"),
"la página #{page.number} lleva marca de producción en el pie: #{contenido.trim()}"
}
}
text::extract_with_normalization()
El texto del documento ya normalizado (minúsculas, espacios colapsados). Atajo
de text::normalize(text::extract_all()).
check "Buscar sin preocuparse por las mayúsculas" {
texto = text::extract_with_normalization()
require texto.contains("condiciones generales") // encuentra "CONDICIONES GENERALES"
}
3.2 Normalización y división
text::normalize([texto])
Minúsculas y espacios colapsados (varios espacios se convierten en uno).
check "Normalización" {
require text::normalize(" HOLA Mundo ") == "hola mundo"
// Sin argumento, normaliza todo el documento
print("el documento normalizado tiene", text::normalize().length, "caracteres")
}
text::split_words([texto])
Divide en palabras, quitando la puntuación de los extremos.
check "Palabras" {
palabras = text::split_words("Hola, mundo! (prueba)")
require palabras.length == 3
require palabras.first() == "Hola"
require palabras.contains("prueba")
}
text::split_sentences([texto])
Divide en oraciones (separadas por ., ! o ? seguidos de espacio).
check "Oraciones demasiado largas" {
// Los prospectos y los contratos tienen un límite práctico de legibilidad
text::split_sentences().each { |frase|
assert frase.length < 400,
"oración de #{frase.length} caracteres: difícil de leer"
}
}
text::split_paragraphs([texto])
Divide en párrafos (separados por una línea en blanco).
check "Estructura del documento" {
parrafos = text::split_paragraphs()
print("párrafos:", parrafos.length)
require parrafos.length >= 3
}
text::count_words([texto]) y text::count_characters([texto])
check "Volumen de texto" {
require text::count_words() > 100
require text::count_characters() > 500
// También funcionan sobre una cadena cualquiera
resumen = text::extract_from_page(1)
assert text::count_words(resumen) <= 250,
"resumen de #{text::count_words(resumen)} palabras (máximo 250)"
}
text::detect_language([texto])
Devuelve "pt", "en", "es" o "unknown" (heurística por palabras
frecuentes).
check "Idioma del documento" {
idioma = text::detect_language()
assert idioma == "es",
"el documento debería estar en español; he detectado: #{idioma}"
}
3.3 Búsqueda y contenido obligatorio
text::require_text(termino) y text::forbid_text(termino)
Devuelven verdadero/falso. La comparación ignora mayúsculas y espaciado.
profile "contrato" {
check "Cláusulas obligatorias" {
assert text::require_text("fuero de la comarca"),
"contrato sin cláusula de fuero"
assert text::require_text("plazo de vigencia"),
"contrato sin plazo de vigencia"
}
check "Términos prohibidos" {
assert text::forbid_text("BORRADOR"),
"el documento sigue marcado como borrador"
assert text::forbid_text("lorem ipsum"),
"texto de relleno sin sustituir"
}
}
text::require_match(regex) y text::forbid_match(regex)
Como los anteriores, pero con expresión regular.
check "Patrones en el documento" {
// Debe llevar un número de contrato con el formato 2026/0001
assert text::require_match("\d{4}/\d{4}"),
"no se encontró el número de contrato"
// No puede llevar fechas en formato estadounidense
assert text::forbid_match("\d{2}-\d{2}-\d{4}"),
"se encontró una fecha en formato estadounidense"
}
text::fuzzy_match(a, b)
Similitud entre dos textos, de 0.0 (nada que ver) a 1.0 (idénticos). Útil
cuando se esperan erratas o errores de OCR.
check "Nombre del producto con tolerancia" {
esperado = "Paracetamol 750mg"
encontrado = text::extract_from_region(1, region(50, 700, 300, 40))
similitud = text::fuzzy_match(esperado, encontrado)
assert similitud > 0.9,
"el nombre del producto no coincide con el esperado (#{round(similitud * 100)}% de similitud)"
}
3.4 Datos personales
text::detect_personal_data() y text::detect_pii()
Son sinónimos. Devuelven la lista de datos personales encontrados: CPF, CNPJ, correo electrónico y teléfono.
El CPF y el CNPJ (documentos brasileños) solo entran en la lista si el dígito verificador es válido. Un número que solo se parece a un CPF (por ejemplo,
111.111.111-12) no dispara la alarma.
check "Un documento público no puede llevar datos personales" {
hallazgos = text::detect_personal_data()
assert hallazgos.length == 0,
"datos personales expuestos: #{hallazgos.join("; ")}"
}
check "Informe de lo encontrado" {
// Cada elemento viene con el formato "CPF: 529.982.247-25"
text::detect_pii().each { |item|
print("encontrado:", item)
}
}
3.5 Validaciones brasileñas
text::validate_cpf(texto) y text::validate_cnpj(texto)
Validan el dígito verificador (mod 11). Aceptan el número con o sin puntuación y
rechazan las secuencias repetidas (111.111.111-11).
check "CPF del titular" {
cpf = text::extract_from_region(1, region(100, 600, 200, 20)).trim()
assert text::validate_cpf(cpf),
"CPF inválido en el registro: #{cpf}"
}
check "CNPJ de la empresa" {
require text::validate_cnpj("11.222.333/0001-81")
require !text::validate_cnpj("11.222.333/0001-82") // dígito erróneo
}
text::validate_date_format(texto [, formato])
Comprueba que sea una fecha válida en el calendario (tiene en cuenta los
años bisiestos y los días de cada mes). Formatos aceptados: "dd/mm/aaaa" y
"aaaa-mm-dd"; sin el segundo argumento, acepta ambos.
check "Fechas del documento" {
require text::validate_date_format("29/02/2024") // 2024 es bisiesto
require !text::validate_date_format("29/02/2023") // 2023 no lo es
require !text::validate_date_format("31/04/2026") // abril tiene 30 días
// Exigiendo un formato concreto
require text::validate_date_format("02/08/2026", "dd/mm/aaaa")
require !text::validate_date_format("2026-08-02", "dd/mm/aaaa")
}
text::validate_phone_format(texto)
Teléfono brasileño: (DD) 9XXXX-XXXX o (DD) XXXX-XXXX, con puntuación
opcional.
check "Teléfono de contacto" {
require text::validate_phone_format("(11) 98765-4321")
require text::validate_phone_format("1198765432")
require !text::validate_phone_format("12345")
}
text::validate_format(texto, regex)
Verdadero si la cadena entera casa con la expresión regular.
check "Código de lote con el patrón de fábrica" {
lote = text::extract_from_region(1, region(400, 50, 150, 20)).trim()
assert text::validate_format(lote, "L\d{4}-\d{2}"),
"código de lote fuera del patrón L0000-00: #{lote}"
}
3.6 Comparación y diagnóstico
text::diff(a, b)
Lista las líneas que cambiaron entre dos textos: - para las que salieron, +
para las que entraron.
check "Comparando dos páginas" {
antes = text::extract_from_page(1)
despues = text::extract_from_page(2)
cambios = text::diff(antes, despues)
print("líneas modificadas:", cambios.length)
cambios.each { |linea| print(linea) }
}
Para comparar dos archivos, usa el comando
pdfl compare: alinea las páginas automáticamente. Consulta el capítulo 11.
text::detect_rasterized_text()
Verdadero si alguna página no tiene texto extraíble pero sí una imagen que cubre la mitad del área o más, señal de texto convertido en imagen.
check "El texto tiene que ser texto" {
// Una página escaneada o con el texto vectorizado no permite búsquedas,
// accesibilidad ni corrección ortográfica
assert !text::detect_rasterized_text(),
"hay páginas con texto rasterizado (escaneado o convertido en imagen)"
}
3.7 Ejemplo completo
// documento_juridico.pdfl — validación de un contrato
profile "contrato-estandar" {
check "Contenido obligatorio" tags: ["juridico"] {
assert text::require_text("fuero de la comarca"), "sin cláusula de fuero"
assert text::require_text("plazo de vigencia"), "sin plazo de vigencia"
assert text::require_match("\d{4}/\d{4}"), "sin número de contrato"
}
check "Nada de borradores" tags: ["juridico"] {
assert text::forbid_text("BORRADOR"), "marcado como borrador"
assert text::forbid_text("lorem ipsum"), "hay texto de relleno"
assert text::forbid_match("XXX+"), "campos sin rellenar (XXX)"
}
check "Datos personales" tags: ["compliance"] {
hallazgos = text::detect_personal_data()
assert hallazgos.length == 0,
"datos personales en el documento público: #{hallazgos.join("; ")}"
}
check "Calidad del texto" tags: ["texto"] {
assert text::detect_language() == "es", "el documento no está en español"
assert !text::detect_rasterized_text(), "el texto rasterizado impide la búsqueda"
require text::count_words() > 200
}
}