SNSテキストから顔文字・絵文字・URLを抽出する

この記事は，NTTドコモアドベントカレンダー第11日目の記事になります。

# こんな人に読んでほしい
- 口コミやツイートの分析をしている人
- 顔文字・絵文字・URLを抽出するツールを探している人
- 自然言語処理に興味がある人

**※ 最後の付録で実装したソースコードを公開してます！**

# 1. はじめに
自然言語処理にとって前処理は非常に重要です。
特に、商品の口コミやツイートは顔文字やURLなどのノイズが多く、前処理しないままでは思い通りの分析ができません😇
そこで本記事では、SNSテキストから**顔文字・絵文字・URL**を抽出して**削除/置換**するための前処理方法について紹介したいと思います。

**前処理例**
<img width="745" alt="prepro_example.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525979/bc2fba83-b460-ac40-4512-736e6bf2dc81.png" width=80%>

# 2. 必要なもの
### ツール
- [nagisa](https://github.com/taishi-i/nagisa)
	- 顔文字・URLの検出に使います。日本語テキスト用の形態素解析ツールです。
	- *参考記事*： [nagisa: RNNによる日本語単語分割・品詞タグ付けツール](https://qiita.com/taishi-i/items/5b9275a606b392f7f58e)
- [emoji](https://pypi.org/project/emoji/)
	- 絵文字の検出に使います。
	- *参考記事*: [pythonで絵文字を駆逐する](https://qiita.com/yoshimo123/items/85331d881aed9ad41020)

##### インストール方法
```
$ pip install nagisa emoji
```

### データ
抽出対象のSNSテキストデータです。ご自身で収集した口コミやツイートをご用意下さい。
もし手元に無い場合は、MTNTデータなどを使って試しましょう。

- [MTNT: Machine Translation of Noisy Text](https://www.cs.cmu.edu/~pmichel1/mtnt/)
	- MTNTデータセットは、Redditという投稿型のソーシャルサイトの書き込みを収集したものです（日本の2ちゃんねるのような掲示板サイトに近いです）。
	- スラングや文法誤り、顔文字・絵文字などのノイズを多く含むSNSテキストの機械翻訳の研究用に無償公開されています。

# 3. 実践: SNSテキストを前処理してみよう
それでは、実際にSNSテキストから顔文字・絵文字・URLを抽出してみましょう。

## 3.1 方針
顔文字・URLの抽出は[nagisa](https://pypi.org/project/nagisa/)、絵文字の抽出は[emoji](https://pypi.org/project/emoji/)を使います。

## 3.2 顔文字の抽出方法
`nagisa`は日本語テキスト用の形態素解析ツールですが、
うまく使いこなせば顔文字やURLを抽出することができます。

[こちらの記事](https://qiita.com/taishi-i/items/5b9275a606b392f7f58e)でも言及されている通り、nagisaでは顔文字やURLを1つの単語として単語分割することができます。

しかし、`＼(^o^)／` などの**手**を含む顔文字は1つの単語として分割することは難しいようです。
`\/補助記号 (^o^)/補助記号 //補助記号` のように、3つの単語に分かれてしまいます。

**nagisaの解析結果**

![nagisa_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525979/37e14d09-c212-41ed-ba57-44cc5ecc327b.png)

ですが、うまく処理すれば、このような顔文字も抽出することができます。
仕組みは単純で、顔文字の手を事前に定義して、顔文字の周辺から**手**を一緒に抽出します。

```py
import nagisa
import unicodedata
def extract_kaomoji(text):
    """ 与えられたテキストから抽出した顔文字リストを返却する。
        → ＼(^o^)／, m(_ _)m などの 手を含む顔文字があれば、それも抽出する
    """
    results = nagisa.extract(text, extract_postags=['補助記号'])
    words = results.words
    kaomoji_words = []
    kaomoji_idx = [i for i, w in enumerate(words) if len(w) >= KAOMOJI_LEN]
    kaomoji_hands = ['ノ', 'ヽ', '∑', 'm', 'O', 'o', '┐', '/', '\\', '┌'] 
    # 顔文字と手を検索
    for i in kaomoji_idx:
        kaomoji = words[i] # 顔文字列
        try:
            # 顔文字の左手
            if words[i-1] in kaomoji_hands and 0 < i:
                kaomoji = words[i-1] + kaomoji
            # 顔文字の右手
            if words[i+1] in kaomoji_hands:
                 kaomoji = kaomoji + words[i+1]
        except IndexError:
            pass
        finally:
            kaomoji_words.append(kaomoji)
    return kaomoji_words

text = "今日は渋谷スクランブルスクエアに行ってきた＼(^o^)／ 夜景🏙サイコー❗️ https://hogehogehogehoge.jpg"
text = unicodedata.normalize('NFKC', text) # NFKC正規化
print(extract_kaomoji(text))
# => ['\\(^o^)/']

text = "ごめんなさいm(-_-)m"
text = unicodedata.normalize('NFKC', text) # NFKC正規化
print(extract_kaomoji(text))
# => ['m(-_-)m']
```

これで、テキストから顔文字を抽出することができました。

**抽出結果**

```
今日は渋谷スクランブルスクエアに行ってきた＼(^o^)／ 夜景🏙サイコー❗️ https://hogehogehogehoge.jpg
顔文字: \(^o^)/

ごめんなさいm(-_-)m
顔文字: m(-_-)m
```

## 3.3 URLの抽出
URLの抽出はシンプルです。 
必要な品詞の単語だけを抽出する関数`nagisa.extract`で必要な品詞タグに`URL`を条件付けるだけで、URLを抽出することができます。

```py
import nagisa
import unicodedata

def extract_url(words):
    results = nagisa.extract(text, extract_postags=['URL'])
    return results.words

text = "今日は渋谷スクランブルスクエアに行ってきた＼(^o^)／ 夜景🏙サイコー❗️ https://hogehogehogehoge.jpg"
text = unicodedata.normalize('NFKC', text) # NFKC正規化
print(extract_url(text))
# => ['https://hogehogehogehoge.jpg']
```

**抽出結果**

```
今日は渋谷スクランブルスクエアに行ってきた＼(^o^)／ 夜景🏙サイコー❗️ https://hogehogehogehoge.jpg
URL: https://hogehogehogehoge.jpg
```


## 3.4 絵文字の抽出

絵文字の抽出も簡単です。
絵文字は[Unicodeで定義されている](http://www.unicode.org/emoji/charts/full-emoji-list.html)ため、文字のUnicodeを確認してその文字が絵文字かどうかを判定します。
`emoji`で絵文字のUnicodeリストを使うことができます。

```py
import emoji
import nagisa
import unicodedata

def extract_emoji(words):
    return [w for w in words if w in emoji.UNICODE_EMOJI]


text = "今日は渋谷スクランブルスクエアに行ってきた＼(^o^)／ 夜景🏙サイコー❗️ https://hogehogehogehoge.jpg"
text = unicodedata.normalize('NFKC', text) # NFKC正規化
print(extract_emoji(text))
# => ['🏙', '❗']

text = "日本語のテキストから絵文字😉を抽出するよ❗"
text = unicodedata.normalize('NFKC', text) # NFKC正規化
print(extract_emoji(text))
# => ['😉', '❗']
```

**抽出結果**

```
今日は渋谷スクランブルスクエアに行ってきた＼(^o^)／ 夜景🏙サイコー❗️ https://hogehogehogehoge.jpg
絵文字: 🏙, ❗

日本語のテキストから絵文字😉を抽出するよ❗
絵文字: 😉, ❗
```

# 4. おわりに
テキストから顔文字・絵文字・URLを抽出する前処理方法を紹介しました。

本記事で紹介した前処理（一部）は、今年ドコモとNTTが参加したWMT19のRobustness taskに投稿したシステムでも使っています。
論文でも前処理について書いていますので、ツイートや口コミの分析に取り組んでいる方のお役に立てれば幸いです。

- [NTT’s Machine Translation Systems for WMT19 Robustness Task](https://www.aclweb.org/anthology/W19-5365/)


最後に、本記事を書く際に参考にさせていただいた記事を紹介します。
テキスト前処理についてとても良くまとめられているので、ぜひ参考にしてみて下さい。

#### テキスト前処理に関する記事のまとめ
- [自然言語処理における前処理の種類とその威力](https://qiita.com/Hironsan/items/2466fe0f344115aff177)
- [Python3×日本語：自然言語処理の前処理まとめ](https://qiita.com/chamao/items/7edaba62b120a660657e)
- [自然言語（前）処理](https://qiita.com/dcm_sawayama/items/406408e8bda0840a8106)
- [自然言語のpythonでの前処理のかんたん早見表（テキストクリーニング、分割、ストップワード、辞書の作成、数値化）](https://qiita.com/kakiuchis/items/9149c7249f748668bb9b)
- [Pythonを使って自然言語処理の前処理を行う](https://www.dskomei.com/entry/2019/04/04/191506)
- [形態素解析前の日本語文書の前処理 (Python)](https://ohke.hateblo.jp/entry/2019/02/09/141500)

---

# 付録
本記事で実装したコード（全文）です。

#### ソースコード
```py
import emoji
import nagisa
import unicodedata

KAOMOJI_PH = "<Kaomoji>"
URL_PH = "<URL>" 
EMOJI_PH = "<Emoji>"
KAOMOJI_LEN = 5

def extract_kaomoji(text):
    """ 与えられたテキストから抽出した顔文字リストを返却する。
        → ＼(^o^)／, m(_ _)m などの 手を含む顔文字があれば、それも抽出する
    """
    results = nagisa.extract(text, extract_postags=['補助記号'])
    words = results.words
    kaomoji_words = []
    kaomoji_idx = [i for i, w in enumerate(words) if len(w) >= KAOMOJI_LEN]
    kaomoji_hands = ['ノ', 'ヽ', '∑', 'm', 'O', 'o', '┐', '/', '\\', '┌'] 
    # 顔文字と手を検索
    for i in kaomoji_idx:
        kaomoji = words[i] # 顔文字列
        try:
            # 顔文字の左手
            if words[i-1] in kaomoji_hands and 0 < i:
                kaomoji = words[i-1] + kaomoji
            # 顔文字の右手
            if words[i+1] in kaomoji_hands:
                 kaomoji = kaomoji + words[i+1]
        except IndexError:
            pass
        finally:
            kaomoji_words.append(kaomoji)
    return kaomoji_words

def extract_url(words):
    results = nagisa.extract(text, extract_postags=['URL'])
    return results.words

def extract_emoji(text):
    results = nagisa.tagging(text) # 形態素解析
    words = results.words
    return [w for w in words if w in emoji.UNICODE_EMOJI]

def replace(text, target_list, PH):
    for trg in target_list:
        text = text.replace(trg, PH)
    return text

def delete(text, target_list):
    for trg in target_list:
        text = text.replace(trg, "")
    return text

text = "今日は渋谷スクランブルスクエアに行ってきた＼(^o^)／ 夜景🏙サイコー❗️ https://hogehogehogehoge.jpg"
text = unicodedata.normalize('NFKC', text) # NFKC正規化

# 入力
print("対象テキスト: {}\n".format(text))

# 抽出
kaomoji_list = extract_kaomoji(text)
url_list = extract_url(text)
emoji_list = extract_emoji(text)

# 抽出結果
print("顔文字: {}".format(kaomoji_list))
print("URL: {}".format(url_list))
print("絵文字: {}\n".format(emoji_list))


# 削除
deleted_text = delete(text, kaomoji_list + url_list + emoji_list)
print("削除: {} \n".format(deleted_text))

# 置換
replaced_text = replace(text, kaomoji_list, KAOMOJI_PH)
replaced_text = replace(replaced_text, url_list, URL_PH)
replaced_text = replace(replaced_text, emoji_list, EMOJI_PH)
print("置換: {} \n".format(replaced_text))

```

#### 出力例
```
対象テキスト: 今日は渋谷スクランブルスクエアに行ってきた\(^o^)/ 夜景🏙サイコー❗️ https://hogehogehogehoge.jpg

顔文字: ['\\(^o^)/']
URL: ['https://hogehogehogehoge.jpg']
絵文字: ['🏙', '❗']

削除: 今日は渋谷スクランブルスクエアに行ってきた 夜景サイコー️  

置換: 今日は渋谷スクランブルスクエアに行ってきた<Kaomoji> 夜景<Emoji>サイコー<Emoji>️ <URL> 
```