GoogleChrome拡張作成してみた

ChromeのextensionはJSON、HTML、JSのみで書けるので比較的簡単に作成することができます。

Chromeの拡張を作成するためにはWindowsであればdev版が必要なため

http://www.google.com/chrome/intl/en/eula_dev.html

からdev版をDLしてきましょう。

今回作成するのは右上のアイコンをクリックするとサイトの名前の一覧がポップアップで表示されクリックするとそのサイトを新しいページで開く、という拡張です。サイトの名前とURLはオプションから登録します。

とりあえず適当な場所にフォルダを作成します。私は\マイドキュメント\chrome_extension\sampleとしました。sampleが今回の拡張を作成するフォルダになります。
右上に表示するタイプの拡張で作成しなければならないファイルは

manifest.json - 設定ファイル
popup.html - ポップアップするHTML
icon.png - 右上に表示するアイコン

の三つです。HTML、JS、画像の名前はなんでもいいですがJSONはmanifest.jsonという名前にしておいてください。
今回はオプションページも作成し、私がjQueryを利用しないとJSが書けないのでフォルダの構成は

manifest.json
popup.html
icon.png
options.html
js/jquery-1.4.2.min.js

こんな感じです。

manifest.json

{
	"name":"my shortcuts",
	"version" : "1.0",
	"browser_action":{
		"default_title":"shortcuts",
		"default_icon":"icon.png",
		"popup": "popup.html"
	},
	"options_page":"options.html"
}

設定ファイルです。

  • name -> 拡張の名前
  • version -> バージョン情報
  • brower_action -> 右上に表示させる拡張の詳細
    • default_title -> アイコンにカーソルを乗せたときに表示するタイトル
    • default_icon -> アイコンの画像
    • popup -> アイコンをクリックしたときにポップアップとして表示するHTMLファイル
  • options_page -> オプションページのHTMLファイル

となります。

options.html

<html>
	<head>
		<script type="text/javascript" src="./js/jquery-1.4.2.min.js"></script>
		<style>
			#validate{
				display:none;
			}
		</style>
		<script type="text/javascript">
			$(function(){
			
				if(!localStorage){
					return;
				}
				for(var key in localStorage){
					$('ul#shortcuts_list').append('<li name="'+ key +'">title:'+ key + ' url: ' + localStorage[key] + '<button class="remove" name="' + key + '">remove</button></li>');
				}
			
				$('#add').click(function(){
					if($('#site_title').val() != '' && $('#site_url').val() != ''){
						var site_title = $('#site_title').val();
						var site_url = $('#site_url').val();
						localStorage[site_title] = site_url;
						$('ul#shortcuts_list').append('<li name="' + site_title  + '">title:' + site_title + ' url: ' + localStorage[site_title] + '<button class="remove" name="' + site_title + '">remove</button></li>');
						$('#site_title').val('');
						$('#site_url').val('');
						var status = $('div#status');
						status.innerHTML = "Options Saved.";
						setTimeout(function(){status.innerHTML = "";}, 750);
					}else{
						$('validate').removeClass('validate');
						setTimeout(function(){
							$('validate').addClass('validate');
						},750);
					}
				});
				
				$('button.remove').live('click',function(){
					var key = $(this).attr('name');
					localStorage.removeItem(key);
					$('li[name="' + key + '"]').remove();
				});
				
			});
		</script>
	</head>
	<body>
		<ul id="shortcuts_list"></ul>
		title:<input type="text" id="site_title" /><br />
		url:<input type="text" id="site_url" /><br />
		<button id="add" >Save</button>
		<div id="status"></div>
		<div id="validate">必要項目を入力してください</div>
	</body>
</html>

localStorageを利用するとデータが保存できます。このあたりは特に説明はいらないと思います。

popup.html

<html>
	<head>
		<script type="text/javascript" src="./js/jquery-1.4.2.min.js"></script>
		<script type="text/javascript">
			$(function(){
			
				for(var key in localStorage){
					$('ul#list').append('<li><a class="shortcut" href="'+ localStorage[key] + '">' + key  +'</a></li>');
				}
				
				$('.shortcut').click(function(){
					var url = $(this).attr('href');
					chrome.tabs.create({url: url});
					window.close();
				});

			});
		</script>
		<style>
		</style>
	</head>
	<body>
		<ul id="list"></ul>
	</body>
</html>

アイコンはここからもってきましょう。
http://code.google.com/chrome/extensions/getstarted.html

作成したら
chrome://extensions/
を開いてデベロッパーモードにして「パッケージ化されていない拡張機能を読み込みます」をクリックしてsampleフォルダを選びましょう。すると右上にアイコンが追加されたと思います。
オプションでサイトのタイトルとURLをいくつか適当に保存してアイコンをクリックすると保存したサイトのタイトルがリンクとなって表示されたと思います。クリックすると新しいタブとしてそのページが開きます。

結構簡単ですね!

ちなみにGoogleの拡張を一般に公開するためには最初に5ドルかかるみたいです。でも英語が読めないからよくわからないです。
ここからこの拡張をDLできるかも!
私の方ではGoogleアカウントにサインインした状態なら見ることができるのですが...
https://chrome.google.com/extensions/detail/hdbadpbameljedcohfclapmpfeocepff?hl=ja

導入してるChromeアドオンまとめ

広告とかブロックしてくれます

次ページをどんどん読み込んでくれます。

GoogleReaderを使っている場合必須です。検索バーからサイトのRSSを登録できます。

Chrome版です。大体できます。Chrome付属の要素検証が機能充実しているので別にいらないかもしれません。

Gmailをいちいち開かなくても新着メールの確認ができます。通知もしてくれるので便利。

ページ開いたときにページの右下にポップアップが表示され検索したときのキーワードまでジャンプできます。

パスワード管理です。

Googleのサービスを沢山利用しているなら便利です。サービスをクリックするとそのサービスにジャンプできます。

見ているページをTumblrに投稿できます。

TwitterのWebページを拡張してくれます。クライアント使わずWebからの人はぜひ。

見ているページをブックマークできます。

ExcelとかPPTをGoogleDocで閲覧できます。

Firefoxと違って確か日本語対応してないと思います。

Flaskで作るTwitterBot on GAE

python twitter
GAEでbotを作るときのテンプレートです。

  • 一定時間ごとにランダムでつぶやく
  • 自分でもOAuth認証でポストできる
  • フォームからランダム投稿のDBに登録ができる

この機能です。ホントに最小限です。

http://github.com/gigq/flasktodo/

とりあえずここからファイルを落としてきます。これでGAE上でFlaskが利用できます。

次に

http://github.com/joshthecoder/tweepy

ここからtweepyをもってきます。この中のtweepyフォルダをflasktodoフォルダにぶち込みましょう。
後はflasktodoフォルダの中を書き換えましょう。

application.py

#coding: utf-8
from flask import Flask
app = Flask(__name__)
app.debug = True

from google.appengine.ext import db

from flask import redirect, url_for, request, render_template, abort, flash, get_flashed_messages
import tweepy
import random

CONSUMER_KEY = 'your own consumer key'
CONSUMER_SECRET = 'your own consumer secret'
TOKEN_KEY = 'token key'
TOKEN_SECRET = 'token secret'

class Contexts(db.Model):
    context = db.StringProperty()

@app.route('/post_form/')
def post_form(context=None):
    return render_template('form.html')

@app.route('/post_update/',methods=['GET','POST'])
def post_update():
    if request.method == 'POST':
        context = request.form['context']
        update(context)
        return redirect(url_for('post_form'))
    return 'Sample'

@app.route('/rnd_update/')
def rnd_update():
    query = Contexts.all()
    results = query.fetch(1000)
    result = results[random.randint(len(results)-1,0)]
    update(result.context)
    return None
    
@app.route('/insert_form/',methods=['GET','POST'])
def insert_form(context = None):
    if request.method == 'POST':
        context_str = request.form['context']
        cont = Contexts(context=context_str)
        db.put(cont)
    else:
        pass
    return render_template('insert_form.html')

    

@app.route('/update/')
def update(context = None):
    if context != None:
        context = context.encode('utf-8')
        auth = tweepy.OAuthHandler(CONSUMER_KEY,CONSUMER_SECRET)
        auth.set_access_token(TOKEN_KEY,TOKEN_SECRET)
        api = tweepy.API(auth)
        api.update_status(context)

# set the secret key.  keep this really secret:

if __name__ == '__main__':
    app.run()

若干@app.routeが無駄についていたり改善できますがそこはほっときましょう。

cron.yaml

cron:
- description: random tweet
  url: /rnd_update
  schedule: every 2 hours

templates/form.html

<html>
	<body>
	<form action="/post_update/" method="POST">
		<textarea cols="70" rows="5" name="context"></textarea>
		<input type="submit" value="post" />
	</form>
	{% if context %}
	<div>{{ context }}</div>
	{% endif %}
	</body>
</html>

templates/insert_form.html

<html>
	<body>
	<form action="/insert_form/" method="POST">
		<textarea cols="70" rows="5" name="context"></textarea>
		<input type="submit" value="insert" />
	</form>
	{% if context %}
	<div>{{ context }}</div>
	{% endif %}
	</body>
</html>

終わりです。
hogehoge/insert_form のアドレスからはDBに挿入
hogehgoe/post_form からはツイートができます。

CakePHPのfind('list')でとってくるときの注意点

php cakephp

昨日の続き。コメントを頂いたのでSet::extract('/Blog/id',$this->Blog->find('all');を利用すると

Array
(
[0] => 1
[1] => 2
[2] => 3
[3] => 4
[4] => 5
[5] => 6
)

あらすてき!!

セレクタとかラジオボタンの設置ならこれが楽ですね。
また

$this->Blog->find('list');

というものがあります。デフォルトでは主キーが返ってくるみたいなのです。これも簡単ですね。
でも注意を。

Array
(
[1] => 1
[2] => 2
[3] => 3
[4] => 4
[5] => 5
[6] => 6
)

返ってくる結果はこれです。
[主キー] => 主キーの配列っぽいです。
Oh...ちょっと使い辛い...。ちょっとどころじゃない...。前者を使いましょう。

listは

$this-Blog->find('list',array('fields' => array('Blog.id','Blog.shopid')));

とか指定して

Array
(
//[主キー] => 'shopid'
[1] => testuser2
[2] => testu3
[3] => hoge3
[4] => hoge4
[5] => hoge3
[6] => hoge3
)

こんなときに使えます

CakePHPでつまってるところまとめ

とりあえず色々なところを参考にサンプルを書いてみた

  • view部分

search.ctp

<h2>検索画面</h2>

<?php echo $form->create(array('action' => 'search', 'type' => 'post'));?>

ショップID
<?php echo $form->text('Blog.shopid', $options=array('size' => '40', 'maxlength' => '40'));?>

<br />

合計値
<?php echo $form->text('Blog.amount', $options=array('size' => '10', 'maxlength' => '10'));?>
円以上

<div>こっから検索結果</div>

<?php
echo $paginator->counter(array(
'format' => __('Page %page% of %pages%, showing %current% records out of %count% total, starting on record %start%, ending on %end%', true)
));
?></p>

<?php /* 2008/12/25修正 この例の$searchwordはArrayデータなのでStringを引数にとるurlencodeだとうまく動かないため、コントローラ側でurlencodeするように修正しました。 */ ?>
<?php $paginator->options(array('url' => $searchword  )); ?>


<table cellpadding="0" cellspacing="0">
<tr>
	<th><?php echo $paginator->sort('id');?></th>
	<th><?php echo $paginator->sort('売り上げ年月','orderdate');?></th>

	<th><?php echo $paginator->sort('ショップID','shopid');?></th>

	<th><?php echo $paginator->sort('合計金額','amount');?></th>
</tr>
<?php

foreach ($blogs as $blog):

?>
	<tr>
		<td>
			<?php echo h( $blog['Blog']['id'] ); ?>
		</td>
		<td>
			<?php echo h( $blog['Blog']['orderdate']) ; ?>

		</td>

		<td>
			<?php echo h( $blog['Blog']['shopid'] ); ?>

		</td>

		<td>
			<?php echo h( $blog['Blog']['amount'] ); ?>
		</td>

	</tr>
<?php endforeach; ?>
</table>
</div>

<div class="paging">
	<?php echo $paginator->prev('<< '.__('previous', true), array(), null, array('class'=>'disabled'));?>
 | 	<?php echo $paginator->numbers();?>
	<?php echo $paginator->next(__('next', true).' >>', array(), null, array('class'=>'disabled'));?>
</div>
  • モデル部分

blog.php

<?php
class Blog extends AppModel{
	var $name = 'Blogs';
	var $useTable = 'blogs';
}
?>
  • コントローラ部分

blogs_controller.php

<?php
class BlogsController extends AppController{
	
	var $name = 'Blogs';
	var $helpers = array('Html','Form','paginator');
	
	var $paginate = array('order'=> array('Blog.orderdate' => 'desc'),
		'limit' => 1,
	);
	
	function search(){
		
		$this->Blog->recursive = 0;
		
		if(!empty($this->data)){
//			$shopid = $this->data['Blog']['shopid'];
//			$amount = $this->data['Blog']['amount'];
			$search['shopid'] = $this->data['Blog']['shopid'];
			$search['amount'] = $this->data['Blog']['amount'];
			
		}elseif(count($this->passedArgs)){
			$search = $this->passedArgs;
		
			$this->data['Blog']['shopid'] = $search['shopid'];
			$this->data['Blog']['amount'] = $search['amount'];
		}
		
		$search_list = array(
			'shopid',
			'amount',
		);
		
		$search_value = array();
		foreach($search_list as $value){
			$search_value[$value] = urlencode($search[$value]);
		}
		
		$condition = array();
		$condition = array(
			'shopid like' => '%' . $search['shopid'] .'%',
			'amount >=' => $search['amount'], 
		);
		
		
		$this->set('amount_list',$this->Blog->find('all'));
		debug($this->Blog->find('all'));
		$this->set('searchword', $search_value);
		$this->set('blogs',$this->paginate($condition));
		
	}
	
	function searchresult(){
		$this->Blog->recursive = 0;
		
		if(!empty($this->data)){
//			$shopid = $this->data['Blog']['shopid'];
//			$amount = $this->data['Blog']['amount'];
			$search['shopid'] = $this->data['Blog']['shopid'];
			$search['amount'] = $this->data['Blog']['amount'];
			
		}elseif(count($this->passedArgs)){
			$search = $this->passedArgs;
		
			$this->data['Blog']['shopid'] = $search['shopid'];
			$this->data['Blog']['amount'] = $search['amount'];
		}
		
		$search_list = array(
			'shopid',
			'amount',
		);
		
		$search_value = array();
		foreach($search_list as $value){
			$search_value[$value] = urlencode($search[$value]);
		}
		
		$condition = array();
		$condition = array(
			'shopid like' => '%' . $search['shopid'] .'%',
			'amount >=' => $search['amount'], 
		);
		
		
		
		$this->set('searchword', $search_value);
		$this->set('blogs',$this->paginate($condition));
	}
}

テーブルは
blogsテーブルで
中身が
id,shopid,orderdate,amountです

表示を二列にしたい

今ビュー部分で検索結果を一列で表示してるけど二列にしたい。CSSでなんとかなるのかな。
それともロジックでなんとかなるのかしら。

ラジオボタンのセット

サンプルがてらidも検索対象にしようと思いプルダウンかラジオボタンでもどちらでもいいから実装しようと思った。しかしフォームの中に

<?php echo $form->radio('Blog.amount',
						$amount_list,
						array(
							'value' => 1
							)
);
?>

といれたらArray,Array,Array...とならんでいた。

このときのamount_listは

Array
(
[0] => Array
(
[Blog] => Array
(
[id] => 1
[shopid] => testuser2
[orderdate] => 2010-08-26 22:10:06
[amount] => 100
)

)

[1] => Array
(
[Blog] => Array
(
[id] => 2
[shopid] => testu3
[orderdate] => 2010-08-26 22:10:06
[amount] => 200
)

)

[2] => Array
(
[Blog] => Array
(
[id] => 3
[shopid] => hoge3
[orderdate] => 2010-08-26 22:11:40
[amount] => 1000
)

)

[3] => Array
(
[Blog] => Array
(
[id] => 4
[shopid] => hoge4
[orderdate] => 2010-08-26 22:11:40
[amount] => 399
)

)

[4] => Array
(
[Blog] => Array
(
[id] => 5
[shopid] => hoge3
[orderdate] => 2010-08-26 22:46:49
[amount] => 1000
)

)

[5] => Array
(
[Blog] => Array
(
[id] => 6
[shopid] => hoge3
[orderdate] => 2010-08-26 22:46:49
[amount] => 10000
)

)

)

だった。結果をidの配列で欲しいのだけれど...。

良い方法を知っている方がいれば教えていただきたいです...。

HadoopでJSONデータを扱う

hadooppython

HadoopJSONを扱うのにはどうしたらいいのだろうとググっても出なかったのでかいた。
Twitterから取得できるデータは基本JSON。yatsのAPIを叩いてデータを保存しておく。search.jsonという名前で保存した。
やりたいことはツイートの内容をYahoo!形態素解析にかけて分解して単語を数える。今回はyatsから"政治"で取得してきたので"政治"と一緒に使われている単語がわかるわけですね。

hadoopがあるフォルダ直下にpythonフォルダを作成して下記の3つのファイルを作成

jsonmapper.py

#!/usr/bin/env python

import sys
import simplejson
import yahooMorph


#print sys.stdin.read()
#print sys.stdin.read()
json_str = simplejson.loads(sys.stdin.read())

for line in json_str:
    #print s['user'] + "\t" + s['content']
    result = yahooMorph.morph(sentence=line['content'],ma_filter="9")
    for word in result:
        word = word + "\t1"
        #print "%s\t%s" % (word,1)
        word = word.encode('utf-8')
        print word

yahooMorph.py

#!/usr/bin/env python
#coding:utf-8

import urllib
import urllib2
from BeautifulSoup import BeautifulSoup

APPID = "apiのid"
PAGEURL = "http://jlp.yahooapis.jp/MAService/V1/parse"

def morph(sentence, appid=APPID, results="ma", ma_filter="1|2|3|4|5|6|7|8|9|10|11|12|13"):
    sentence = sentence.encode('utf-8')
    params = urllib.urlencode({'appid':appid,'results':results,'ma_filter':ma_filter,'sentence':sentence})
    #query = "%s?appid=%s&results=%s&uniq_filter=%s&sentence=%s" % (PAGEURL, appid, results, uniq_filter, sentence)
    c = urllib2.urlopen(PAGEURL,params)
    soup = BeautifulSoup(c.read())
    #return [(w.surface.string, w.reading.string, w.pos.string)
    #        for w in soup.ma_result.word_list]
    return [w.surface.string for w in soup.ma_result.word_list]

reducer.py

#!/usr/bin/env python

from operator import itemgetter
import sys

word2count = {}

for line in sys.stdin:
  line = line.strip()

  word,count = line.split("\t",1)
  try:
    count = int(count)
    word2count[word] = word2count.get(word, 0) + count
  except ValueError:
    pass

sorted_word2count = sorted(word2count.items(),key=itemgetter(0))

for word, count in sorted_word2count:
  print "%s\t%s" % (word, count)

とりあえず

cat inputjson/search.json | python python/jsonmapper.py | sort | python python/reducer.py

確認できました。

hadoop jar contrib/streaming/hadoop-streaming-*.jar \

  • input input \
  • output outputs \
  • mapper python/jsonmapper \
  • reducer python/reducer.py \
  • file /usr/lib/hadoop-0.20/python/jsonmapper.py \
  • file /usr/lib/hadoop-0.20/python/reducer.py

packageJobJar: [/usr/lib/hadoop-0.20/python/jsonmapper.py, /usr/lib/hadoop-0.20/python/reducer.py, /var/lib/hadoop-0.20/cache/hadoop/hadoop-unjar4015039559084905408/] [] /tmp/streamjob7352753504723779858.jar tmpDir=null
10/08/12 14:13:15 INFO mapred.FileInputFormat: Total input paths to process : 3
10/08/12 14:13:15 INFO streaming.StreamJob: getLocalDirs(): [/var/lib/hadoop-0.20/cache/hadoop/mapred/local]
10/08/12 14:13:15 INFO streaming.StreamJob: Running job: job_201008111735_0019
10/08/12 14:13:15 INFO streaming.StreamJob: To kill this job, run:
10/08/12 14:13:15 INFO streaming.StreamJob: /usr/lib/hadoop-0.20/bin/hadoop job -Dmapred.job.tracker=localhost:8021 -kill job_201008111735_0019
10/08/12 14:13:15 INFO streaming.StreamJob: Tracking URL: http://localhost:50030/jobdetails.jsp?jobid=job_201008111735_0019
10/08/12 14:13:16 INFO streaming.StreamJob: map 0% reduce 0%
10/08/12 14:13:48 INFO streaming.StreamJob: map 100% reduce 100%
10/08/12 14:13:48 INFO streaming.StreamJob: To kill this job, run:
10/08/12 14:13:48 INFO streaming.StreamJob: /usr/lib/hadoop-0.20/bin/hadoop job -Dmapred.job.tracker=localhost:8021 -kill job_201008111735_0019
10/08/12 14:13:48 INFO streaming.StreamJob: Tracking URL: http://localhost:50030/jobdetails.jsp?jobid=job_201008111735_0019
10/08/12 14:13:48 ERROR streaming.StreamJob: Job not Successful!
10/08/12 14:13:48 INFO streaming.StreamJob: killJob...
Streaming Command Failed!

あ、あれぇ.....