👺

GASでOCRをやってみた

2022/01/17に公開

はじめに

Zennへの投稿は初となります。
GASでのOCRをやってみたくて以下の記事を参考(ほぼコピペ)にしながらレシートの合計金額の部分だけを返すLine Botを作成しました。
LINEBotとGASでOCR

const ACCESS_TOKEN = '<LINEのコンソールで取得したチャンネルアクセストークン>';
const FOLDER_ID = '<画像を置くフォルダーID>'

const doPost = async (e) => {
  for (let i = 0; i < JSON.parse(e.postData.contents).events.length; i++) {
    const event = JSON.parse(e.postData.contents).events[i];
    const message = await eventHandle(event);
    // 応答するメッセージがあった場合
    if (message !== undefined) reply(message, event.replyToken);
  }
  return ContentService.createTextOutput(
    JSON.stringify({ content: 'post ok' })
  ).setMimeType(ContentService.MimeType.JSON);
}

const eventHandle = async (event) => {
  let message;
  switch (event.type) {
    case 'message':
      message = await messageFunc(event);
      break;
    case 'follow':
      message = followFunc(event);
      break;
  }
  return message;
}

// メッセージイベントの処理
const messageFunc = async (event) => {
  let message;
  if (event.message.type === 'image') {
    message = await ocrImageFunc(event);
  } else {
    message = { type: 'text', text: '文字が入った画像を送信すると、OCRするよ!' }
  }
  return message;
}

// 友達登録時の処理
const followFunc = () => {
  return { type: 'text', text: '文字が入った画像を送信すると、OCRするよ!' };
}

const ocrImageFunc = async (event) => {
  //送られてきた画像をダウンロードする
  const img = await getImageFunc(event.message.id);
  //送信された画像をDriveにアップロードする
  const imageId = await saveImageFunc(img);
  // 設定事項
  const resource = {
    title: 'test' // 途中で生成されるコピーのファイル名の指定
  }
  const option = {
    'ocr': true,// OCRを行う
    'ocrLanguage': 'ja',// OCRを行う言語
  }

  // 指定したfileIdのファイルをコピー
  const copyImage = Drive.Files.copy(resource, imageId, option);
  // コピー先ファイルにはOCRのデータが含まれているのでテキストを取得
  const ocrText = DocumentApp.openById(copyImage.id).getBody().getText();
  // Driveに保存されたファイルとコピーファイルは不要なので削除
  Drive.Files.remove(copyImage.id);
  Drive.Files.remove(imageId);
  // OCRした内容を返却
  return { type: 'text', text: getTotalPayment(ocrText) }
}

// OCRから抜き出したテキストに含まれる支払い金額を取得
const getTotalPayment = str => {
  const regex = /合計\n¥[0-9,]+/g;
  const found = str.match(regex);
  return found.length <= 0 ? 0 : found[0].match(/[0-9,]+/g)[0];
}

// ユーザーから送られてきた画像をダウンロード
const getImageFunc = async(id) => {
  const url = 'https://api-data.line.me/v2/bot/message/' + id + '/content';
  const data = UrlFetchApp.fetch(url, {
    'headers': {
      'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    'method': 'get'
  });
  const img = data.getBlob().getAs('image/png').setName(Number(new Date()) + '.png');
  return img;
}

// 画像をDriveに保存する関数
const saveImageFunc = async (img) => {
  const folder = DriveApp.getFolderById(FOLDER_ID);
  const file = folder.createFile(img);
  return file.getId();
}

// botの返信を行う
const reply = (message, replyToken) => {
  const replyUrl = 'https://api.line.me/v2/bot/message/reply';
  UrlFetchApp.fetch(replyUrl, {
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
      Authorization: 'Bearer ' + ACCESS_TOKEN,
    },
    method: 'post',
    payload: JSON.stringify({
      replyToken: replyToken,
      messages: [message],
    }),
  });
}

参考にした記事と異なる点

合計金額部分だけの抜き出し

今回の主題でもありますので当然ですが、ただOCRの結果を返すのではなく、合計金額の部分だけを抜き出しBotからの返信にしています。

Driveにアップロードした画像の削除

無料でアップロードできる容量を減らさないためにも最終的には画像を削除しています。来た時よりも美しくというやつです。

これからやってみたいこと

家計管理Botとして動作させる

マネーフォワードの家計管理アプリを前に使用していましたが、カテゴリ別に分けることが面倒に感じやめました。しかし1ヶ月の支出は把握したいので今回のBotを作ろうというのが背景にありました。現在は合計金額を返すだけですが、DBに結果を保存し1ヶ月の支出を見える化したいと思っています。

サーバーに移設する

どうしてもGASでは実行に時間がかかりすぎています。個別のサーバーに移して実行時間を短くしたいです。
(お金がなぁ、、。かかるんだよなぁ、、。)
やったことがないのでLambdaでやってみようと思います。

保守性、拡張性を意識したコード

現在のコードは個人開発だからできるようなコードになっています。チーム開発で重要視される保守性、拡張性は皆無です。個人開発でもそれらの視点を意識したコードに直したいと思います。

Discussion