LINE BotでOCRとGoogle Spread Sheet保存を、ChatGPTを活用しGASで改良できた話

2024/02/27　一部記事を折り畳みへと変更しました
2024/03/06　画像から書類の判定を疑似的に実行できた続編記事のリンクを追記しました
# 業者さんの出店日数が多すぎて、Makeでは無料制限枠内に収まりません！
おはようございます、こんにちは、こんばんは！
いつでも笑顔、いけちゃんです。

:::note
今回の記事は、過去に作った下記のMakeを活用したOCR・Google Spread Sheet保存の改良版（使用ツール変更）となります。


記事は読まなくても問題はありませんが、プログラムコードを使用しない「Make」を活用した記事なのでノーコードツールに興味があればご覧ください。
:::

https://qiita.com/ikeuchi-kota/items/7e35be6313a5b60087b4

何故使用ツールを変更するに考えが至ったかというと、Makeを活用した際に下記の課題があり、それを解決することができないかと考えたためです。

:::note alert
<details><summary>課題①　Makeでは無料枠がかなり少ない（詳細は折り畳んでいます）</summary>
Makeでは1つのモジュールが動作するたび、`Operations`という、いわゆる1の容量が使用されます。
こちらは、30日間内で1,000`Operations`の無料枠があるのですが、過去の記事の仕組みでは1回LINE Botに画像を送ると7`Operations`を使用するため、実質140回ほど使用するとしばらくは無料で使用することができなくなります。
:::

:::note alert
<details><summary>課題②　一方で、業者さんのLINE送信数（推定）は月間1,200回に及ぶ（詳細は折り畳んでいます）</summary>
催事業者さんには毎日の出店の度に、お店と相互に品質管理を確認したチェックリストを提出していただいてます。
参考として、2024年3月に出店する催事業者さんの数はおよそ50社、出店する日数は延べ1,200日！

つまり、最大で1,200回もの画像送信が発生する可能性があるのです。
また、もらっている資料は他にもあり、更に多くの紙やメールがやり取りされています。

現在は1社の業者さんに試験運用を実施していますが、仮に全業者さんがLINEを使用することになると、業者さんの出店日数が多すぎて、Makeでは無料制限枠内に収まりません！
:::

:::note alert
<details><summary>課題③　Teachable Machineとの連携ができていない（詳細は折り畳んでいます）</summary>
現時点で、Teachable Machineによる送られた画像の内容判別はMakeではできておりません。
こちらは、あると便利な機能なので実装したいのですが・・・
:::

課題解決のため、LINE Botの作り方ついて調べたところ、`Google Apps Script（以下、GASと表記）`を活用することで、`Make`で作ったときとほぼ同じ動作を作れそうだとわかりましたので、まずはそちらを試してみることにしました。

# 何をしたいのか、の整理

今回は、やりたいことがやや複雑なので図でまとめてみました。
![やりたいこと図.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637980/025b4f19-7347-2091-3e45-9c747179fadc.jpeg)


### 【感謝】やりたかったことをほぼ完成させている記事を知った！
この度やりたいことを実施するにあたり、なんとほぼ同じことをされている記事に出会いました！

https://qiita.com/21HideK/items/4005559995ac2f270528

こちらの記事では、GASを使用してOCRをし、`Google Drive`や`Google Spread Sheet`への保存のプログラムコードを作成しているだけでなく、`LINE Bot`の設定の際にメニューなどの画面を追加できる`リッチメニュー`の設定方法にも触れています。

まずは、この記事を<font color="Red">100%真似ること</font>から始めました。

これより先の記事は上記をすべて読み終え、設定方法を理解していることを前提としています。
本当に凄いわかりやすい記事なので、是非読んで試してみてください！

# GASを活用してLINE Botで送られた画像のOCRをかけ、テキストデータと画像を保存することに成功した！
### まずは結果から、動作状況は下記動画をご確認ください。

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">現在の最新GASでのLINE Bot状況<br>Teachable Machineの連携が上手くいかないんだよなぁ、誰か知ってる人いたら教えて欲しいです！<a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a> <a href="https://twitter.com/hashtag/%E3%83%96%E3%83%83%E3%82%AF%E3%82%AA%E3%83%95%E3%81%A7%E3%83%97%E3%83%AD%E3%83%9D%E3%83%BC%E3%82%BA%E3%81%99%E3%82%8B%E3%83%80%E3%82%A4%E3%82%A8%E3%83%83%E3%83%88%E4%B8%AD%E3%81%AE%E3%83%A4%E3%83%9E%E3%83%88%E9%81%8B%E8%BC%B8?src=hash&amp;ref_src=twsrc%5Etfw">#ブックオフでプロポーズするダイエット中のヤマト運輸</a><a href="https://twitter.com/hashtag/GoogleAppsScript?src=hash&amp;ref_src=twsrc%5Etfw">#GoogleAppsScript</a> <a href="https://twitter.com/hashtag/LINEBot?src=hash&amp;ref_src=twsrc%5Etfw">#LINEBot</a> <a href="https://t.co/bH6f1jLynE">pic.twitter.com/bH6f1jLynE</a></p>&mdash; いけちゃん (@since2023_kota) <a href="https://twitter.com/since2023_kota/status/1759971073196720508?ref_src=twsrc%5Etfw">February 20, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

### 上記を実現したGASのプログラムコード
先ほどの記事を100%真似ることから始まり、コードが上手く動作しなかった部分を修正し、他にやりたいことを追加実装した上で、最終的に下記コードで完成しました。

:::note warn
コード使用時の注意点
1. "自分のチャネルアクセストークンを入れてください"に、<font color="Red">自分のLINE Messaging APIのチャネルアクセストークン</font>を入力してください
1. "自分のGoogle DriveのフォルダIDを入れてください"に、<font color="Red">LINEから送られてきた画像を保存したいGoogle DriveのフォルダID</font>を入力してください
1. "画像を●●●で受け取り、記録しました。"に、<font color="Red">LINEの返信メッセージ</font>を入力してください。
:::

```javascript
//★★LINE Messaging APIのチャネルアクセストークン★★
var LINE_ACCESS_TOKEN = "自分のチャネルアクセストークンを入れてください";

//★★画像を保存するフォルダーID★★
var GOOGLE_DRIVE_FOLDER_ID = "自分のGoogle DriveのフォルダIDを入れてください";

//ファイル名に使う現在日時をMomentライブラリーを使って取得
var date = Moment.moment(); //現在日時を取得
var formattedDate = date.format("YYYYMMDD_HHmmss");

//LINE Messaging APIからPOST送信を受けたときに起動する
// e はJSON文字列
function doPost(e){
  if (typeof e === "undefined"){
    //eがundefinedの場合動作を終了する
    return;
  } 

  //JSON文字列をパース(解析)し、変数jsonに格納する
  var json = JSON.parse(e.postData.contents);

  //受信したメッセージ情報を変数に格納する
  var reply_token = json.events[0].replyToken; //reply token
  var messageId = json.events[0].message.id; //メッセージID
  var messageType = json.events[0].message.type; //メッセージタイプ

  //LINEで送信されたものが画像以外の場合、LINEで返信し動作を終了する
  if(messageType !== "image"){
    var messageNotImage = "画像を送信してください"
    //変数reply_tokenとmessageNotImageを関数sendMessageに渡し、sendMessageを起動する
    sendMessage(reply_token, messageNotImage)
    return;
  }

  var LINE_END_POINT = "https://api-data.line.me/v2/bot/message/" + messageId + "/content";
  
  //変数LINE_END_POINTとreply_tokenを関数get_ocr_Imageに渡し、get_ocr_Imageを起動する
  getImage(LINE_END_POINT, reply_token);
}

//Blob形式で画像を取得する
function getImage(LINE_END_POINT, reply_token){
  try {
    var url = LINE_END_POINT;

    var headers = {
      "Content-Type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + LINE_ACCESS_TOKEN
    };

    var options = {
      "method" : "get",
      "headers" : headers,
    };

    var res = UrlFetchApp.fetch(url, options);

    //Blob形式で画像を取得し、ファイル名を設定する
    //ファイル名: LINE画像_YYYYMMDD_HHmmss.png
    var imageBlob = res.getBlob().getAs("image/png").setName("LINE画像_" + formattedDate + ".png")

    //変数imageBlobとreply_tokenを関数saveImageに渡し、saveImageを起動する
    save_ocr_Image(imageBlob, reply_token)

  } catch(e) {
    //例外エラーが起きた時にログを残す
    Logger.log(e.message);
  }
}

//画像をGoogle Driveのフォルダーに画像を保存（アップロード）、OCRをかけテキストを取得。
//保存された画像、作成されたドキュメントファイルは残す。
//OCRで得られたテキストをLine Botへ返信する。
function save_ocr_Image(imageBlob, reply_token){
  try{
    //画像をGoogle Driveのフォルダーに画像を保存（アップロード）
    var folder = DriveApp.getFolderById(GOOGLE_DRIVE_FOLDER_ID);
    var file = folder.createFile(imageBlob);

    //ここからOCRをかけていく
    var files = folder.getFiles();
    while (files.hasNext()) {
      var file = files.next();
      // ファイルの種類が画像であることを確認する
      if (file.getMimeType().indexOf('image/') !== -1) {
        var resource = {
          title: "OCR結果_" + formattedDate, // 生成されるGoogleドキュメントのファイル名
          mimeType: "application/vnd.google-apps.document" // Googleドキュメント形式
        };
        var option = {
          "ocr": true,// OCRを行うかの設定です
          "ocrLanguage": "ja",// OCRを行う言語の設定です
        };
        var image = Drive.Files.copy(resource, file.getId(), option);

        //テキストを加工する
        var text_O = DocumentApp.openById(image.id).getBody().getText();
        var text_R01 = text_O.replace(/([^ -~｡-ﾟ]),/g, '$1，')   
                             .replace(/([^ -~｡-ﾟ])\./g, '$1．')   
                             .replace(/(\d)\．/g, '$1\.')        
                             .replace(/([ -~｡-ﾟ])、/g, '$1, ')    
                             .replace(/([ -~｡-ﾟ])。/g, '$1. ')    
                             .replace(/([^ -~｡-ﾟ])\(/g, '$1（')   
                             .replace(/([^ -~｡-ﾟ])\)/g, '$1）')   
                             .replace(/\(([^ -~｡-ﾟ])/g, '（$1')   
                             .replace(/\)([^ -~｡-ﾟ])/g, '）$1')   
                             .replace(/([^ -~｡-ﾟ])\:/g, '$1：')  
                             .replace(/([^ -~｡-ﾟ]) /g, '$1');     
        var text_R02 = text_R01.match(/(，|．)/) ? text_R01.replace(/([^ -~｡-ﾟ])、/g, '$1，').replace(/([^ -~｡-ﾟ])。/g, '$1．') : text_R01;
        var text = text_R02;

        // OCRで得られたテキストを、現在のスプレッドシートの一番最終行+1行に張り付ける。
        var ss = SpreadsheetApp.getActiveSpreadsheet()
        var sheet = ss.getActiveSheet()
        var lastRow = sheet.getLastRow();
        sheet.getRange(lastRow+1, 1).setValue(text);

        // 画像の保管先URLも記録する
        sheet.getRange(lastRow+1, 2).setValue(file.getUrl());

        // OCRで得られたテキストを、Line Botへ返信する。
        var message = "画像を●●●で受け取り、記録しました。"
        //変数reply_tokenとmessageを関数sendMessageに渡し、sendMessageを起動する
        sendMessage(reply_token, message);

        // アップロードした写真、作成したOCRファイルが不要であれば、以下のコードをここに書いて削除する（先頭の"//"を消す）
        //file.setTrashed(true); //アップロードした写真を削除
        Drive.Files.remove(image.id); //作成したOCRファイルを削除

        break; // 一つだけ処理するためループを抜ける
      }
    }
  } catch(e){
    //例外エラーが起きた時にログを残す
    Logger.log(e);
  }
}

//ユーザーにメッセージを送信する
function sendMessage(reply_token, text){
  //返信先URL
  var replyUrl = "https://api.line.me/v2/bot/message/reply";

  var headers = {
    "Content-Type": "application/json; charset=UTF-8",
    "Authorization": "Bearer " + LINE_ACCESS_TOKEN
  };
  
  var postData = {
    "replyToken": reply_token,
    "messages": [{
                  "type": "text",
                  "text": text
                  }]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  //LINE Messaging APIにデータを送信する
  UrlFetchApp.fetch(replyUrl, options);
}
```

### 実装時の注意点
先ほどの記事を100%真似る際、コードの入力は`Google Drive`上から直接GASを選択しますが、本記事では`Google Spread Sheet`を立ち上げ、そちらの`拡張機能`から選択できるGASを選択して貼り付けてください。

方法は下記の通りです。
![image (16).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637980/08b9d7c1-f203-b0cc-4d3f-42c6726153ea.png)
![image (15).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637980/1781b834-98d4-83a4-5b65-2722a0ffeda5.png)
![image (17).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637980/ecfc73b8-5a48-3b22-ac33-b669fb5bc069.png)
![image (18).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637980/58b4c6b0-9bbe-aeed-20de-42837f5d8494.png)

### 何が解決したの？
どこまでが出来たかも図でまとめてみました。
![やりたいこと図2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637980/e90939dc-a590-a0ce-ff78-bc8d817c2f0e.jpeg)

# 途中でつまずいた部分：設定しても動かない・・・助けて、ChatGPT！
上記の動作実現にあたり、いくつかの課題がありましたので紹介いたします。
解決にあたっては`ChatGPT`を多く活用しています、凄いぞ`ChatGPT`！

### 解決した課題

:::note warn
<details><summary>課題①　アクセスできるユーザーの制限（詳細は折り畳んでいます）</summary>

下記画像ですが、会社アカウントでのGASを使用したところ、デプロイの設定時に`アクセスできるユーザー`が`会社内の全員`のみに制限されていました。
![image (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637980/c577d8d5-20f5-b65b-bb72-82d545d82560.png)
GASのプログラムコードを作成後、必ずデプロイの設定が必要になりますが、その際に`アクセスできるユーザー`が`全員`以外の場合はLINE Botが動作しません。

:::

:::note
<details><summary>解決①　個人アカウントでの使用で制限解除（折り畳んでいます）</summary>

課題解決を優先し、今回に限り個人アカウントで作成したところ、無事`アクセスできるユーザー`を`全員`に設定することが出来ました。
![image (14).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637980/a5f9cb5a-e5e5-f475-891e-e71fefad82b9.png)
この設定により、LINE Botの動作に成功しました！
:::

:::note warn
<details><summary>課題②　OCRから先が動作しない（折り畳んでいます）</summary>
記事を100%真似る段階でプログラムコードをコピーしそのまま使用したのですが、どうしてもOCRが進まない状況となってしまいました。
具体的には、画像が`Google Drive`まで保存されるにも関わらず、下記コードから先の動作が確認できなくなり、`Google Spread Sheet`への保存がなされず、`LINE Bot`への返信もされない状況となりました。

```
//ここからOCRをかけていく
let a = Drive.Children.list(GOOGLE_DRIVE_FOLDER_ID)
```

この部分については、プログラムコードに詳しい方にエラー箇所を調べる方法を伺い、何度もChatGPTに確認したのですが、聞き方が悪いらしく正確な動作をするまでに時間がかかりました。
ところが・・・
:::

:::note
<details><summary>解決②　質問内容の変更（折り畳んでいます）</summary>
最終的に「え、こんな聞き方でも良かったの？」という内容で修正が出来ました！
その方法については下記の通りです。

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">ChatGPTのおかげでOCRされない問題が解決！<br>以前Makeで作ったLINE Botを活用したOCRとGoogleSpreadSheetへの画像およびテキストデータ保存が出来るようになりました！！<a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a><a href="https://twitter.com/hashtag/%E3%83%96%E3%83%83%E3%82%AF%E3%82%AA%E3%83%95%E3%81%A7%E3%83%97%E3%83%AD%E3%83%9D%E3%83%BC%E3%82%BA%E3%81%99%E3%82%8B%E3%83%80%E3%82%A4%E3%82%A8%E3%83%83%E3%83%88%E4%B8%AD%E3%81%AE%E3%83%A4%E3%83%9E%E3%83%88%E9%81%8B%E8%BC%B8?src=hash&amp;ref_src=twsrc%5Etfw">#ブックオフでプロポーズするダイエット中のヤマト運輸</a><a href="https://twitter.com/hashtag/GoogleAppsScript?src=hash&amp;ref_src=twsrc%5Etfw">#GoogleAppsScript</a><a href="https://twitter.com/hashtag/OCR?src=hash&amp;ref_src=twsrc%5Etfw">#OCR</a><a href="https://twitter.com/hashtag/LINEBot?src=hash&amp;ref_src=twsrc%5Etfw">#LINEBot</a><a href="https://twitter.com/hashtag/ChatGPT?src=hash&amp;ref_src=twsrc%5Etfw">#ChatGPT</a> <a href="https://t.co/laSeiVK6fb">pic.twitter.com/laSeiVK6fb</a></p>&mdash; いけちゃん (@since2023_kota) <a href="https://twitter.com/since2023_kota/status/1759261488563593564?ref_src=twsrc%5Etfw">February 18, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

なんと、「このコードについての問題点を確認し、修正コードを提供してください。」という質問だけで十分でした。
これまでも様々な場面で`ChatGPT`を使ってきたので、かなり細かい指示が必要だと考えていたのですが、思いのほかAIに対する質問の常識はこうだ！という考えに囚われていたようです。
:::

### 解決できていない課題
一つだけ、解決したいのにできていない課題があります。

:::note warn
`Teachable Machine`で書類を判定するGASのプログラムコードが書けていない
☞何度も`ChatGPT`も駆使して試しているのですが、実装に至っておりません。
これが実装できると、書類の仕分け、催促も非常にスムーズなのですが・・・
:::

解決できるように、今も鋭意検索中ですので、今後の更新をお待ちください！

### 2024/03/06追記
課題になっていた書類の判定を、テキストデータから疑似的に行うことに成功！
続編記事を公開しましたのでご覧ください。

https://qiita.com/ikeuchi-kota/items/8972a1f32197f0bf26b7

### 試してもらう業者さんを増やしてさらなるブラッシュアップ中！
無料で無制限にできるLINE Botを作ることが出来ましたので、試してもらう業者さんを1社⇒4社に増やし、意見を伺っている最中です。

今回試験をしてもらう業者さんは比較的多く催事の出店をしており、4社だけで延べ300日程度となるため、上手に運用が出来ればいよいよ完全実装　⇒　紙のやり取りゼロ化への目標達成に大きく近づきます。
具体的な声を伺い、そちらも踏まえて新たに改良に繋げていきます。

# まとめ：プログラムコードを知らなくても、書いて、修正して、実装できる！
ここまでなんとなく読まれた方は、「いけちゃんはプログラムまで書けるようになったのか」とか「知らないからやっぱり難しいよ・・・」と感じるかもしれません。
ですが、私は<font color="Red">今もプログラムコードは一つも書けません（汗）。</font>

ChatGPTは、こんな風に何も知らない人間でも簡単に扱えるし、仕組みづくりをすることができるので本当に凄い！ということを知っていただきたかったです。

皆さんも、簡単なことからでも「こういうことが出来たらな・・・」と思ったら気軽にChatGPTに聞いてみてください。
思いもよらない解決方法や、とんでもない業務改善に繋がるかも！？

最後までご覧いただき、ありがとうございました！