毎月届く請求書を開き、会社名、請求日、請求番号、金額、支払期限をExcelへ入力する。

見積書、注文書、納品書、検査成績書、ミルシート、作業日報についても、ほぼ同じ作業を繰り返す。

一枚に数分しかかからない作業でも、月に100枚、500枚と増えれば、非常に大きな時間になります。

しかも、単純な転記作業ほど、次のようなミスが発生します。

  • 税抜金額と税込金額を取り違える
  • 請求番号を一文字間違える
  • 請求日と支払期限を逆にする
  • 同じ請求書を二重登録する
  • 取引先名の表記が統一されない
  • 入力後に原本を探し直せない

OCRソフトを導入すれば、文字を読み取ることはできます。

しかし、請求書には決まった統一レイアウトがありません。

会社ごとに、項目の位置、表の作り方、金額の表記、日付の形式が異なります。

従来型OCRだけでは、次のような判断が難しい場合があります。

  • どの会社名が請求元なのか
  • 複数ある日付のうち、どれが請求日なのか
  • 小計、消費税、合計の対応関係
  • 表のどの列が数量、単価、金額なのか
  • 検査成績書のどこが判定結果なのか

そこで本記事では、次の二つのオープンソース技術を組み合わせます。

  • Docling:PDFやOffice文書の構造、表、文字、読み順を解析する
  • LangExtract:解析した文章から、指定した項目を抽出し、原文との対応を残す

最終的に、次のWebアプリを作ります。

  1. ブラウザへPDFをドラッグ&ドロップする
  2. Doclingが文書を構造化する
  3. LangExtractが必要項目を抽出する
  4. 抽出した原文の根拠を表示する
  5. 人間が結果を確認・修正する
  6. 確認後だけExcel台帳へ追記する
  7. 元PDFの該当箇所を色付けする

クラウドAIを利用するGemini版と、文書を社外へ送らず処理できるOllama版の両方を用意します。

完成したソースコードはMITライセンスで公開できる構成にします。


今回作るWebアプリ

アプリ名は、Document Ledger AIとします。

対応する入力形式は次のとおりです。

  • PDF
  • Word:DOCX
  • Excel:XLSX
  • PowerPoint:PPTX
  • PNG、JPEG、TIFF画像

最初のバージョンでは、次の文書種別を用意します。

  • 請求書
  • 見積書
  • 注文書・発注書
  • 検査成績書

抽出する項目は次のとおりです。

  • 取引先・発行者
  • 文書日付
  • 請求番号、見積番号、注文番号、ロット番号
  • 小計
  • 消費税
  • 合計
  • 支払期限、納期、有効期限
  • 振込先
  • 備考、検査判定

なぜDoclingとLangExtractを組み合わせるのか

Doclingの役割

Doclingは、単にPDFから文字列を抜き出すだけではありません。

ページ内の構造を理解し、次の情報を整理します。

  • 見出し
  • 本文
  • 読み順
  • 数式
  • 画像
  • ページ構成

解析結果はMarkdown、HTML、JSONなどへ変換できます。

請求書のように、表と文字が混在する文書をLLMへ直接送るより、Doclingで読みやすい構造へ変換してから送る方が、項目抽出を安定させやすくなります。

LangExtractの役割

LangExtractは、抽出する項目を自然言語と例で定義します。

例えば次のように指定できます。

請求元会社名、請求日、請求番号、小計、消費税、
税込合計、支払期限を抽出してください。

原文に存在する文字だけを抽出し、
存在しない項目を推測してはいけません。

LangExtractの大きな特徴は、抽出結果を元の文章へ対応付けるSource Groundingです。

単に次のJSONを返すだけではありません。

{
  "supplier_name": "株式会社サンプル商事",
  "total": "110,000円"
}

「株式会社サンプル商事」「110,000円」が、入力文章のどこに存在したかを保持します。

そのため、利用者はAIの回答を盲信せず、抽出根拠を確認できます。


アプリ全体の処理

ブラウザ
  ↓ 文書アップロード
Streamlit
  ↓
Docling
  ↓ 構造化Markdown
LangExtract
  ↓ 抽出項目+原文位置
確認画面
  ↓ 人間が修正・確定
Excel台帳

Ollama版では、次の構成になります。

ブラウザ
  ↓
Document Ledger AI
  ├─ Docling
  ├─ LangExtract
  └─ Ollama
       └─ ローカルLLM

文書を外部AIへ送信しないため、社内機密文書を扱いやすくなります。

ただし、ローカルモデルはクラウドの高性能モデルより抽出精度が低くなる場合があります。


必要なPC・サーバー

Gemini版

  • CPU:4コア以上
  • メモリ:8GB以上、推奨16GB
  • 空き容量:20GB以上
  • Ubuntu 22.04または24.04
  • Docker

Ollama版

  • CPU:8コア以上
  • メモリ:16GB以上、推奨32GB
  • 空き容量:30GB以上
  • NVIDIA GPUがあれば高速化可能

GPUがなくても、小型モデルならCPUで実行できます。

ただし、一枚の文書に数十秒から数分かかる場合があります。


スタータープロジェクトを取得する

ダウンロードしたZIPを展開します。

unzip document-ledger-ai.zip
cd document-ledger-ai

構成は次のとおりです。

document-ledger-ai/
├── app.py
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── .env.example
├── README.md
├── LICENSE
├── SECURITY.md
├── deploy/
│   └── nginx.conf
└── data/
    ├── uploads/
    └── outputs/

各ファイルの役割

ファイル 役割
app.py Web画面と文書処理
requirements.txt Pythonライブラリ
Dockerfile アプリ用Dockerイメージ
docker-compose.yml アプリとOllamaを起動
.env.example 環境変数の見本
nginx.conf サーバー公開用設定
LICENSE MITライセンス
SECURITY.md セキュリティ方針

Dockerを導入する

UbuntuへDocker公式版を導入します。

sudo apt update

sudo apt install -y \
  ca-certificates \
  curl

sudo install -m 0755 -d /etc/apt/keyrings

sudo curl -fsSL \
  https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc

sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo ${UBUNTU_CODENAME:-$VERSION_CODENAME}) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update

sudo apt install -y \
  docker-ce \
  docker-ce-cli \
  containerd.io \
  docker-buildx-plugin \
  docker-compose-plugin

確認します。

sudo docker run --rm hello-world
docker compose version

環境設定ファイルを作る

cp .env.example .env

内容を確認します。

nano .env

初期値は次のようになっています。

MAX_UPLOAD_MB=20
LANGEXTRACT_API_KEY=
GEMINI_MODEL=gemini-3.5-flash
OLLAMA_MODEL=gemma2:2b
OLLAMA_URL=http://ollama:11434

Geminiを使う場合

Google AI Studioなどで取得したキーを設定します。

LANGEXTRACT_API_KEY=ここへAPIキー
GEMINI_MODEL=gemini-3.5-flash

APIキーをGitHubへ登録してはいけません。

.gitignoreには、最初から.envが含まれています。

Ollamaを使う場合

APIキーは不要です。

OLLAMA_MODEL=gemma2:2b
OLLAMA_URL=http://ollama:11434

より高性能なモデルを使用する場合は、サーバーのメモリとGPUに合わせて変更してください。


Webアプリを起動する

docker compose up -d --build

初回はDocling、PyTorchなどを取得するため時間がかかります。

状態を確認します。

docker compose ps

ログを確認します。

docker compose logs -f app

同じPCのブラウザで、次を開きます。

http://127.0.0.1:8501

Ollamaモデルを取得する

docker compose exec ollama \
  ollama pull gemma2:2b

モデル一覧を確認します。

docker compose exec ollama \
  ollama list

Ollamaが応答するか確認します。

curl http://127.0.0.1:11434/api/tags

実際に請求書を読み取る

  1. 文書種別で「請求書」を選ぶ
  2. AIでOllamaまたはGeminiを選ぶ
  3. PDFをアップロードする
  4. 「読み取りを開始」を押す
  5. 抽出結果を確認する
  6. 間違いがあれば修正する
  7. 「確認済みとしてExcelへ追記」を押す
  8. Excel台帳をダウンロードする

Excelには次の列が作られます。

文書種別
元ファイル名
取引先・発行者
文書日付
文書番号・ロット
小計
税
合計
支払期限・納期
振込先
備考・判定

Doclingによる文書変換

中心となる処理は次の部分です。

from docling.document_converter import DocumentConverter

converted = DocumentConverter().convert(stored_path)
source_text = converted.document.export_to_markdown()

DocumentConverterへアップロードファイルを渡し、DoclingDocumentへ変換します。

その後、Markdownとして取り出します。

請求書に次の表があった場合、通常のOCRよりも構造を維持した状態で出力しやすくなります。

| 品名 | 数量 | 単価 | 金額 |
|---|---:|---:|---:|
| 商品A | 10 | 1,000 | 10,000 |
| 商品B | 5 | 2,000 | 10,000 |

LangExtractへ抽出ルールを教える

請求書用の指示は、次のように定義しています。

Extract only text explicitly present in the invoice.
Use verbatim source text.
Do not infer missing values.

Extract:
supplier_name
document_date
document_number
subtotal
tax
total
payment_due_date
bank_account
notes

重要なのは、次の三つです。

  • 原文に存在する文字だけを使う
  • 存在しない値を推測しない
  • 原文の表記をそのまま抽出する

AIへ単に「請求書をJSONにしてください」と頼むと、値を推測したり、表記を書き換えたりする場合があります。

会計処理で推測値を使うことは危険です。


Few-shot exampleが精度を左右する

LangExtractでは、見本となる文章と正解を与えます。

example_text = """
株式会社サンプル商事
請求日 2026年7月1日
請求番号 INV-0701
小計 100,000円
消費税 10,000円
合計 110,000円
支払期限 2026年7月31日
"""

正解例:

supplier_name = 株式会社サンプル商事
document_date = 2026年7月1日
document_number = INV-0701
subtotal = 100,000円
tax = 10,000円
total = 110,000円
payment_due_date = 2026年7月31日

実際の自社帳票に近い匿名化サンプルを複数用意すると、抽出精度を改善できます。

ただし、例の中に実在する会社名、口座番号、個人情報を残さないでください。


AIが作った値ではなく、原文根拠を確認する

抽出結果には、原文中の位置情報が入ります。

アプリでは、次のように確認します。

interval = extraction.char_interval
grounded = interval is not None

原文で位置を特定できなかった値は、「要確認」と表示します。

LangExtractが生成する確認HTMLもダウンロードできます。

確認HTMLでは、抽出した項目が元文章内で色分けされます。


元PDFへ色を付ける

PyMuPDFを使い、抽出文字をPDF内で検索します。

for page in document:
    for text in extracted_terms:
        rectangles = page.search_for(text)

        for rectangle in rectangles:
            annotation = page.add_highlight_annot(rectangle)
            annotation.update()

ただし、この方法には制約があります。

色付けできるPDF

  • 文字をマウスで選択できるPDF
  • テキストレイヤーが存在するPDF
  • 抽出結果とPDF内表記が一致するPDF

色付けできない場合があるPDF

  • 紙をスキャンしただけのPDF
  • 文字が画像になっているPDF
  • 特殊な文字コードを使うPDF
  • 文字が細かく分割されているPDF

色付けできない場合でも、LangExtractの根拠HTMLは確認できます。


確認後だけExcelへ保存する

AIの結果を直接会計台帳へ登録してはいけません。

本アプリでは、確認ボタンを押すまでExcelへ追記しません。

if confirmed:
    append_excel(
        edited_values,
        document_type,
        original_filename
    )

このHuman-in-the-Loopは、業務AIで特に重要です。

次の項目は必ず人間が確認してください。

  • 合計金額
  • 消費税
  • 振込先
  • 口座番号
  • 支払期限
  • 納期
  • 検査判定

検査成績書へ横展開する

文書種別ごとにPromptと例を追加します。

"検査成績書": {
    "prompt": """
    発行者、発行日、ロット番号、
    証明書番号、総合判定を抽出してください。
    """,
    "example": "...",
    "extractions": [...]
}

さらに次の項目を追加できます。

  • 品番
  • 品名
  • ロット
  • 規格値
  • 測定値
  • 単位
  • 上限値
  • 下限値
  • 合否
  • 検査者

明細表をExcelへ出す場合は、ヘッダー項目と明細項目を分けて抽出します。


ミルシートへ横展開する

鋼材などのミルシートでは、次の項目を対象にできます。

  • 製造者
  • 規格
  • 鋼種
  • Heat Number
  • 寸法
  • 数量
  • 化学成分
  • 引張強度
  • 降伏点
  • 伸び

数値の桁や小数点を誤ると品質保証上の問題になるため、AIによる自動確定は禁止し、原本照合を必須にします。


サーバーへ配置する

Ubuntuサーバーで、次の場所へ配置します。

sudo mkdir -p /opt/document-ledger-ai
sudo chown "$USER":"$USER" /opt/document-ledger-ai

cd /opt/document-ledger-ai

GitHubへ公開後はcloneします。

git clone \
  https://github.com/あなたのユーザー名/document-ledger-ai.git \
  .
cp .env.example .env
nano .env

docker compose up -d --build

Nginxを設定する

sudo apt install -y nginx

設定ファイルを配置します。

sudo cp deploy/nginx.conf \
  /etc/nginx/sites-available/document-ledger-ai

ドメインを変更します。

sudo nano \
  /etc/nginx/sites-available/document-ledger-ai

有効化します。

sudo ln -s \
  /etc/nginx/sites-available/document-ledger-ai \
  /etc/nginx/sites-enabled/document-ledger-ai

確認して再読み込みします。

sudo nginx -t
sudo systemctl reload nginx

HTTPSを設定する

sudo apt install -y \
  certbot \
  python3-certbot-nginx
sudo certbot \
  --nginx \
  -d document-ai.example.com

文書アップロードアプリをHTTPのまま利用してはいけません。

通信途中で文書が盗み見られる可能性があります。


認証なしで公開してはいけない

このアプリには、請求書や検査成績書などの機密文書が投入されます。

最低でも次のいずれかを実施します。

  • 社内LANだけからアクセス
  • VPN経由だけでアクセス
  • Nginx Basic認証
  • Google・MicrosoftアカウントによるSSO
  • Cloudflare Access

Basic認証の例です。

sudo apt install -y apache2-utils

sudo htpasswd -c \
  /etc/nginx/.htpasswd \
  admin

Nginxのlocationへ追加します。

auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;

文書の保存期間を決める

アップロードした文書を無期限に残すべきではありません。

30日以上経過したファイルを削除する例:

find /opt/document-ledger-ai/data/uploads \
  -type f \
  -mtime +30 \
  -delete

cronへ登録できます。

0 3 * * * find /opt/document-ledger-ai/data/uploads -type f -mtime +30 -delete

会社の文書保存規程や監査要件に合わせて変更してください。


GitHubでOSSとして公開する

Gitリポジトリを作る

git init
git add .
git commit -m "Initial open source release"

GitHubへ登録する

git branch -M main

git remote add origin \
  https://github.com/あなたのユーザー名/document-ledger-ai.git

git push -u origin main

公開前の確認

git status
git grep -n "API_KEY"
git log --all -p -- .env

APIキーを一度でもコミットした場合、ファイルを削除するだけでは不十分です。

キーを失効し、再発行してください。

OSSに必要な文書

  • README.md
  • LICENSE
  • SECURITY.md
  • CONTRIBUTING.md
  • スクリーンショット
  • サンプル文書
  • 利用上の注意

READMEへ載せるスクリーンショット

次の三枚を掲載すると、利用者が理解しやすくなります。

  1. アップロード画面
  2. 抽出結果の確認画面
  3. 完成したExcel台帳

実在する取引先名、金額、口座番号を含むスクリーンショットを公開してはいけません。

必ず架空のサンプルを使います。


精度を上げる方法

文書種別を細かく分ける

「業務文書」という一つのPromptにまとめず、請求書、見積書、検査成績書ごとに分けます。

会社別プロファイルを作る

同じ取引先から毎月同じ形式で届く場合は、会社別の見本を用意します。

原文の表記を維持する

LangExtractへ、要約・推測・書き換えを禁止します。

複数回抽出する

extraction_passesを増やすと、長い文書で見落としを減らせる可能性があります。

ただし、処理時間とAPI費用が増えます。

検証ルールを追加する

例えば次を自動確認します。

小計+消費税=合計

一致しない場合は警告を出します。


本番利用前に追加すべき機能

  • ログイン機能
  • 権限管理
  • 二重登録防止
  • 文書ハッシュによる重複確認
  • 取引先マスターとの照合
  • 金額の数値変換
  • 日付形式の統一
  • 処理履歴
  • 修正履歴
  • 監査ログ
  • ウイルススキャン
  • バックアップ

AIへ渡す文書の危険性

請求書や履歴書には、次の情報が含まれる可能性があります。

  • 会社名
  • 個人名
  • 住所
  • 電話番号
  • メールアドレス
  • 口座情報
  • 取引金額
  • 購買内容
  • 製造条件

クラウドAIを使う場合は、サービスの保存、学習利用、ログ保持、契約条件を確認してください。

社外送信が許可されない文書では、Ollamaなどのローカル環境を使用します。


まとめ

DoclingとLangExtractを組み合わせることで、単純なOCRより一歩進んだ業務文書処理を作れます。

Doclingが文書の構造を読み、LangExtractが必要な項目を原文根拠付きで抽出します。

完成したWebアプリでは、次の処理を実現しました。

  • PDF・Office文書・画像のアップロード
  • 請求書、見積書、注文書、検査成績書の切り替え
  • 取引先、日付、番号、金額の抽出
  • 原文根拠の表示
  • 元PDFへの色付け
  • 人間による確認・修正
  • Excel台帳への追記
  • GeminiとOllamaの切り替え
  • Dockerによるサーバー配置
  • GitHubでのOSS公開

ただし、業務AIで重要なのは、自動化率を100%にすることではありません。

誤った金額や口座番号を自動登録しないために、人間が短時間で確認できる仕組みを作ることが重要です。

AIに入力作業を任せ、人間は判断と最終確認に集中する。

これが、事務作業へAIを安全に導入する現実的な方法です。


参考リンク

Docling、LangExtract、Gemini、Ollamaの仕様やモデル名は今後変更される可能性があります。導入時は各公式ドキュメントの最新版を確認してください。

ABOUT ME
Yuusuke
業務システムの要件定義・設計・実装を担当。Python、PHP、JavaScript、VBA、Oracle、MySQLなど、実務と個人開発で検証した内容を発信しています。