【UiPath】GoogleCloudVisionOCRで複数のフォーマットの請求書からテキストを抽出するRPAを作ってみた

# 最初に
UiPathにあるOCR - 中国語、日本語、韓国語という
アクティビティ（以下、CjkOCRと呼ぶ）で
エラーが出て使えなかったので
Googleが提供しているCloud Vision OCRを使用してRPAを作ってみた話。

参考にした記事は以下。

https://qiita.com/takachan_coding/items/871483f2d7bf6865bae0

https://cloud.google.com/vision

https://www.freee.co.jp/kb/kb-invoice/rule-and-manner-invoice/


<details><summary>断念したが一応参考にしたUiPathのOCR関連の記事</summary>

* [OCR - 中国語、日本語、韓国語](https://docs.uipath.com/ja/activities/other/latest/document-understanding/cjk-ocr)
* [Tesseract OCR](https://docs.uipath.com/ja/activities/other/latest/ui-automation/google-ocr)
* [パブリック エンドポイント](https://docs.uipath.com/ja/document-understanding/automation-cloud/latest/user-guide/public-endpoints)

</details>


<details><summary>他にも参考にした記事</summary>

* [GCPのCloud Vsion APIを使ってOCR（画像からテキスト抽出）してみる](https://qiita.com/sugimochi_1019/items/0b91ce59765d2e38de51)
* [【随時更新】元コンサルが UiPath Document Understanding をわかりやすく説明するコーナー](https://qiita.com/Kajiita/items/0dd8d410b572e076dcfe)
* [Enterprise Document OCR](https://cloud.google.com/document-ai/docs/enterprise-document-ocr)
* [Use Enterprise Document OCR to process documents](https://cloud.google.com/document-ai/docs/process-documents-ocr)
* [Creating and managing processors](https://cloud.google.com/document-ai/docs/create-processor#create-processor)

</details>

# ざっくり動作の流れ・設計
<dl>
  <dt>1.フォルダを選択</dt>
  <dt>2.フォルダ内のPDFを1つずつ処理</dt>
      <dd>2-1.画像へ変換</dd>
      <dd>2-2.Cloud Vision OCRで画像からテキストを抽出</dd>
      <dd>2-3.テキストから会社名、合計金額、請求書のNoを抽出</dd>
      <dd>2-4. 2-3で抽出した情報をデータテーブルへ格納</dd>
  <dt>3.出力先のExcelを選択し、データテーブルをシートへ書き込む</dt>
</dl>






# 0.請求書の準備

実際の請求書とは勝手が違うかもしれないが、
RPAで使用する請求書は以下のサイトで作成。

https://bizroute.net/seikyu-pdf-tools.html

RPAでは異なるフォーマットの請求書から
それぞれテキストを抽出できるよう作るため、
いくつか請求書を作成。

# 2.フォルダ内のPDFを1つずつ処理
以下のアクティビティを使用。
~~気づいたら便利そうなアクティビティがあったので~~
* 繰り返し（フォルダ内の各ファイル）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/f207b0eb-5e02-5e06-fe91-4f4ce6d3e743.png)

フォルダに格納されている請求書のみを
繰り返し処理したいので
フィルター条件は請求書の拡張子（.pdf）を設定する。
繰り返しの対象となる各ファイルは``CurrentFile``に格納されている。









## 2-1.画像へ変換
以下のアクティビティを使用。
* PDFページを画像としてエクスポート

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/c787174c-7a86-8763-eb53-2e983ef690de.png)


繰り返しの対象となるファイルは``CurrentFile``に格納されているため、
ファイル名は``CurrentFile.FullName``を入力する。
これで``CurrentFile``を画像としてエクスポートすることができる。


今回は変数``image_file_pass``には
``CurrentFile.FullName.Replace(".pdf", "_" + mk_datetime + ".png")``
という値が入っている為、
PDFと同じフォルダにPNGで画像がエクスポートされる。

 <font color=#737373>``mk_datetime``も変数で``DateTime.Now.ToString("yyyyMMddhhmm")``
 という値が入っていたりする、画像名が重複しないように。</font>










## 2-2.Cloud Vision OCRで画像からテキストを抽出
以下のアクティビティを使用。
* 画像を読み込み
* Google Cloud Vision OCR

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/80504755-1cff-b294-03cf-e9422e125975.png)


OCRで処理する画像はImage変数に格納されている必要があるため、
まずは``image_file_pass``の画像を読み込み、
Image型の変数（今回は``input_img``）へ出力する。

次にGoogle Cloud Vision OCRのプロパティは以下内容で設定する。

| 項目 | 値 |
|:-----------   |:------------|
| リージョン     | US（GoogleCloudRegion.US）        |
| 検出モード     | TextDetection      |
| APIキー        | Google Cloudで発行したAPIキー  |
| 画像           | ``input_img``  |
| テキスト       | ``ocr_text_detection``　|

OCRで抽出したテキストは変数``ocr_text_detection``へ格納するように設定している。

### Google Cloud APIキー発行方法＆Cloud Vision APIの有効化

とりあえず無料トライアルでGoogle Cloudの使用を開始する。
APIキーを発行するプロジェクトを選択する。
``My First Project``というプロジェクトが初めから作成されているので
とりあえずこちらを選択する。
メニューを開いて、``APIとサービス`` → ``認証情報``へ移動する。
![240214_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/c03572fb-613d-9dd7-7c6b-921fc4d37270.png)
上部の``＋認証情報を作成`` → ``APIキー``をクリック。
![240214_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/538a59b8-ea86-80ee-8d05-ccb0d51bb854.png)
数秒待つと作成完了メッセージが表示されるので、コピーして閉じる。
<font color=#737373>キーの制限はよしなに。ちなみにアクセス元（IPやアプリの種類など）や、
キーで呼び出すAPIを制限できるらしい。</font>
<img width="550" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/e2d0fefc-04ed-b83c-d0f7-64a3a25e445b.png">
認証情報ページで作成されたAPIキーが一覧に表示されていれば発行は完了。
![240214_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/8ace2281-b24b-b660-8a7e-611f64d31998.png)



次にCloud Vision OCRを使用するため、Cloud Vision APIを有効化する。
``APIとサービス`` → ``ライブラリ``へ移動する。
![240214_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/bf4b4abb-ad0b-b14d-d557-e96bbec58fef.png)
検索窓で``Cloud Vision API``と検索する。
<img width="550" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/1fe2f732-6ff5-d110-263e-32bd312ce9bf.png">
検索できたら、``有効にする``を押下する。
<img width="550" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/49cc4cc9-084e-7518-5a35-dc4a9f592c51.png">
数秒待って以下画面へ遷移していれば有効化は完了。
![240214_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/d475bbfd-8adb-b43f-13d8-39808a81785e.png)







### UiPathのCjkOCRでも一応作っていた話
Google Cloud Vision OCRアクティビティと同じ位置に
元々はCjkOCRアクティビティを入れていた。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/0b0a6420-1670-74fb-8a54-55cdad7c1823.png">

その際の設定内容は以下の通り。
| 項目 | 値 |
|:-----------   |:------------|
| エンドポイント  | "https://du-jp.uipath.com/cjk-ocr" |
| 画像           | ``input_img``  |
| テキスト       | ``ocr_text_detection``|

UiPathが提供しているOCRにはエンドポイントを設定する必要がある。
どのOCRにどのエンドポイントを設定する必要があるかは
以下の公式ページに記載されている。

https://docs.uipath.com/ja/document-understanding/automation-cloud/latest/user-guide/public-endpoints

作成途中までは問題なく動いていたもののCjkOCRを設定したUiPathのプロジェクトを複製、
複製したプロジェクトをデバッグしたところ、CjkOCRでエラーが発生。

<details><summary>エラー内容（抜粋）</summary>

```
OCR - 日本語、中国語、韓国語: Request CorrelationId: ほにゃらら
Request PredictionId: 
Error performing OCR: Response indicates an error: Forbidden, Error: <!DOCTYPE html>

～省略～

<head>
<title>Attention Required! | Cloudflare</title>

～省略～

</head>
<body>
  <div id="cf-wrapper">
    <div class="cf-alert cf-alert-error cf-cookie-error" id="cookie-alert" data-translate="enable_cookies">Please enable cookies.</div>
    <div id="cf-error-details" class="cf-error-details-wrapper">
      <div class="cf-wrapper cf-header cf-error-overview">
        <h1 data-translate="block_headline">Sorry, you have been blocked</h1>
        <h2 class="cf-subheadline"><span data-translate="unable_to_access">You are unable to access</span> uipath.com</h2>
      </div><!-- /.header -->

～省略～

```
</details>

UiPathのフォーラムを見たところ、
全く同じエラーについての質問は見当たらなかったものの
OCRでCloudflareのCookieのエラーが起きたというQ＆Aはあった。
そのQ＆AではUiPathの言語設定を英語に変更すると直ったような話だった。

https://forum.uipath.com/t/cloudflare-cookie-error-with-ocr/708294/5

今回は折角なら別のOCRを使おうという方向へ変更したので、
Q＆Aと同じような方法で解消するか試せていないが、備忘録として記しておく。



## 2-3.テキストから会社名、合計金額、請求書のNoを抽出

ここからは、OCRで抽出したテキストを格納した
変数``ocr_text_detection``から
必要な情報を取り出す為の処理をする。



まずは以下のアクティビティを使用。
* テキストを変更

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/68515cdd-2bc6-dbb7-8f6b-84bd2daa1ff7.png)
この後に、必要な情報を取り出しやすいよう
半角スペースは念のため削除しておいたり、
合計金額の前後に記載されている``￥``や``-``が全角の場合は
半角へ置き換えるように変更させておく。
変更後のテキストは変数``edit_ocr_text_detection``に格納する。

次に``edit_ocr_text_detection``から必要な情報を取り出す処理を行う。

1つ目は請求書の番号（No.12345でいうところの``12345``の部分）を取り出す処理。
以下のアクティビティを使用。
* 一致するパターンを探す
* テキストを左右に分割

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/605893fc-f20c-fafe-15a6-b586e51df0d6.png)
``一致するパターンを探す``の正規表現は以下内容で設定。
一致した値は変数``pic_file_number``へ格納するようにも設定しておく。
| 正規表現    | 値                 | 量指定子     |
|:-----------|:------------       |:------------|
| カスタム    | .\*(No)(¥\.\|¥．).*|指定回数 1    |

正規表現で取り出しただけだと、番号以外の文字列も含んでいる状態なので
``pic_file_number``を``No.``で区切った右側（``12345``の部分）のみ
``pic_file_number``へ格納する。
これで番号の取り出しは完了。

2つ目は合計金額を取り出す処理を行う。
以下のアクティビティを使用。
* テキストを取得

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/1edead7a-47e1-b656-84eb-ea745ab23e53.png)
今回、請求書に記載されている合計金額はいずれも
``￥``と``-``の間にあるので、
その間にあるテキストを抽出するよう設定する。
抽出したテキストは変数``pic_total_amount``へ格納する。
これで合計金額の取り出しは完了。

3つ目は請求元の会社名を取り出す処理を行う。
<font color=#737373>ついでにデータテーブルへ会社名を格納する。</font>
以下のアクティビティを使用。
* 一致するパターンを探す
* 繰り返し（コレクションの各要素）
    * 型を変更
    * 条件分岐（if）
    * データ行を追加

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/daf50df0-3e98-ccf2-7af1-820d66d21be3.png)
``一致するパターンを探す``の正規表現は以下内容で設定。
一致した値は変数``pic_company_name``へ格納するようにも設定しておく。
| 正規表現    | 値                 | 量指定子     |
|:-----------|:------------       |:------------|
| カスタム    | .\*(株式会社).*     |指定回数 1    |

請求書には請求元の会社名、記載されていれば自分の会社名があるため
``pic_company_name``には1つ又は2つの会社名が格納されている状態となる。
請求元の会社名のみを抽出したい為、``pic_company_name``から
自分の会社名ではない方の値（つまり請求元の会社名）であれば
データテーブルへ格納するように条件分岐(if)を設定した。

 <font color=#737373>``pic_company_name``は``IEnumerable<Match>``型で
 String型ではないため、条件分岐（if）では
``currentItem.Contains("自分の会社名")``という条件は
設定できず、エラーが発生する。
そのため型を変更を行っているが、
``currentItem.Value.Contains("自分の会社名")``という条件の場合、
問題なく動くので``型を変更``は不要だと、
この記事を書いている最中に確認できてしまったのでこっそり記しておく。</font>

何はともあれ、これで会社名の取り出しは完了。






## 2-4. 2-3で抽出した情報をデータテーブルへ格納

前述で請求元の会社名だけデータテーブルへの格納が済んでしまっているので
分けて章立てる必要ある？と思うでしょう。
アクティビティとしては分けている為、
このタイミングで請求書の番号、合計金額をデータテーブルへ格納する。

以下のアクティビティを使用。
* 複数代入

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/d79770c3-07ce-caf1-4807-36549b95a6dd.png)

以下内容で設定。

| 保存先                                           | 保存する値         |
|:-----------                                      |:------------      |
| pic_all_data.Rows(CurrentIndex - 1).Item("No")   | pic_file_number   |
| pic_all_data.Rows(CurrentIndex - 1).Item("金額")  | pic_total_amount  |

``CurrentIndex - 1``と設定している理由は
データテーブルの開始は``0``、CurrentIndexの開始は``1``となっている為。
``.Rows(CurrentIndex)``とした場合、
データテーブルの開始0行目ではなく、1行目を指定している事になり
0行目を飛ばしている為にエラーが発生する。
<font color=#737373>1行目に入れるとしても0行目は空欄？
0行目に何か値入れる？どうする？となると思う。</font>

ちなみにCurrentIndexは``2.フォルダ内のPDFを１つずつ処理``で
出てきた``CurrentFile``のIndexを指している。


これでExcelへ書き込むデータテーブルの作成部分は完了。







# 3.出力先のExcelを選択し、データテーブルをシートへ書き込む

以下のアクティビティを使用。
* ファイルを参照
* Excelプロセススコープ
    * Excelファイルを使用
        * シートを挿入
        * データテーブルをExcelに書き込み
        * 最初/最後のデータ行を検索
        * 範囲を自動調整
        * Excelファイルを保存


出力先のExcelはファイルを選択する形で、
選択されたExcelを使用して、書き込みを行う。

選択されたExcelは``Excelファイルを使用``の
``参照名``（今回は``Export_Excel``）で指定できる。
<font color=#737373>変数みたいなもの？今まではわざわざExcelのフルパスを
変数に入れて呼び出ししていたような気がするので
かゆい所に手が届く便利なアクティビティだと思ったり。</font>
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/2c079d6f-6c3a-baf1-397a-4fdd812e364b.png">
``Excelファイルを使用``配下の書き込み部分は以下。
![240216_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/5699228e-42d4-dc53-a2ac-329698312086.png)

それぞれのアクティビティは以下内容で設定。

<details><summary>シートを挿入</summary>

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/ec7ba368-5887-9135-cda2-ab08555c5986.png">

| 項目                   | 値                                        |
|:-----------            |:------------                              |
| シートを作成するブック   | ``Export_Excel``                          |
| シート名  　            | "【出力結果】請求書情報_" + ``mk_datetime`` |
| 新しいシートの参照名     | ``Export_Sheet``                          |

念のため挿入されるシート名が他のシート名と
重複しないように``DateTime.Now.ToString("yyyyMMddhhmm")``
という値が入っている変数``mk_datetime``を含めている。

挿入されるシートも``Export_Sheet``で指定できる。（便利）

</details>

<details><summary>データテーブルをExcelに書き込み</summary>

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/38626f14-faa7-06d7-7c2d-6c2e9aee2866.png">

| 項目        | 値                                                |
|:----------- |:------------                                      |
| 書き込む内容 | ``pic_all_data``                                      |
| ターゲット   | ``Export_Excel``.Sheet(``Export_Sheet``.Name).Range("B2") |

``pic_all_data``は請求元の会社名、請求書の番号、合計金額が
格納されているデータテーブル。
このデータテーブルを、選択したExcelの
挿入したシート（B2セルを起点）へ書き込むように設定。

</details>

<details><summary>最初/最後のデータ行を検索</summary>

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/15e9b86a-cb10-0685-5328-c94578470f3c.png">

| 項目                | 値                  |
|:-----------         |:------------        |
| ソース              | ``Export_Excel``.Sheet(``Export_Sheet``.Name)     |
| 列名                | "B"                  |
| 最初の行の番号を保存 | ``first_row_num``    |
| 最後の行として設定   | 入力されている最後の行 |
| 最後の行の番号を保存 | ``last_row_num``     |

先ほど書き込んだシートのセルのサイズを
次のアクティビティで調整したいので範囲を確認したい。

そのため起点としたB2セルの列を基準に
値が入っている最初の行、最後の行を確認する。
行数はそれぞれ変数``first_row_num``、``last_row_num``へ格納するように設定。

ちなみに、シートにはデータテーブルのヘッダーも書き込んでいるので
オプションにある``先頭行をヘッダーとする``にはチェックを入れる。

</details>

<details><summary>範囲を自動調整</summary>

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/ec1f14ff-b370-3b89-2a2b-ffe227d03b86.png">

| 項目        | 値                  |
|:----------- |:------------        |
| ソースを選択 | ``Export_Excel``.Sheet(``Export_Sheet``.Name).Range("B" + ``first_row_num``.ToString +":E"+ ``last_row_num``.ToString) |

値が入っているセルのサイズを調整したいので、自動調整の範囲を指定する。
先ほど確認した最初の行、最後の行が格納されている変数は
Int32型となっており、``ソースを選択``の欄に記入する際は
String型で指定する必要があるので、.ToStringを付けている。

また、オプションでは列、行それぞれ自動調整するかをチェックを入れて設定できる。
今回は列、行どちらもチェックを入れる。

</details>

最後にExcelを保存して終了。


# 完成

出来上がったExcelを見てみると、
以下のように表が出力されているシートを確認できる。
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2608279/6188d42f-23dd-16a7-3de7-44a4258cb860.png">



# 最後に
UiPathのアクティビティを見てみると他にもOCRアクティビティがある。

ただし、英文を読み込む前提らしく
（試しに使用してみたところ文字化けした日本語が抽出されたりされなかったり）
日本語を読み込む場合は日本語のデータを
UiPathの特定のファイルに入れておく必要があるものもあった。

また、AというパッケージのOCR、BというパッケージのOCRを
組み合わせようとインストールすると競合する為
組み合わせることが出来ないという事もあった。

なので今回少し触ってみて
今のところはUiPath以外のOCRを使用した方が困りごとは少なそうだと感じた。

今後UiPathのOCRがアップデートされたり、
より良いOCRの活用方法を思いついたらまた記事にしたい。