無駄と文化

実用的ブログ

Python2 で日本語(Unicode)対応の PrettyPrint

通常、print はユニコード文字列であっても読みやすく表示してくれます。

print(u"ほげ")
# => ほげ

しかし pprint モジュールの pprint() を使うと、なぜだかユニコード文字列がエスケープされるようになります。

pprint.pprint(u"ほげ")
# => u'\u307b\u3052'

ちょっとしたデバッグ目的で PrettyPrint するときはユニコード文字列の部分もちゃんと読みたいし不便ですね。


ユニコード対応版

というわけで、

# -*- coding: utf-8 -*-

from __future__ import print_function

import pprint


def pp(obj, end="\n"):
    print(pf(obj), end)

def pf(obj):
    return (pprint
        .pformat(obj, indent=4)
        .decode("unicode-escape")
    )


if __name__ == "__main__":
    obj = {
        u"ほげ": u"ぴよ",
        u"nums": [1, 2, 3, u"四", u"五"],
        u"bool": True,
    }

    pprint.pprint(obj, indent=4)  # 通常
    # =>
    # {   u'bool': True,
    #     u'nums': [1, 2, 3, u'\u56db', u'\u4e94'],
    #     u'\u307b\u3052': u'\u3074\u3088'}

    pp(obj)                       # ユニコード対応版
    # =>
    # {   u'bool': True,
    #     u'nums': [1, 2, 3, u'四', u'五'],
    #     u'ほげ': u'ぴよ'}

はい


pformat() した後に .decode("unicode-escape") でユニコードエスケープされたところを元に戻しています。


参考にしました

qiita.com

d.hatena.ne.jp

感謝!


私からは以上です。

JavaScript のコールスタックが溢れていたのをどうにかしたら JS 要らなくなった話

約5ヶ月前にこんな記事を書いたわけですが、

blog.mudatobunka.org

この記事が今になってよく見られるようになってます。

f:id:todays_mitsui:20161119224602p:plain

先週までは毎日1桁のPVをコツコツを積み重ねていたのが本日だけで130PVです。
どこかの誰かに拾っていただいたんでしょうね。はてブもジワッと増えていますし。


んで、はてなブックマークのコメントの中にこんな意見が、

【今日のバグ取り】 JavaScript でコールスタックが溢れていたのをどうにかした話 - 無駄と文化

js(es6)にtail call optimizationはあるし、アニメーション後のcallbackは非同期だから22時間たとうが問題ないし、cssにするならtransitionよりcss-animationのほうが向いてるし、最近ならjsでwebanimation使えるし。。。

2016/11/19 21:57
b.hatena.ne.jp

...、確かに!!

なんで執筆当初これに気づかんかったんやろ。
完全に『JS を修正する』というところに脳みそを持っていかれてました。


改良版

アドバイス通り CSS の animation プロパティを使ったバージョン書きました。

コードの主要部分はこんな感じ。

<style>
/* 背景色をゆっくりと変化させるために 5s * 7F = 35s でループさせるアニメーションを設定 */
body { animation: bgcolor 35s linear infinite; }

/* 切り替えの基点になる色を設定 */
@keyframes bgcolor {
  0%    { background-color: #346caa; }
  14.3% { background-color: #3386c8; }
  28.6% { background-color: #4aa45d; }
  42.9% { background-color: #8fb84f; }
  57.1% { background-color: #debc1c; }
  72.4% { background-color: #e09532; }
  85.7% { background-color: #eb7889; }
  100%  { background-color: #346caa; }
}
</style>

完全に JavaScript が不要になりました。
CSS アニメーションの表現力はゴイスー


末尾再帰最適化

あと、末尾再帰最適化についても lazexさまが云われてるとおり ES6 からは効くようになるみたいですね。

時が経てば「JavaScript に末尾再帰最適化は無い」という文言は嘘になることでしょう。

現時点でのブラウザ対応がいかほど進んでいるのか把握していないんですよね。
Babel が末尾再帰最適化に対応したって話 は聞いたんですが。

元記事の表記についても検討しますかね。


私からは以上です。

毎月数時間を要していたスキャンデータ整理を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


私からは以上です。