社会性フィルター実装したtwitterクライアントを作った

~~こちらです~~
~~https://socialfiltertwitter.kktnhrms.com/~~
-> 公開終了しました

コードはこちら
https://github.com/xKxAxKx/social_filter_twitter

クライアントというか、つぶやき専用機というか、まあ、そういうものです
Python/Djangoで実装しました
んで、やったことないことがいろいろあったので、以下、知見です

## そもそも社会性フィルターってなに？
これです
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">図です <a href="https://t.co/2OFfuLPk2l">pic.twitter.com/2OFfuLPk2l</a></p>&mdash; 4869 (@sh4869sh) <a href="https://twitter.com/sh4869sh/status/767244989503901696">2016年8月21日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>


## 環境
- Python == 3.6.2
- Django == 1.11.4


## ①Twitterアカウントを利用したログインの実装
前提として、Twitterアカウントでログインしたユーザ情報はデフォルトのユーザテーブルに書き込まれるものとします
なので、`$ python manage.py migrate`でmigrateしておく必要があります

### Python Social Authの導入
PythonでTwitterやFacebook、Google等のアカウントでログインさせるためのパッケージはいろいろとあるみたいなのですが、今回はpython-social-authを利用しました
合わせて、requests, requests_oauthlibもインストール

```
$ pip install requests
$ pip install requests_oauthlib
$ pip install python-social-auth[django]
```

API KEYなどはhttps://apps.twitter.com/ で取得する
ちなみに登録するURLは`http://localhost:8000`や`http://127.0.0.1:8000`でも問題なし
コールバックURLも設定しておくこと(登録するURLと同じで良いです)

API KEYを取得したらsettings.pyを編集

```python:settings.py
INSTALLED_APPS = [
	...
    'social_django',
]

TEMPLATES = [
    {
		 ...
        'OPTIONS': {
            'context_processors': [
                ...
                'social_django.context_processors.backends',
                'social_django.context_processors.login_redirect',
            ],
        },
    },
]

SOCIAL_AUTH_PIPELINE = [
    'social_core.pipeline.social_auth.social_details',
    'social.pipeline.social_auth.social_uid',
    'social_core.pipeline.social_auth.social_user',
    'social_core.pipeline.user.get_username',
    'social_core.pipeline.user.create_user',
    'social.pipeline.social_auth.associate_user',
    'social.pipeline.social_auth.load_extra_data',
    'social.pipeline.user.user_details',
]

SOCIAL_AUTH_TWITTER_KEY = 'xxxxxxxxxx'
SOCIAL_AUTH_TWITTER_SECRET = 'xxxxxxxxxxxx'
```

SOCIAL_AUTH_PIPELINEについては[こちら](http://python-social-auth.readthedocs.io/en/latest/pipeline.html)を参照して、設定しましたが、もしかしたら不要なものがあるかもしれません

ログイン後にリダイレクトさせたいURLがある場合は、以下のようにsettings.pyに書いておく

```python:settings.py
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/home/'
```

### ログイン用のURLを用意する
urls.pyを以下のように編集する

```python:urls.py
from django.conf.urls import url, include # includeを追加
from django.contrib import admin, auth # authを追加
from django.conf import settings


urlpatterns = [
	...
    url(r'', include('social_django.urls', namespace = 'social')),
]
```

### ログイン用のリンクを用意する

```hogehoge.html
# リンクの場合
<a href="{% url 'social:begin' 'twitter' %}">Twitterでログイン</a>

# ボタンの場合
<button type="button" onclick="location.href='{% url 'social:begin' 'twitter' %}'">Twitterでログイン</button>
```

ここまでやったら`python manage.py makemigrations`、`python manage.py migrate`しておく

その後、ログインリンク/ログインボタンをクリックするとおなじみに画面が出てて、Twitterアカウントでアプリケーションへのログインができる
![スクリーンショット 2017-08-14 17.43.09.png](https://qiita-image-store.s3.amazonaws.com/0/80787/9c0e54f7-dc95-9e11-701d-8c9647c39e5e.png "スクリーンショット 2017-08-14 17.43.09.png")

adminサイトの方も見てみると、ちゃんとTwitterアカウントでユーザ登録がされている
![スクリーンショット 2017-08-14 17.45.23.png](https://qiita-image-store.s3.amazonaws.com/0/80787/e4ec9b54-e56d-7f11-5f52-cdec97290026.png "スクリーンショット 2017-08-14 17.45.23.png")

### ログアウト用のリンクを用意する
ログアウトについてはDjangoのデフォルトのAuthを利用していく

```python:urls.py
...
from django.contrib.auth.views import logout

urlpatterns = [
	...
    url(r'', include('django.contrib.auth.urls', namespace='auth')),
]
```

```hogehoge.html
# リンクの場合
<a href="{% url 'auth:logout' %}?next={{ '/' }}">ログアウト</a>

# ボタンの場合
<button type="button" onclick="location.href='{% url 'auth:logout' %}?next={{ '/' }}'">ログアウト</button>
```

デフォルトではauthのログアウトをした場合、`/logout/`にリダイレクトされてしまうため、リダイレクト先を指定しておくと良い

## ②TwitterへのつぶやきのPOST
### Twitterのパッケージをインストール
pipでtwitterにPOSTするためのパッケージをインストールする

```
$ pip install twitter
```


### つぶやき用のフォームを作る

```python:forms.py
from django import forms
import os
from django.contrib.admin import widgets
from django.core.exceptions import ValidationError

class TweetForm(forms.Form):
    tweet = forms.CharField(
        label="",
        widget=forms.Textarea(attrs={'placeholder': 'いまどうしてる？'}),
        max_length = 140,
```

```views.py
from .forms import TweetForm

def home(request):
    form = TweetForm

    if request.user.is_authenticated():
        return render(request,
            'home.html',
            dict(form = form)
        )
    else:
        return redirect('index')
```

```home.html
<form method="post">
  {{ form.as_p }}
  {% csrf_token %}
  <button >つぶやく</button>
</form>
```

こんな感じにしておくと、とりあえず、ツイートするためのフォームができる

### ログインユーザのアクセストークンを取得しツイートする
twitterでログインしたユーザは通常のUsersテーブルにリレーションしているUserSocialAuthテーブルに以下のようなJSON形式でユーザデータが格納されている

```
{
    "access_token": {
        "oauth_token": "xxxxxxxxxxxx", 
        "oauth_token_secret": "xxxxxxxxxxxxxxxx", 
        "screen_name": "xKxAxKx", 
        "user_id": "123456789", 
        "x_auth_expires": "0"
    }, 
    "auth_time": 1502701921, 
    "id": 101447225
}
```

ツイートをPOSTするにはこの中の`oauth_token`と`oauth_token_secret`が必要となってくる
また、settingsで書いておいた`SOCIAL_AUTH_TWITTER_KEY`と`SOCIAL_AUTH_TWITTER_SECRET`も必要
なので、以下のようにviews.pyで処理する

```views.py
...
from requests_oauthlib import OAuth1Session
import requests
from django.conf import settings
from social_django.models import UserSocialAuth
import twitter

def home(request):
    # UserSocialAuthからのデータ取得
    social_account = UserSocialAuth.objects.get(user_id=request.user.id)
    user_oauth_token = social_account.extra_data['access_token']['oauth_token']
    user_oauth_token_sercret = social_account.extra_data['access_token']['oauth_token_secret']
    form = TweetForm

    if request.method == 'POST':
        form = TweetForm(request.POST)
        if form.is_valid():
            tweet = request.POST['tweet']

            tweet = tweet_post(tweet, user_oauth_token, user_oauth_token_sercret)

            form = TweetForm
            return render(request,
                'home.html',
                dict(form = form)
            )
    else:
        if request.user.is_authenticated():
            return render(request,
                'home.html',
                dict(form = form)
            )
        else:
            return redirect('index')


def tweet_post(tweet, user_oauth_token, user_oauth_token_sercret):
    auth = twitter.OAuth(consumer_key = settings.SOCIAL_AUTH_TWITTER_KEY,
                     consumer_secret = settings.SOCIAL_AUTH_TWITTER_SECRET,
                     token = user_oauth_token,
                     token_secret = user_oauth_token_sercret)
    post_tweet = twitter.Twitter(auth = auth)

    post_tweet.statuses.update(status = tweet)
    return tweet
```

とりあえず、これでフォームからTwitterへのPOSTができました

今回はツイートをTwitterへPOSTする前に、ツイート以下のようにネガポジ判定しています


## ③ネガポジの判定
形態素解析にはMeCabを使用しています
MeCabをインストールするには以下の記事などが参考になると思います
- [MeCabをMacにインストールする手順 - Qiita](http://qiita.com/nkjm/items/913584c00af199794257)
- [yumでMeCabをインストール - Qiita](http://qiita.com/ikenyal/items/275ca3096002822e8cd6)

併せてmecab-python3とpandasをpipでインストールしました

```
$ pip install mecab-python3
$ pip install pandas
```

### まずはツイートを形態素解析する
ツイートを形態素解析するための関数は以下のように書いた

```python:views.py
import MeCab
mecab = MeCab.Tagger('-Ochasen')

def tweet_mecab_analysis(tweet):
    divided_tweet = mecab.parse(tweet)
    divided_tweet_lines = divided_tweet.split('\n')
    divided_tweet_lines = divided_tweet_lines[0:-2]
    diclist = []
    for word in divided_tweet_lines:
        l = re.split('\t|,',word)
        d = {'Surface':l[0], 'POS1':l[1], 'POS2':l[2], 'POS3':l[3]}
        diclist.append(d)
    return(diclist)
```

この関数にtweet、例えば「なんであいつのために俺が苦労しないといけないんだ」を渡すと

```
[
	{'Surface': 'なんで', 'POS1': 'ナンデ', 'POS2': 'なんで', 'POS3': '副詞-一般'},
	{'Surface': 'あいつ', 'POS1': 'アイツ', 'POS2': 'あいつ', 'POS3': '名詞-代名詞-一般'}, 
	{'Surface': 'の', 'POS1': 'ノ', 'POS2': 'の', 'POS3': '助詞-連体化'}, 
	{'Surface': 'ため', 'POS1': 'タメ', 'POS2': 'ため', 'POS3': '名詞-非自立-副詞可能'}, 
	{'Surface': 'に', 'POS1': 'ニ', 'POS2': 'に', 'POS3': '助詞-格助詞-一般'}, 
	{'Surface': '俺', 'POS1': 'オレ', 'POS2': '俺', 'POS3': '名詞-代名詞-一般'}, 
	{'Surface': 'が', 'POS1': 'ガ', 'POS2': 'が', 'POS3': '助詞-格助詞-一般'}, 
	{'Surface': '苦労', 'POS1': 'クロウ', 'POS2': '苦労', 'POS3': '名詞-サ変接続'}, 
	{'Surface': 'し', 'POS1': 'シ', 'POS2': 'する', 'POS3': '動詞-自立'}, 
	{'Surface': 'ない', 'POS1': 'ナイ', 'POS2': 'ない', 'POS3': '助動詞'}, 
	{'Surface': 'と', 'POS1': 'ト', 'POS2': 'と', 'POS3': '助詞-接続助詞'}, 
	{'Surface': 'いけ', 'POS1': 'イケ', 'POS2': 'いける', 'POS3': '動詞-非自立'}, 
	{'Surface': 'ない', 'POS1': 'ナイ', 'POS2': 'ない', 'POS3': '助動詞'}, 
	{'Surface': 'ん', 'POS1': 'ン', 'POS2': 'ん', 'POS3': '名詞-非自立-一般'}, 
	{'Surface': 'だ', 'POS1': 'ダ', 'POS2': 'だ', 'POS3': '助動詞'}
]
```

このような感じで、形態素解析される


### 単語感情極性対応表のダウンロード
[単語感情極性対応表](http://www.lr.pi.titech.ac.jp/~takamura/pndic_ja.html)というものがあり、これを用いてネガポジ判定をしました
表はダウンロードして、適当なとこにおいておく

まずは、対応表を下記のようにし、dict型に変換しておく

```views.py
import pandas

PN_TXT= 'pn_ja.dic.txt'

pn_df = pandas.read_csv(PN_TXT,\
                    sep=':',
                    encoding='utf-8',
                    names=('Word','Reading','POS', 'PN')
                   )
# PN Tableをデータフレームからdict型に変換しておく
word_list = list(pn_df['Word'])
pn_list = list(pn_df['PN'])  # 中身の型はnumpy.float64
pn_dict = dict(zip(word_list, pn_list))
```

pn_dictは下記のような辞書となる

```
{
	'優れる': 1.0, 
	'最高': 1.0, 
	'良い': 0.99999500000000008, 
	'喜ぶ': 0.99997900000000006, 
	...
	...
	...
	'病気': -0.99999799999999994, 
	'死ぬ': -0.99999899999999997, 
	'悪い': -1.0, 
	'死ねる': -1.0
}
```

### 解析したツイートとpn_dictをマッチさせる
形態素解析で分割したツイートとpn_dictをマッチさせる

```views.py
def add_pnvalue(diclist_old):
    diclist_new = []
    for word in diclist_old:
        base = word['POS2'] # 個々の辞書から基本形を取得
        if base in pn_dict:
            pn = float(pn_dict[base])
        else:
            pn = 'notfound'
        word['PN'] = pn
        diclist_new.append(word)
    import pdb; pdb.set_trace()
    return(diclist_new)
```

このようにPOS2の値がpn_dictに存在するかどうかをチェック、あった場合はPNにその単語のスコアを、なかった場合はnotfoundを挿入
結果としては以下のようになります

```
[
	{'Surface': 'なんで', 'POS1': 'ナンデ', 'POS2': 'なんで', 'POS3': '副詞-一般', 'PN': 'notfound'}, 
	{'Surface': 'あいつ', 'POS1': 'アイツ', 'POS2': 'あいつ', 'POS3': '名詞-代名詞-一般', 'PN': 'notfound'}, 
	{'Surface': 'の', 'POS1': 'ノ', 'POS2': 'の', 'POS3': '助詞-連体化', 'PN': 'notfound'}, 
	{'Surface': 'ため', 'POS1': 'タメ', 'POS2': 'ため', 'POS3': '名詞-非自立-副詞可能', 'PN': 'notfound'}, 
	{'Surface': 'に', 'POS1': 'ニ', 'POS2': 'に', 'POS3': '助詞-格助詞-一般', 'PN': 'notfound'}, 
	{'Surface': '俺', 'POS1': 'オレ', 'POS2': '俺', 'POS3': '名詞-代名詞-一般', 'PN': 'notfound'}, 
	{'Surface': 'が', 'POS1': 'ガ', 'POS2': 'が', 'POS3': '助詞-格助詞-一般', 'PN': 'notfound'}, 
	{'Surface': '苦労', 'POS1': 'クロウ', 'POS2': '苦労', 'POS3': '名詞-サ変接続', 'PN': -0.822364}, 
	{'Surface': 'し', 'POS1': 'シ', 'POS2': 'する', 'POS3': '動詞-自立', 'PN': 'notfound'}, 
	{'Surface': 'ない', 'POS1': 'ナイ', 'POS2': 'ない', 'POS3': '助動詞', 'PN': 'notfound'}, 
	{'Surface': 'と', 'POS1': 'ト', 'POS2': 'と', 'POS3': '助詞-接続助詞', 'PN': -0.21579299999999998}, 
	{'Surface': 'いけ', 'POS1': 'イケ', 'POS2': 'いける', 'POS3': '動詞-非自立', 'PN': 'notfound'}, 
	{'Surface': 'ない', 'POS1': 'ナイ', 'POS2': 'ない', 'POS3': '助動詞', 'PN': 'notfound'}, 
	{'Surface': 'ん', 'POS1': 'ン', 'POS2': 'ん', 'POS3': '名詞-非自立-一般', 'PN': 'notfound'}, 
	{'Surface': 'だ', 'POS1': 'ダ', 'POS2': 'だ', 'POS3': '助動詞', 'PN': 'notfound'}
]
```

このように「と」でスコアが「-0.21579299999999998」になっていたりするので、対応表は各々でスコアの調整が必要になってくると思います
ちなみにこの「なんであいつのために俺が苦労しないといけないんだ」は最終的に「にゃーん」に変換したかったのですが、「いける」という単語がかなりのプラスのスコアを叩き出してしまったため、最終的に対応表から「いける」は除外したりしました

### ツイートの平均スコアを取得
マッチさせたリストから平均スコアを取る関数は以下のとおり

```views.py
def get_tweet_score(diclist):
    pn_list = []
    for word in diclist:
        pn = word['PN']
        if pn != 'notfound':
            pn_list.append(pn)  # notfoundだった場合は追加もしない
    if len(pn_list) > 0:        # 「全部notfound」じゃなければ
        score = mean(pn_list)
    else:
        score = 0 # 全部notfoundならゼロにする
    return(score)


# 平均値を出す関数
def mean(numbers):
    return float(sum(numbers)) / max(len(numbers), 1)
```


### 「にゃーん」に変換
これは単純に返り値(score)の値によってPOSTするツイートを変換しているだけ
一連のツイートをPOSTする関数は以下のようになります

```views.py
def tweet_post(tweet, user_oauth_token, user_oauth_token_sercret):
    auth = twitter.OAuth(consumer_key = settings.SOCIAL_AUTH_TWITTER_KEY,
                     consumer_secret = settings.SOCIAL_AUTH_TWITTER_SECRET,
                     token = user_oauth_token,
                     token_secret = user_oauth_token_sercret)
    t = twitter.Twitter(auth = auth)

    # ツイートのネガポジ判定&ツイートの差し替え
    analyzed_tweet = tweet_mecab_analysis(tweet)
    tweet_pnvalue_list = add_pnvalue(analyzed_tweet)
    tweet_score = get_tweet_score(tweet_pnvalue_list)
    
    if tweet_score <= -0.50:
        tweet = "にゃーん"

    # 例外が発生した場合、tweet=にゃーんだったらにゃーんを追加して再チャレンジ
    # にゃーん以外だったらNoneで返す(エラーの内容は問わず)
    try:
        t.statuses.update(status = tweet)
        return tweet
    except:
        if tweet.startswith("にゃーん"):
            for i in range(1, 10):
                if i == 10:
                    return None
                    break
                else:
                    tweet += "にゃーん"
                try:
                    t.statuses.update(status = tweet)
                    return tweet
                except:
                    continue
        else:
            return None
```

twitterの仕様上、同じ内容のツイートは連続でポストできないので、2回目以降の「にゃーん」は「にゃーん」が追加されるようにしています


## その他
本番ではアプリケーションをgunicornを使って立ち上げ、supervisorでプロセスをデーモン化しています
やり方は以下に書いた

[EC2にNginx + Gunicorn + SupervisorでDjangoアプリケーションをデプロイする - Qiita](http://qiita.com/xKxAxKx/items/da9dcc8caa36e1c303c7)


あとは何すかね、もうちょっと文脈とかを理解できるようした上でネガポジ判定できればいいな、って感じです
単語だけの解析だとやっぱり少し無理があるな、と感じました
優秀な辞書とか表があれば良いんでしょうか