Rechnungsdaten mit GCP OCR und Java extrahieren
Eine OCR-Antwort liefert Text; Ihr Rechnungsworkflow braucht jedoch Felder, die Sie prüfen können. Diese Maven-CLI sendet eine generierte Beispielrechnung an Google Cloud Vision, parst die beschrifteten Zeilen und schreibt JSON mit Rechnungsnummer, Datum, Lieferant und USD-Gesamtbetrag. Die Ausgabe muss vor jeder Übermittlung an Finanzsysteme von einem Menschen geprüft werden.
Voraussetzungen
Die folgenden Befehle verwenden eine Bash-Shell unter Linux oder macOS. Nutzen Sie für dieses
Beispiel JDK 21.0.12.1 und Maven 3.10.0 sowie die
Google Cloud CLI. Die Java-Clientbibliotheken von Google
unterstützen Java 21. Installieren Sie Maven gemäß der
offiziellen Installationsanleitung,
falls mvn nicht verfügbar ist.
Sie benötigen außerdem ein Projekt, für das die Abrechnung und die Cloud Vision API bereits aktiviert sind, sowie ein Konto, das die API und das Kontingent dieses Projekts nutzen darf. Folgen Sie der Vision-Einrichtungsanleitung von Google, falls diese Voraussetzungen fehlen. Beginnen Sie mit der unten generierten Rechnung ohne sensible Daten; jeder OCR-Aufruf sendet das Bild an Google und kann Kosten verursachen.
Rechnungslayout definieren
Dieser Parser verarbeitet vier beschriftete Zeilen mit US-Datumsangaben im Format Monat/Tag/Jahr und einem expliziten USD-Betrag:
Invoice No: INV-123
Date: 03/19/2025
Vendor: Example Ltd
Total: USD 1,234.56
Er prüft, ob Felder vorhanden sind, ob sich Beschriftungen wiederholen, ob das Datum kalendarisch gültig ist und ob die Betragssyntax stimmt. Er kann nicht feststellen, ob die OCR einen plausiblen Wert korrekt gelesen hat. Vergleichen Sie das JSON vor der Verwendung mit dem Bild. Bei Rechnungen mit anderen Layouts oder Währungen müssen Sie den Parser anpassen und anhand dieser Dokumente testen, statt davon auszugehen, dass diese Regeln gelten.
Java-SDK authentifizieren
Für die lokale Entwicklung verwendet das SDK
Application Default Credentials. Ersetzen Sie
YOUR_PROJECT_ID in beiden Befehlen durch Ihre Projekt-ID:
gcloud auth application-default login --project=YOUR_PROJECT_ID &&
gcloud auth application-default set-quota-project YOUR_PROJECT_ID
Das Kontingentprojekt legt fest, welches Projekt Kontingent und Abrechnung bereitstellt.
Eine Sitzung mit gcloud auth login allein stellt dem SDK keine ADC bereit.
Wenn Sie lokale Benutzer-ADC verwenden, entfernen Sie eine veraltete Überschreibung durch
GOOGLE_APPLICATION_CREDENTIALS aus Ihrer Shell, damit das SDK keine andere Zugangsdaten-Datei
wählt. Für dieses lokale Beispiel ist kein Dienstkontoschlüssel erforderlich.
Maven-Projekt erstellen
Führen Sie diesen Block in einem Verzeichnis außerhalb eines bestehenden Maven-Projekts aus.
Er prüft die Werkzeuge und bricht bei einer übergeordneten Konfiguration durch
pom.xml oder .mvn ab, bevor Dateien erstellt werden:
(
cd -P . || exit 1
parent="$PWD"
while :; do
if [ -e "$parent/pom.xml" ] || [ -e "$parent/.mvn" ]; then
printf '%s\n' 'Choose a directory outside an existing Maven project.' >&2
exit 1
fi
[ "$parent" = / ] && break
parent="${parent%/*}"
[ -n "$parent" ] || parent=/
done
java -version && javac -version && mvn -version && gcloud --version || exit 1
mkdir invoice-ocr && mkdir -p invoice-ocr/src/main/java
)
Ein vorhandenes Verzeichnis invoice-ocr bleibt erhalten und wird abgelehnt.
Wählen Sie ein neues übergeordnetes Verzeichnis, statt ein bestehendes Projekt zu löschen.
Falls die Erstellung nach dem Anlegen eines leeren Verzeichnisses abbricht, beheben Sie die Ursache
und schließen Sie src/main/java in diesem Verzeichnis ab, bevor Sie die folgenden
Dateien speichern.
Speichern Sie diese vollständige pom.xml als
invoice-ocr/pom.xml. Die
Google Cloud BOM verwaltet die Bibliotheksversionen.
Diese Versionsbindung löst Vision 3.97.0 und Gson 2.14.0 auf. Der Paketierungsschritt kompiliert die
Klassen und kopiert Laufzeitabhängigkeiten für den Java-Launcher nach
target/dependency.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>invoice-ocr</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>26.90.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>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.9.0</version>
<executions>
<execution>
<id>runtime-dependencies</id>
<phase>package</phase>
<goals><goal>copy-dependencies</goal></goals>
<configuration><includeScope>runtime</includeScope></configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Beispielrechnung generieren
Speichern Sie GenerateInvoice.java in invoice-ocr/src/main/java.
Das Programm erstellt ein PNG aus den oben bekannten Feldern, ohne dass ein Kundendokument oder
ein Bilddownload nötig ist. Ein vorhandenes Ziel wird abgelehnt.
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import javax.imageio.ImageIO;
public class GenerateInvoice {
public static void main(String[] args) {
try {
if (args.length != 1) {
throw new IllegalArgumentException("Expected one PNG destination");
}
BufferedImage image = new BufferedImage(1600, 600, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = image.createGraphics();
try {
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
graphics.setColor(Color.BLACK);
graphics.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 48));
String[] lines = {
"Invoice No: INV-123", "Date: 03/19/2025",
"Vendor: Example Ltd", "Total: USD 1,234.56"
};
for (int index = 0; index < lines.length; index++) {
graphics.drawString(lines[index], 80, 120 + index * 110);
}
} finally {
graphics.dispose();
}
try (OutputStream output = Files.newOutputStream(Path.of(args[0]),
StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
if (!ImageIO.write(image, "png", output)) {
throw new IllegalStateException("PNG encoder unavailable");
}
}
} catch (Exception error) {
System.err.println("Sample creation failed (" + error.getClass().getSimpleName() + ")");
System.exit(1);
}
}
}
Beschriftete Felder parsen und validieren
Speichern Sie InvoiceParser.java neben dem Generator. Vollständige Beschriftungen
unterscheiden Total von Subtotal.
Aliasse wie Invoice Date und Amount Due werden akzeptiert,
aber zwei Beschriftungen für dasselbe Feld werden auch bei übereinstimmenden Werten abgelehnt.
Datumsangaben verwenden einen strikten Kalenderparser; 03/04/2025 bedeutet
in diesem ausdrücklich als US-Format festgelegten Format den 4. März. Beträge mit Dezimalkomma,
andere Währungen und zusätzlicher Text bei Beträgen oder Datumsangaben werden abgelehnt.
Beträge bleiben dezimale Zeichenfolgen, damit JSON-verarbeitende Anwendungen für Geldbeträge keine
binäre Gleitkommaarithmetik verwenden müssen.
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
public class InvoiceParser {
public static Map<String, String> parseInvoiceData(String text) {
Map<String, String> fields = new LinkedHashMap<>();
for (String line : text.split("\\R")) {
String[] parts = line.strip().split(":", 2);
if (parts.length != 2) continue;
String field = switch (parts[0].strip().toLowerCase(Locale.ROOT)) {
case "invoice no", "invoice no.", "invoice number", "invoice #" -> "invoiceNumber";
case "date", "invoice date" -> "date";
case "total", "amount due", "balance due" -> "totalAmount";
case "vendor", "supplier", "from", "company" -> "vendor";
default -> null;
};
if (field == null) continue;
if (fields.putIfAbsent(field, parts[1].strip()) != null) {
throw new IllegalArgumentException("Duplicate invoice field: " + field);
}
}
for (String field : new String[] {"invoiceNumber", "date", "vendor", "totalAmount"}) {
if (!fields.containsKey(field) || fields.get(field).isBlank()) {
throw new IllegalArgumentException("Missing invoice field: " + field);
}
}
if (!fields.get("invoiceNumber").matches("[A-Za-z0-9][A-Za-z0-9/-]*")) {
throw new IllegalArgumentException("Invalid invoice number");
}
if (!fields.get("date").matches("[0-9]{1,2}/[0-9]{1,2}/[0-9]{4}")) {
throw new IllegalArgumentException("Expected a US month/day/year date");
}
DateTimeFormatter dates = DateTimeFormatter.ofPattern("M/d/uuuu", Locale.US)
.withResolverStyle(ResolverStyle.STRICT);
fields.put("date", LocalDate.parse(fields.get("date"), dates).toString());
String total = fields.get("totalAmount");
if (!total.matches("USD[ \\t]+(?:[0-9]+|[0-9]{1,3}(?:,[0-9]{3})+)\\.[0-9]{2}")) {
throw new IllegalArgumentException("Expected one USD amount with two decimal places");
}
fields.put("totalAmount", new BigDecimal(total.substring(3).strip()
.replace(",", "")).toPlainString());
fields.put("currency", "USD");
return fields;
}
}
Vision OCR mit der JSON-Ausgabe verbinden
Speichern Sie InvoiceOCR.java im selben Verzeichnis. Die CLI liest ein lokales Bild,
ruft Vision auf, übergibt den zurückgegebenen Text direkt an InvoiceParser und
serialisiert die geparsten Felder mit Gson. Sie verwendet DOCUMENT_TEXT_DETECTION, dessen
Antwort für dichten Text und Dokumente optimiert ist.
Die Antwort von Vision enthält Layoutinformationen; dieser kleine Parser verwendet nur den Text.
Der Code wählt den EU-OCR-Endpunkt von Google und einen expliziten EU-Parent für die Anfrage. Er deaktiviert automatische Wiederholungen der Erkennung und setzt ein RPC-Timeout von 20 Sekunden. Eine fehlgeschlagene Anfrage oder eine Anfrage mit Zeitüberschreitung kann Google dennoch erreicht haben; ein weiterer Aufruf kann erneut Kosten verursachen.
import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
import com.google.api.gax.rpc.ApiException;
import com.google.cloud.vision.v1.AnnotateImageRequest;
import com.google.cloud.vision.v1.AnnotateImageResponse;
import com.google.cloud.vision.v1.BatchAnnotateImagesRequest;
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.ImageAnnotatorSettings;
import com.google.gson.GsonBuilder;
import com.google.protobuf.ByteString;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.DateTimeException;
import java.time.Duration;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.imageio.ImageIO;
public class InvoiceOCR {
public static void main(String[] args) {
try {
if (args.length != 3 || !args[0].matches("[a-z][a-z0-9-]{4,28}[a-z0-9]")) {
System.err.println("Usage: InvoiceOCR PROJECT_ID IMAGE_PATH NEW_JSON_PATH");
System.exit(1);
}
Path output = Path.of(args[2]);
if (Files.exists(output)) {
throw new InvoiceFailure("Output already exists; choose a fresh JSON destination.");
}
byte[] bytes = Files.readAllBytes(Path.of(args[1]));
if (ImageIO.read(new ByteArrayInputStream(bytes)) == null) {
throw new InvoiceFailure("Image cannot be decoded; check the input file.");
}
String text = recognize(args[0], bytes);
Map<String, String> invoice = InvoiceParser.parseInvoiceData(text);
Map<String, Object> review = new LinkedHashMap<>();
review.put("status", "needs_review");
review.put("invoice", invoice);
review.put("ocrText", text);
String json = new GsonBuilder().setPrettyPrinting().create().toJson(review) + "\n";
Files.writeString(output, json, StandardCharsets.UTF_8,
StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
System.out.println("Saved JSON for human review.");
} catch (InvoiceFailure error) {
System.err.println(error.getMessage());
System.exit(1);
} catch (IllegalArgumentException | DateTimeException error) {
System.err.println("Invoice rejected: check labels, duplicate fields, US dates, and USD amounts.");
System.exit(1);
} catch (ApiException error) {
System.err.println("Vision request failed (" + error.getStatusCode().getCode() + ").");
System.exit(1);
} catch (Exception error) {
System.err.println("Invoice processing failed (" + error.getClass().getSimpleName()
+ "). Check input, output, and ADC; no invoice was submitted.");
System.exit(1);
}
}
private static String recognize(String project, byte[] bytes) throws IOException {
ImageAnnotatorSettings.Builder settings = ImageAnnotatorSettings.newBuilder()
.setEndpoint("eu-vision.googleapis.com:443")
.setQuotaProjectId(project)
.setTransportChannelProvider(InstantiatingGrpcChannelProvider.newBuilder()
.setChannelConfigurator(channel -> channel.disableRetry()).build());
settings.batchAnnotateImagesSettings().setRetryableCodes(Collections.emptySet());
settings.batchAnnotateImagesSettings().setRetrySettings(
settings.batchAnnotateImagesSettings().getRetrySettings().toBuilder()
.setMaxAttempts(1)
.setInitialRpcTimeoutDuration(Duration.ofSeconds(20))
.setMaxRpcTimeoutDuration(Duration.ofSeconds(20))
.setTotalTimeoutDuration(Duration.ofSeconds(20)).build());
AnnotateImageRequest imageRequest = AnnotateImageRequest.newBuilder()
.setImage(Image.newBuilder().setContent(ByteString.copyFrom(bytes)).build())
.addFeatures(Feature.newBuilder().setType(Feature.Type.DOCUMENT_TEXT_DETECTION).build())
.build();
BatchAnnotateImagesRequest request = BatchAnnotateImagesRequest.newBuilder()
.setParent("projects/" + project + "/locations/eu")
.addRequests(imageRequest)
.build();
try (ImageAnnotatorClient client = ImageAnnotatorClient.create(settings.build())) {
BatchAnnotateImagesResponse batch = client.batchAnnotateImages(request);
if (batch.getResponsesCount() != 1) {
throw new InvoiceFailure("Expected one Vision response; output was not created.");
}
AnnotateImageResponse response = batch.getResponses(0);
if (response.hasError()) {
throw new InvoiceFailure("Vision returned an image error (code "
+ response.getError().getCode() + "); output was not created.");
}
String text = response.getFullTextAnnotation().getText();
if (text.isBlank()) {
throw new InvoiceFailure("No text found; output was not created.");
}
return text;
}
}
private static class InvoiceFailure extends IOException {
private static final long serialVersionUID = 1L;
InvoiceFailure(String message) {
super(message);
}
}
}
Das Beispiel verwendet ein Rasterbild. PDF/TIFF-Eingaben benötigen eine
Dateiannotationsanfrage, statt Dokumentbytes in
Image zu übergeben. Der Parser erwartet außerdem jede Beschriftung und
ihren Wert in derselben OCR-Zeile. Er ignoriert nicht erkannte Zeilen, etwa Artikelbeschreibungen,
statt eine Tabelle zu interpretieren.
Vollständiges Beispiel erstellen und ausführen
Ersetzen Sie im übergeordneten Verzeichnis von invoice-ocr den Platzhalter
YOUR_PROJECT_ID und fügen Sie Folgendes ein:
(
cd invoice-ocr &&
mvn -q package &&
java -Djava.awt.headless=true -cp 'target/classes:target/dependency/*' GenerateInvoice invoice.png &&
java -cp 'target/classes:target/dependency/*' InvoiceOCR YOUR_PROJECT_ID invoice.png invoice.json &&
cat invoice.json
)
Das Classpath-Trennzeichen ist hier :, wie unter Linux und macOS üblich.
Ein fehlgeschlagener Build verhindert den Start, auch wenn eine ältere kompilierte Klasse vorhanden
ist. Ein erfolgreicher Durchlauf erstellt invoice.png und
invoice.json. Das Objekt invoice im JSON sollte unabhängig
von der Schlüsselreihenfolge diese Werte enthalten:
{
"invoiceNumber": "INV-123",
"date": "2025-03-19",
"vendor": "Example Ltd",
"totalAmount": "1234.56",
"currency": "USD"
}
Das umgebende Objekt enthält status: "needs_review" und den tatsächlichen Wert von
ocrText, damit Sie prüfen können, was der Parser verarbeitet hat.
Gleichen Sie alle fünf Werte mit invoice.png ab; syntaktisch gültige Felder
können dennoch falsch sein. Das JSON ist in UTF-8 kodiert und endet mit einem Zeilenumbruch.
Der Generator und die CLI lehnen vorhandene Ausgabepfade ab. Eine Wiederholung des gesamten Blocks
stoppt daher bei einer vorhandenen invoice.png, bevor eine weitere
Erkennungsanfrage erfolgt. Behalten Sie das bereits vorhandene Bild und JSON bei.
Um nach der Fehlerbehebung nur die OCR erneut zu versuchen, verwenden Sie ein neues JSON-Ziel:
(
cd invoice-ocr &&
java -cp 'target/classes:target/dependency/*' InvoiceOCR YOUR_PROJECT_ID invoice.png invoice-retry.json &&
cat invoice-retry.json
)
Wenn die CLI mit einem Exit-Code ungleich null endet, prüfen Sie vor einem erneuten Versuch, ob eine Ausgabedatei vorhanden ist. Fehler bei Argumenten, beim Decodieren, beim Anbieter, durch leeren Text und beim Parsen treten vor der JSON-Erstellung auf. Ein lokaler Schreibfehler kann eine unvollständige neue Datei hinterlassen; behalten Sie diese zur Prüfung bei und wählen Sie ein anderes Ziel. Bereits zuvor vorhandene Ausgaben bleiben erhalten. Prüfen Sie bei einem leeren Bild den Scan. Prüfen Sie bei einem abgelehnten Feld die vier beschrifteten Zeilen und ihre Formate. Prüfen Sie bei einem Vision-Fehler ADC, API-Zugriff, Kontingent und Netzwerkverbindung. Die CLI unterdrückt unverarbeitete Anbieterfehlermeldungen und Zugangsdaten-Details.
Finanzdaten vor der Verwendung prüfen
Dieser Workflow endet mit einem lokalen Prüfergebnis. Er sendet nichts an einen Buchhaltungsdienst. Nachdem die Felder mit dem Bild abgeglichen wurden, kann eine separate Integration sie dem dokumentierten Schema des Ziels zuordnen und dessen Idempotenzmechanismus bei der Übermittlung oder beim Abgleich wiederholter Versuche nutzen. Die Prüfung auf Vorhandensein und Format autorisiert keine Zahlung und weist nicht nach, dass eine Rechnung echt ist.
