こちらです
https://socialfiltertwitter.kktnhrms.com/
-> 公開終了しました
コードはこちら
https://github.com/xKxAxKx/social_filter_twitter
クライアントというか、つぶやき専用機というか、まあ、そういうものです
Python/Djangoで実装しました
んで、やったことないことがいろいろあったので、以下、知見です
そもそも社会性フィルターってなに?
これです
図です pic.twitter.com/2OFfuLPk2l
— 4869 (@sh4869sh) 2016年8月21日
環境
- 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を編集
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に書いておく
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/home/'
ログイン用のURLを用意する
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')),
]
ログイン用のリンクを用意する
# リンクの場合
<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アカウントでアプリケーションへのログインができる
adminサイトの方も見てみると、ちゃんとTwitterアカウントでユーザ登録がされている
ログアウト用のリンクを用意する
ログアウトについてはDjangoのデフォルトのAuthを利用していく
...
from django.contrib.auth.views import logout
urlpatterns = [
...
url(r'', include('django.contrib.auth.urls', namespace='auth')),
]
# リンクの場合
<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
つぶやき用のフォームを作る
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,
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')
<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で処理する
...
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-python3とpandasをpipでインストールしました
$ pip install mecab-python3
$ pip install pandas
まずはツイートを形態素解析する
ツイートを形態素解析するための関数は以下のように書いた
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型に変換しておく
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をマッチさせる
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」になっていたりするので、対応表は各々でスコアの調整が必要になってくると思います
ちなみにこの「なんであいつのために俺が苦労しないといけないんだ」は最終的に「にゃーん」に変換したかったのですが、「いける」という単語がかなりのプラスのスコアを叩き出してしまったため、最終的に対応表から「いける」は除外したりしました
ツイートの平均スコアを取得
マッチさせたリストから平均スコアを取る関数は以下のとおり
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する関数は以下のようになります
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
あとは何すかね、もうちょっと文脈とかを理解できるようした上でネガポジ判定できればいいな、って感じです
単語だけの解析だとやっぱり少し無理があるな、と感じました
優秀な辞書とか表があれば良いんでしょうか