Edited at

スクレイピングすればいいのにOCRで挑んだ


要約

スクレイピングすればいいのにOCRでやってしまった話

(正攻法教えてください)


背景

Google Mapで何かしらの検索をかけると、例えば以下のような結果が得られます。

result_overall.png

この時、画像左に一覧で表示される検索結果のタイトル(今回だとラーメン店の名前)を取得することが目的となります。

本来ならAPIから取得すべきこと(多分)だとは重々承知しているのですが、知見が無さすぎてGoogleMapsplatformなどを介して取得する方法がいまいちわかりませんでした。

そこで、API無しで実現できないかと迷走した結果を伝えることで、「そもそもAPIでこうやれば取れません?」or「API無しでももっと効率的にできません?」のような鋭いご意見をお待ちしたいという試みです。


手法の流れ


  1. Seleniumを用いてChromeを自動操作する(Google Mapを開き、検索結果をスクリーンショットする)

  2. スクリーンショットにOCRを適用(PyOCR)


環境

それぞれのライブラリのインストールなどは別途参照して入れてください。


OCRのツールはTesseractを使用しています

MacOS >= 10.14.5

Python >= 3.5
- chromedriver-binary == 75.0.3770.8.0
- selenium == 3.141.0
- pyocr == 0.7


実装

全体はここにあげています


検索結果のスクリーンショット

import chromedriver_binary

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time

# url(任意の検索結果)
url_str = "https://www.google.co.jp/maps/search/%E3%81%A4%E3%81%8F%E3%81%B0%E3%80%80%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3/@36.0927356,139.9444544,11z/data\=\!3m1\!4b1\?hl\=ja\&authuser\=0"
options = Options()
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
# urlからページを取得する
driver.get(url_str)

# すぐにスクリーンショットを撮ると検索結果が撮影されない(Seleniumによる操作のため)
time.sleep(10)
# スクリーンショット
driver.save_screenshot("./screen_shot.png")
driver.quit()


得られる画像

先ほどの画像全体を用いると、OCRだけ綺麗に判別することが困難なため、出力される画像を検索結果の部分のみ切り出した後にOCRを適用します。(本当はここも自動化したいですが、seleniumが取得する画像の左1/3が検索結果になっているため、手動で指定し切り出しを実行しています)


OCR部分


手法

画像を切り出してOCRを適用しても、場合によっては正しく認識されないため、OCRの適用手法自体にも工夫を設けます。


  1. 切り出した画像に対してOCRを適用し、文字があると認識した箇所を記憶(矩形領域で取れます)

  2. 検索結果として妥当なフォントサイズ(今回は12px)を想定して

    矩形領域がそのフォントサイズに最も近い領域のみ抽出
    result_OCR.png

  3. 抽出された領域を拡大した上で再びOCRを適用


コード

import cv2

from pathlib import Path
from PIL import Image
import pyocr
import pyocr.builders

tools = pyocr.get_available_tools()
tool = tools[0]

target_img_path = Path("./screen_shot.png")
res = tool.image_to_string(
Image.open(target_img_path.as_posix()),
lang="jpn",
builder=pyocr.builders.WordBoxBuilder(tesseract_layout=6)
)

# OCR時の認識を阻害しないよう、切り出す領域にはある程度の幅を持たせる
margin = 6
# 認識対象のフォントサイズに合わせる
font_size = 12
# 拡大率
OCR_image_rate = 2
out = cv2.imread(target_img_path.as_posix())
out_r, out_c, _ = out.shape
OCR_image_r_size = 2*margin+font_size
# OCRする領域のなかで文字が含まれている領域を指定
OCR_image_c_size = int(out_c*(2.3/3))-15

stock_arr = []
# resには左上の座標(d.position[0])と右下の座標(d.position[1])が含まれている
for d in res:
diff = np.array(d.position[1]) - np.array(d.position[0])
# 認識対象か判定
if diff[1] > font_size:
stock_arr.append(d.position[0])

stock_arr = [sa for sa in stock_arr if sa[0] != 0 and sa[1 != 0]]
ocr_arr = []

# 認識対象領域の左上の座標を格納(多少の誤差がある場合は一つにまとめる)
for (sa_r, sa_c) in stock_arr:
if not ocr_arr:
ocr_arr.append((sa_r, sa_c))
elif abs(ocr_arr[-1][1] - sa_c) > 5:
ocr_arr.append((sa_r, sa_c))

for (_, ocr_c) in ocr_arr:
ocr_save_path = Path(target_img_path.parent,
"tmp{}.png".format(ocr_c))
ocr_img = out[ocr_c-margin:ocr_c-margin+OCR_image_r_size,
15:15+OCR_image_c_size]
# 対象の領域の保存と読み込み(抽出された領域の確認)
imsave(ocr_save_path.as_posix(),
ocr_img)
ocr_img = Image.open(ocr_save_path.as_posix())
# 拡大することでOCRで認識しやすくする(約2倍)
ocr_img = ocr_img.resize((OCR_image_c_size*OCR_image_rate,
OCR_image_r_size*OCR_image_rate))
txt_sub = OCR_tool.image_to_string(
ocr_img,
lang="jpn",
builder=pyocr.builders.TextBuilder(tesseract_layout=7)
)
# 確認せず削除する場合
# ocr_save_path.unlink()
print(txt_sub)


実行結果

$ python3 main.py

つ く ば ら 一 め ん 鬼 者 語
は り け ん ラ ー メ ン 本 店
ス タ ミ ナ ラ ー メ ン が む し ゃ


まとめと反省

SeleniumとPyOCRによって、GoogleMapにおける検索結果から結果の名前を抽出することに成功しました。思いつきにしてはかなり正確に取り出すことができていますが、検索結果の内容によっては微妙に精度が落ちてしまう場合があります。(複雑な感じや、英語が混じる場合など)

また、実行時間についてもseleniumの軌道を含めると20s、OCR部分のみで5sなので使いどころが難しい手法になっています。もちろん精度改善の余地や、より効率的な手法は突き詰めればあるかと思いますが、そもそも正しいやり方があるはずなのでこれ以上改良することはほとんどないかと思います。(ご指摘いただいた場合は修正したいです)

もっとちゃんとAPIに向き合えば抽出できるような気もしますし、正攻法から逃げずにトライすることを心がけたいです


最後に

今回の手法でもっと効率的な方法があると考えている方、APIからどう取得できるかご存知の方、それ以外でもっとスマートな方法が思いついている方は何卒ご教授いただけたら幸いです