開発情報・ナレッジ

投稿者: ShiningStar株式会社 2025年5月9日 (金)

OpenAIとpdf.jsで実現するPDFを自動解析しフォーム入力するサンプルプログラム

本記事では、PDFをアップロードして自動解析し、
フォームのフィールドへ情報を自動入力する仕組みを2種類ご紹介します。
pdf.js を用いてブラウザ上でPDFを解析し、
OpenAIのサービスを利用してテキスト情報を抽出する方法になります。

2種類の解析手法をご紹介します。
1. テキスト解析:PDF内部にテキストデータが存在する場合に有効で、素早く高精度なテキスト抽出が可能。
2. 画像解析:スキャンされた手書きドキュメントや画像主体のPDFで利用可能。OCR的なプロセスでテキストを抽出。

今回の記事では請求書を例にとっていますが、
プロンプトやフィールドの対応付けを変えることで、
契約書、見積書、保険証券、レジュメなど、あらゆるドキュメントから必要な情報を自動的に抽出が可能です。

テキストPDFの場合はテキスト解析を、
手書きやスキャン画像が多い場合は画像解析を用いることで、
様々なフォーマットでのドキュメント処理が行いやすくなります。


注意点

・利用するpdf.jsは 公式サイト を参照し、実装方法を確認してください。
cdnを利用し、headタブに貼り付けることで、簡単に利用できます。

・テキスト解析はPDF内のテキストデータに依存するため、デジタル生成されたPDFに適しています。
・画像解析は手書きやスキャン画像などOCRが必要なケースで有効ですが、精度は画像品質・openAIのモデル性能に依存します。
・OpenAI APIを利用するため、APIキーやモデル設定が必要です(今回はgpt-4oを使用しています)。
利用規約や料金体系を確認してください。


DB設定

PDFから読み取った値を格納したいフィールドを適宜作成してください。
今回は請求書を想定して、下記のフィールドで作成します。

フィールド名 フィールドタイプ
請求書番号 テキストフィールド
請求先 テキストフィールド
請求日 テキストフィールド
支払期限 テキストフィールド
合計金額 テキストフィールド

実際に利用する場合は、PDFのフォーマットに合わせて適宜フィールドを選択して作成してください。
セレクトフィールドや日付,時刻系等フォーマットエラーが起きやすいフィールドを使用する場合は、
プロンプトにその制約を記載してエラーが起きないように調整が必要です。

コード設定方法

以下は、サンプルコードです。
フォームを設置しているページのJavaScriptタブにJavascriptを設定し、
カスタムAPIにPHPをを設定してご利用ください。
ソースはサンプルのため、環境に合わせて変更してください。

read-pdfのidがついたボタンを押下すると、
PDFを選択するダイアログが表示され、PDFを選択すると、
PDFの内容を解析し、フィールドに値が自動入力されます。
ですので、PDFを選択するボタンをread-pdfのIDを指定して設置してください。

【テキスト解析版:カスタムAPI PHP例】
<?php
// OpenAI APIキーを設定
$api_key = 'あなたのOpenAI API KEYを入力してください。';

// SPIRAL から受信したデータを取得
$input = $SPIRAL->getCustomApiRequestBody();
$text = $input['text'] ?? '';

// レスポンス用配列を初期化
$response = [
    'status' => '',
    'data' => []
];

// テキストが空の場合はエラーで返却
if (empty($text)) {
    $response['status'] = 'failed';
    $response['data']['success'] = false;
    $response['data']['error'] = 'テキストが提供されていません。';
    $SPIRAL->setCustomApiResponse($response);
    return;
}

// OpenAI API に送るプロンプトを作成
$prompt = <<<EOT
以下のテキストは請求書の内容です。請求書番号、請求先名、請求日、支払期限、合計金額を抽出し、以下のJSON形式で返してください。

{
    "invoice_number": "",
    "invoice_name": "",
    "invoice_date": "",
    "due_date": "",
    "total_amount": ""
}
---
$text
EOT;

// cURL セットアップ
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer ' . $api_key
]);

// リクエストペイロード
$data = [
    'model' => 'gpt-4o',
    'messages' => [
        ['role' => 'system', 'content' => 'あなたは請求書から重要な情報を抽出するアシスタントです。'],
        ['role' => 'user',   'content' => $prompt]
    ],
    'max_tokens' => 500,
    'temperature' => 0.2,
    'response_format' => ['type' => 'json_object'],
];

// JSON 化して送信
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

// 実行&取得
$result = curl_exec($ch);
if (curl_errno($ch)) {
    $response['status'] = 'failed';
    $response['data']['success'] = false;
    $response['data']['error'] = 'cURLエラー: ' . curl_error($ch);
    curl_close($ch);
    $SPIRAL->setCustomApiResponse($response);
    return;
}
curl_close($ch);

// レスポンスをデコード
$responseData = json_decode($result, true);

if (
    isset($responseData['choices'][0]['message']['content'])
    && $extracted = json_decode($responseData['choices'][0]['message']['content'], true)
) {
    // 成功
    $response['status'] = 'success';
    $response['data']['success'] = true;
    $response['data']['data'] = [
        'result' => $extracted,
    ];
} else {
    // API レスポンス異常
    $response['status'] = 'failed';
    $response['data']['success'] = false;
    $response['data']['error'] = 'OpenAI APIから有効なレスポンスが得られませんでした。';
}

// 最後に必ずレスポンスを返す
$SPIRAL->setCustomApiResponse($response);

            

【テキスト解析版:Javascript例】
// フィールドマッピング設定
const FIELD_MAPPING = {
    invoice_number: 'input[name="f01"]', //請求書番号
    invoice_name:   'input[name="f02"]', //請求先
    invoice_date:   'input[name="f03"]', //請求日
    due_date:       'input[name="f04"]', //支払期限
    total_amount:   'input[name="f05"]' //合計金額
};
//接続先設定
const apiUrl      = '/_program/pdfopenAI'; //カスタムAPIのURLに修正してください。

document.addEventListener('DOMContentLoaded', () => {

    const readPdfButton = document.getElementById('read-pdf');
    if (!readPdfButton) {
        console.error('Button with id "read-pdf" not found.');
        return;
    }

    readPdfButton.addEventListener('click', async () => {
        const fileInput = document.getElementById('pdf-file');
        const loader    = document.getElementById('loader');
        const errorDiv  = document.getElementById('error');

        // エラーメッセージをクリア
        errorDiv.textContent   = '';
        errorDiv.style.display = 'none';

        if (!fileInput || fileInput.files.length === 0) {
            alert('請求書PDFファイルを選択してください。');
            return;
        }

        loader.style.display = 'block';
        const file = fileInput.files[0];
        const reader = new FileReader();

        reader.onload = async function() {
            try {
                // PDF.js でテキスト抽出
                const typedarray = new Uint8Array(this.result);
                const pdf        = await pdfjsLib.getDocument(typedarray).promise;
                let extractedText = '';
                for (let i = 1; i <= pdf.numPages; i++) {
                    const page        = await pdf.getPage(i);
                    const textContent = await page.getTextContent();
                    extractedText    += textContent.items.map(item => item.str).join(' ') + '\n';
                }

                // API リクエスト
                const response = await fetch(apiUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ text: extractedText })
                });

                if (!response.ok) {
                    throw new Error(`HTTP エラー: ${response.status}`);
                }

                const json = await response.json();
                console.log('API レスポンス:', json);

                // ネストされたステータスチェック
                if (json.status !== 'success') {
                    throw new Error(json.message || 'API レベルで失敗しました。');
                }
                const level2 = json.data;
                if (level2.status !== 'success') {
                    throw new Error('プログラムレベルで失敗しました。');
                }
                const level3 = level2.data;
                if (!level3.success) {
                    throw new Error(level3.error || '処理に失敗しました。');
                }

                // 抽出結果を取得
                const result = (level3.data && level3.data.result) || {};

                // フォームに反映
                for (const [key, selector] of Object.entries(FIELD_MAPPING)) {
                    const el = document.querySelector(selector);
                    if (el) {
                        el.value = result[key] || '';
                    } else {
                        console.warn(`セレクター "${selector}" に対応する要素が見つかりません。`);
                    }
                }
            } catch (err) {
                console.error(err);
                errorDiv.textContent   = err.message || 'エラーが発生しました。';
                errorDiv.style.display = 'block';
            } finally {
                loader.style.display = 'none';
            }
        };

        reader.readAsArrayBuffer(file);
    });
});

            

【画像解析版:カスタムAPI PHP例】
<?php
// OpenAI API キー
$api_key = 'あなたのOpenAI API KEYを記載してください。';

// リクエストボディを取得(JSON)
$input = $SPIRAL->getCustomApiRequestBody();
$images = $input['images'] ?? [];

if (empty($images) || !is_array($images)) {
    $SPIRAL->setCustomApiResponse([
        'status' => 'failed',
        'data' => [
            'status' => 'failed',
            'data' => [
                'success' => false,
                'error' => '画像データが提供されていません。'
            ]
        ]
    ]);
    return;
}

// プロンプト作成
$prompt = <<<EOT
以下の画像は請求書のスキャンです。請求書番号、請求先名、請求日、支払期限、合計金額を抽出し、JSONで返してください。
{
  "invoice_number": "",
  "invoice_name": "",
  "invoice_date": "",
  "due_date": "",
  "total_amount": ""
}
EOT;

// OpenAI へ送信(最初の画像のみ利用例。複数画像を並べる場合は messages を拡張)
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer ' . $api_key
]);
$payload = [
    'model' => 'gpt-4o',
    'messages' => [
        ['role'=>'system', 'content'=>'あなたは請求書情報を正確にJSONで抽出するAIです。'],
        ['role'=>'user',   'content'=>[
            ['type'=>'text',      'text'=>$prompt],
            ['type'=>'image_url', 'image_url'=>['url'=>"data:image/png;base64,{$images[0]}"]]
        ]]
    ],
    'temperature' => 0.2,
    'max_tokens'  => 500,
    'response_format' => ['type' => 'json_object'],
];
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
$result = curl_exec($ch);
if (curl_errno($ch)) {
    $err = curl_error($ch);
    curl_close($ch);
    $SPIRAL->setCustomApiResponse([
        'status'=>'failed',
        'data'=>[
            'status'=>'failed',
            'data'=>[
                'success'=>false,
                'error'=>"cURLエラー: {$err}"
            ]
        ]
    ]);
    return;
}
curl_close($ch);

// レスポンス解析
$apiResp = json_decode($result, true);
$content = $apiResp['choices'][0]['message']['content'] ?? '';
$extracted = json_decode($content, true);

if (json_last_error() !== JSON_ERROR_NONE || !is_array($extracted)) {
    $SPIRAL->setCustomApiResponse([
        'status'=>'failed',
        'data'=>[
            'status'=>'failed',
            'data'=>[
                'success'=>false,
                'error'=>'JSON形式で情報を抽出できませんでした: '.$content
            ]
        ]
    ]);
    return;
}

// 成功レスポンスを返却
$SPIRAL->setCustomApiResponse([
    'status' => 'success',
    'data' => [
        'status' => 'success',
        'data' => [
            'success' => true,
            'data' => [
                'result' => [
                    'invoice_number'=> $extracted['invoice_number'] ?? '',
                    'invoice_name'  => $extracted['invoice_name'] ?? '',
                    'invoice_date'  => $extracted['invoice_date'] ?? '',
                    'due_date'      => $extracted['due_date'] ?? '',
                    'total_amount'  => $extracted['total_amount'] ?? ''
                ]
            ]
        ]
    ]
]);
?>

            

【画像解析版:Javascript例】
// フィールドマッピング設定
const FIELD_MAPPING = {
    invoice_number: 'input[name="f01"]', //請求書番号
    invoice_name:   'input[name="f02"]', //請求先
    invoice_date:   'input[name="f03"]', //請求日
    due_date:       'input[name="f04"]', //支払期限
    total_amount:   'input[name="f05"]' //合計金額
};
//接続先設定
const apiUrl      = '/_program/pdfopenAI'; //カスタムAPIのURLに修正してください。

document.addEventListener('DOMContentLoaded', () => {
  const readPdfButton = document.getElementById('read-pdf');
  if (!readPdfButton) {
    console.error('Button with id "read-pdf" not found.');
    return;
  }

  readPdfButton.addEventListener('click', async () => {
    const fileInput = document.getElementById('pdf-file');
    const loader    = document.getElementById('loader');
    const errorDiv  = document.getElementById('error');

    // エラーメッセージをクリア
    errorDiv.textContent   = '';
    errorDiv.style.display = 'none';

    if (!fileInput || fileInput.files.length === 0) {
      alert('請求書PDFファイルを選択してください。');
      return;
    }

    loader.style.display = 'block';
    const file   = fileInput.files[0];
    const reader = new FileReader();

    reader.onload = async function() {
      try {
        // PDF.js でページごとに Canvas 描画 → Base64 取得
        const pdf        = await pdfjsLib.getDocument(new Uint8Array(this.result)).promise;
        const base64Imgs = [];
        for (let i = 1; i <= pdf.numPages; i++) {
          const page    = await pdf.getPage(i);
          const vp      = page.getViewport({ scale: 2 });
          const canvas  = document.createElement('canvas');
          canvas.width  = vp.width;
          canvas.height = vp.height;
          await page.render({ canvasContext: canvas.getContext('2d'), viewport: vp }).promise;
          base64Imgs.push(canvas.toDataURL('image/png').split(',')[1]);
        }

        // JSON で送信
        const resp = await fetch(apiUrl, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ images: base64Imgs })
        });
        if (!resp.ok) {
          throw new Error(`HTTP エラー: ${resp.status}`);
        }

        const json = await resp.json();
        console.log('API レスポンス:', json);

        // 深いネストに対応したステータスチェック
        if (json.status !== 'success')                    throw new Error(json.message || 'API レベルで失敗');
        const lvl2 = json.data;
        if (lvl2.status !== 'success')                    throw new Error('プログラムレベル1で失敗');
        const lvl3 = lvl2.data;
        if (lvl3.status !== 'success')                    throw new Error('プログラムレベル2で失敗');
        const lvl4 = lvl3.data;
        if (!lvl4.success)                                throw new Error(lvl4.error || '処理に失敗');
        const result = (lvl4.data && lvl4.data.result) || {};

        // フォームに反映
        for (const [key, selector] of Object.entries(FIELD_MAPPING)) {
          const el = document.querySelector(selector);
          if (el) el.value = result[key] || '';
          else   console.warn(`セレクター "${selector}" が見つかりません`);
        }
      } catch (err) {
        console.error(err);
        errorDiv.textContent   = err.message;
        errorDiv.style.display = 'block';
      } finally {
        loader.style.display = 'none';
      }
    };

    reader.readAsArrayBuffer(file);
  });
});

            

プロンプトについて

以下の画像は請求書のスキャン画像です。請求書番号、請求先名、請求日、支払期限、合計金額を抽出し、以下のJSON形式で返してください。
{
    "invoice_number": "",
    "invoice_name": "",
    "invoice_date": "",
    "due_date": "",
    "total_amount": ""
}
            
「以下の画像は請求書のスキャン画像です。請求書番号、請求先名、請求日、支払期限、合計金額を抽出し、以下のJSON形式で返してください。」
定義したい項目を上記のように日本語で大丈夫なので記載してください。
見積書の場合、見積番号、見積先、案件名等に変更が必要です。
OpenAIのAPIにてJSONモードを使用する為、仕様上「JSON」という単語をプロンプトに必ず含める必要があります。
また、JSONの例も提示が必要です。
OpenAIのAPIではPDFをこのJSONに変換する処理を行っています。
JSONに変換されたデータは、「フィールドマッピングについて」に記載されている、
マッピングでデータを挿入する処理がJavaScriptにて実行されます。

フィールドマッピングについて

// フィールドマッピング設定
const FIELD_MAPPING = {
    invoice_number: 'input[name="f01"]', //請求書番号
    invoice_name:   'input[name="f02"]', //請求先
    invoice_date:   'input[name="f03"]', //請求日
    due_date:       'input[name="f04"]', //支払期限
    total_amount:   'input[name="f05"]' //合計金額
};
            
JSONとフォーム上のフィールドの紐づけ等の設定を行います。
プロンプトにて定義したJSONのキーとフォームのname値を合わせて定義してください。
"f0XX"の"XX"の個所は、登録フォームで使用しているDBのフィールドのIDとなります。

最後に

設定後は動作確認を必ず行い、動作に問題がないか確認をしてください。
特にプロンプトの調整は何回かトライエンドエラーが必要な箇所だと思いますので、十分に確認してください。

不具合やほかのやり方が知りたい等あれば、下記の「コンテンツに関しての要望はこちら」からご連絡ください。

解決しない場合はこちら コンテンツに関しての
要望はこちら