📑

F# - Google Drive API で OCR

2021/05/31に公開

前提

  • Google Drive には PDF や画像ファイルをドキュメントに変換する OCR 機能があります[1]

事前準備

  1. Drive API、Docs API が有効な Google Cloud Platform のプロジェクトを用意する [↗]
  2. credentials.json を作る [↗]

環境

プロジェクトの構成

プロジェクトを作成する

dotnet new console -lang "F#" -o 場所\プロジェクト名

ライブラリを追加する

作成したプロジェクトを Visual Studio Code で開き、ターミナル (Ctrl + `) で下記のコマンドを実行する。

dotnet add package Google.Apis.Drive.v3
dotnet add package Google.Apis.Docs.v1

credentials.json を利用できるようにする

プロジェクトフォルダの直下に credentials.json を配置。

  プロジェクト
   ├─ プロジェクト.fsproj
   ├─ Program.fs
+  └─ credentials.json

.fsprojcredentials.jsonbin\Debug\net5.0 にコピーする設定を追記する。

.fsproj
    <ItemGroup>
+     <Content Include="credentials.json">
+       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+     </Content>
      <Compile Include="Program.fs" />
    </ItemGroup>

Program.fs を変更する

変数 driveServicedocsService から Google Drive API、Google Docs API の機能を呼び出します。

Program.fs
let Scopes = [|
    Google.Apis.Drive.v3.DriveService.Scope.Drive
    Google.Apis.Docs.v1.DocsService.Scope.DocumentsReadonly
|]

let ApplicationName = "Google Drive OCR"

let fileStream = new System.IO.FileStream("credentials.json", System.IO.FileMode.Open, System.IO.FileAccess.Read)
let clientSecrets = Google.Apis.Auth.OAuth2.GoogleClientSecrets.Load(fileStream).Secrets
fileStream.Dispose()
let cancellationToken = System.Threading.CancellationToken.None
let fileDataStore = new Google.Apis.Util.Store.FileDataStore("token.json", true)
let userCredential = Google.Apis.Auth.OAuth2.GoogleWebAuthorizationBroker.AuthorizeAsync(clientSecrets, Scopes, "user", cancellationToken, fileDataStore).Result

let initializer = new Google.Apis.Services.BaseClientService.Initializer()
initializer.ApplicationName <- ApplicationName
initializer.HttpClientInitializer <- userCredential

let driveService = new Google.Apis.Drive.v3.DriveService(initializer)
let docsService = new Google.Apis.Docs.v1.DocsService(initializer)

Scopes

プログラムが利用する機能。

実装例

let DeleteFile fileId =
    let deleteRequest = driveService.Files.Delete fileId
    deleteRequest.Execute() |> ignore

let CheckMimeMapping (filePath: string) =
    let ext = System.IO.Path.GetExtension(filePath).ToLower()
    match ext with
    | ".jpeg" | ".jpg" -> "image/jpeg"
    | ".png" -> "image/png"
    | ".gif" -> "image/gif"
    | ".pdf" -> "application/pdf"
    | _ -> failwith "It's not an image!"

let UploadImageFile (filePath: string) =
    let generateIdsRequest = driveService.Files.GenerateIds()
    generateIdsRequest.Count <- 1
    let id = generateIdsRequest.Execute().Ids.[0]

    let metadata = new Google.Apis.Drive.v3.Data.File()
    metadata.Id <- id

    let fileStream = new System.IO.FileStream(filePath, System.IO.FileMode.Open)
    let createMediaUpload = driveService.Files.Create(metadata, fileStream, CheckMimeMapping filePath)
    let iUploadProgress = createMediaUpload.Upload()
    fileStream.Dispose()

    if iUploadProgress.Status = Google.Apis.Upload.UploadStatus.Failed then
        raise iUploadProgress.Exception
    else
        id

let ConvertImageToDocument imageId =
    let metadata = new Google.Apis.Drive.v3.Data.File()
    metadata.MimeType <- "application/vnd.google-apps.document"

    let copyRequest = driveService.Files.Copy(metadata, imageId)
    copyRequest.OcrLanguage <- "ja"
    let documentId = copyRequest.Execute().Id

    DeleteFile imageId
    documentId

let ReadParagraphElements (element: Google.Apis.Docs.v1.Data.ParagraphElement) =
    let textRun = element.TextRun
    if textRun = null || textRun.Content = null then
        ""
    else
        textRun.Content

let rec ReadStructuralElements (elements: System.Collections.Generic.IList<Google.Apis.Docs.v1.Data.StructuralElement>) =
    let mutable text = ""

    for element in elements do
        if element.Paragraph <> null then
            for paragraphElement in element.Paragraph.Elements do
                text <- $"{text}{ReadParagraphElements paragraphElement}"
        elif element.Table <> null then
            for tableRow in element.Table.TableRows do
                for tableCell in tableRow.TableCells do
                    text <- $"{text}{ReadStructuralElements tableCell.Content}"
        elif element.TableOfContents <> null then
            text <- $"{text}{ReadStructuralElements element.TableOfContents.Content}"

    text

let GetDocumentText documentId =
    let getRequest = docsService.Documents.Get documentId
    let elements = getRequest.Execute().Body.Content
    DeleteFile documentId
    ReadStructuralElements elements

let Ocr =
    UploadImageFile
    >> ConvertImageToDocument
    >> GetDocumentText
Ocr @"画像ファイルのパス"
|> printfn "%s"

解説

今回の処理における中心的な内容を解説します。
Google.Apis.Drive.v3 を F# で使う上での基本知識は以下の記事で扱っています。

MIME タイプの取得

ファイルパスを渡すと MIME タイプを返す関数です。
Google Drive で変換可能な形式のみ対応させています。

let CheckMimeMapping (filePath: string) =
    let ext = System.IO.Path.GetExtension(filePath).ToLower()
    match ext with
    | ".jpeg" | ".jpg" -> "image/jpeg"
    | ".png" -> "image/png"
    | ".gif" -> "image/gif"
    | ".pdf" -> "application/pdf"
    | _ -> failwith "It's not an image!"

画像をドキュメントに変換する

ドキュメントファイルを表す MIME タイプに変更する。

metadata.MimeType <- "application/vnd.google-apps.document"

OCR で認識する言語 (ISO 639-1 code) を設定する。
日本語を選択すれば英字も扱えるようです。

copyRequest.OcrLanguage <- "ja"

テキストの取得

ドキュメントのデータは階層構造に格納されていて、テキスト全文が欲しい場合は自分で処理を書く必要があるようです。
今回は 公式のサンプル [↗] を F# に書き換えてみました。

let ReadParagraphElements (element: Google.Apis.Docs.v1.Data.ParagraphElement) =
    let textRun = element.TextRun
    // 存在しない場合がある。
    if textRun = null || textRun.Content = null then
        ""
    else
        textRun.Content

let rec ReadStructuralElements (elements: System.Collections.Generic.IList<Google.Apis.Docs.v1.Data.StructuralElement>) =
    let mutable text = ""

    for element in elements do
        if element.Paragraph <> null then
            for paragraphElement in element.Paragraph.Elements do
                text <- $"{text}{ReadParagraphElements paragraphElement}"
        elif element.Table <> null then
            for tableRow in element.Table.TableRows do
                for tableCell in tableRow.TableCells do
                    text <- $"{text}{ReadStructuralElements tableCell.Content}"
        elif element.TableOfContents <> null then
            text <- $"{text}{ReadStructuralElements element.TableOfContents.Content}"

    text

関連記事

end

脚注
  1. https://support.google.com/drive/answer/176692 ↩︎

GitHubで編集を提案

Discussion