Extraction de champs de facture en temps réel (PHP, AWS Textract)
Utilisez l’opération synchrone AnalyzeExpense d’AWS Textract pour transformer une
image locale de facture en données candidates au format JSON, à examiner. L’outil PHP en ligne de
commande ci-dessous conserve les champs répétés, les lignes de facture et les scores de confiance
numériques. Il ne valide pas les calculs d’une facture et n’autorise aucun paiement.
Configurer AWS Textract et le SDK v3 pour PHP
Vous avez besoin de PHP avec cURL, Fileinfo, SimpleXML et GD prenant en charge PNG/JPEG, ainsi que de Composer et de Bash, installés séparément, pour les commandes shell. Cet exemple a été testé avec PHP 8.5.11 et Composer 2.10.3. Utilisez une version maintenue de PHP 8.5 ; le SDK lui-même prend en charge PHP 8.1 et les versions ultérieures, mais cela ne garantit pas la compatibilité de chaque dépendance de cette installation.
Configurez les informations d’accès AWS à l’aide de la
chaîne de fournisseurs d’informations d’accès du SDK.
L’entité appelante doit disposer de l’autorisation textract:AnalyzeExpense dans la région
sélectionnée. Cet exemple envoie directement les octets et n’utilise ni bucket S3, ni sujet SNS, ni
file d’attente SQS. Chaque requête au service peut entraîner des frais.
Collez les commandes suivantes depuis le répertoire où vous souhaitez créer un projet
invoice-ocr. Le sous-shell préserve votre répertoire courant et les options du
shell. Si le répertoire de destination existe déjà, la configuration s’arrête avant l’installation.
Les versions des paquets fixent celles du SDK testé et du transport Guzzle utilisé directement par
l’exemple. Le manifeste local et la suppression des surcharges de chemins de Composer maintiennent
l’installation dans ce projet.
(
for tool in php composer; do
command -v "$tool" >/dev/null || { printf 'Missing prerequisite: %s\n' "$tool" >&2; exit 1; }
done &&
php -r 'foreach (["curl", "fileinfo", "SimpleXML", "gd"] as $extension) { if (!extension_loaded($extension)) { fwrite(STDERR, "Missing PHP extension: $extension\n"); exit(1); } } if (!(imagetypes() & IMG_PNG) || !(imagetypes() & IMG_JPG)) { fwrite(STDERR, "GD needs PNG and JPEG support\n"); exit(1); }' &&
unset COMPOSER COMPOSER_VENDOR_DIR COMPOSER_BIN_DIR &&
mkdir invoice-ocr &&
cd invoice-ocr &&
printf '{"require": {}, "config": {"vendor-dir": "vendor", "bin-dir": "vendor/bin"}}\n' > composer.json &&
composer require aws/aws-sdk-php:3.400.1 guzzlehttp/guzzle:8.2.0 &&
php -r 'require "vendor/autoload.php"; if (!class_exists(Aws\Textract\TextractClient::class) || !class_exists(GuzzleHttp\Handler\CurlHandler::class)) { exit(1); }'
)
Conservez composer.json et composer.lock. Si l’installation échoue
après la création du répertoire, corrigez le problème de prérequis ou de réseau signalé, puis
réessayez dans ce répertoire en préservant les fichiers existants :
(
unset COMPOSER COMPOSER_VENDOR_DIR COMPOSER_BIN_DIR &&
cd invoice-ocr &&
{ [ -f composer.json ] || printf '{"require": {}, "config": {"vendor-dir": "vendor", "bin-dir": "vendor/bin"}}\n' > composer.json; } &&
composer require aws/aws-sdk-php:3.400.1 guzzlehttp/guzzle:8.2.0 &&
php -r 'require "vendor/autoload.php"; if (!class_exists(Aws\Textract\TextractClient::class) || !class_exists(GuzzleHttp\Handler\CurlHandler::class)) { exit(1); }'
)
Comprendre l’API AnalyzeExpense
AnalyzeExpense
renvoie ExpenseDocuments, avec SummaryFields et des
LineItemGroups imbriqués. Les classifications des champs sont déduites : le nom d’un
fournisseur peut être absent même lorsque des totaux et des lignes de facture utiles sont reconnus.
Cet outil en ligne de commande accepte un fichier PNG ou JPEG local non vide contenant au maximum 5 000 000 octets, dont chaque dimension ne dépasse pas 5 000 pixels et dont le nombre total de pixels ne dépasse pas 10 000 000 pixels. Ces limites sont propres à l’application et incluent les octets présents après les données de l’image ; elles ne décrivent pas la taille maximale des données acceptées par AWS. GD doit décoder l’image sans émettre d’avertissement. Le décodage vérifie la lisibilité, sans prouver que chaque pixel d’une image endommagée est intact. Cet outil ne prend en charge ni les formats PDF et TIFF, ni les URL, ni le traitement multipage.
Créer un outil synchrone d’extraction de factures en ligne de commande
Enregistrez ce programme complet dans invoice-ocr/invoice.php. Il envoie les octets de
l’image d’origine et n’affiche le JSON qu’après la réussite de l’analyse par Textract et de l’analyse
syntaxique de la réponse. Le SDK gère la
sérialisation des octets ;
n’encodez pas vous-même les données d’entrée en base64.
<?php
use Aws\Handler\Guzzle\GuzzleHandler;
use Aws\Textract\TextractClient;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\HandlerStack;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
if ($argc !== 2) {
fwrite(STDERR, "Usage: php invoice.php <local-invoice.png|local-invoice.jpg>\n");
exit(1);
}
function parseExpense(array $result): array
{
return array_map(function (array $document): array {
$field = static fn (array $value): array => [
'type' => $value['Type']['Text'] ?? null,
'text' => $value['ValueDetection']['Text'] ?? null,
'confidence' => $value['ValueDetection']['Confidence'] ?? null,
];
$groups = [];
foreach ($document['LineItemGroups'] ?? [] as $group) {
$rows = [];
foreach ($group['LineItems'] ?? [] as $row) {
$rows[] = array_map($field, $row['LineItemExpenseFields'] ?? []);
}
$groups[] = ['index' => $group['LineItemGroupIndex'] ?? null, 'rows' => $rows];
}
return [
'expense_index' => $document['ExpenseIndex'] ?? null,
'summary' => array_map($field, $document['SummaryFields'] ?? []),
'line_item_groups' => $groups,
'requires_review' => true,
];
}, $result['ExpenseDocuments'] ?? []);
}
try {
foreach (['curl', 'fileinfo', 'SimpleXML', 'gd'] as $extension) {
if (!extension_loaded($extension)) {
throw new RuntimeException('Required PHP extension unavailable');
}
}
set_error_handler(static function (int $severity, string $message): never {
throw new RuntimeException('Local image or dependency could not be read');
});
$path = $argv[1];
if (str_contains($path, '://') || !is_file($path) || !is_readable($path)) {
throw new RuntimeException('Use a readable local file');
}
$bytes = file_get_contents($path, false, null, 0, 5_000_001);
if ($bytes === false || $bytes === '' || strlen($bytes) > 5_000_000) {
throw new RuntimeException('Use 1 to 5,000,000 bytes');
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->buffer($bytes);
if (!in_array($mime, ['image/png', 'image/jpeg'], true)) {
throw new RuntimeException('Use PNG or JPEG');
}
$size = getimagesizefromstring($bytes);
if ($size === false || $size[0] > 5_000 || $size[1] > 5_000
|| $size[0] * $size[1] > 10_000_000) {
throw new RuntimeException('Image dimensions exceed the application bounds');
}
$image = imagecreatefromstring($bytes);
if ($image === false) {
throw new RuntimeException('Image cannot be decoded');
}
unset($image);
require __DIR__ . '/vendor/autoload.php';
$textract = new TextractClient([
'region' => getenv('AWS_REGION') ?: 'us-east-1',
'version' => '2018-06-27',
'http_handler' => new GuzzleHandler(new Client([
'handler' => HandlerStack::create(new CurlHandler()),
])),
'http' => ['connect_timeout' => 5, 'timeout' => 30],
'retries' => 2,
]);
$result = $textract->analyzeExpense(['Document' => ['Bytes' => $bytes]]);
$documents = parseExpense($result->toArray());
if ($documents === []) {
throw new RuntimeException('No expense documents returned');
}
echo json_encode($documents, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), "\n";
} catch (Throwable $error) {
fwrite(STDERR, "Invoice analysis failed. Check local file readability, PNG/JPEG decoding, byte and pixel bounds, PHP extensions, SDK installation, credentials, region, and AnalyzeExpense permission.\n");
exit(1);
}
Placez l’image de votre facture dans le projet, puis exécutez la commande suivante depuis le
répertoire contenant invoice-ocr :
(cd invoice-ocr && php invoice.php ./invoice.png)
Pour un fichier JPEG, passez plutôt ./invoice.jpg. Les chemins contenant des espaces
doivent être placés entre guillemets dans le shell. Utilisez ./ avant un
nom de fichier commençant par un tiret. Le programme écrit uniquement sur stdout et stderr ; il ne
modifie ni le fichier d’entrée ni les fichiers de résultats existants. Évitez de rediriger stdout
vers un résultat précédent avant d’avoir vérifié la réussite de l’opération, car le shell peut
tronquer ce fichier même si l’analyse échoue. Le JSON de la facture peut contenir des données
sensibles : ne l’insérez pas dans des journaux publics.
Extraire et structurer les données des factures
L’analyseur syntaxique projette chaque donnée candidate du récapitulatif et des lignes de facture dans
type, text et confidence, en
préservant leur ordre et les limites entre documents et groupes. Les tableaux préservent les types
répétés qu’une structure associative indexée par TOTAL,
NAME ou OTHER écraserait. La valeur des propriétés
absentes reste null ; le score de confiance est celui du détecteur de valeur,
pas une garantie comptable.
Extrait d’une réponse d’exemple
Ce document fourni à titre d’illustration contient deux valeurs candidates du même type. Les catégories, valeurs et scores réels varient. La projection omet la géométrie, les libellés, les métadonnées de devise et les références aux pages sources.
[
{
"expense_index": 1,
"summary": [
{ "type": "TOTAL", "text": "125.00", "confidence": 97.2 },
{ "type": "TOTAL", "text": "125.00 USD", "confidence": 88.5 }
],
"line_item_groups": [],
"requires_review": true
}
]
Avant d’importer les données candidates dans la comptabilité fournisseurs, inspectez l’image source, interprétez les conventions décimales et monétaires, vérifiez la concordance des totaux avec les lignes de facture et consignez la décision prise lors de l’examen. Échappez le texte issu de l’OCR dans un tableau de bord. Si l’examen nécessite des coordonnées ou des références de pages, conservez la réponse complète du SDK conformément à votre politique de gestion des données plutôt que de vous appuyer sur cette projection réduite. Pour le prétraitement et la conversion de documents, consultez notre service de traitement de documents.
Gestion des erreurs et stratégies de nouvelle tentative
Le gestionnaire cURL impose un délai de connexion maximal de cinq secondes et un délai maximal de 30 secondes par tentative. Le réglage des nouvelles tentatives du SDK autorise deux nouvelles tentatives après la première. Le délai d’attente entre les tentatives, la recherche d’informations d’accès et le traitement local s’y ajoutent ; ces réglages ne fixent donc pas de durée maximale pour l’ensemble de la commande. Une connexion lente peut provoquer un échec, même pour une petite image. Vérifiez la connexion et relancez en connaissance de cause ; les appels répétés peuvent entraîner des frais.
Conservez HandlerStack::create : le
middleware par défaut
de Guzzle transforme les échecs HTTP en promesses rejetées que le SDK peut interpréter. Supprimer
cette pile modifie le chemin de traitement des erreurs du service. Une requête refusée se solde par
un code de sortie non nul, sans sortie sur stdout et avec un diagnostic expurgé. Ne considérez pas
un fichier vide créé par une redirection du shell comme un résultat d’extraction de facture réussi.
Aide-mémoire de dépannage
| Symptôme | Vérification suivante |
|---|---|
| La configuration s’est arrêtée avant la création du projet | Vérifiez chaque exécutable et chaque extension PHP requis |
| Composer a échoué après la création du projet | Corrigez l’erreur signalée, puis utilisez la procédure de nouvelle tentative d’installation ci-dessus |
| L’image locale est refusée | Vérifiez que le fichier n’est pas vide, qu’il contient bien une image PNG/JPEG, que le décodage réussit et que les deux limites en pixels sont respectées |
| L’analyse a échoué après les vérifications locales | Vérifiez les informations d’accès, la région, l’accès réseau et l’autorisation textract:AnalyzeExpense |
| Un champ attendu est absent | Inspectez la source et les autres données candidates ; les catégories et le rappel ne sont pas garantis |
| Les valeurs semblent plausibles | Conservez requires_review ; le score de confiance ne prouve pas l’exactitude des totaux, de la devise ou du paiement |
Mettre en œuvre le traitement asynchrone avec SNS + SQS
Pour les factures multipages ou une tâche qui doit se poursuivre après l’arrêt de son appelant,
choisissez le processus asynchrone de traitement des dépenses.
StartExpenseAnalysis lit les données depuis S3 ; GetExpenseAnalysis récupère les
résultats terminés, avec pagination. Le guide du traitement asynchrone
d’AWS décrit les notifications de fin de traitement via SNS et un consommateur SQS. Il s’agit d’une
application distincte : provisionnez ses ressources et ses autorisations, conservez durablement les
informations de propriété des tâches et l’état d’idempotence, et n’acquittez les messages qu’après
un traitement durable. L’outil synchrone en ligne de commande ci-dessus ne met pas en œuvre ce
processus.
