DockerでMeCabとHanDicを使用して韓国語の形態素解析(Python)

# はじめに
開発中のWebアプリ(Flask)にて韓国語の形態素解析が必要になったので、DockerでMeCabとHandicを動かします。
開発当初は韓国語の形態素解析は辞書はOpen Korean Textを使用してKoNLPyで行っていましたが、辞書の取得に時間がかかりサーバ上ではタイムアウトになってしまうため、軽量に動くHanDicで動かすことにしました。
- [KoNLPy: Korean NLP in Python — KoNLPy 0.6.0 documentation](https://konlpy.org/en/latest/)
- [open-korean-text/open-korean-text: Open Korean Text Processor - An Open-source Korean Text Processor](https://github.com/open-korean-text/open-korean-text)

# MeCabとは
[MeCab: Yet Another Part-of-Speech and Morphological Analyzer](https://taku910.github.io/mecab/)
>MeCabは 京都大学情報学研究科−日本電信電話株式会社コミュニケーション科学基礎研究所 共同研究ユニットプロジェクトを通じて開発されたオープンソース 形態素解析エンジンです。

# HanDicとは
[MeCabで韓国語 [コンピュータと朝鮮語のための覚え書き]](https://porocise.sakura.ne.jp/wiki/korean/mecab)
>HanDic（ハンディク、한딕）は、形態素解析器MeCabで利用することができる、現代韓国語解析用辞書です。([HanDicの概要 [コンピュータと朝鮮語のための覚え書き]](https://porocise.sakura.ne.jp/wiki/korean/mecab/summary))

# 準備
## HanDicのファイルのダウンロード
[ダウンロードファイル一覧 - HanDic - OSDN](https://ja.osdn.net/projects/handic/releases/)
上記からhandic-mecabのファイル（私は最新のhandic-mecab-20230109_src.tar.gzを使用しました）をダウンロードして、Dockerfileと同じディレクトリに保存します。

## ディレクトリ構成
形態素解析のみ行うFlaskアプリをDockerコンテナで動かします。
```
flask
├ src
│ ├ templates
│ │  └ test.html
│ ├ app.py
│ └ k2jamo.py
├ compose.yaml
├ Dockerfile
└ handic-mecab-20230109_src.tar.gz
```

## compose.yaml
Docker Compose用のファイルです。
```compose.yaml
services:
  web:
    build: .
    environment:
      FLASK_DEBUG: 1
    ports:
      - "5000:5000"
    volumes:
      - ./src:/usr/src/app
```
Dockerfileを使用してビルドします。
「environment」のFLASK_DEBUGを「1」に設定し、デバッグモードにします。
「ports」にて、5000で接続できるようにします。
「volumes」にて、srcディレクトリと/usr/src/appに
をバインドマウントします。

## Dockerfile

mecab関連のインストール、Pythonのライブラリのインストール、HanDicのインストールを行い、flaskコマンドでFlaskアプリを起動します。

```Dockerfile
FROM python:3.9.16
WORKDIR /usr/src/app

RUN apt-get update && apt-get install -y \
  mecab \
  mecab-ipadic-utf8 \
  libmecab-dev

RUN pip install flask==2.2.3
RUN pip install mecab-python3==1.0.6
RUN cp /etc/mecabrc /usr/local/etc/

COPY handic-mecab-20230109_src.tar.gz /usr/src/handic/handic-mecab-20230109_src.tar.gz
RUN cd /usr/src/handic && \
    tar zxvf handic-mecab-20230109_src.tar.gz

RUN cd /usr/src/handic/handic-mecab-20230109_src && \
     ./configure --with-dicdir="/usr/src/dict/handic" && \
    make && \
    make install

RUN cd /usr/src/app

CMD ["flask", "run", "--host=0.0.0.0"]
```
それぞれの内容です。

```Dockerfile
RUN apt-get update && apt-get install -y \
  mecab \
  mecab-ipadic-utf8 \
  libmecab-dev
RUN cp /etc/mecabrc /usr/local/etc/
```
apt-getを使用してMeCabに使用するものをインストールします。
「mecab」は日本語版・韓国語版共に必要です。
「mecab-ipadic-utf8」は日本語版の辞書です。
「libmecab-dev」はHanDicを使用する際にmecab-configが必要になるためインストールします。インストールしない場合、エラーとなります。

「RUN cp」の行にて「mecabrc」をコピーします。DockerでMeCabを使用する際は、この移動が必要になるようです。

参考:[fedora30 python3 から MeCabを使う → エラー「 [ifs] no such file or directory: /usr/local/etc/mecabrc」が出たら sudo cp /etc/mecabrc /usr/local/etc/ で解決 - min117の日記](https://min117.hatenablog.com/entry/2020/07/11/145738)

```Dockerfile
RUN pip install flask==2.2.3
RUN pip install mecab-python3==1.0.6
```
Pythonのライブラリです。Flaskを動かすためにflask、MeCabを動かすためにmecab-python3をインストールします。

```Dockerfile
COPY handic-mecab-20230109_src.tar.gz /usr/src/handic/handic-mecab-20230109_src.tar.gz
RUN cd /usr/src/handic && \
    tar zxvf handic-mecab-20230109_src.tar.gz

RUN cd /usr/src/handic/handic-mecab-20230109_src && \
     ./configure --with-dicdir="/usr/src/dict/handic" && \
    make && \
    make install
```
ダウンロードしたHanDicのファイルを適当な場所にコピー（今回は/usr/src/handic）し、tarコマンドで展開、ファイル出力します。
展開されたディレクトリにて「./configure」で環境設定、「make」でコンパイル、「make install」でインストールします。

参考: [configure, make, make install とは何か - Qiita](https://qiita.com/chihiro/items/f270744d7e09c58a50a5)

「./configure --with-dicdir="/usr/src/dict/handic"」とすることで、辞書の場所を指定できます。


```Dockerfile
RUN cd /usr/src/app

CMD ["flask", "run", "--host=0.0.0.0"]
```
flaskコマンドでFlaskを起動します。


# Flaskアプリの内容
## templates
```test.html
<html>
    <head>test</head>
    <body>
        <p>{{j_words}}</p>
        <p>{{k_words}}</p>
    </body>
</html>
```
ただ抽出した単語を表示するだけのHTMLファイルです。

## MeCabによる形態素解析(Python)
日本語、韓国語共に、<font color="red">名詞(Noun)、動詞(Verb)、形容詞(Adjective)、副詞(Adverb)の原型</font>を抽出するようにしています。

```app.py
from flask import Flask, render_template
import MeCab
import k2jamo

app = Flask(__name__)

@app.route('/')
def test():
    # 日本語形態素解析
    mecab = MeCab.Tagger()
    text='母は医者です。'
    node = mecab.parseToNode(text)

    j_words = []
    while node:
        p = node.feature.split(',')[0]
        if p == '名詞' or p == '動詞' or p== '形容詞' or p =='副詞':
            j_words.append(node.feature.split(",")[6])
        node = node.next

    # 韓国語形態素解析
    input = u'술울 별로 안 마십니다.'
    input = k2jamo.substitute(input)
    tokenizer = MeCab.Tagger('-d /usr/src/dict/handic')

    node = tokenizer.parseToNode(input)
    k_words = []
    while node:
        p = node.feature.split(',')[0]

        if p == 'Noun':
            k_words.append(node.feature.split(",")[6])
        
        if p == 'Verb' or p== 'Adjective' or p =='Adverb':
            word = node.feature.split(",")[5]
            if word[-1].isdigit():
                k_words.append(word[:-2])
            else:
                k_words.append(word)
        node = node.next

    return render_template('test.html', j_words=j_words, k_words=k_words)
```

>MeCab::Tagger クラスの, parseToNode という メソッドを呼ぶことで, 「文頭」という特別な形態素が MeCab::Node クラスのインスタンスとして 取得できます.
[スクリプト言語のバインディング](http://taku910.github.io/mecab/bindings.html)

TaggerクラスのparseToNode()メソッドを使用して解析し、そのうちのfeatureで得られる結果をsplit()関数で「,」で区切ったリストとします。最初の要素（要素番号0）に品詞名があります。

### 日本語の形態素解析
日本語の場合は品詞名は「名詞」、「動詞」、「形容詞」、「副詞」になるため、最初の要素がこれらの時に、リストの７番目の要素（要素番号6）の原型をj_wordsというリストに格納します。

参考: [MeCab：品詞ごとに取り出す - Qiita](https://qiita.com/Sue_chan/items/091e2b77e6c0b26d4106)

### 韓国語の形態素解析
Taggerクラスのインスタンス生成時、インストールしたHandicの辞書を引数にて指定します。今回の場合「./configure」の実行時に「/usr/src/dict/handic」に指定しています。

HanDicで形態素解析する場合は、後述する「k2jamo.py」のsubstitude関数が必要になります。

HanDicの辞書にてMeCabで形態素解析した場合、韓国語の場合は「名詞」、「動詞」、「形容詞」、「副詞」がそれぞれ英語で「Noun」、「Verb」、「Adjective」、「Adverb」となるため、これらが最初の要素にある場合、f_wordsというリストに原型を格納します。

参考: [HanDicの品詞体系 [コンピュータと朝鮮語のための覚え書き]](https://porocise.sakura.ne.jp/wiki/korean/mecab/pos)

HanDicの場合、名詞は要素番号6の「出現型」、動詞・形容詞・副詞は要素番号5の辞書型が原型と言えると思いますので、それぞれ抽出します。
ただ、要素番号5の辞書型については、「타다01」 「가다01」のように数字の付くもの、「사다」のように数字の付かないものが混在しているため、数字が付いている場合は数字を削除し、そうでない場合はそのまま格納するようにしています。

参考： [HanDicの概要 [コンピュータと朝鮮語のための覚え書き]](https://porocise.sakura.ne.jp/wiki/korean/mecab/summary)

## 完成形ハングルを字母に分解するスクリプト

```k2jamo.py
# k2jamo
# substitute(text)
import re
 
initial = ["ᄀ", "ᄁ", "ᄂ", "ᄃ", "ᄄ", "ᄅ", "ᄆ", "ᄇ", "ᄈ", "ᄉ", "ᄊ", "ᄋ",
			"ᄌ", "ᄍ", "ᄎ", "ᄏ", "ᄐ", "ᄑ", "ᄒ"]
medial = ["ᅡ", "ᅢ", "ᅣ", "ᅤ", "ᅥ", "ᅦ", "ᅧ", "ᅨ", "ᅩ", "ᅪ", "ᅫ", "ᅬ",
		   "ᅭ", "ᅮ", "ᅯ", "ᅰ", "ᅱ", "ᅲ", "ᅳ", "ᅴ", "ᅵ", ""]
final = ["", "ᆨ", "ᆩ", "ᆪ", "ᆫ", "ᆬ", "ᆭ", "ᆮ", "ᆯ", "ᆰ", "ᆱ", "ᆲ",
		  "ᆳ", "ᆴ", "ᆵ", "ᆶ", "ᆷ", "ᆸ", "ᆹ", "ᆺ", "ᆻ", "ᆼ", "ᆽ", "ᆾ",
		  "ᆿ", "ᇀ", "ᇁ", "ᇂ"]

REGEX = '[가-힣]'
pattern = re.compile(REGEX)
 
 
def convert_main(match):
    '正規表現でマッチした完成形ハングル<match>を字母に分解する'
    # ord...文字をUnicodeにする
    # group(0)...パターンにマッチした文字列全体を返す
    value = ord(match.group(0))
    my_int = value - 44032
    my_int_index = int(my_int / 588)
    my_final_index = my_int % 28
    my_medial_index = \
        int((my_int - (my_int_index * 588) - my_final_index) / 28)
    # Unicodeの番号からハングルを割り出す
    result = initial[my_int_index] + \
        medial[my_medial_index] + \
        final[my_final_index]
    return result
 
 
def substitute(text):
    '正規表現で完成形ハングルにマッチした入力<text>を置換する'
    # convert_main Unicordから割り出されたハングル文字
    result = re.sub(pattern, convert_main, text)
    return result
 
 
if __name__ == '__main__':
    for letters in ['가', '안녕', '한국어 문장입니다.', '영문자 a/b를 포함']:
        output = substitute(letters)
        print(letters, '=>', output)
```
HanDicのサイトの「[HanDicをPythonで利用する [コンピュータと朝鮮語のための覚え書き]](https://porocise.sakura.ne.jp/wiki/korean/mecab/python)」のコードをほとんどそのまま使用しています。

>HanDicでは完成型のハングルではなく，字母に分解した文字列を入力として用いるため，完成形ハングルを字母に分解するスクリプトを用意します．
[HanDicをPythonで利用する [コンピュータと朝鮮語のための覚え書き]](https://porocise.sakura.ne.jp/wiki/korean/mecab/python)

ただこのコードのみでは正しく動かないため、HanDicのダウンロードページ（[https://ja.osdn.net/projects/handic/releases/63505](https://ja.osdn.net/projects/handic/releases/63505))のファイルを参考にinitial, medial, finalの内容を追加しました。現在このダウンロードページにアクセスできないので、ファイル名などは確認できません。

### 余談
k2jamo.pyでは完成形ハングルのUnicodeから、「initial」「medial」「final」をそれぞれ割り出し、字母（Jamo）に分解した文字列とします。ハングルのUnicodeについては以下のページに解説がありました。
>The decimal unicode character value is calculated by:
>1. multiplying the value of the ‘initial’ letter's jamo by 588
>2. multiplying the value of the ‘medial’ letter's jamo by 28
>3. adding the two values together (along with the value of a ‘final’ letter's jamo, if there is one)
>4. and finally, adding 44032 to this.
as an equation:
[ { ( initial )  ×  588 } + { ( medial ) × 28 } + ( final ) ] + 44032
[Hangŭl Unicode From Jamo Values | Codewars](https://www.codewars.com/kata/58223a9f70ca2844e0000592)


# Flaskアプリの起動
compose.yamlファイルのあるディレクトリにて以下のコマンドでコンテナを起動し、Flaskアプリを起動します。
```
docker compose up -d
```
「localhost:5000」にアクセスすると、以下のように表示されます。
![スクリーンショット 2023-07-15 10.37.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/925094/de0f82b0-ed0b-0583-ae3d-d91830f1166f.png)
単純にリストに格納されたものを表示しています。


# 参考
- [Mecabで形態素解析：品詞分解する方法【Python】 - Your 3D](https://your-3d.com/mecab-hinshi-separate/)
- [MeCab：品詞ごとに取り出す - Qiita](https://qiita.com/Sue_chan/items/091e2b77e6c0b26d4106)
- [Pythonで形態素解析して韓国語学習 - Qiita](https://qiita.com/mnmandahalf/items/96a753df086b81dd1014)
- [pythonで韓国語の形態素解析 - mnmandahalfのブログ](https://mnmandahalf.hatenablog.com/entry/2020/04/25/224413)
- [dockerで形態素解析の環境構築（jupyter + mecab） - Qiita](https://qiita.com/rasyo/items/9756b1a7bb739bec319a)