ただそれだけ

自分がMacを使い始めたのは6年前。
音楽に強いらしいという曖昧な理由で購入した。
大学1年生の時だ。


その頃はそれほどMacユーザはいなかったのだが、
それでも自分の周りには愛用者が多かった気がする。


配属された研究室でもMac
インターンシップでもMac
依然としてMacはマイノリティのはずなのだが、
どうも自分はMacを利用する機会に恵まれてきた。


4月から社会人としての人生が始まる。
勤めることになる企業が何のPCを使っているか分からないが、
漠然と今後もMacを愛用していける気がする。


と、まぁぐだぐだ書いてきたが、
そんなことはどうでもよくて、
言いたいことはただ一つ


MacBook Air 11インチ欲しい!

Followingのお気に入りをランダムに返信するbot

botに対してつぶやくと、フォローしてる人がお気に入りにいれているTweetをランダムに選んで返信するというtwitterbotです。

rubyで書いてます。
Twitter gemのバージョン1.0.0を使ってます。

# -*- encoding: utf-8 -*-

require 'twitter'
require 'sqlite3'

class Frienvorite
  def initialize(hash)
    Twitter.configure do |config|
      config.consumer_key = hash.fetch('c-key')
      config.consumer_secret = hash.fetch('c-secret')
      config.oauth_token = hash.fetch('o-token')
      config.oauth_token_secret = hash.fetch('o-token-secret')
    end
    reply_to_mentions
  end

  def choice_favorite(friend)
    favorites = Twitter.favorites(friend)
    if favorites != []
      fav = favorites[rand(favorites.size)]
      return "RT #{fav.user.screen_name} #{fav.text}" if fav != []
    end
    return
  end

  def choice_friend(mention_user)
    friends = Twitter.friends(mention_user).users
    if friends != []
      for i in 1..10
        friend = friends[rand(friends.size)].screen_name
        favorite_retweet = choice_favorite(friend)
        if favorite_retweet != nil
          return "@#{mention_user} IN #{friend} #{favorite_retweet}"
        end
      end
    end
    return
  end

  def reply_to_mentions
    Twitter.mentions.each do |mention|
      tweet = choice_friend(mention.user.screen_name)
      Twitter.update(tweet) if tweet != nil
    end
  end
end

Frienvorite.new({'c-key' => 'コンシューマー・キー',
                 'c-secret' => 'コンシューマー・シークレット',
                 'o-token' => 'オーオース・トークン',
                 'o-token-secret' => 'オーオース・トークン・シークレット'});

このままでは、mentions全てに返信するので、返信済みのmentionは無視するなどしたい場合は、時間を保持する必要があります。


作ったものはこれです。

Plaggerを使ってTwitterの複数人のFavoritesをまとめてRSS表示

Plaggerというプラガブルな RSS/Atom フィードアグリゲータを使って、Twitter複数アカウントのFavoritesをまとめてRSSで表示する、というプログラムです。


ServersMan@VPSが2ヶ月無料(2010年11月)ということで登録しまして、何かサーバを使って面白いことは出来ないかと考えていたところ、以前から気になっていたPlaggerを試してみようとなったわけです。

ちなみに、VPSの初期設定でやるべきことはここできれいにまとめてくれています。


さて、まずはPlaggerをインストールするわけですが、これがなかなかタフな作業です。
まず、時間がかかります。
そして依存モジュールが沢山あります。
CentOSだと、CPANでゴリゴリインストールすることになると思うんですが、Plaggerがインストール出来るかテストしては、エラー見て足らないモジュール追加という作業で、僕の場合は数時間かかりました。
Plaggerのインストールは時間に余裕のあるときにすることをおすすめします。


実際に、FavoritesをまとめてRSS出力するPlaggerプログラムは以下のようになります。


favorites.yaml

global:
  log:
    level: debug
plugins:
  - module: Subscription::Config
    config:
      feed:
        - url: http://twitter.com/favorites/アカウント名1.atom
        - url: http://twitter.com/favorites/アカウント名2.atom

  - module: SmartFeed::All
    config:
      title: sbmix

  - module: Publish::Feed
    rule:
      expression: $args->{feed}->id eq 'smartfeed:all'
    config:
      dir: /var/www/html/
      filename: output.rss
      format: RSS

/var/www/html/ のディレクトリに出力してますが、権限ないとここには書けません。


以下のようにコマンドを打ち、

$ plagger -c favorites.yaml

アドレス先にきちんと表示されていたらOKです。


定期的に動いてもらうために、cronで動かします。
crontabは以下のように記述しました。

PERL5LIB=/usr/lib/perl5:/usr/lib/perl5/site_perl
0 * * * * /usr/bin/plagger -c /home/ユーザ名/plagger/favorites.yaml > /dev/null


あとは、RSSリーダーなどにアドレスを登録して、みんなのFavoritesを見て楽しむだけです。


まぁこんなめんどくさいことしなくても、 http://twitter.com/favorites/アカウント名.atomRSSリーダーに登録すればRSSで表示することは出来るんですけどね。

GAEでTwitterのツイートを圧縮新聞っぽくまとめてみる(2)

GAEでTwitterのツイートを圧縮新聞っぽくまとめてみる(1)という記事を書いたのですが、その続きです。


「圧縮新聞」を作ったを見ると、どうも品詞情報などは全く使っていないようなので、Yahoo! 形態素解析サービスで分割だけして、あとのロジックはそのまま使わせていただきました。


ソースコードはこんな感じ。

# -*- coding: utf-8 -*-

import os
import random
from urllib import urlencode
from google.appengine.ext.webapp import template
from google.appengine.ext import webapp
from google.appengine.api import urlfetch
from django.utils import simplejson
from BeautifulSoup import BeautifulStoneSoup

# ユーザのツイートを圧縮する
class Asshukutter(webapp.RequestHandler):
	def get(self):
		# リクエストを取得
		user = self.request.params['user'] # twitterアカウントが入る
		# 形態素解析したユーザのツイートを取得
		dic = self.segmentMorpheme(user)
		# 圧縮結果を取得
		result = self.asshuku(dic)
		# ちゃんとした結果が得られなかったら得られるまで
		while result == "。".decode('utf_8'):
			result = self.asshuku(dic)
		data = {'result' : result.rstrip("。".decode('utf_8'))}
		path = os.path.join(os.path.dirname(__file__), '../html/index.html')
		self.response.out.write(template.render(path, data))

	# ツイートを圧縮する
	def asshuku(self, dic):
		result = ""
		i = 0
		punctuation = "。".decode('utf_8')
		# 木構造を繋げていく
		preSeg = dic[punctuation][random.randint(0,len(dic[punctuation])-1)]
		result = result + preSeg
		# 「。」が出てくるまで繋げる
		while preSeg != punctuation:
			i = i + 1
			nextSeg = dic[preSeg][random.randint(0,len(dic[preSeg])-1)]
			result = result + nextSeg
			preSeg = nextSeg
			if i == 100:
				preSeg = punctuation
		return result


	# Yahoo! 形態素解析サービスを使って、ユーザのツイートを形態素で分割する
	def segmentMorpheme(self, user):
		dic = {}
		# ユーザのツイートを取得
		tweets = self.searchUserTweet(user)
		# ツイートごとに形態素解析を行う
		for t in tweets:
			urlResult = urlfetch.fetch('http://jlp.yahooapis.jp/MAService/V1/parse',
				method=urlfetch.POST,
				headers={'Content-Type':'application/x-www-form-urlencoded'},
				payload=urlencode({
					'appid': 'アプリケーションID',
					'sentence' : t.encode('utf-8')}))
			# BeautifulStoneSoupを使って結果のxmlをパースする
			soup = BeautifulStoneSoup(urlResult.content.decode('utf_8'))
			preMorpheme = "。".decode('utf_8')
			flag = False
			for morpheme in soup('surface'):
				# 英数字のみからなる文字列を除外
				if not morpheme.renderContents().isalnum() and not flag:
					nowMorpheme = morpheme.renderContents().decode('utf_8')
					# 木構造を作成
					if dic.has_key(preMorpheme):
						dic[preMorpheme].append(nowMorpheme)
					else:
						dic[preMorpheme] = [nowMorpheme]
					preMorpheme = nowMorpheme
				# 英数字直後の形態素は除外
				elif  not morpheme.renderContents().isalnum() and flag:
					flag = False
				else:
					flag = True
		return dic


	# Twitter APIのuser_timelineを使ってユーザのツイートを取得する
	def searchUserTweet(self, user):
		text = []
		preText = ""
		num = 0
		# 入力したユーザ名をクエリとするURL
		url = "http://api.twitter.com/1/statuses/user_timeline.json?screen_name=" + user
		urlresult = urlfetch.fetch(url)
		if urlresult.status_code == 200:
			# 結果をsimplejsonを使ってパース
			jsonload = simplejson.loads(urlresult.content)
			for result in jsonload:
				num = num + 1
				if num % 7 == 0:
					preText = preText + result['text'] + "。".decode('utf_8')
					text.append(preText)
					preText = ""
				else:
					preText = preText + result['text'] + "。".decode('utf_8')
			text.append(preText)
		else:
			text.append("存在しないか非公開なアカウントです。".decode('utf_8'))
		return text

Twitter APIのuser_timelineを使ってユーザのツイートを取得し、それをYahoo! API形態素解析にかけ、それを圧縮新聞と同様にマルコフ連鎖で繋げていきます。
Yahoo!形態素解析は、1リクエストの最大サイズが100KBなので、ツイートを3分割くらいにしてます。


マルコフ連鎖を実現するために、ある形態素をキー、その後ろにくる形態素のリストをバリューをするディクショナリを作成します。
なので、形態素解析にかけ、連続した形態素の、前の形態素をディクショナリのキー、後ろの形態素をディクショナリのバリューのリストに挿入します。


あとはランダムに繋げていきます。
圧縮新聞は4単語を繋げてますが、これは2単語を繋げていきます。


リンクの文字列が結構やっかいで英数字を無視してしまっているんですが、そのためかエラーが出ます。
まぁ出来れば整形して載せます。


一応、作ったものは → 圧縮ったー
while文動きっぱが怖いので上のソースちょっといじって運用してます。


マルコフ連鎖に品詞情報付与したら精度良くなる気がするけど、そうでもないのかな…。

GAEでTwitterのツイートを圧縮新聞っぽくまとめてみる(1)

圧縮新聞というものがあるんですが、Twitterのツイートを圧縮新聞っぽくまとめたら面白いんじゃないかと思い、いろいろやってみました。まだ途中です。


とりあえずTwitterユーザのツイートを取得します。
どうも直近から20件までしかとれないらしく、まぁとれるだけとります。
下のsearchUserTweet.pyがそれです。

# -*- coding: utf-8 -*-

from google.appengine.ext import webapp
from google.appengine.api import urlfetch
from django.utils import simplejson

# Twitter APIのuser_timelineを使ってユーザのタイムラインを取得する
class SearchUserTweet(webapp.RequestHandler):
	def sut(self, user):
		text = ""
		# 入力したユーザ名をクエリとするURL
		url = "http://api.twitter.com/1/statuses/user_timeline.json?screen_name=" + user
		urlresult = urlfetch.fetch(url)
		if urlresult.status_code == 200:
			# 結果をsimplejsonを使ってパース
			jsonload = simplejson.loads(urlresult.content)
			for result in jsonload:
				text = text + result['text']
		else:
			text = "error"
		return text

userのところにTwitterのアカウント名が入ります。
simplejson使ってパースしてます。


そしてツイートを形態素解析するんですが、どうもGAEは形態素解析が出来ないらしく、Yahoo! APIの形態素解析サービスを使います。
でも、困ったことに「リクエストの最大サイズを100KBに制限している」らしく、20ツイートは場合によっては(というか高確率で)はじかれます。
なのでTinySegmenterを使おうかと思ったのですが、品詞情報はないので、TinySegmenterで分割して出現頻度の高い形態素Yahoo!形態素解析サービスに突っ込もうと算段しました。
VPSを持っていればMeCabでやっちゃえばおしまいなんですが…。
そもそもGAE/Jなら形態素解析できるっぽいんですが…。


それをやるのが下のsegmentMorpheme.pyです。

# -*- coding: utf-8 -*-

from google.appengine.ext import webapp
from google.appengine.api import urlfetch
from libs.tiny_segmenter import TinySegmenter
from application.searchUserTweet import SearchUserTweet

# TinySegmenterでテキストを分かち書きする
class SegmentMorpheme(webapp.RequestHandler):
	def sm(self, user):
		text = ""
		morpheme = {}
		# ユーザのツイートを取得
		tweet = SearchUserTweet().sut(user)
		
		ts = TinySegmenter()
		result = ts.segment(tweet)
		# 形態素を出現頻度を値にHashに入れる
		for seg in result:
			if morpheme.has_key(seg):
				morpheme[seg] = morpheme[seg] + 1
			else:
				morpheme[seg] = 1
		# 2回以上出現した形態素を保持
		for (k, v) in sorted(morpheme.items(), key=lambda x:int(x[1]), reverse=True):
			if v >= 2 and k != "":
				text = text + " " + k
		return text

ここからsearchUserTweet.pyの結果を見にいってます。
ディレクトリ構造を色々やってるのでfrom libs.tiny_segmenterとかfrom application.searchUserTweetになってます。
__init__.pyファイルつくってやってます。


これでユーザのツイートをまとめて分かち書きされたものをゲットできます。
この結果をYahoo!形態素解析サービスに突っ込むんですが、結果のxmlパースにハマっていてハマッてます。
BeautifulSoupにハマッてます。
できたらまた書きます。

Google App Engineを使ってtwitterのbotを動かす

作ったものはこちら → 読了bot [twitter:@dokuryo_bot]
Twitter SearchAPIを使って、"読了"をキーワードに検索し、ツイートから書籍名と覚しき文字列を取り出して、出現回数によってテンション変えてつぶやいています。
といっても鉤括弧で囲まれた文字列を書籍名としているだけなんですが。


Twitterbotを作るためには、定期的にツイートをポストさせるためのマシンが必要になります。
自分のパソコンをサーバとして利用してもいいんですが、経済的ではないのでGoogle App Engine(以下GAE)を利用しました。
GAEの実装環境は、PythonJavaが提供されているのですが、今回はPythonを使用しています。


ディレクトリ構造はこのようになっています。

/
|-main.py
|-tweetPost.py
|-searchByKeyWord.py
|-app.yaml
|-cron.yaml
|-oauth.py
|-simplejson/

ファイルは全てルートに突っ込んでいます。
cron.yamlが定期的にアクションを起こし、searchByKeyWord.pyがSearch APIを使ってツイートを検索し、その結果をtweetPost.pyでツイートしています。
oauth.pyは、AppEngine-OAuth-Libraryのライブラリを使わせて頂き、simplejsonはライブラリのフォルダごと突っ込んでます。


まず、searchByKeyWord.pyはこのような感じ。

# -*- coding: utf-8 -*-

from google.appengine.ext import webapp
from google.appengine.api import urlfetch
from django.utils import simplejson

# Twitter Search API を使って"読了"の検索結果を取得する
class SearchByKeyWord(webapp.RequestHandler):
	def searchKW(self):
		# "読了"をクエリとするURLを投げる
		twitterSerchUrl = "http://search.twitter.com/search.json?q=%E8%AA%AD%E4%BA%86&lang=ja&rpp=100&result_type=recent"
		urlresult = urlfetch.fetch( twitterSerchUrl )
		if urlresult.status_code == 200:
			# 結果をsimplejsonを使ってパース
			jsonload = simplejson.loads(urlresult.content)
			results = jsonload['results']
			book = {}
			for r in results:
				# RTで始まるツイートは無視
				if r['text'].split(" ")[0] != "RT":
					# 『』で囲われた文字列を抽出
					doubleBrackets = r['text'].split("『".decode('utf_8'))
					if len(doubleBrackets) >= 2:
						bookName = doubleBrackets[1].split("』".decode('utf_8'))[0]
						if book.has_key(bookName):
							book[bookName] = book[bookName] + 1
						else:
							book[bookName] = 1
					# 「」で囲われた文字列を抽出
					brackets = r['text'].split("「".decode('utf_8'))
					if len(brackets) >= 2:
						bookName = brackets[1].split("」".decode('utf_8'))[0]
						if book.has_key(bookName):
							book[bookName] = book[bookName] + 1
						else:
							book[bookName] = 1				
			array = []
			bookcount = True
			# 抽出した書籍名を出現回数の降順に並べて配列に挿入
			for (k, v) in sorted(book.items(), key=lambda x:int(x[1]), reverse=True):
				if v >= 2 and k != "":
					array.append(k + ":" + str(v))
				# 出現回数1回の書籍は1つだけ挿入
				elif v == 1 and k != "" and bookcount == True:
					array.append(k + ":" + str(v))
					bookcount = False
			return array
		else:
			self.response.out.write("error")

Twitter SearchのAPIは、フォーマットがjsonatomで選べるのですが、jsonを選択し、simplejsonを使って内容をパースしています。
urlのresult_typeのパラメータはrecentにしています。デフォルトのmixedはリツイートされたツイートが出たりしてあまりよろしくないので。
あとはまぁ書籍名と出現回数を保持していきます。


次に、tweetPost.py。

# -*- coding: utf-8 -*-

import os
import oauth
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template, util
from searchByKeyWord import SearchByKeyWord

TWITTER_CONSUMER_KEY = 'アプリのconsumer key'
TWITTER_CONSUMER_SECRET = 'アプリのconsumer secret'
TWITTER_ACCESS_TOKEN = 'ボットのアカウントのアクセス・トークン'
TWITTER_ACCESS_TOKEN_SECRET =  'ボットのアカウントのアクセス・トークン・シークレット'

# 生成した文字列をtwitterにpostする
class TweetPost(webapp.RequestHandler):
	def get(self):
		postText = ""
		currentCount = 2
		result = SearchByKeyWord().searchKW()
		for r in result:
			bookName = r.split(":")[0]
			bookCount = int(r.split(":")[1])
			if bookCount >= currentCount:
				# 書籍名を追加
				postText = postText + "『".decode('utf_8') + bookName + "』".decode('utf_8')
				# 追加した書籍の出現回数を保持
				currentCount = bookCount
			elif bookCount < currentCount:
				# 書籍の出現回数によって紹介のテンションを変える
				if currentCount == 2 and postText != "":
					postText = postText + "がまぁまぁ読まれてます。あと".decode('utf_8')
				elif currentCount == 3:
					postText = postText + "がめちゃ読まれてます。あと".decode('utf_8')
				elif currentCount >= 4:
					postText = postText + "がヤバい読まれてます。あと".decode('utf_8')
				# 書籍名を追加
				postText = postText + "『".decode('utf_8') + bookName + "』".decode('utf_8')
				if bookCount == 1:
					postText = postText + "とか読まれてるっぽいです。".decode('utf_8')
				# 追加した書籍の出現回数を保持
				currentCount = bookCount	
		client = oauth.TwitterClient(TWITTER_CONSUMER_KEY,
							TWITTER_CONSUMER_SECRET, None)
		param = {'status': postText}
		client.make_request('http://twitter.com/statuses/update.json',
						token=TWITTER_ACCESS_TOKEN,
						secret=TWITTER_ACCESS_TOKEN_SECRET,
						additional_params=param,
						protected=True,
						method='POST')
		self.response.out.write('ok')

searchByKeyWord.pyの結果をハッシュで返せばいいんですが、配列で返してるので色々ここでいじってます。
ちなみに文字数140字への制約はかけてません。今後の課題。


2010年8月でTwitter APIBASIC認証は廃止されたので、認証にはOAuthを使う必要があります。
こちらのサイトを参考にさせて頂きました。
TwitterボットをOAuthに対応させてみた - Google App Engine(Python)


app.yamlから最初に呼び出すmain.pyはこんな感じ。

from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from tweetPost import TweetPost

application = webapp.WSGIApplication([('/*', TweetPost),
							],
							debug=True)

def main():
	run_wsgi_app(application)

if __name__ == '__main__':
	main()

あとは、app.yamlとcron.yamlを設定すれば完成。


課題としては、

  • 文字数を140文字で処理してないので、越えていたらエラーとなる
  • 鉤括弧、二重鉤括弧のみで書籍名を推定しているので、より高性能な方法(係り受け関係など)を使えないか
  • 非公式RTを削除すべきか(でも非公式RTしてる人も読了したものとみなすことができる?)
  • 相互フォローとか