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 OCR | Azure Document Intelligence | |
|---|---|---|
| OCR精度(活字) | ○ | ◎ |
| 手書き認識 | × | ◎ |
| 表・レイアウト保持 | △ | ◎ |
| 自動実行 | ×(手動操作が必要) | ◎(完全自動) |
| 他サービス連携 | × | ◎(freee、Notion等) |
| コスト | 無料(付属ソフト) | 従量課金(Readモデル: 約0.2円/ページ、F0なら月500ページ無料) |
本当の価値は最後の2行です。Azure Functionsの上に構築すれば、OCR結果をfreee APIで自動仕訳したり、Notionにメタデータを保存したり、後続処理をいくらでも追加できます。ScanSnapのOCRは「その場でテキスト検索できるPDFを作る」で完結しますが、Azureなら「スキャンした瞬間にバックオフィス業務が動き出す」基盤になります。
全体のアーキテクチャ
今回構築したシステムの全体像です。
| ステップ | やること | 使うサービス |
|---|---|---|
| 1 | 複合機でスキャン → OneDriveに自動保存 | エプソン複合機 + OneDrive for Business |
| 2 | 10分ごとに新しいPDFを検知 | Azure Functions(TimerTrigger) |
| 3 | PDFをダウンロード | Microsoft Graph API |
| 4 | OCR処理 → 検索可能なPDFを生成 | Azure Document Intelligence |
| 5 | OCR済み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 Business | Microsoft 365に含まれる |
Free(F0)ティアなら月500ページまで無料ですが、ファイルサイズ上限が4MB・分析は最初の2ページのみという制限があります。複合機のスキャンPDFは数十MBになることが多いため、実運用ではS0ティアが必須です。
| Free(F0) | S0(有料) | |
|---|---|---|
| ファイルサイズ上限 | 4MB | 500MB |
| 分析ページ数 | 最初の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との連携を含めた業務フロー全体を見直したい
お気軽にご相談ください。