2026/04/09

はじめに

エプソンの複合機でスキャンしたPDFを、OneDriveに保存するだけで自動的にOCR処理される仕組みをAzure Functionsで構築しました。

きっかけは「ScanSnapのOCRで十分じゃない?」という疑問。実際に試した結果、単純なOCRだけならScanSnapで事足りますが、「スキャンしたら経理まで終わる」という業務フロー全体の自動化を目指すなら、Azure Document Intelligenceの方が圧倒的に優位でした。

当社はMicrosoftパートナーとして、Microsoft 365やAzureを活用した業務効率化を支援しています。以前はAzure Communication Servicesでwp_mailを置き換えるプラグインを作りましたが、今回もAzureの力を活かした自動化です。この記事では、自社で実際に運用している仕組みをそのまま公開します。

なぜAzure Document Intelligenceを選んだのか

ScanSnapやAdobe Acrobatにも内蔵OCR機能はあります。にもかかわらずAzureを選んだ理由は、OCRの精度ではなくその先の自動化にあります。

ScanSnap OCRAzure Document Intelligence
OCR精度(活字)
手書き認識×
表・レイアウト保持
自動実行×(手動操作が必要)◎(完全自動)
他サービス連携×◎(freee、Notion等)
コスト無料(付属ソフト)従量課金(Readモデル: 約0.2円/ページ、F0なら月500ページ無料)

本当の価値は最後の2行です。Azure Functionsの上に構築すれば、OCR結果をfreee APIで自動仕訳したり、Notionにメタデータを保存したり、後続処理をいくらでも追加できます。ScanSnapのOCRは「その場でテキスト検索できるPDFを作る」で完結しますが、Azureなら「スキャンした瞬間にバックオフィス業務が動き出す」基盤になります。

全体のアーキテクチャ

今回構築したシステムの全体像です。

スキャン → OCR → ファイル整理 全自動フロー図

ステップやること使うサービス
1複合機でスキャン → OneDriveに自動保存エプソン複合機 + OneDrive for Business
210分ごとに新しいPDFを検知Azure Functions(TimerTrigger)
3PDFをダウンロードMicrosoft Graph API
4OCR処理 → 検索可能なPDFを生成Azure Document Intelligence
5OCR済みPDFをアップロード、元ファイルを移動Microsoft Graph API

ユーザーが行う操作は「複合機のスキャンボタンを押す」だけです。それ以降はすべて自動で処理されます。

やったこと

1. Azure Functions プロジェクトの作成

今回はC#(.NET 8 isolated worker)で実装しました。Azure Functions v4 + TimerTriggerの構成です。

dotnet new func --name functions-ocr --worker-runtime dotnet-isolated
cd functions-ocr
dotnet add package Microsoft.Graph
dotnet add package Azure.Identity

TimerTriggerは10分間隔で実行します。スキャン後すぐにOCRされるわけではありませんが、業務上は10分以内に処理されれば十分です。

[Function("OcrProcessor")]
public async Task Run([TimerTrigger("0 */10 * * * *")] TimerInfo timer)
{
    _logger.LogInformation("OCR Processor started at {time}", DateTime.UtcNow);
    // ...
}

2. OneDriveからPDFを取得(Microsoft Graph API)

Microsoft Graph APIでOneDrive for Businessにアクセスします。認証はAzure AD(Entra ID)のクライアント資格情報フローを使用。

private GraphServiceClient CreateGraphClient()
{
    var credential = new ClientSecretCredential(TenantId, ClientId, ClientSecret);
    return new GraphServiceClient(credential);
}

private async Task<List<DriveItem>> ListPdfFiles(GraphServiceClient client, string driveId)
{
    var children = await client.Drives[driveId]
        .Items["root"]
        .ItemWithPath("epson_scan")
        .Children
        .GetAsync();

    return children?.Value?
        .Where(f => f.Name != null && f.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
        .ToList() ?? new List<DriveItem>();
}

フォルダ構成は以下の通りです。

フォルダ役割
epson_scan/複合機からのスキャン保存先(入力)
epson_scan_ocr/OCR済みPDF保存先(出力)
epson_scan_done/処理済み原本の退避先

参考: Microsoft Graph API – DriveItem の子アイテムを一覧表示する

3. Azure Document IntelligenceでOCR

OCR処理にはAzure Document Intelligenceのprebuilt-readモデルを使用します。PDFをPOSTすると、テキストレイヤーが追加された検索可能なPDFを返してくれます。

// OCRリクエスト送信
var reqContent = new ByteArrayContent(pdfBytes);
reqContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
var analyzeResponse = await httpClient.PostAsync(
    $"{DiEndpoint}/documentintelligence/documentModels/prebuilt-read:analyze?api-version=2024-11-30&output=pdf",
    reqContent
);

// ポーリングで完了を待つ
string status = "running";
while (status is "running" or "notStarted")
{
    await Task.Delay(3000);
    var pollResponse = await httpClient.GetAsync(operationUrl);
    // ... statusを確認
}

// 検索可能PDFを取得
var pdfResponse = await httpClient.GetAsync(
    $"{DiEndpoint}/documentintelligence/documentModels/prebuilt-read/analyzeResults/{operationId}/pdf?api-version=2024-11-30"
);

Document Intelligenceの処理は非同期で、POSTリクエスト後にポーリングで完了を待つ形式です。S0ティアでは500MBまでのPDFに対応しています。

参考: Azure Document Intelligence – REST API を使用する

4. レート制限への対策

Document IntelligenceのS0ティアにはリクエストレート制限があります。大量のスキャンを一度に処理するとHTTP 429(Too Many Requests)が返ることがあるため、リトライ処理を入れています。

if (analyzeResponse.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
    _logger.LogWarning("  Rate limited, waiting 30 seconds...");
    await Task.Delay(30000);
    continue;
}

5. 重複処理の防止

TimerTriggerは10分ごとに実行されるため、前回の実行でOCR済みだがまだ移動されていないファイルを再処理しないよう、出力フォルダの既存ファイル名をチェックしています。

var processedNames = await GetProcessedFileNames(graphClient, driveId);

if (processedNames.Contains(ocrFileName))
{
    _logger.LogInformation("Already OCR'd, moving: {file}", file.Name);
    await MoveFile(graphClient, driveId, file);
    continue;
}

つまったところ

Document Intelligenceの非同期処理

Document IntelligenceのREST APIは同期的にOCR結果を返しません。POSTリクエスト後にレスポンスヘッダーのOperation-Locationからポーリング用URLを取得し、完了するまでGETリクエストを繰り返す必要があります。

最初はLocationヘッダーだけを見ていてURLが取れず詰まりました。正しくはOperation-Locationヘッダーです。

operationUrl = analyzeResponse.Headers.Location?.ToString();
if (operationUrl == null && analyzeResponse.Headers.TryGetValues("Operation-Location", out var vals))
    operationUrl = vals.FirstOrDefault();

検索可能PDFの取得エンドポイント

output=pdfパラメータを付けてanalyzeリクエストを送ると、通常のJSON結果に加えて検索可能なPDFを取得できます。これは比較的新しいAPIで、ドキュメントでも見つけにくい機能でした。

今後の展開:スキャンしたら経理が終わる世界

現在はOCR処理と自動ファイル整理までですが、この仕組みの本当の価値はここから先にあります。

拡張内容使うサービス
請求書データ抽出OCR結果から金額・日付・取引先を自動抽出Document Intelligence(prebuilt-invoice)
自動仕訳登録抽出データをfreeeに自動登録freee API
メタデータ管理処理履歴・検索用データをNotionに保存Notion API
通知処理完了をTeams/Slackに通知Webhook

つまり、複合機のスキャンボタンを押すだけで「OCR → データ抽出 → 仕訳登録 → 記録保存」まで完了する世界です。月末の経理作業が「スキャンの束をトレイに置く」だけになります。

コスト

サービス料金目安
Azure Functions無料枠(月100万回実行まで)
Document Intelligence(Free F0)月500ページまで無料
Document Intelligence(S0 Read)$1.50/1,000ページ(約0.2円/ページ)
Document Intelligence(S0 Prebuilt)$10/1,000ページ(約1.5円/ページ、請求書モデル等)
OneDrive for BusinessMicrosoft 365に含まれる

Free(F0)ティアなら月500ページまで無料ですが、ファイルサイズ上限が4MB・分析は最初の2ページのみという制限があります。複合機のスキャンPDFは数十MBになることが多いため、実運用ではS0ティアが必須です。

Free(F0)S0(有料)
ファイルサイズ上限4MB500MB
分析ページ数最初の2ページのみ制限なし
月間ページ数500ページ従量課金

今回はS0のReadモデルをベースに採用しつつ、ファイルサイズとページ数でF0(無料)とS0(有料)を自動振り分けする仕組みを実装しました。

条件使用ティアコスト
4MB以下 かつ 2ページ以下F0無料
上記以外S0$1.50/1,000ページ

請求書や領収書など1〜2枚ものの書類はF0で無料処理し、複数ページの契約書や資料だけS0で課金される仕組みです。PDFのページ数はバイナリから直接読み取るので外部ライブラリも不要です。

// PDFバイナリからページ数を取得
private static int CountPdfPages(byte[] pdfBytes)
{
    var text = System.Text.Encoding.ASCII.GetString(pdfBytes);
    var match = Regex.Match(text, @"/Count\s+(\d+)");
    if (match.Success && int.TryParse(match.Groups[1].Value, out var count))
        return count;
    return Regex.Matches(text, @"/Type\s*/Page[^s]").Count;
}

// ファイルサイズ + ページ数でF0/S0を自動振り分け
var pageCount = CountPdfPages(pdfBytes);
var useF0 = f0Available && fileSize <= 4 * 1024 * 1024 && pageCount <= 2;
var endpoint = useF0 ? DiEndpointF0 : DiEndpoint;

実際の運用では、スキャン書類の半数以上が1〜2枚ものの請求書・領収書です。この振り分けだけでS0の課金量を大幅に削減できます。

参考: Azure Document Intelligence の価格

まとめ

  • ScanSnapの内蔵OCRは手軽だが「OCRして終わり」。Azure Document Intelligenceは業務自動化の入口になる
  • Azure Functions + Microsoft Graph API + Document Intelligenceの組み合わせで、スキャンからOCRまで完全自動化
  • F0/S0自動振り分けで請求書・領収書は無料処理。Microsoft 365を使っている企業ならすぐに導入できる
  • 今後はfreee API連携で「スキャンしたら仕訳まで終わる」仕組みに拡張予定
  • ソースコードの公開も検討中。この記事の内容で導入をお考えの方はお問い合わせください

業務自動化のご相談

株式会社ビギニングは、Microsoftパートナーとして中小企業のバックオフィス業務自動化を支援しています。

  • 紙書類のスキャン → OCR → 会計ソフト連携を自動化したい
  • Microsoft 365の環境を活かしてAzure Functionsで業務を効率化したい
  • freeeやNotionとの連携を含めた業務フロー全体を見直したい

お気軽にご相談ください。

お問い合わせはこちら