全文検索サーバを構築した話 その3（OCR環境構築編）

# なぜOCRが必要になったのか？
要求があり構築を進めた全文検索システムですが、環境を作る前にそもそも問題がありまして…
**PDFは基本押印されたものをスキャンして保存している**という点。
つまり、PDFファイル内にテキストデータは保存されていない状態ということです。
画像ファイルと同じってことですね。

まぁ、全文検索できないですよね。
検索キーワードを紐づけるデータがそもそもファイルに書いてないわけですから。
しかもPDFにはパスワードも設定しているらしく、色々と難航しそうなので
半分諦めモードでしたが…

調べてみるとそれなりに解決方法があるようなので、機械的な文書起こしでも
ないよりはマシかと思い、やってみることにしました。

# OCRするために使うツール
<dl>
<dt>tesseract-ocr
<dd>
画像データをOCRして抽出した文字情報を埋め込んだPDFファイルを生成するために使用します。<br/>
この作業で一番処理に時間のかかる工程になります。<br/>
インストールするパッケージは以下の2つ。<br/>
<em><strong>tesseract tesseract-langpack-jpn</strong></em><br/>
</dd>
</dt>
<br/>
<dt>poppler-utils
<dd>
poppler-utilsはPDFのコマンドラインツールらしいです。今回は元のPDFをpngに変換する、<br/>
元のPDFから情報（パスワード設定されているか、印刷可能か）を参照するために使用します。<br/>
以下の2つのコマンドをこのパッケージから使用しています。<br/>
<em><strong>pdfinfo pdftoppm</strong></em>
</dd>
</dt>
<br/>
<dt>pdftk
<dd>
こちらもPDFのコマンドラインツールなんですが、メインはPDFファイルの結合に使用します。<br/>
PDFの結合自体はpoppler-utilsに含まれているpdfuniteコマンドでも実装できるのですが、<br/>
<strong>PDFファイルにパスワードを設定すること、印刷可否をコントロールすること</strong><br/>
これができません。<br/>
一見pdftkのほうが高機能な感じがするのですが、<strong>PDFをpngファイルに変換する機能はありません。</strong><br/>
なので、今回の作業では両方のパッケージが必要となりました。
</dd>
</dt>
</dl>

# 必要なパッケージのインストール
## tesseract-ocr
tesseractはCentOS標準リポジトリでは提供されていません。
なので、epelのリポジトリを追加してインストールします。

```bash
# epelをインストール
$ sudo yum install epel-release

インストール:
  epel-release.noarch 0:7-11

完了しました!

# tesseractをインストール
$ sudo yum install tesseract tesseract-langpack-jpn

インストール:
  tesseract.x86_64 0:3.04.00-3.el7                     tesseract-langpack-jpn.noarch 0:3.04.00-3.el7

依存性関連をインストールしました:
  cairo.x86_64 0:1.15.12-3.el7                               dejavu-fonts-common.noarch 0:2.33-6.el7
  dejavu-sans-fonts.noarch 0:2.33-6.el7                      fontconfig.x86_64 0:2.13.0-4.3.el7
  fontpackages-filesystem.noarch 0:1.44-8.el7                fribidi.x86_64 0:1.0.2-1.el7
  giflib.x86_64 0:4.1.6-9.el7                                graphite2.x86_64 0:1.3.10-1.el7_3
  harfbuzz.x86_64 0:1.7.5-2.el7                              jbigkit-libs.x86_64 0:2.0-11.el7
  leptonica.x86_64 0:1.72-2.el7                              libICE.x86_64 0:1.0.9-9.el7
  libSM.x86_64 0:1.2.2-2.el7                                 libX11.x86_64 0:1.6.5-2.el7
  libX11-common.noarch 0:1.6.5-2.el7                         libXau.x86_64 0:1.0.8-2.1.el7
  libXdamage.x86_64 0:1.1.4-4.1.el7                          libXext.x86_64 0:1.3.3-3.el7
  libXfixes.x86_64 0:5.0.3-1.el7                             libXft.x86_64 0:2.3.2-2.el7
  libXrender.x86_64 0:0.9.10-1.el7                           libXxf86vm.x86_64 0:1.1.4-1.el7
  libglvnd.x86_64 1:1.0.1-0.8.git5baa1e5.el7                 libglvnd-egl.x86_64 1:1.0.1-0.8.git5baa1e5.el7
  libglvnd-glx.x86_64 1:1.0.1-0.8.git5baa1e5.el7             libicu.x86_64 0:50.1.2-17.el7
  libjpeg-turbo.x86_64 0:1.2.90-6.el7                        libthai.x86_64 0:0.1.14-9.el7
  libtiff.x86_64 0:4.0.3-27.el7_3                            libwayland-client.x86_64 0:1.15.0-1.el7
  libwayland-server.x86_64 0:1.15.0-1.el7                    libwebp.x86_64 0:0.3.0-7.el7
  libxcb.x86_64 0:1.13-1.el7                                 libxshmfence.x86_64 0:1.2-1.el7
  mesa-libEGL.x86_64 0:18.0.5-4.el7_6                        mesa-libGL.x86_64 0:18.0.5-4.el7_6
  mesa-libgbm.x86_64 0:18.0.5-4.el7_6                        mesa-libglapi.x86_64 0:18.0.5-4.el7_6
  pango.x86_64 0:1.42.4-2.el7_6                              pixman.x86_64 0:0.34.0-1.el7

完了しました!
```
tesseractは依存関係が多いので結構追加パッケージが入りますね。
変換作業もCPUリソースを食うので、PDFコンバート用サーバが用意できるなら
別に用意するほうが良いかもしれません。

### 追加手順
yumでtesseractをインストールしてtesseractを実行すると、以下のようなエラーが出ます。

```shell-session
Tesseract Open Source OCR Engine v3.04.00 with Leptonica
Error opening data file /usr/share/tesseract/tessdata/osd.traineddata
Please make sure the TESSDATA_PREFIX environment variable is set to the parent directory of your "tessdata" directory.
Failed loading language 'osd'
Tesseract couldn't load any languages!
Warning: Auto orientation and script detection requested, but osd language failed to load
```

理由はよくわかりませんが、言語ファイルを読み込むための大元のファイル、"osd.traineddata"というファイルが
初期状態で欠如してしましっているようです。
なので、バージョンに合わせた"osd.traineddata"ファイルを取得して、/usr/share/tesseract/tessdata/に
保存します。

```bash
$ cd /usr/share/tesseract/tessdata/
$ sudo curl -OL https://github.com/tesseract-ocr/tessdata/raw/3.04.00/osd.traineddata
```

これで、tesseractが正常に動作して日本語を読み込める状態になります。


##poppler-utils
こちらは標準レポジトリでインストールできます。

```bash
$ yum install poppler-utils

インストール:
  poppler-utils.x86_64 0:0.26.5-20.el7

依存性関連をインストールしました:
  lcms2.x86_64 0:2.6-3.el7                openjpeg-libs.x86_64 0:1.5.1-18.el7       poppler.x86_64 0:0.26.5-20.el7
  poppler-data.noarch 0:0.4.6-3.el7

完了しました!
```

## pdftk
pdftkのインストールはまた一癖ありました。
nuxというリポジトリからrpmでリポジトリを追加してpdftkをインストールします。

```bash
$ sudo rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpm
http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpm を取得中
警告: /var/tmp/rpm-tmp.5Nweqm: ヘッダー V4 RSA/SHA1 Signature、鍵 ID 85c6cd8a: NOKEY
準備しています...              ################################# [100%]
更新中 / インストール中...
   1:nux-dextop-release-0-5.el7.nux   ################################# [100%]
```

そのあと、yumでインストール。

```bash
$ sudo yum install pdftk

インストール:
  pdftk.i686 0:2.02-1.el7.nux

完了しました!
```

これでpdftkが使えるかと思うと…エラーが出ます。
どうやら64ビット用のpdftkはないようでして、32ビット用しかないようです。
（pdftk.i686ですからね…）
なので、追加で以下3点をインストールします。

```bash
$ sudo yum install ld-linux.so.2

インストール:
  glibc.i686 0:2.17-260.el7_6.5

依存性関連をインストールしました:
  nss-softokn-freebl.i686 0:3.36.0-5.el7_5

完了しました!

$ sudo yum install libstdc++.so.6

インストール:
  libstdc++.i686 0:4.8.5-36.el7_6.2

依存性関連をインストールしました:
  libgcc.i686 0:4.8.5-36.el7_6.2

完了しました!

$ sudo yum install libz.so.1

インストール:
  zlib.i686 0:1.2.7-18.el7

完了しました!
```

これで、無事pdftkが使える状態になります。

ここからは、変換作業になります。

# 上記ツールを使って画像PDFをテキスト埋め込みPDFに変換する
一つ一つのファイルを操作するのは流石に面倒なので、スクリプト化してしまいました。

```bash:pdfconvert.sh
#!/bin/bash

# 引数のチェック
if [ $# -eq 0 ]; then
   echo "usage : $ pdfconvert.sh [対象ディレクトリ] [pdfパスワード]"
   echo "   [対象ディレクトリ]は必須"
   echo "   [pdfパスワード]を指定しない場合、作成したPDFにパスワードは設定されません."
   echo "終了します."
   exit 1
fi

# 引数に指定したディレクトリが存在しない場合は処理を中止する
if [ ! -d $1 ]; then
   echo "指定したディレクトリが存在しません."
   echo "処理を終了します."
   exit 1
fi

# PDFパスワードを読み込む
if [ ! x"$2" = "x" ];then
   PWDTXT=$2
else
   PWDTXT=0
fi

# PDFファイルのリスト化
LIST=`find "$1" -type f -name "*.pdf"`

### リストをループに読み込み ###
for TARGET in ${LIST}; do

# 拡張子を除いたファイル名を抽出
FILENAME=`basename ${TARGET} .pdf`

# ディレクトリ名を抽出
DIRNAME=`dirname ${TARGET}`

# パスワード付きPDFかをチェック
PDFPWD=`pdfinfo ${TARGET} | grep "^Encrypted" | awk '{ print $2;}'`

# 画像保存ディレクトリの作成
TMPDIR=${TARGET}.d
mkdir -p ${TMPDIR}

# 画像変換処理実行
pdftoppm -png -r 200 ${TARGET} ${TMPDIR}/page

# 文字抽出したPDFを作成
find "${TMPDIR}/" -type f -name "*.png" | sed 's/\.png$//' | xargs -P8 -n1 -I% tesseract %.png % -l eng+jpn pdf

# 作成したPDFを1ファイルにまとめる
# PDFパスワード有無により処理を分岐
if [ ${PDFPWD} = "yes" ] && [ ${PWDTXT} != "0" ]; then
   # ファイルの印刷可否を抽出
   PRINTABLE=`pdfinfo ${TARGET} | grep "^Encrypted" | awk '{ print $3;}' | cut -d':' -f2`
   # 印刷可否によって処理を分岐
   if [ ${PRINTABLE} = "yes" ]; then
      # パスワード付きで印刷可の場合
      pdftk ${TMPDIR}/*.pdf output ${TARGET} owner_pw ${PWDTXT} allow printing
   else
      # パスワード付きで印刷不可の場合
      pdftk ${TMPDIR}/*.pdf output ${TARGET} owner_pw ${PWDTXT}
   fi
else
   # パスワード無の場合
   pdftk ${TMPDIR}/*.pdf output ${TARGET}
fi

# 作成したディレクトリを削除する
rm -rf ${TMPDIR}

### リストループ終了 ###
done
```

ざっと仕様を記載します。

- 第一引数は、変換するファイルが格納されたディレクトリを指定します。
- 第二引数は、変換後のPDFファイルに設定するパスワードを指定します。
- 第一引数に指定されたディレクトリ内のPDFファイルはテキスト埋め込みPDFに置換されます。オリジナルを取っておく必要がある場合は、必ずバックアップしてから実行してください。
- 元のPDFファイル名.dフォルダを一時ディレクトリとして作成し、その中にpngファイルを作成します。作成したpngファイルをtesseractでOCRしてpngファイルと同名のテキスト埋め込みPDFファイルを作成し、最後にpdftkで結合と同時にパスワード設定、印刷設定をしています。
- ログ出力機能は実装していません。必要は方は"/bin/bash -x pdfconvert.sh [対象ディレクトリ] [pdfパスワード] > /tmp/pdfconvert.log 2>&1 &"的にコマンドを工夫してログを出力させてください。
- 細かいところはスクリプトをご覧ください。イケてないところ、間違いがございましたらご指摘頂けると幸いです。

# 最後に
上記スクリプトで1.2GB弱、1500程度のファイルを変換するのに約2日かかりました。
CPUの性能にもよるとは思いますが、Xeron X3430でxargs8パラレルで実行してこれだけ時間がかかりました。
参考程度ですが、検討する際に数値をご検討にご利用いただければと思います。
変換後のファイルサイズですが、1.2GB弱→約5GBに増加しました。
この辺もストレージとの相談になると思いますので、数値をご参考までにご利用いただければと思います。

次は、変換したPDFファイルをFessでクロールしてインデックス作成する手順を気が向いたら書きます…