GCP OCR y Java: automatización de facturas
El procesamiento manual de facturas puede ser tedioso, propenso a errores y lento. La tecnología de reconocimiento óptico de caracteres (OCR, por sus siglas en inglés) ofrece una solución potente al automatizar la extracción de texto de las facturas, lo que mejora de forma significativa la precisión y la eficiencia en los flujos de trabajo financieros.
Requisitos previos
Antes de comenzar con GCP OCR y Java, asegúrate de tener:
- Java 11 o posterior instalado
- Maven 3.8+ para la gestión de dependencias
- Una cuenta de Google Cloud con la facturación habilitada
- La API de Cloud Vision habilitada en tu proyecto de Google Cloud
- La CLI de Google Cloud instalada
Por qué el OCR es importante en el procesamiento de facturas
La tecnología OCR convierte imágenes de texto en datos legibles por máquina. Integrar el OCR en los flujos de trabajo de procesamiento de facturas permite:
- Reducir los errores de entrada manual de datos
- Acelerar los tiempos de procesamiento de facturas
- Habilitar operaciones financieras escalables y automatizadas
Configurar la API de Google Cloud Vision con Java
Configuración de la autenticación
Antes de usar la API de Cloud Vision, necesitas configurar la autenticación:
-
Instala la CLI de Google Cloud si aún no lo has hecho.
-
Inicializa la CLI ejecutando:
gcloud init -
Configura las credenciales predeterminadas de la aplicación:
gcloud auth application-default login -
Asegúrate de que la API de Cloud Vision esté habilitada en tu proyecto de Google Cloud:
gcloud services enable vision.googleapis.com
Agregar dependencias
Agrega la dependencia de Cloud Vision a tu proyecto de Java con Maven. El enfoque recomendado es usar el BOM (Bill of Materials) de Google Cloud:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>26.56.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-vision</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
El BOM gestiona ambas versiones. Configura el release del compilador de Maven en 11 o posterior. Guarda cada clase de Java que aparece abajo en su propio archivo con el nombre de la clase; el ejemplo final usa Gson para serializar JSON.
Extraer texto de las facturas
Este ejemplo extrae texto de una imagen de factura JPEG o PNG. Los documentos PDF/TIFF requieren la
API de anotación de archivos de Vision con un InputConfig, en lugar de
pasar los bytes del documento a Image. Cada página de PDF procesada es una imagen facturable independiente.
import com.google.cloud.vision.v1.AnnotateImageRequest;
import com.google.cloud.vision.v1.AnnotateImageResponse;
import com.google.cloud.vision.v1.BatchAnnotateImagesResponse;
import com.google.cloud.vision.v1.Feature;
import com.google.cloud.vision.v1.Image;
import com.google.cloud.vision.v1.ImageAnnotatorClient;
import com.google.cloud.vision.v1.TextAnnotation;
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
public class InvoiceOCR {
public static void main(String[] args) {
// The path to your invoice image
String filePath = "invoice.jpg";
try {
// Load the image
ByteString imgBytes = ByteString.copyFrom(Files.readAllBytes(Paths.get(filePath)));
Image img = Image.newBuilder().setContent(imgBytes).build();
// Create feature for text detection
Feature feature = Feature.newBuilder()
.setType(Feature.Type.DOCUMENT_TEXT_DETECTION)
.build();
// Build the request
AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
.addFeatures(feature)
.setImage(img)
.build();
List<AnnotateImageRequest> requests = new ArrayList<>();
requests.add(request);
// Process the request
try (ImageAnnotatorClient client = ImageAnnotatorClient.create()) {
BatchAnnotateImagesResponse response = client.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = response.getResponsesList();
for (AnnotateImageResponse res : responses) {
if (res.hasError()) {
throw new IOException("Vision OCR failed (code " + res.getError().getCode() + ")");
}
if (!res.hasFullTextAnnotation() || res.getFullTextAnnotation().getText().isBlank()) {
System.out.println("No text found in image");
return;
}
TextAnnotation annotation = res.getFullTextAnnotation();
System.out.println("Extracted Text:\n" + annotation.getText());
}
}
} catch (Exception e) {
System.err.println("Invoice OCR failed (" + e.getClass().getSimpleName() + ")");
System.exit(1);
}
}
}
Detección de texto de documentos vs. detección de texto
Google Cloud Vision ofrece dos modos principales de OCR:
-
DOCUMENT_TEXT_DETECTION: optimizado para texto denso en documentos estructurados como las facturas. Conserva el diseño y la estructura del texto, lo que lo hace ideal para el procesamiento de facturas.
-
TEXT_DETECTION: mejor para texto de escenas o imágenes con texto disperso. Es menos estructurado, pero funciona bien para capturar texto en escenas naturales.
Para el procesamiento de facturas, DOCUMENT_TEXT_DETECTION suele ser la mejor opción, ya que conserva la
estructura del documento.
Analizar y estructurar los datos de la factura
Después de extraer el texto sin procesar, tendrás que analizarlo para convertirlo en datos estructurados. Las expresiones regulares o las bibliotecas de NLP pueden ayudar a identificar campos clave como el número de factura, la fecha, el importe total y los datos del proveedor.
El siguiente analizador se limita deliberadamente a las líneas etiquetadas: Invoice No: INV-123,
Date: 03/19/2025, Total: $1,234.56 y Vendor: Example Ltd. Acepta fechas estadounidenses
en formato mes/día/año e importes con punto decimal, y rechaza los campos ausentes o las fechas
inválidas. El texto de OCR con otros diseños o configuraciones regionales necesita un analizador
distinto y revisión humana antes de cualquier envío financiero.
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class InvoiceParser {
public static Map<String, String> parseInvoiceData(String extractedText) {
Map<String, String> invoiceData = new HashMap<>();
// Match whole labeled lines so "Subtotal" and adjacent fields cannot be captured.
Pattern invoiceNumberPattern = Pattern.compile("(?im)^Invoice(?:[ \\t]+(?:No\\.?|Number)|[ \\t]*#)?[ \\t]*:[ \\t]*([A-Z0-9][A-Z0-9/-]*)[ \\t]*$");
Pattern datePattern = Pattern.compile("(?im)^(?:Invoice Date|Date)[ \\t]*:[ \\t]*(\\d{1,2}/\\d{1,2}/\\d{4})[ \\t]*$");
Pattern totalPattern = Pattern.compile("(?im)^(?:Total|Amount Due|Balance Due)[ \\t]*:[ \\t]*[$€£]?[ \\t]*((?:\\d{1,3}(?:,\\d{3})+|\\d+)\\.\\d{2})[ \\t]*$");
Pattern vendorPattern = Pattern.compile("(?im)^(?:From|Vendor|Supplier|Company)[ \\t]*:[ \\t]*([^\\r\\n]+)$");
// Extract invoice number
Matcher invoiceMatcher = invoiceNumberPattern.matcher(extractedText);
if (invoiceMatcher.find()) {
invoiceData.put("invoiceNumber", invoiceMatcher.group(1).trim());
}
// Extract date
Matcher dateMatcher = datePattern.matcher(extractedText);
if (dateMatcher.find()) {
invoiceData.put("date", dateMatcher.group(1).trim());
}
// Extract total amount
Matcher totalMatcher = totalPattern.matcher(extractedText);
if (totalMatcher.find()) {
invoiceData.put("totalAmount", totalMatcher.group(1).replace(",", ""));
}
// Extract vendor
Matcher vendorMatcher = vendorPattern.matcher(extractedText);
if (vendorMatcher.find()) {
invoiceData.put("vendor", vendorMatcher.group(1).trim());
}
validateInvoiceData(invoiceData);
return invoiceData;
}
private static void validateInvoiceData(Map<String, String> data) {
for (String field : new String[] {"invoiceNumber", "date", "totalAmount", "vendor"}) {
if (!data.containsKey(field) || data.get(field).isBlank()) {
throw new IllegalArgumentException("Missing invoice field: " + field);
}
}
DateTimeFormatter inputFormatter = DateTimeFormatter.ofPattern("M/d/uuuu")
.withResolverStyle(ResolverStyle.STRICT);
LocalDate date = LocalDate.parse(data.get("date"), inputFormatter);
data.put("date", DateTimeFormatter.ISO_LOCAL_DATE.format(date));
}
}
Automatizar la entrada de datos en sistemas financieros
Después de la validación y la revisión, los datos estructurados de la factura se pueden ingresar en sistemas financieros mediante API o integraciones con bases de datos. La sola presencia de un campo no demuestra que el OCR haya leído los valores correctos.
Este es un ejemplo conceptual de cómo integrarse con un sistema financiero:
import com.google.gson.Gson;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
public class FinancialSystemIntegration {
private static final String API_ENDPOINT = "https://financial-system.example.invalid/invoices";
public static void submitInvoiceData(Map<String, String> invoiceData)
throws IOException, InterruptedException {
String apiKey = System.getenv("FINANCIAL_API_KEY");
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException("Set FINANCIAL_API_KEY before submitting invoices");
}
String jsonData = convertToJson(invoiceData);
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_ENDPOINT))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.timeout(Duration.ofSeconds(30))
.POST(HttpRequest.BodyPublishers.ofString(jsonData))
.build();
HttpResponse<Void> response = client.send(request, HttpResponse.BodyHandlers.discarding());
if (response.statusCode() >= 200 && response.statusCode() < 300) {
System.out.println("Invoice successfully submitted to financial system");
} else {
throw new IOException("Invoice submission failed (HTTP " + response.statusCode() + ")");
}
}
private static String convertToJson(Map<String, String> data) {
return new Gson().toJson(data);
}
}
Reemplaza el endpoint de ejemplo reservado por el endpoint HTTPS documentado de tu sistema e inyecta la credencial mediante la gestión de secretos de tu despliegue. Quien hace la llamada debe manejar los fallos y las interrupciones. Antes de reintentar un envío, usa el mecanismo de idempotencia del destino o concilia su estado para evitar crear facturas duplicadas.
