PyKNPの利用環境をDockerで建てる

# はじめに
PyKNPを利用して固有表現抽出をやってみたいなと思ってちょっと環境構築を調べてみました．メインで使用しているmacOSにはHomebrew経由で先人の用意したリポジトリ[uetchy/homebrew-nlp
](https://github.com/uetchy/homebrew-nlp)を使わせてもらうことで準備できました．
ただ，この環境をDockerでイメージ化しておきたいなーという欲求が生まれたので，勉強がてら構築してみることにしました．

# 結論
忙しい方のためにTL;DR風に先に結論を書いておくことにします．
## ファイル構成
開発用のディレクトリ(とはいえただのテスト)の構成は以下のような感じです．近年は`requirements.txt`といったパッケージ関係をどう管理するかについて様々な流儀がありますがこのエントリでは触れないことにします．

```bash
  % tree
.
├── Dockerfile
├── docker-compose.yml
├── pyknp_test.py
└── requirements.txt

0 directories, 4 files
```

それぞれのファイルは以下のように用意しました．

```dockerfile:Dockerfile
FROM python:3.6-slim

# create user as "app"
RUN useradd --user-group --create-home --shell /bin/false app

# set home directory
ENV HOME=/home/app

# copy source file
COPY . $HOME/pyknp_test

# install dependencies
RUN apt-get update && apt-get install -y \
  build-essential \
  curl \
  bzip2 \
  libjuman \
  libcdb-dev \
  zlib1g-dev
RUN pip3 install --requirement $HOME/pyknp_test/requirements.txt

# install juman and KNP and copy .jumanrc file
RUN mkdir $HOME/src && \
  cd $HOME/src && \
  curl -L -o juman-7.01.tar.bz2 http://nlp.ist.i.kyoto-u.ac.jp/nl-resource/juman/juman-7.01.tar.bz2 && \
  tar xf juman-7.01.tar.bz2 && \
  cd juman-7.01 && \
  ./configure && \
  make && \
  make install && \
  cp /usr/local/etc/jumanrc $HOME/.jumanrc
RUN cd $HOME/src && \
  curl -L -o knp-4.19.tar.bz2 http://nlp.ist.i.kyoto-u.ac.jp/nl-resource/knp/knp-4.19.tar.bz2 && \
  tar xf knp-4.19.tar.bz2 && \
  cd knp-4.19 && \
  ./configure && \
  make && \
  make install

# delete download cache
RUN rm -rf $HOME/src && \
  apt-get purge -y \
  build-essential \
  curl \
  bzip2

# change user
RUN chown -R app:app $HOME/*
WORKDIR $HOME/pyknp_test
USER app

# set CMD
CMD ["python3", "pyknp_test.py"]
```

```yaml:docker-compose.yml
version: '3'
services:
  pyknp:
    build: .
    volumes:
      - .:/home/app/pyknp_test
```

```python:pyknp_test.py
# coding: utf-8
import pyknp
import re

# KNP prepairing
knp = pyknp.KNP(option="-tab -dpnd", jumanpp=False)

def make_np_tagged_text(src_text: str):
    tagged_text = src_text # copy
    result = knp.parse(src_text) # tagging

    for tag in result.tag_list():
        if "NE:" in tag.fstring: # if fstring has NE phrase
            # extract NE phrase
            tagged_ne_phrase = re.search("<NE:(.*):(.*)>", tag.fstring).group(0)
            ne_phrase = re.search("<NE:(.*):(.*)>", tag.fstring).group(2)

            # overwrite to src text
            tagged_text = tagged_text.replace(ne_phrase, tagged_ne_phrase)

    return tagged_text

def main():
    line = "太郎は5月18日の朝9時に花子に会いに行った．"

    print(line)
    print(make_np_tagged_text(line))

if __name__ == "__main__":
    main()
```

```text:requirements.txt
pyknp
```

## 実行
docker-composeを介して実行してみます．

```bash
  % docker-compose build
(ガガッとビルドされる)

  % docker-compose up
Starting pyknp_test_pyknp_1 ... done
Attaching to pyknp_test_pyknp_1
pyknp_1  | 太郎は5月18日の朝9時に花子に会いに行った．
pyknp_1  | <NE:PERSON:太郎>は5月18日の朝9時に<NE:LOCATION:花子>に会いに行った．
pyknp_test_pyknp_1 exited with code 0
```

`pyknp_test.py`の変数`line`を変えて再び実行してみましょう．

```python:pyknp_test.py(抜粋)
    line = "ほげほげ〜"
```

```bash
  % docker-compose up
Starting pyknp_test_pyknp_1 ... done
Attaching to pyknp_test_pyknp_1
pyknp_1  | ほげほげ〜
pyknp_1  | ほげほげ〜
pyknp_test_pyknp_1 exited with code 0

```
いい感じですね:+1:

# 構成の説明
## 環境
ざっくり以下のような環境でテストしています．

|||
|---|---|
|OS|macOS 10.13.6|
|Docker|18.06.1-ce-mac73|

## Dockerfile
いくつかの手順に分解して`Dockerfile`を読み解いていきます．筆者は，主に個人でのツールやサービス開発でDockerを利用しているのでそのあたりのtipsが入っていると感じる方もいるかも知れません．

### 元イメージの設定，ユーザ設定
```dockerfile:Dockerfile(抜粋)
FROM python:3.6-slim

# create user as "app"
RUN useradd --user-group --create-home --shell /bin/false app

# set home directory
ENV HOME=/home/app

# copy source file
COPY . $HOME/pyknp_test
```
この部分は，構築するイメージの元イメージの設定，実行ユーザの設定，開発ディレクトリのコピーをおこなっています．
最終的に実行するのはappユーザとなるようこのあと準備していきます．

### JUMAN, KNPのインストール
最近だとJUMANの代わりにJUMAN++を使用することが多いのでしょうか?解析器に強くないので分かりませんが・・・
その話は置いて，[JUMAN](http://nlp.ist.i.kyoto-u.ac.jp/index.php?JUMAN), [KNP](http://nlp.ist.i.kyoto-u.ac.jp/?KNP)をインストールします．どちらも黒橋・河原研究室から発表されているものです．公開に感謝しながら使っていきます．

では，JUMAN, KNPのインストールを抜粋しながら見ていきます．

#### ビルド環境・ライブラリ等の準備
```dockerfile:Dockerfile(抜粋)
# install dependencies
RUN apt-get update && apt-get install -y \
  build-essential \
  curl \
  bzip2 \
  libjuman \
  libcdb-dev \
  zlib1g-dev
```
この部分は，ビルド環境(`build-essential`)とライブラリ等を準備します．
今回は構築するイメージのもととして`python:3.6-slim`を使用しているので，このあと使用するファイルダウンロードと展開のため`curl`と`bzip2`も同時にインストールしています．

#### pipからのインストール
```dockerfile:Dockerfile(抜粋)
RUN pip3 install --requirement $HOME/pyknp_test/requirements.txt
```
この部分は，開発ディレクトリの`requirements.txt`を元にpipからパッケージをインストールしています．`Dockerfile`の記述を見ればわかりますが，最初に開発ディレクトリの内容をイメージにコピーしているため実行できます．

#### JUMAN, KNPのインストール
```dockerfile:Dockerfile(抜粋)
# install juman and KNP and copy .jumanrc file
RUN mkdir $HOME/src && \
  cd $HOME/src && \
  curl -L -o juman-7.01.tar.bz2 http://nlp.ist.i.kyoto-u.ac.jp/nl-resource/juman/juman-7.01.tar.bz2 && \
  tar xf juman-7.01.tar.bz2 && \
  cd juman-7.01 && \
  ./configure && \
  make && \
  make install && \
  cp /usr/local/etc/jumanrc $HOME/.jumanrc
RUN cd $HOME/src && \
  curl -L -o knp-4.19.tar.bz2 http://nlp.ist.i.kyoto-u.ac.jp/nl-resource/knp/knp-4.19.tar.bz2 && \
  tar xf knp-4.19.tar.bz2 && \
  cd knp-4.19 && \
  ./configure && \
  make && \
  make install
```
この部分は，JUMAN, KNPのソースをダウンロードして展開，make，make installしている部分です．
特徴は，`.jumanrc`ファイルを**ホームディレクトリにコピー**しているところです．この処理は現在rootで実行しているのでこの処理をやっておきます．

ちなみに，Dockerの公式ドキュメントを読む感じだとそれぞれのソースはcurlからファイルに保存せずに直接tarに渡して実行するほうが一般的なのかもしれません．

JUMAN++を利用したいと考えている方は，ダウンロードするソースをJUMAN++に変えて実行してください．

### ビルド環境等の削除
```dockerfile:Dockerfile(抜粋)
# delete download cache
RUN rm -rf $HOME/src && \
  apt-get purge -y \
  build-essential \
  curl \
  bzip2
```
この部分は，インストール済みのJUMAN, KNPのソースの削除，ビルド環境等の削除をおこなっています．
イメージを軽くしたいのでcurl等も削除しています．

### ユーザ変更と実行コマンドの指定
```dockerfile:Dockerfile(抜粋)
# change user
RUN chown -R app:app $HOME/*
WORKDIR $HOME/pyknp_test
USER app

# set CMD
CMD ["python3", "pyknp_test.py"]
```
この部分で，ホームディレクトリの中身の権限をappユーザに変更しています．これにより，コマンドの実行をappユーザとしておこなえるようにしています．
また，ワーキングディレクトリをコピーした開発ディレクトリにしています．`docker-compose.yml`側でイメージ内のパス指定をしなくて済むようこうしておくと良いでしょう．

## docker-compose.yml
```yaml:docker-compose.yml
version: '3'
services:
  pyknp:
    build: .
    volumes:
      - .:/home/app/pyknp_test
```
`docker-compose.yml`は結構シンプルです．`pyknp`というサービス名で先程の`Dockerfile`をビルドして実行させています．
また，先程設定したワーキングディレクトリ直下に開発ディレクトリの中身をバインドします．これで**開発ディレクトリの中身を変更しても再度ビルドすること無く**実行できます．

# おわりに
何となくいい感じに開発環境ができたのではないでしょうか．
ディレクトリの構成が簡単なのでイメージ公開やらリポジトリ公開までやらなくてもいいかな〜と勝手に思っています．要望あれば挑戦してみようと思いますのでコメントください．

他にも，何か良いプラクティス等あればコメント頂けると幸いです．