3. Namespace text:: — texto
← Tipos · Índice · Próximo: struct:: →
25 funções para extrair, normalizar, buscar e validar o texto do documento.
Nas funções marcadas com
[texto], o argumento é opcional: sem ele, a função trabalha sobre o texto do documento inteiro; com ele, sobre a string que você passar.
3.1 Extração
text::extract_all()
Todo o texto do documento (páginas unidas por quebra de linha).
check "Documento tem conteúdo" {
texto = text::extract_all()
assert texto.trim() != "", "PDF sem texto extraível"
print("total de caracteres:", texto.length)
}
text::extract_from_page(pagina)
Texto de uma página (1-based). Erro amigável se a página não existe.
check "Capa e contracapa" {
capa = text::extract_from_page(1)
assert capa.contains("Manual do Usuário"), "capa sem o título esperado"
ultima = text::extract_from_page(doc.page_count)
assert ultima.contains("ISBN"), "última página sem ISBN"
}
text::extract_from_region(pagina, regiao)
Texto contido em uma área específica. Devolve string vazia se a região não tem texto (não é erro).
check "Rodapé técnico não pode sobrar" {
// Rodapés de produção (nome do arquivo .indd, data de exportação) às vezes
// escapam para o arquivo final
rodape = region(0, 0, 467, 40, "rodapé")
doc.pages.each { |page|
conteudo = text::extract_from_region(page.number, rodape)
assert !conteudo.contains(".indd"),
"página #{page.number} com marca de produção no rodapé: #{conteudo.trim()}"
}
}
text::extract_with_normalization()
O texto do documento já normalizado (minúsculas, espaços colapsados). Atalho
para text::normalize(text::extract_all()).
check "Busca sem se preocupar com maiúsculas" {
texto = text::extract_with_normalization()
require texto.contains("condições gerais") // acha "CONDIÇÕES GERAIS"
}
3.2 Normalização e divisão
text::normalize([texto])
Minúsculas e espaços colapsados (múltiplos espaços viram um só).
check "Normalização" {
require text::normalize(" OLÁ Mundo ") == "olá mundo"
// Sem argumento, normaliza o documento inteiro
print("documento normalizado tem", text::normalize().length, "caracteres")
}
text::split_words([texto])
Divide em palavras, removendo pontuação das bordas.
check "Palavras" {
palavras = text::split_words("Olá, mundo! (teste)")
require palavras.length == 3
require palavras.first() == "Olá"
require palavras.contains("teste")
}
text::split_sentences([texto])
Divide em sentenças (separadas por ., ! ou ? seguidos de espaço).
check "Sentenças longas demais" {
// Bulas e contratos têm limite prático de legibilidade
text::split_sentences().each { |frase|
assert frase.length < 400,
"sentença com #{frase.length} caracteres — difícil de ler"
}
}
text::split_paragraphs([texto])
Divide em parágrafos (separados por linha em branco).
check "Estrutura do documento" {
paragrafos = text::split_paragraphs()
print("parágrafos:", paragrafos.length)
require paragrafos.length >= 3
}
text::count_words([texto]) e text::count_characters([texto])
check "Volume de texto" {
require text::count_words() > 100
require text::count_characters() > 500
// Também funcionam sobre uma string qualquer
resumo = text::extract_from_page(1)
assert text::count_words(resumo) <= 250,
"resumo com #{text::count_words(resumo)} palavras (máximo 250)"
}
text::detect_language([texto])
Devolve "pt", "en", "es" ou "unknown" (heurística por palavras comuns).
check "Idioma do documento" {
idioma = text::detect_language()
assert idioma == "pt",
"documento deveria estar em português, detectei: #{idioma}"
}
3.3 Busca e conteúdo obrigatório
text::require_text(termo) e text::forbid_text(termo)
Devolvem verdadeiro/falso. A comparação ignora maiúsculas e espaçamento.
profile "contrato" {
check "Cláusulas obrigatórias" {
assert text::require_text("foro da comarca"),
"contrato sem cláusula de foro"
assert text::require_text("prazo de vigência"),
"contrato sem prazo de vigência"
}
check "Termos proibidos" {
assert text::forbid_text("RASCUNHO"),
"documento ainda marcado como rascunho"
assert text::forbid_text("lorem ipsum"),
"texto de preenchimento não substituído"
}
}
text::require_match(regex) e text::forbid_match(regex)
Como os anteriores, mas com expressão regular.
check "Padrões no documento" {
// Precisa ter um número de contrato no formato 2026/0001
assert text::require_match("\d{4}/\d{4}"),
"número de contrato não encontrado"
// Não pode ter data no formato americano
assert text::forbid_match("\d{2}-\d{2}-\d{4}"),
"data em formato americano encontrada"
}
text::fuzzy_match(a, b)
Similaridade entre dois textos, de 0.0 (nada a ver) a 1.0 (idênticos).
Útil quando erros de digitação ou OCR são esperados.
check "Nome do produto com tolerância" {
esperado = "Paracetamol 750mg"
encontrado = text::extract_from_region(1, region(50, 700, 300, 40))
similaridade = text::fuzzy_match(esperado, encontrado)
assert similaridade > 0.9,
"nome do produto diferente do esperado (#{round(similaridade * 100)}% de similaridade)"
}
3.4 Dados pessoais (LGPD)
text::detect_personal_data() e text::detect_pii()
São sinônimos. Devolvem a lista de dados pessoais encontrados: CPF, CNPJ, e-mail e telefone.
CPF e CNPJ só entram na lista se o dígito verificador for válido. Um número que apenas se parece com CPF (ex.:
111.111.111-12) não gera alarme.
check "Documento público não pode ter dados pessoais" {
achados = text::detect_personal_data()
assert achados.length == 0,
"dados pessoais expostos: #{achados.join("; ")}"
}
check "Relatório do que foi encontrado" {
// Cada item vem no formato "CPF: 529.982.247-25"
text::detect_pii().each { |item|
print("encontrado:", item)
}
}
3.5 Validações brasileiras
text::validate_cpf(texto) e text::validate_cnpj(texto)
Validam o dígito verificador (mod 11). Aceitam com ou sem pontuação e rejeitam
sequências repetidas (111.111.111-11).
check "CPF do titular" {
cpf = text::extract_from_region(1, region(100, 600, 200, 20)).trim()
assert text::validate_cpf(cpf),
"CPF inválido no cadastro: #{cpf}"
}
check "CNPJ da empresa" {
require text::validate_cnpj("11.222.333/0001-81")
require !text::validate_cnpj("11.222.333/0001-82") // dígito errado
}
text::validate_date_format(texto [, formato])
Verifica se é uma data válida no calendário (considera bissexto e dias por
mês). Formatos aceitos: "dd/mm/aaaa" e "aaaa-mm-dd"; sem o segundo
argumento, aceita os dois.
check "Datas do documento" {
require text::validate_date_format("29/02/2024") // 2024 é bissexto
require !text::validate_date_format("29/02/2023") // 2023 não é
require !text::validate_date_format("31/04/2026") // abril tem 30 dias
// Exigindo um formato específico
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)
Telefone brasileiro: (DD) 9XXXX-XXXX ou (DD) XXXX-XXXX, com pontuação
opcional.
check "Telefone de contato" {
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)
Verdadeiro se a string inteira casa com a expressão regular.
check "Código de lote no padrão da 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 fora do padrão L0000-00: #{lote}"
}
3.6 Comparação e diagnóstico
text::diff(a, b)
Lista as linhas que mudaram entre dois textos: - para as que saíram, + para
as que entraram.
check "Comparando duas páginas" {
antes = text::extract_from_page(1)
depois = text::extract_from_page(2)
mudancas = text::diff(antes, depois)
print("linhas alteradas:", mudancas.length)
mudancas.each { |linha| print(linha) }
}
Para comparar dois arquivos, use o comando
pdfl compare— ele alinha as páginas automaticamente. Veja o capítulo 11.
text::detect_rasterized_text()
Verdadeiro se alguma página não tem texto extraível mas tem imagem cobrindo metade ou mais da área — sinal de texto convertido em imagem.
check "Texto precisa ser texto" {
// Página escaneada ou com texto vetorizado não permite busca,
// acessibilidade nem correção ortográfica
assert !text::detect_rasterized_text(),
"há páginas com texto rasterizado (escaneado ou convertido em imagem)"
}
3.7 Exemplo completo
// documento_juridico.pdfl — validação de contrato
profile "contrato-padrao" {
check "Conteúdo obrigatório" tags: ["juridico"] {
assert text::require_text("foro da comarca"), "sem cláusula de foro"
assert text::require_text("prazo de vigência"), "sem prazo de vigência"
assert text::require_match("\d{4}/\d{4}"), "sem número de contrato"
}
check "Nada de rascunho" tags: ["juridico"] {
assert text::forbid_text("RASCUNHO"), "marcado como rascunho"
assert text::forbid_text("lorem ipsum"), "texto de preenchimento presente"
assert text::forbid_match("XXX+"), "campos não preenchidos (XXX)"
}
check "LGPD" tags: ["compliance"] {
achados = text::detect_personal_data()
assert achados.length == 0,
"dados pessoais no documento público: #{achados.join("; ")}"
}
check "Qualidade do texto" tags: ["texto"] {
assert text::detect_language() == "pt", "documento não está em português"
assert !text::detect_rasterized_text(), "texto rasterizado impede busca"
require text::count_words() > 200
}
}