Agentes y percepción · Media · 25 min

Facturas a datos: OCR local

El OCR te da texto con erratas. Saca número, fecha, CIF, NIF e importes; comprueba la letra del NIF, el dígito del CIF y que base más IVA sea el total.

OCR_SAMPLE es lo que leyó tesseract.js en una factura sintética nuestra, con sus erratas. OCR es ese texto, o el de tu propia imagen si activas «Un modelo en tu navegador». Escribe cinco funciones:

validateNIF(s): un DNI son 8 dígitos y una letra: la letra es NIF_LETTERS[n % 23]. Un NIE empieza por X, Y o Z, que valen 0, 1 y 2 delante de 7 dígitos, y después la misma regla. Acepta minúsculas y espacios alrededor.

validateCIF(s): letra, 7 dígitos y un control. Suma los dígitos en posición par (2.ª, 4.ª, 6.ª) y, de los impares, el doble reducido a una cifra (sumando sus dígitos); c = (10 - suma % 10) % 10. Si la primera letra es K, P, Q, S, N, W o R, el control debe ser la letra "JABCDEFGHI"[c]; si es A, B, E o H, el dígito c; con cualquier otra letra, vale uno u otro.

parseAmount(s): "1.250,00 €" es 1250; sin dígitos, null.

extractFields(text): { numero, fecha, cif, nif, base, iva, total } con null en lo que no encuentre. numero tiene forma 2026-0147, fecha dd/mm/aaaa; base, iva y total son el primer importe con decimales en la línea de «Base imponible», «IVA» y «TOTAL».

checkTotals(f): { ok, diff } con diff = base + iva - total redondeado a céntimos, y ok solo si los tres existen y diff es 0.

Retos 0/6

  • La letra del NIF, con DNI y NIE, minúsculas y espacios
  • El control del CIF: dígito para una S.L., letra para un organismo público
  • Importes con punto de millar, coma decimal y símbolo
  • Los siete campos de la factura de muestra, erratas incluidas
  • Base más IVA es el total; una diferencia se mide en céntimos
  • Con el texto del modelo (tu imagen) o la muestra: campos y veredicto

function validateNIF(s) {
  // trim + mayúsculas; DNI: 8 dígitos + NIF_LETTERS[n % 23]; NIE: X/Y/Z = 0/1/2 delante de 7 dígitos, misma regla
  return false;
}

function validateCIF(s) {
  // letra + 7 dígitos + control; posiciones pares tal cual, impares dobladas y reducidas a una cifra
  // c = (10 - suma % 10) % 10; K P Q S N W R exigen la letra "JABCDEFGHI"[c]; A B E H el dígito; el resto cualquiera
  return false;
}

function parseAmount(s) {
  // quita los puntos de millar, coma a punto, Number; null si no hay dígitos
  return null;
}

function extractFields(text) {
  // numero 9999-9999, fecha dd/mm/aaaa, cif letra+7 dígitos+control, nif 8 dígitos+letra o NIE
  // base / iva / total: primer importe con dos decimales en la línea de «Base imponible» / «IVA» / «TOTAL»
  return { numero: null, fecha: null, cif: null, nif: null, base: null, iva: null, total: null };
}

function checkTotals(f) {
  // diff = base + iva - total redondeado a céntimos; ok solo si existen los tres y diff es 0
  return { ok: false, diff: null };
}

console.log(extractFields(OCR), checkTotals(extractFields(OCR)));
La consola aparece aquí (console.log).

La idea

Un OCR no devuelve una factura: devuelve texto, y texto con fallos. En nuestra muestra leyó «FACTU RA», partió el número en dos líneas y escribió «ESOO» en el IBAN. El trabajo de convertir eso en datos es tuyo, y se hace con reglas que puedes defender ante la gestoría: el número tiene esta forma, la fecha esta otra, y los importes van en la línea que empieza por «Base imponible».

Las comprobaciones son lo que separa un extractor de un juguete. La letra del NIF y el dígito del CIF son sumas de control: si el OCR leyó un 3 por un 8, no cuadran y lo sabes antes de contabilizar nada. Y si base más IVA no da el total, o el OCR falló o la factura está mal; en ambos casos la revisa una persona.

Lo que medimos: tesseract.js 6 con el modelo rápido en español (1,1 MB comprimido) leyó la factura de 1240 × 1000 px en 290–475 ms en el DGX Spark el 18-10-2026, con una confianza media del 92 %, en Node. En el navegador tarda algo más y corre en un worker. El modelo «best» (8,4 MB) no mejoró esta muestra.

Todo ocurre en tu pestaña. Al pulsar «Cargar el modelo» se descargan 5,3 MB de este mismo sitio; la imagen que subas no sale de tu navegador, y eso es exactamente lo que necesitas para procesar facturas con datos de terceros sin un encargado de tratamiento más.

Hazlo en tu máquina

Con Python, sobre el texto que produjo el OCR (CC BY 4.0). Si tienes Tesseract instalado, tesseract factura-muestra.png salida -l spa lo reproduce a partir de la imagen:

curl -O https://vorluxai.com/data/labs/factura-ocr.txt
python3 -c "
import re
t = open('factura-ocr.txt').read()
amount = lambda label: float(re.search(label + r'[^\n]*?(\d[\d.]*,\d{2})', t, re.I).group(1).replace('.', '').replace(',', '.'))
nif = re.search(r'\b(\d{8})([A-Z])\b', t)
print('NIF ok:', 'TRWAGMYFPDXBNJZSQVHLCKE'[int(nif.group(1)) % 23] == nif.group(2))
base, iva, total = amount('Base imponible'), amount('IVA'), amount('TOTAL')
print('base + IVA == total:', round(base + iva - total, 2) == 0, base, iva, total)
"

Comprueba lo que has entendido

1. El OCR leyó un 3 donde había un 8 en el NIF. ¿Cómo te enteras?
2. ¿Por qué el importe del IVA se busca con un `*?` perezoso en la línea?
3. Base más IVA no da el total. ¿Qué concluyes?
4. Al pulsar «Cargar el modelo», ¿adónde va la factura que subes?

Para profundizar: la referencia de Tesseract (en inglés) →

¿Esto en producción, con tus datos? Hablemos 15 minutos →