スーパーのチラシをOCRしてSlackに通知したら便利だった

スーパーのチラシを毎日見てチェックするのは大変なので、
OCRをかけて自分が狙っている商品が出たら通知が来るようにしたら便利でした。

## 仕組み
1. スーパーの公式サイトからチラシPDFのリンク一覧を取得
1. 新規のチラシかどうかチェック
1. 新規のチラシのPDFをDLする
1. PDFをJPGに変換する
1. Google Cloudの [Cloud Vision API
](https://cloud.google.com/vision?hl=ja)でOCRをかける
1. 指定したキーワードが存在するかチェック
1. キーワードに引っかかればSlackに通知を送る

Pythonで作りました。

### 1. 公式サイトからリンク一覧を取得
[BeautifulSoup4](https://www.crummy.com/software/BeautifulSoup/bs4/doc/)というHTMLを解析してくれるライブラリを用いました。
公式サイトのHTML構成を確認しながら実装すれば簡単にできます。

```py
# 公式サイトからpdfリンク一覧取得
def get_urls():
  import requests
  from bs4 import BeautifulSoup

  html = requests.get('★公式サイトURL')
  soup = BeautifulSoup(html.text, 'html.parser')

  # ★以下は実際のHTML構成に合わせて実装する
  flyer_list = soup.find_all('table')
  url_list = []
  for flyer in flyer_list:
    # 日付
    date = flyer.find('div', {'class': 'sale'}).find('a').get_text(strip=True).replace(' ', '').replace('（', '(').replace('）', ')')
    
    # PDF
    url_info = {}
    url_info['date'] = date
    url_info['url'] = flyer.find('a', {'title': 'PDF'})['href']
    url_list.append(url_info)

  return url_list
```

### 2. 新規のチラシかどうかチェックする
チラシは何日かにわたって掲載されますが、同じチラシで通知しても意味がないため、
前日に確認したチラシのURL一覧の情報を`urls.txt`に保持しておいて新規のチラシかどうかチェックしています。

```py
# 未解析のチラシURLを取得
def get_new_urls(url_list):
  # urls.txt読込
  old_url_list = []
  with open('/urls.txt', 'r') as f:
    old_url_list = f.read().splitlines()

  new_url_list = []
  urls_text = []
  count = 0
  for url_info in url_list:
    urls_text.append(url_info['url'] + '\n')

    if url_info['url'] not in old_url_list:
      # 新規
      url_info['number'] = count
      new_url_list.append(url_info)
      count += 1
  
  # 今回のURL一覧をurls.txtに書込
  f = open('/urls.txt', 'w')
  f.writelines(urls_text)
  f.close()

  return new_url_list
```

### 3. 新規のチラシのPDFをDLする
公式サイトのサーバーに負荷がかからないように時間をおいてDLしています。

```py
# 新規のチラシPDFをDL
def dl_pdfs(new_url_list):
  import urllib.request
  import time

  pdf_list = []
  for url_info in new_url_list:
    file_name = f'./pdf/{url_info["number"]}.pdf'
    urllib.request.urlretrieve(url_info['url'], file_name)
    url_info['pdf_path'] = file_name
    # サーバーに負荷がかからないように時間をおく
    time.sleep(2)

    pdf_list.append(url_info)

  return pdf_list
```

### 4. PDFをJPGに変換
Vision APIでPDFをそのままOCRしてもうまくいかなかったので、
[pdf2image](https://github.com/Belval/pdf2image)というライブラリを使用してJPGに変換かけました。

これは[Poppler](https://github.com/oschwartz10612/poppler-windows)というフリーのPDFコマンドラインツールを背後で用いているため、
Popplerを`lib/poppler`にダウンロードしています。

[こちらのブログ記事](https://gammasoft.jp/blog/convert-pdf-to-image-by-python/)を参考にさせていただきました。

```py
# PDFをJPGに変換
def pdf_to_jpeg(path):
  import os
  from pathlib import Path
  from pdf2image import convert_from_path

  # poppler/binを環境変数PATHに追加する
  poppler_dir = '/lib/poppler/bin'
  os.environ['PATH'] += os.pathsep + str(poppler_dir)

  image_paths = []

  pdf_path = Path(path)
  # PDF -> Image に変換（150dpi）
  pages = convert_from_path(str(pdf_path), 150)

  # 画像ファイルを１ページずつ保存
  image_dir = Path('./jpg')
  for i, page in enumerate(pages):
    file_name = pdf_path.stem + '_{:02d}'.format(i + 1) + '.jpeg'
    image_path = image_dir / file_name
    # JPEGで保存
    page.save(str(image_path), 'JPEG')
    image_paths.append(image_path)

  return image_paths
```

### 5. Google Cloudの [Cloud Vision API](https://cloud.google.com/vision?hl=ja)でOCRをかける
Cloud Vision APIは[毎月1000ユニットまでは無料](https://cloud.google.com/vision/pricing)なので
毎日チラシをOCRかけるくらいであれば無料で利用できます。

Vision APIを使うためには事前にいろいろと設定する必要があります。

- Google Cloudでプロジェクトを作成
- Vision APIを有効にする
- 認証の設定をする

詳しいやり方は調べればたくさん出てくるので、他のサイトを参考にしてください。

```py
# OCR
def detect_text(image_paths):
  from google.cloud import vision
  import io
  client = vision.ImageAnnotatorClient()

  all_text = ''

  for image_path in image_paths:
    with io.open(image_path, 'rb') as image_file:
      content = image_file.read()

    image = vision.Image(content=content)
    
    response = client.text_detection(image=image)
    texts = response.text_annotations

    for text in texts:
      all_text += str(text.description)

    if response.error.message:
      raise Exception(
        '{}\nFor more info on error messages, check: '
        'https://cloud.google.com/apis/design/errors'.format(
          response.error.message))

  return all_text
```

### 6. 指定したキーワードが存在するかチェック
```py
# キーワード検索
def search_words(all_text):
  hitwords = []
  # ★任意のキーワード（商品名）を設定
  keywords = ['ヨーグルト', '若鶏もも肉']
  for keyword in keywords:
    if keyword in all_text:
      hitwords.append(keyword)

  return hitwords
```

### 7. Slackに通知を送る
[Incoming Webhook](https://slack.com/intl/ja-jp/help/articles/115005265063-Slack-%E3%81%A7%E3%81%AE-Incoming-Webhook-%E3%81%AE%E5%88%A9%E7%94%A8)を用いてSlackに通知を送ります。

```py
def slack_notice(results):
  import slackweb
  slack = slackweb.Slack(url='★WebhookのURL')
  for result in results:
    text = f'{result["date"]} チラシ掲載商品：{",".join(result["hitwords"])}\n<{result["url"]}|チラシを見る>'
    slack.notify(text=text)
```

### 結果
こんな感じになりました。
毎日チラシをチェックしなくても通知してくれるのでとても便利です！
![チラシOCR_slack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120823/834cb5c4-bc92-7f0b-41eb-b8732e2ac4c2.png)


本当はキーワードごとに値段を記録して、閾値より安ければ通知するといった方法を取りたかったのですが、
チラシは文字の配置や配色が複雑すぎて商品名と値段を紐づけて取得することが難しかったので断念しました。

全ソースはこちらに置いてあります。
https://github.com/yakipudding/flyer-ocr