この記事は「Qiita Advent Calendar 2021 / 完全に理解したTalk Advent Calendar 2021」1日目の投稿です。
日頃 Swift で iOS アプリの開発を行っている私が、Ruby を用いてスクリプトを書くことが時々あります。ということで、今回はその一例として OCR スクリプト のライティングテクニックをご紹介いたします。
はじめに
OCR に関する API は様々なサービスで提供されていますが、今回は ONE の OCR API を利用しました。ONE は 累計1億枚 を遥かに超えるレシート収集実績を持つレシート買取アプリです。
有料でのご案内とはなりますが、レシート画像のデータ化に興味を持っていただけましたら wow.one よりお問い合わせいただけますと幸いです。
方針
- 画像データを読み込む
- 画像を Base64 形式にエンコード
- 画像の枚数だけ API を叩く
- レスポンスを蓄積
- CSV 形式に整形してファイル出力
1. 画像データを読み込む
require 'base64'
IMAGE_DIR = 'images'
SEARCH_RULES = ['**/*.jpg', '**/*.jpeg']
Dir.glob(SEARCH_RULES, base: IMAGE_DIR) do |path|
image_path = File.join(IMAGE_DIR, path)
binary_data = File.read(image_path)
end
パスの取得は Dir の glob メソッドで行います。第一引数で 取得したいファイル形式 を配列で指定することが可能で、第二引数では 探索対象のディレクトリ を指定することが可能です。
パスが取得できると、メソッドで指定したブロックがその数だけ呼ばれることになります。ですから、後々ここに API を叩く処理も記述していくことになります。
ですが、ここでは一旦、パスを元に画像ファイルをバイナリデータとして取得し、メモリに格納することとします。ここで意識しなければならないのは、 メモリの容量は有限である ということです。数十枚の画像であれば問題ありませんが、数万枚規模となると「一旦全てバイナリ化」なんてことはほぼ不可能に近いです。
2. 画像を Base64 形式にエンコード
binary_data = File.read(image_path)
base64_data = Base64.strict_encode64(binary_data)
Base64 形式へのエンコードは 非常にシンプル です。
ちなみに Base64 にエンコードされた結果は 64種類の英数字のみ で構成されています。ぜひ、vim などのエディタで覗いてみてください。
※ OCR API によってエンコード形式が異なる場合があります。API 仕様書をご確認ください
3. 画像の枚数だけ API を叩く
require "json"
require "net/http"
OCR_API_URL = 'https://xxx'
SSL = 'https'
AUTHORIZATION = 'Bearer XXXXXXXXXXXXX'
def ocr_request(base64_image)
uri = URI.parse(OCR_API_URL)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme === SSL
params = {
:image => base64_image
}
headers = {
:Authorization => AUTHORIZATION
}
response = http.post(uri.path, params.to_json, headers)
yield(response)
end
まず、API は画像の枚数だけ叩く必要があるということを意識しなければなりません。ということで、Base64 にエンコードした画像データを引数に取る メソッドを定義 してみました。
次に、メソッド内の処理を見ていきましょう。API 通信には Net::HTTP
を利用します。API が HTTPS に対応している場合は use_ssl
が true になるように処理を記述していますが、非セキュアな方が珍しいので true 固定でも良いかもしれません。
最後に、Base64 形式の画像データをパラメータとして、認証情報をヘッダに指定して POST します。yield
に API を叩いて返ってきたレスポンスを渡して完了です。このように yield
を利用することで、このメソッドを使うときに指定するブロックに対して レスポンスを引数に渡した状態で呼び出す ことが可能になります。
4. レスポンスを蓄積
require "json"
SUCCESS = '200'
ocr_request(base64_data) do |response|
case response.code
when SUCCESS then
result = JSON.parse(response.body)
else
p "ERROR #{response.code}:#{response.message}"
end
end
先程実装したメソッドを呼び出し、その結果を取得していきましょう。メソッド内の yield
が呼び出されたタイミングでこのブロックが呼び出されることになります。ですから、まずは引数で渡ってきた レスポンスから通信の成功・失敗を判断 し、成功していればレスポンスボディを結果として保持します。
今回紹介する例ではエラー内容を文字列出力しているだけですが、処理するデータ量が多かったり、処理に時間がかかるスクリプトの場合は エラーハンドリング や ログの仕組み をある程度充実させておくことをお勧めします。なぜなら、途中で失敗したものの 救済が困難 になり、最悪初めからやり直しなんてこともあり得るからです。API を叩く場合、一般的にその回数に応じて費用も増加するするので特に注意が必要です。
5. CSV 形式に整形してファイル出力
receipts = []
receipts.push(result.to_h['file_name'])
receipts.push(result.to_h['phone_number'])
receipts.push(result.to_h['unit_price'])
receipts.push(result.to_h['total_price'])
データの整形は非常にシンプルで、配列へ順に格納していくだけです。これによって CSV 形式でのデータ保存がやりやすくなります。ただし、1つの CSV ファイルに保存するデータ群は、 カラムの順番を統一 する必要があることに注意してください。
require "csv"
CSV_FILE_NAME = 'ocr_result.csv'
CSV_HEADER =<<-EOS
file_name, phone_number, unit_price, total_price
EOS
File.write(CSV_FILE_NAME, CSV_HEADER) if !File.exist?(CSV_FILE_NAME)
CSV.open(CSV_FILE_NAME, 'a', headers: true) do |csv|
receipts.each { |receipt|
csv << receipt.array
}
end
最後は CSV 形式でのファイル保存です。 File の write メソッド でファイル名とヘッダを指定してファイルを生成します。ヘッダは CSV のファイルフォーマットに準拠した文字列です。
File の exist メソッド を使うと、引数に指定した名前のファイルが存在するか否かを返してくれます。ですから、ここではファイルが存在しないときだけ write メソッドによるファイル生成処理が走るということになります。
File の write メソッドで CSV の初期ファイルは作成済み状態になるので、CSV の open メソッド でデータの書き込み処理を行います。メソッドの引数には、書き込み対象のファイル名、追加書き込みを示す 'a'
、ヘッダの有無をそれぞれ指定しています。後は、先程の配列データを全て書き込むだけです。
さいごに
いかがだったでしょうか。
日頃ネイティブアプリを開発している私でも サクッと書けてしまう Ruby は凄いなと改めて感じます(コードが綺麗かは別問題として…笑)。ぜひ皆さんも Ruby でスクリプトを書いてみてください!
アイデア、ご指摘等ございましたら、このホームページの連絡フォームより気軽に送っていただけると嬉しいです。
最後までお読みいただき、ありがとうございます。