Extração de dados de faturas em tempo real com PHP e AWS Textract
Use a operação síncrona AnalyzeExpense do AWS Textract para transformar uma imagem local
de fatura em dados candidatos em JSON para revisão. A CLI em PHP abaixo preserva campos repetidos,
itens da fatura e escores numéricos de confiança. Ela não valida os cálculos de uma fatura nem autoriza
um pagamento.
Configurar o AWS Textract e o SDK para PHP v3
Você precisa de PHP com cURL, Fileinfo, SimpleXML e GD com suporte a PNG/JPEG, além do Composer instalado separadamente e do Bash para os comandos de shell. Este exemplo foi testado com PHP 8.5.11 e Composer 2.10.3. Use uma versão do PHP 8.5 que ainda receba manutenção; o próprio SDK oferece suporte ao PHP 8.1 e posteriores, mas isso não comprova a compatibilidade de todas as dependências desta instalação.
Configure as credenciais da AWS usando a
cadeia de provedores de credenciais do SDK.
A identidade que faz a chamada precisa da permissão textract:AnalyzeExpense na região
selecionada. Este exemplo de upload de bytes não usa bucket S3, tópico SNS nem fila SQS. Cada
requisição ao serviço pode gerar uma cobrança.
Cole os comandos a seguir no diretório onde deseja criar um novo projeto
invoice-ocr. O subshell mantém intactos o diretório atual e as opções do shell.
Se o destino já existir, a configuração será interrompida antes da instalação.
As versões dos pacotes fixam o SDK testado e o transporte Guzzle usado diretamente pelo exemplo.
O manifesto local e a limpeza das sobrescritas de caminho do Composer mantêm a instalação dentro
deste projeto.
(
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); }'
)
Mantenha composer.json e composer.lock. Se a instalação falhar após a
criação do diretório, corrija o problema de pré-requisito ou de rede informado e tente novamente dentro
desse diretório, preservando os arquivos existentes:
(
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); }'
)
Entender a API AnalyzeExpense
AnalyzeExpense
retorna ExpenseDocuments, com SummaryFields e
LineItemGroups aninhados. As classificações dos campos são inferidas: o nome de um
fornecedor pode estar ausente mesmo quando totais e itens úteis são reconhecidos.
Esta CLI aceita uma imagem local PNG ou JPEG não vazia com no máximo 5.000.000 bytes, com cada dimensão de no máximo 5.000 pixels e no máximo 10.000.000 pixels no total. Esses são limites da aplicação, incluindo bytes adicionais no fim do arquivo, e não uma afirmação sobre o tamanho máximo de payload aceito pela AWS. O GD precisa decodificar a imagem sem avisos. A decodificação verifica se a imagem pode ser lida, mas não prova que cada pixel de uma imagem danificada esteja intacto. PDF, TIFF, URLs e processamento de várias páginas estão fora do escopo desta CLI.
Criar uma CLI síncrona para faturas
Salve este programa completo como invoice-ocr/invoice.php. Ele envia os bytes originais da
imagem e imprime JSON somente depois que a análise e a interpretação da resposta são concluídas com
sucesso. O SDK cuida da
serialização dos bytes;
não codifique a entrada em base64 por conta própria.
<?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);
}
Coloque a imagem da fatura no projeto e execute os comandos a seguir no diretório que contém
invoice-ocr:
(cd invoice-ocr && php invoice.php ./invoice.png)
Para JPEG, passe ./invoice.jpg em vez disso. Caminhos com espaços precisam de aspas no
shell. Use ./ antes de um nome de arquivo que comece com hífen. O programa
grava apenas em stdout e stderr; ele não altera a entrada nem os arquivos de resultado existentes.
Evite redirecionar stdout para um arquivo de resultado anterior antes de confirmar o sucesso, pois o
shell pode truncar esse arquivo mesmo se a análise falhar. O JSON da fatura pode conter dados
sensíveis, então não o inclua em logs públicos.
Extrair e estruturar dados de faturas
O analisador projeta todos os dados candidatos de resumo e de itens da fatura em
type, text e confidence,
preservando sua ordem e os limites entre documentos e grupos. Arrays preservam tipos repetidos que
um mapa indexado por TOTAL, NAME ou
OTHER sobrescreveria. As propriedades ausentes permanecem
null; a confiança é o escore atribuído pelo detector de valores, não uma
garantia contábil.
Trecho de exemplo de resposta
Este documento ilustrativo tem dois dados candidatos do mesmo tipo. As categorias, os valores e os escores reais variam. A projeção omite geometria, rótulos, metadados de moeda e referências às páginas de origem.
[
{
"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
}
]
Antes de importar os dados candidatos para contas a pagar, inspecione a imagem de origem, interprete as convenções decimais e monetárias, concilie os totais com os itens da fatura e registre uma decisão de revisão. Em um painel, faça o escape do texto de OCR. Se a revisão precisar de coordenadas ou referências de página, retenha a resposta completa do SDK de acordo com sua política de dados, em vez de depender desta pequena projeção. Para pré-processamento e conversão de documentos, consulte nosso serviço de processamento de documentos.
Tratamento de erros e estratégias de novas tentativas
O manipulador cURL impõe um tempo limite de conexão de cinco segundos e um tempo limite de 30 segundos por tentativa. A configuração de novas tentativas do SDK permite duas novas tentativas após a primeira. O backoff, a busca de credenciais e o processamento local levam tempo adicional, portanto essas configurações não definem um prazo máximo para a execução completa do comando. Uma conexão lenta pode falhar mesmo com uma imagem pequena. Verifique a conexão e tente novamente com critério; chamadas repetidas podem gerar custos.
Mantenha HandlerStack::create: o
middleware padrão do Guzzle
transforma falhas HTTP em promises rejeitadas que o SDK consegue interpretar. Remover esse stack
altera o fluxo de tratamento de erros do serviço. Quando uma requisição é recusada, o processo
termina com código diferente de zero, stdout vazio e um diagnóstico sanitizado. Não trate um arquivo
vazio criado por redirecionamento do shell como um resultado de fatura bem-sucedido.
Guia rápido de solução de problemas
| Sintoma | Próxima verificação |
|---|---|
| Configuração interrompida antes de criar o projeto | Verifique todos os executáveis e extensões do PHP exigidos |
| Falha do Composer após a criação do projeto | Corrija o erro informado e use os comandos acima para tentar a instalação novamente |
| Imagem local recusada | Verifique se o arquivo não está vazio e confira o conteúdo PNG/JPEG real, a decodificação e ambos os limites de pixels |
| Falha na análise após as verificações locais | Verifique as credenciais, a região, o acesso à rede e a permissão textract:AnalyzeExpense |
| Campo esperado ausente | Inspecione a origem e os outros dados candidatos; as categorias e a revocação não são garantidas |
| Valores parecem plausíveis | Mantenha requires_review; a confiança não comprova a correção dos totais, da moeda ou do pagamento |
Implementar o processamento assíncrono com SNS + SQS
Para faturas com várias páginas ou uma tarefa que precise continuar mesmo após o encerramento do
chamador, escolha o fluxo de trabalho assíncrono de despesas.
StartExpenseAnalysis lê dados do S3; GetExpenseAnalysis recupera resultados
concluídos, com paginação. O guia de processamento assíncrono
da AWS descreve notificações de conclusão via SNS e um consumidor SQS. Essa é uma aplicação separada:
provisione seus recursos e permissões, persista o vínculo de cada tarefa com seu proprietário e o
estado de idempotência e confirme o recebimento das mensagens somente após o processamento durável.
A CLI síncrona acima não implementa esse fluxo de trabalho.
