無駄と文化

実用的ブログ

毎月数時間を要していたスキャンデータ整理をOCRで自動化した

f:id:todays_mitsui:20161119134329j:plain


企業活動をするなかで見積書や請求書といった書類を発送するシーンは多いですよね。
私が勤める会社でもそういった書類をクライアントに郵送していますが、郵送する前の書類をスキャンしてスキャンデータを残しておく決まりになっています。

書類を作るのに必要なデータはすべて手元にあるものの、現物のスキャンデータがあれば安心なのも分かります。
書類に押したハンコを記録しておく意味もあるのかも知れません。


スキャンしたPDFの整理が負担に

しかし、毎月何百枚という書類のスキャンを取り発送するなかで、スキャンデータを整理する作業が負担になっていました。

スキャンを取る作業自体は書類の束をスキャナーに突っ込むだけなのですが、そうやって出来上がったPDFファイルはファイル名が 無機質な連番 になっています。
後で参照するときに目的のスキャンデータを探すことを考えると、一つひとつに適切なファイル名を付け直しておく必要があるわけです。

数百というファイルに適切な名前を付けるのは、単純ながら手間の掛かる作業です。
これまでは手作業でデータ整理をしていたようですが、スキャンデータのリネームだけで 毎月数時間を掛けていた ようです。
必要な作業とはいえあまり生産的とも思えませんし、今回は OCR 技術を使ってこのスキャンデータの整理を自動化してみました。

(※ OCR: Optical character recognition, 光学文字認識)


名前の付け方を確認しよう

最初にスキャンデータに付けるべき 適切なファイル名 について確認しました。

だいたいどの書類にも右上に「注文番号」と「発行した日付」が印字されています。
スキャンデータのPDFに付けるファイル名は注文番号と発行日付を単に並べたものでいいそうです。

f:id:todays_mitsui:20161119133353p:plain

(※画像は Image です)

例えば、注文番号が「123456」で発行日付が「2016年11月15日」であれば、ファイル名は「123456_20161115.pdf」といった具合ですね。

なるほど、

このファイル名のルール、自動化するにはなかなか都合がよさそうです。
一つずつ見ていきましょう。


1. 読み取る書類は全て弊社のフォーマット

書類毎にフォーマットがバラバラになってしまうと、求めている文字を読み取るだけでもかなり難しい課題になってしまいます。

しかし全てが自社フォーマットであればレイアウトも記載されている内容も揃っています。読み取る場所で悩むことはなさそうです。


2. 読み取り対処の文字は全て活字

手書き文字でもOCRで読めなくはないと思いますが、識字率はかなり低くなってしまうでしょう。
全てが活字で印字されているのは、正確に文字認識するうえでは理想的な条件です。


3. 読み取る文字は数字のみ

数字はたったの10種類しかなく、線もシンプルなのでもっとも文字認識しやすい対象だと云えます。
ひらがな・カタカナ・漢字混じりの文書をOCRで読もうとするとある程度の誤読を覚悟しなければいけませんが、対象が数字のみであることが事前に分かっていれば良い精度を出せそうです。

そんなわけで、充分な勝算があると踏んだ私はこのPDF整理自動化プロジェクトを(週末の連休を使って)粛々と進めることにしました。


自動化の方針

f:id:todays_mitsui:20161119125144p:plain

続いて自動化の方針を決定します。
自動化のプログラムはPythonで書くことにしました。具体的には以下のような手順です。

  1. PDFMiner で PDF ファイルから画像データの抜き出し
  2. 画像データ(生バイナリ)を PIL の Image オブジェクトに変換
  3. Tesseract で文字認識
  4. PDF ファイルを複製しつつリネーム

一つずつ解説します。


各工程の詳細

1. PDFMiner で PDF ファイルから画像データの抜き出し

今回は Tesseract という OCR ツールで画像の中の文字を読み取りますが。この Tesseract は JPEG や PNG などの形式しか受け付けてくれません。
PDF を直接読ませることができないので、事前準備として対応した形式に変換する必要がありました。


PDF をレンダリングして画像にする方法もあるようですが、下準備が煩雑で挫折...。
仕方なく今回は PDF を画像に変換するのではなく、PDF に埋め込まれた画像データを抽出する方法を採りました。

画像の抽出には PDFMiner という Python のライブラリを使います。
以下のコードを実行すると PDF に埋め込まれた全ての画像を取得することが出来ます。

from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.layout import LAParams, LTTextBox, LTTextLine, LTImage,  LTFigure
from pdfminer.converter import PDFPageAggregator


def extract_images(document):
    """PDF ドキュメントから画像形式のデータだけを抽出する"""

    # Create a PDF resource manager object that stores shared resources.
    rsrcmgr = PDFResourceManager()
    # Create a PDF device object.
    device = PDFPageAggregator(rsrcmgr, laparams=LAParams())
    # Create a PDF interpreter object.
    interpreter = PDFPageInterpreter(rsrcmgr, device)

    contents = []

    for page in PDFPage.create_pages(document):
        interpreter.process_page(page)
        layout = device.get_result()

        contents.extend(travarse(layout))

    return [to_pil_image(ltImage) for ltImage in contents]


def travarse(layout):
    """Layout オブジェクトを再帰的に走査して LTImage オブジェクトのみを list で返す"""

    images = []

    for obj in layout:
        if isinstance(obj, LTTextBox) or isinstance(obj, LTTextLine) or isinstance(obj, LTFigure):
            images.extend(travarse(obj))

        elif isinstance(obj, LTImage):
            images.append(obj)

    return images

ちなみに抽出されるデータの形式は生のバイナリ形式になります。


2. 画像データ(生バイナリ)を PIL の Image オブジェクトに変換

画像データを抽出できたのはいいものの、バイナリ形式のままでは何かと扱いにくいので Python で扱いやすい形式にします。 Python における代表的な画像処理ライブラリである Pillow の Image オブジェクト形式に変換しました。

import StringIO
from PIL import Image


def to_pil_image(ltImage):
    """Raw Binary を Image オブジェクトに変換"""

    buffer = StringIO.StringIO()
    buffer.write(ltImage.stream.get_rawdata())
    buffer.seek(0)
    return Image.open(buffer)

Image 形式に変換することで、元々埋め込まれていた画像データが JPG だったのか PNG だったのかを意識する必要がなくなります。
さらに、PIL の機能を使ってトリミングなどの補正処理もやりやすくなるので一石二鳥です。

標準的には Image オブジェクトはファイルストリームから生成することになっているので、 StringIO を使ってバイナリをもとにファイルストリームをエミュレートして Image オブジェクトを作ってあげます。


3. Tesseract で文字認識

いよいよ OCR に掛けて文字を読み取ってみましょう。

今回、OCR のツールとして Tesseract を利用しました。
Tesseract 自体は Python とは直接関係のない一般的な OCR ツールです。
160種類以上もの言語の学習済みデータが最初から付属している のが魅力で、中にはもちろん日本語用のものも含まれています。

事前に Tesseract をセットアップは済ませておく必要があります。
Tesseract を Python から呼び出すのには pyocr というラッパーモジュールを使いました。

試しに注文番号にあたる部分を読ませてみましょう。

f:id:todays_mitsui:20161119125229p:plain

はい、うまく読めているようですね。
先ほど述べたように、活字の数字のみであれば安定して文字認識できます。


続いて日付にあたる部分を読み取りましょう。

日付は「2016年11月15日」というような形式で書かれています。
最終的に正規表現で数字部分だけを抜き出すとはいえ、OCRに掛ける段階では「年」や「月」といった日本語の文字を読み取る必要があります。
Tesseract 標準の日本語用学習済みファイルを使って文字認識してみましょう。上手くいくでしょうか。

f:id:todays_mitsui:20161119125238p:plain

うーん、イマイチですね。
数字の 1] と読み違えています。全く似ていない (長音)に誤読してしまうのは学習に使った教師データに縦書きの文章が含まれているからでしょうか…。

とはいえ幸いなことにいま私が読み取ろうとしている文字は 0~9 と「年月日」の13文字だけです。それ以外の文字はありえない事が分かっているので、誤読の訂正も簡単なのです。
OCRに掛けた結果に ] が含まれれば 1 の誤読であろうと分かります。 Z が含まれれば恐らく 2 の誤読ですね。

というわけで、誤読されていそうな文字を一つひとつ補正する処理を挟みます。

REPLACE_PAIR = (
    (u']', u'1'), (u'}', u'1'), (u'ー', u'1'),
    (u'仔', u'年'), (u'El', u'日'), (u'E|', u'日'),
    (u'E', u'日'), (u'口', u'日'), (u'曰', u'日'),
    (u'Z', u'2'), (u'O', u'0'), (u'〇', u'0'),
    (u'I', u'1'), (u'l', u'1'),
)

# よくある誤読をヒューリスティックに訂正
for before, after in REPLACE_PAIR:
    txt = txt.replace(before, after)

# 空白を削除
txt = re.sub(r'\s+', '', txt)

このような工夫の結果、テスト用に用意したスキャンデータ20個では何とか正答率100%を達成できました。


4. PDF ファイルを複製しつつリネーム

注文番号と日付を読み取ることさえ出来れば、最後のリネームはとても簡単です。
shutil モジュールに含まれる copyfile 関数で複製しつつファイル名を変えてあげます。

import shutil


after = '{0[ordernum]}_{1[year]:0>4}{1[month]:0>2}{1[day]:0>2}.pdf'.format(ordernum, date)
shutil.copyfile(before, 'AFTER/'+after)

このような処理をフォルダに用意したスキャンデータの一つひとつに適用していけば、ファイル整理の自動化は達成されます。

f:id:todays_mitsui:20161119130455p:plain

いい感じです。


配布する

これにて目的は達成出来ましたが、最後にこのプログラムを自分以外の人にも配布することについて検討します。
実務で必要な人が手軽に使えるようにとかんがえると、やはり Windows の実行形式である EXE ファイルを作って配布するべきでしょうね。

Python で書いたプログラムにおいては、Pinstaller というツールを使う事で必要なライブラリなどを自動で取り込んだ EXE ファイルを吐き出してくれます。

$ pyinstaller main.py --onefile --clean

実行するコンピュータに Tesseract が別途セットアップされている必要はあるものの、EXE ファイルを配りさえすれば使ってもらえるのはとても便利です。
Pyinstler のインストールは pip コマンドひとつで済むので使い始めるのも楽でした。


まとめ

というわけで、PDFファイルから画像を抽出し、文字を読み取ってスキャンデータをリネームするところまで一通りの処理を自動化してみました。
Python は様々な分野で必要になる基本的ツールが何かしら揃っていて成熟しているので便利ですね。これはいいものです。

今回書いたコードの一式は GitHub に置いています。

github.com


私からは以上です。