LoginSignup
38

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-08-14

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

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

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

そもそも社会性フィルターってなに?

これです


環境

  • 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:8000http://127.0.0.1:8000でも問題なし
コールバックURLも設定しておくこと(登録するURLと同じで良いです)

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

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についてはこちらを参照して、設定しましたが、もしかしたら不要なものがあるかもしれません

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

settings.py
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/home/'

ログイン用のURLを用意する

urls.pyを以下のように編集する

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 makemigrationspython manage.py migrateしておく

その後、ログインリンク/ログインボタンをクリックするとおなじみに画面が出てて、Twitterアカウントでアプリケーションへのログインができる
スクリーンショット 2017-08-14 17.43.09.png

adminサイトの方も見てみると、ちゃんとTwitterアカウントでユーザ登録がされている
スクリーンショット 2017-08-14 17.45.23.png

ログアウト用のリンクを用意する

ログアウトについてはDjangoのデフォルトのAuthを利用していく

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

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

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_tokenoauth_token_secretが必要となってくる
また、settingsで書いておいたSOCIAL_AUTH_TWITTER_KEYSOCIAL_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
- yumでMeCabをインストール - Qiita

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

$ pip install mecab-python3
$ pip install pandas

まずはツイートを形態素解析する

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

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': '助動詞'}
]

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

単語感情極性対応表のダウンロード

単語感情極性対応表というものがあり、これを用いてネガポジ判定をしました
表はダウンロードして、適当なとこにおいておく

まずは、対応表を下記のようにし、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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38