Gmailアカウントで受信したメールをRedmineにチケット登録する

Redmineを個人タスク管理に使うため、どこからでもメールでチケット登録したいと思って試してみた。

Redmine環境は BitNami :: RedmineVMware Virtual Machines Ubuntu 10.10

まずはRedmine.JPのマニュアルを参照してコマンドを作成。

メールによるチケットの登録 | Redmine.JP

sudo /opt/bitnami/ruby/bin/rake --trace -f \
/opt/bitnami/apps/redmine/Rakefile \
redmine:email:receive_imap \
RAILS_ENV="production" \
host=imap.gmail.com \
port=993 \
ssl=1 \
project=XXX \
username=XXX@gmail.com \
password=XXX

メールを送ったあと上記コマンドを実行してみると、一応チケットが登録されたものの、タイトルが文字化けしている。

探してみたらパッチがあったので対策してみる。
メールでチケット登録時に題名が文字化けする問題を修正 | Redmine.JP Blog

wget http://blog.redmine.jp/assets/2010/07/16/farend_mail_handler_ja_subject_fix.rb
sudo mv farend_mail_handler_ja_subject_fix.rb /opt/bitnami/apps/redmine/config/initializers/
sudo /opt/bitnami/ctlscript.sh restart

↓の通り文字化け解消。

定期的に自動でメールを取得するためcronに登録する。

sudo crontab -e

*/10 * * * * /opt/bitnami/ruby/bin/rake -f /opt/bitnami/apps/redmine/Rakefile redmine:email:receive_imap RAILS_ENV="production" host=imap.gmail.com port=993 ssl=1 project=XXX username=XXX@gmail.com password=XXX

これにて完了。

なお、このままだと送信元メールアドレスとRedmineのユーザのメールアドレスが一致してないといけないという制約がある。任意のメールアドレス(匿名ユーザ)でチケット登録するには unknown_user=accept を付ければいいらしいけど今のところ必要ないのでこのまま。

Twitterの声優クラスタ調査

先日、有志によるオタク達の河口湖合宿にて、ネット上の声優ファンコミュニティの勢い調査のプレゼンをしました。

その中から、Twitterの声優クラスタ調査結果の詳細を紹介します。

Twitterには、mixiのようにわかりやすいコミュニティが存在しません。
そこで今回は、リスト機能を利用して形成されているクラスタを調べました。
調査方法は下記の通りです。

こうして得られたクラスタ人数のランキングは下記の通りです。*2
リンク先は各クラスタのユーザリストです(スクリーンネーム,所属リスト数)。

  1. 水樹奈々 2166人 (80 lists) ユーザリスト
  2. 田村ゆかり 1621人 (81 lists) ユーザリスト
  3. スフィア 1566人 (105 lists) ユーザリスト
  4. 茅原実里 1102人 (90 lists) ユーザリスト
  5. 坂本真綾 775人 (40 lists) ユーザリスト
  6. 堀江由衣 456人 (21 lists) ユーザリスト
  7. 神谷浩史 437人 (7 lists) ユーザリスト
  8. 平野綾 298人 (4 lists) ユーザリスト
  9. 釘宮理恵 96人 (6 lists) ユーザリスト

この中には、複数の声優クラスタに所属しているユーザもいます。そこで、下記の通りユーザの重複度合いを調べてみました。

まずは重複人数。

続いて重複率。重複率は A/(A+B-A∩B)A∩B/(A+B-A∩B) な感じで求めました。

クラスタ人数と重複率を考慮してクラスタを配置してみるとこんな感じ。

純粋に適当なデータ処理だけで出た結果ですが、割と体感通りなんじゃないでしょうか?


追記:

重複率はA∩B/Aの方が体感に合うんじゃね?的な意見があり、確かにと思ったので調べてみました。

新しく求めた重複率=浮気率を使ってクラスタ配置してみるとこんな感じになりました。

こっちの方が体感通りのような…?

*1:@hrdakinori さんの「ついったー リスト検索」 http://723.to/tw/listsearch.php を利用しました。Thanks!!

*2:これ以外には林原めぐみ中原麻衣クラスタを調べましたが、リストが見つかりませんでした。

迷路を解いてみた

http://okajima.air-nifty.com/b/2010/01/post-abc6.html

これを解いてみた。
50分かかりました。

# coding: utf-8

import sys

data = """**************************
*S* *                    *
* * *  *  *************  *
* *   *    ************  *
*    *                   *
************** ***********
*                        *
** ***********************
*      *              G  *
*  *      *********** *  *
*    *        ******* *  *
*       *                *
**************************"""

# 各マスの距離を算出
def solve(x, y, maze):
	# 現在のマスの上下左右を調べる
	for [dx, dy] in [[x, y-1], [x+1, y], [x, y+1], [x-1, y]]:
		if maze[dy][dx] > maze[y][x] + 1:
			maze[dy][dx] = maze[y][x] + 1
			solve(dx, dy, maze)

# ゴールから逆順にたどって$マークを付ける
def route(x, y, maze, out):
	# 現在のマスの上下左右を調べる
	for [dx, dy] in [[x, y-1], [x+1, y], [x, y+1], [x-1, y]]:
		if maze[dy][dx] == maze[y][x] - 1 and maze[dy][dx] > 0:
			out[dy][dx] = "$"
			route(dx, dy, maze, out)
			break

wall = -1 # 壁
inf = 100000 # 探索前の初期値

maze = [] # 距離データ
out = [] # 出力データ
start = [] # スタート位置
goal = [] # ゴール位置

# 入力をリスト構造に変換
iy = 0
ix = 0
for line in data.splitlines():
	lmaze = []
	lout = []
	for c in line:
		lout.append(c)
		if c == "*":
			lmaze.append(wall)
		else:
			lmaze.append(inf)
			if c == "S":
				start = [ix, iy]
			elif c == "G":
				goal = [ix, iy]
		ix += 1
	maze.append(lmaze)
	out.append(lout)
	iy += 1
	ix = 0


# 距離を算出
[x, y] = start
maze[y][x] = 0
solve(x, y, maze)

# ゴールから逆順に辿る
[x, y] = goal
route(x, y, maze, out)

# 標準出力に出力
for l in out:
	for c in l:
		sys.stdout.write(c)
	sys.stdout.write("\n")

↓出力結果

**************************
*S* * $$$$               *
*$* *$$* $*************  *
*$* $$*  $$************  *
*$$$$*    $$$$$          *
**************$***********
* $$$$$$$$$$$$$          *
**$***********************
* $$$$$* $$$$$$$$$$$$$G  *
*  *  $$$$*********** *  *
*    *        ******* *  *
*       *                *
**************************
  • printだとスペースが入ってしまうのでsys.stdout.writeに修正
  • xとy逆だった…

ニコニコ動画の検索結果を取得してリスト化する

# coding: sjis

def login(opener):
	import urllib
	url = "https://secure.nicovideo.jp/secure/login?site=niconico"
	postdata = {}
	postdata["mail"] = "メールアドレス"
	postdata["password"] = "パスワード"
	postdata = urllib.urlencode(postdata)
	r = opener.open(url, postdata)
	html = r.read().decode("utf-8")
	return html

def get_list_html():
	import cookielib, urllib2
	url = u"http://www.nicovideo.jp/tag/%E5%A0%80%E6%B1%9F%E7%94%B1%E8%A1%A3"

	cj = cookielib.LWPCookieJar("cookie.txt")
	cj.load()
	ch = urllib2.HTTPCookieProcessor(cj)
	opener = urllib2.build_opener(ch)

	html = opener.open(url).read().decode("utf-8")
	try:
		html.index('form name="login"')
		login(opener)
		html = opener.open(url).read().decode("utf-8")
	except ValueError:
		pass
	
	cj.save()
	return html

def parse(html):
	from BeautifulSoup import BeautifulSoup
	soup = BeautifulSoup(html)
	
	table = soup.findAll("table")
	td = table[8].findAll("td")
	list = []
	for t in td:
		p = t.findAll("p")
		date = p[1].text
		length = p[3].text
		title = p[4].text
		a = p[4].find("a")
		url = "http://www.nicovideo.jp/" + dict(a.attrs)["href"]
		s = p[5].findAll("strong")
		play = s[0].text
		comment = s[1].text
		mylist = s[2].text
		if int(play.replace(",", "")) > 5000:
			video = {"date" : date, "length" : length, "title" : title, "url" : url, "play" : play, "comment" : comment, "mylist" : mylist}
			list.append(video)

	return list

def main():
	html = get_list_html()
	list = parse(html)
	print list
	
if __name__=="__main__":
	main()
  • Cookieを保存し次回からはそのCookieを使ってアクセス
  • cookie.txtがないとエラーになるかも
  • Cookieが無効 or 中身がないなどでログインフォームが返ってきた場合はログインする
  • 再生数5000以上のみ取得

認証付き&要クッキーのサイトにログインする

baseurl = "https://trading1.sbisec.co.jp/ETGate/"

def write(str):
	f = open("out.html", "w")
	f.write(str.encode("sjis"))
	f.close()
	
def build_opener_cookie(cj):
	import urllib2
	ch = urllib2.HTTPCookieProcessor(cj)
	opener = urllib2.build_opener(ch)
	return opener

def get_form(opener):
	r = opener.open(baseurl)
	html = r.read().decode("sjis")
	return html

def parse_form(html):
	from BeautifulSoup import BeautifulSoup
	soup = BeautifulSoup(html)
	q = {}

	form = soup.findAll("form")[1]
	for i in form.findAll("input"):
		d = dict(i.attrs)
		t = d["type"]
		if t == "hidden" or t == "text" or t == "password":
			q[d["name"]] = d["value"]
	return q

def login(opener, query):
	import urllib
	query["user_id"] = u"hogehoge"
	query["user_password"] = u"hagehage"
	query = urllib.urlencode(query)
	r = opener.open(baseurl, query)
	html = r.read().decode("sjis")
	return html

def main():
	import cookielib
	cj = cookielib.LWPCookieJar("cookie.txt")
	opener = build_opener_cookie(cj)
	html = get_form(opener)
	q = parse_form(html)
	html = login(opener, q)
	
	write(html)
	cj.save()
	
if __name__=="__main__":
	main()
  • ログインページのformを抽出
  • input要素を抽出してコピー
  • id, passを入れてurlencodeしてpost
  • LWPCookieJarを使うとCookieの内容がファイルとして保存できる

Webサイトから特定の情報を抽出する

def get(url, code):
	import urllib
	
	f = urllib.urlopen(url)
	return f.read().decode(code)
	
def parse(html):
	from BeautifulSoup import BeautifulSoup
	soup = BeautifulSoup(html)
	
	entry_table = soup.findAll("table")[3].findAll("td")
	return {"title": entry_table[6].text,"body": entry_table[9].text,"datetime": entry_table[10].text}
	
if __name__=="__main__":
	url = "http://www.starchild.co.jp/artist/horie/diary/index.php"
	html = get(url, "eucjp")
	entry = parse(html)
	print entry
  • BeautifulSoup を使ってみた
  • いろいろ試したけど結局力技に落ち着いた