Google App Engine

2010年8月 8日 (日)

webページをproxyしてくれるwebアプリのソース

悪用されそうで、自重してサービスを止めていたのですが、同じようなツール
を作ってすでに公開(Google App Engineを使って無料で自宅用プロキシを運用)
されているので、私もサービスを再開し、ソースを公開することにしました。
下記で公開中です。
google code webappproxy
メインのプログラム1本とHTML1本の小さなプログラムなので全部掲載します。
guess_charsetに関しては、たばさの:urlfetchとmemcacheを使ってみるテスト
のソースを流用させていただきました。
Python歴3ヶ月程度なのでPythonのほうは滅茶苦茶。

hello.py

 import os
import re
import urllib
import logging
import urlparse
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.api import urlfetch
from google.appengine.ext.webapp import template

def guess_charset(data):
   f = lambda d, enc: d.decode(enc) and enc 
 
   try: return f(data, 'utf-8') 
   except: pass 
   try: return f(data, 'shift-jis') 
   except: pass 
   try: return f(data, 'euc-jp') 
   except: pass 
   try: return f(data, 'iso2022-jp') 
   except: pass 
   return None 

class MainPage(webapp.RequestHandler):
  def get(self):
    path = os.path.join(os.path.dirname(__file__), 'main.html')
    self.response.out.write(template.render(path, {}))

class NotePage(webapp.RequestHandler):
  def get(self):
    url=""
    if(self.request.get("url")):
      if re.compile("^\s*http://").match(self.request.get("url")):
        url = self.request.get("url").strip()
      else:
        url = "http://"+self.request.get("url").strip()
    else:
      if 'host' in self.request.cookies:
        url = self.request.cookies['host'] + self.request.path
    if url!="" :
      result = urlfetch.fetch(url)
      enc=guess_charset(result.content)
      if enc == None or enc == "shift_jis":
        enc="cp932"
      dec_content = result.content.decode(enc)
      enc_content = dec_content.encode("utf_8")
      result.content = re.sub("charset=([a-zA-Z0-9_]*)","charset=UTF-8",enc_content,1)
      if result.status_code == 200:
        for k,v in result.headers.iteritems():
          self.response.headers.add_header(k, v)
        parseString=urlparse.urlparse(url)
        cookie_val = 'host' + '=' + parseString[0]+"://"+parseString[1]
        self.response.headers.add_header('Set-Cookie',cookie_val)
        cookie_val = 'path' + '=' + re.sub("/[^/]*$","",parseString[2])
        self.response.headers.add_header('Set-Cookie',cookie_val)
        self.response.out.write(result.content)
    else:
      self.response.out.write("")

application = webapp.WSGIApplication(
                                     [('/main', MainPage),(r'/[^?]*',NotePage)],
                                     debug=True)
def main():
  run_wsgi_app(application)

if __name__ == "__main__":
  main()

/mainでアクセスされたら、MainPageのgetを呼び出し、main.htmlを返して
います。
それ以外のアドレスはすべてNotePageのgetを呼び出します。
こうすることには 訳があり、後ほど説明します。
main.html画面にurlを入力し、displayボタンを押下すると、/?url=指定したurl
の形式でアクセスされます。
指定されたurlからurl_fetchで内容をごっそり持ってきて、utf-8に変換します。
また、クッキーに指定されたURLのHostをセットしておきます。
こうすることで、表示した画面のパスのみで記述されたリンクをクリックされた
際に、 クッキーに保存しているHost名にそのパスをくっつけることで、再び
コンテンツを 取得することができるようになります。
/main以外のアドレスをすべてNotePageのgetを呼び出すようにしたのもそういう
訳です。

main.html

 <html>
  <head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <title>title</title>
 <link rel="shortcut icon" href="favicon.ico">
 <script type="text/javascript">
 function parseCooke(){
      cookies = document.cookie.split(";");
      obj = {};
 for(var i=0;i<cookies.length;i++){
        pair = cookies[i].split("=");
        obj[pair[0].replace(/^\s+/,"")] = pair[1];
 }
 return obj;
 }
 function loadData()
 {
 document.getElementById('ifm').src = "/?url="+escape(document.getElementById('url').value);
 }
 function modLink()
 {
 var doc;
 var ifm = document.getElementById("ifm");
 if (document.all) {
 // IE
        doc  = ifm.contentWindow.document;
 } else {
 // Mozilla
        doc = ifm.contentDocument;
 }
 var aElems=doc.getElementsByTagName("a");
 var cookie_info = parseCooke();
 var nHost = cookie_info.host;
 var path = cookie_info.path;
 var reg = RegExp(nHost);
 var reg2 = RegExp("http://"+location.host);
 for(var i=0;i<aElems.length;i++)
 {
 var aElem = aElems[i];
 if (document.all) {
 if(aElem.target!=null){
            aElem.target=null;
 }
 }
 else if(aElem.target){
 delete(aElem.target);
 }
 if(aElem.href.match(reg)){
          aElem.href= aElem.href.replace(nHost,"http://"+location.host);
 }
 else if(!aElem.href.match(reg2)){
          aElem.href="http://"+location.host+"?url="+escape(aElem.href);
 }else{
 if(!aElem.getAttributeNode("href").nodeValue.match(/^[\/#]/)){
            aElem.href =  path + "/" + aElem.getAttributeNode("href").nodeValue;
 }
 }
        aElem.onclick=function(ev){
 var elem = null;
 if(ifm.contentWindow.event){
            elem=ifm.contentWindow.event.srcElement;
 }else
 {
            elem=ev.target;
 }
          ifm.src = elem.href;return false;
 };
 }
 var imgElems=doc.getElementsByTagName("img");
 for(var i=0;i<imgElems.length;i++)
 {
 var imgElem = imgElems[i];
 if(!imgElem.getAttributeNode("src").nodeValue.match(/^http:/)){
 if(imgElem.getAttributeNode("src").nodeValue.match(/^\//)){
            imgElem.src = nHost + imgElem.getAttributeNode("src").nodeValue;
 }else{
            imgElem.src = nHost + "/" + path + "/" + imgElem.getAttributeNode("src").nodeValue;
 }
 }
 }
 }
 </script>
 </head>
<body>
  <form id="note_form" target="#" onsubmit="loadData(); return false;">
    URL:<input type="text" name="url" id="url" size="150"/>
    <input type="submit" value="display"/>
  </form>
  <iframe id="ifm" frameborder='0' style="width:100%;height:100%;border:none;" onload="modLink();" >
  </iframe>
</body>
</html>

main.htmlでは、displayボタンが押された時に/?url="URLのテキストボックスに
書かれた値" の形式でwebappproxyのURLを呼び出し、応答をiframeに書き出し
ています。
その際、iframeに書き出されたaタグのリンクをすべて抽出し、srcが同一ホストの
場合は、ホスト名を削除し(クッキーにホストが保存されているため),パスだけに
します。
違うホスト名の場合は/?url=違うホストのURLにします。
上記のようにすることで、proxyして表示されたリンクをたどっていっても、
proxyされ続けるようにしています。

| | コメント (0) | トラックバック (0)

2009年3月29日 (日)

webページをproxyしてくれるwebアプリ

会社でインターネットで調査をしていて、googleの検索結果の一覧でまさしくこの情報!と思ってアクセスしようとすると、フィルタリングのせいで見られないっていうことがままある。
(もちろんターゲットは某巨大掲示板)
そんなときに家のサーバでアクセスしたいwebへのプロキシサーバでも立てるかなーと思っていたのだが、昨日google app engineでできるんじゃないと思って実装してみた。
1日もかからず実装できた。
便利すぐる。フィルタリングがURLだけだったら、通るんじゃない?
ちょっと危険な匂いもします。

アドレスは
http://weightmange.appspot.com/main

2chの一覧を見るにはhttp://menu.2ch.net/bbsmenu.htmlを指定。 文字化けするので、Firefoxの場合は表示→文字エンコーディングでshift_JISを 選択 IEの場合は画面上で右クリックし、エンコード→日本語(シフトJIS)を選択

一応画面上のリンクはjavascriptでプロキシを経由するように設定しているため、マウスリンクでOKですが、駄目なパターンもあり。

セキュリティのため、プロキシ先に対するPOST,クッキーの送信はできません。

Web

| | コメント (0) | トラックバック (0)

2008年11月29日 (土)

google app engine 家計簿 ソース公開

グラフ機能やメモ機能を追加したため、家計簿のソースが大幅に
変わったため、ソース自体をgoogle codeとして下記に公開しました。
結局家計簿をwebアプリにしても、管理者にデータを見られるため、誰も
使いたがらないので、それなら、自分専用のwebアプリにしてしまえと
いうことです。google app engineだったら、無料でwebアプリを作成できるし。

http://code.google.com/p/accountnote/

プログラムの説明は見れば、大したプログラムでもないので見ればわかると
思うし、google app engine自体を私は詳しいわけではないので、やっぱり
説明するのは打ち切ります。質問があれば、コメントしていただければ答えます。

| | コメント (0) | トラックバック (0)

2008年11月14日 (金)

google app engineで家計簿3

accountList.js

var AccountList = Class.create({
  initialize:function(elem,type,form)
  {     this.type=type;
    if(this.type == "list")
    {       this.url="/accountList.json";
    }     else     {       this.url="/account.json";
    }     this.elem = $(elem);
    this.form = $(form);
  },
  loadData:function()
  {     var json=null;
    var a = new Ajax.Request(
        this.url,
        {             "method": "get",
            "parameters": Form.serialize(this.form.id),
            onSuccess: function(request) {               eval("json="+request.responseText);
              this.makeAccountList(json);
            }.bind(this),
            onFailure: function(request) {                 alert('読み込みに失敗しました');
            },
            onException: function (request) {                 alert('読み込み中にエラーが発生しました');
            }         }     );   },
  makeAccountList:function(dataSet)
  {       this.elem.innerHTML=""       var total = 0;
      var tableStr = "";
      if(dataSet.length != 0)
      {         tableStr += '<table id="account_list">';
        if(this.type!="list")
        {           tableStr += '<thead><tr><th>項目</th><th>金額</th><th>内容</th><th>機能</th></tr></thead><tbody>';
        }         else         {           tableStr += '<thead><tr><th>日付</th><th>項目</th><th>金額</th><th>内容</th></tr></thead><tbody>';
        }         for(var i=0;i<dataSet.length;i++)
        {

          var data = dataSet[i];
          tableStr +=
'<tr>';
         
if(this.type=="list")
         
{
            tableStr +=
'<td  width="100px">'+data.year+'/'+data.month+'/'+data.date+'</td>';
         
}
          tableStr +=
'<td class="bold" width="120px">'+data.category+'</td>';
          tableStr +=
'<td width="80px" style="text-align:right">'+myFormatNumber(data.price)+'円</td>';
          tableStr +=
'<td width="400px">'+data.content+'</td>';
         
if(this.type!="list")
         
{
            tableStr +=
'<td style="vertical-align:bottom">';
            tableStr +=
'<input class="deleteOfKey" type="submit" value="削除" id="'+data.key+'" /></td>';
         
}
          tableStr +=
'</tr>';
          total += data.price;
       
}
        tableStr +=
"</tbody></table>";
      
}
      tableStr +=
"<hr/> <div>合計 "+ myFormatNumber(total) + "円";
      
this.elem.innerHTML = tableStr;
      $$(
".deleteOfKey").each(function(elem){
      elem.observe(
'click',
      
function(event){this.deleteAccount(event.element().id);}.bind(this))}.bind(this));
 
},
  deleteAccount:
function(key)
 
{
   
var a = new Ajax.Request(
       
"/deleteAccount",
       
{
            
"method": "post",
            
"parameters": "key="+key,
            onSuccess:
function(request) {
             
this.loadData();
            
}.bind(this),
            onFailure:
function(request) {
               
alert('読み込みに失敗しました');
            
},
            onException:
function (request) {
               
alert('読み込み中にエラーが発生しました');
            
}
       
});
   
return false;
 
}
});


メインページ、一覧画面でも使うので、typeによって、メインページか、一覧画面かを分けているのは
昨日説明した通り。
loadDataをinitializeで呼ばないのは、検索画面で検索条件を設定するまで表示させないようにするため。
loadData呼び出しにより、家計簿データをjsonで取得し、makeAccountListでテーブル一覧として
出力している。メインページでは削除できるように削除ボタンを出し、検索画面では、各レコードの日付を
出力している。
      $$(".deleteOfKey").each(function(elem){
      elem.observe('click',
      function(event){this.deleteAccount(event.element().id);}.bind(this))}.bind(this));
はdeleteOfKeyクラスのボタンに対してクリックされるとボタン作成時にボタンのidとして、該当するレコードの
キーを値としてセットしていたので、idを取得することでキーを指定して、削除要求をサーバに
出している。google app enigineのデータの面白いところは、各レコードはidではなく、keyという値を
持ち、それはテーブルではなく、データベースで一意であるということである。
db.get(db.Key(key))で文字列のkeyを実際のkey値にした値をdbに渡せばレコードを取得できる。
bind(this)をしつこくしているのは、イベントハンドラのthisは通常そのエレメントがthisになってしまうが、
bind.thisにすることによって、accountListのインスタンスをthisにできるからである。そうでないと、accountList
クラスのdeleteAccountを呼び出せない。

| | コメント (0) | トラックバック (0)

2008年11月13日 (木)

google app engineで家計簿作り2

今日は最初にgoogle app engineのgvimの設定から。

function GoogleUpload()
  let s:nowDir = substitute(expand("%:p:h"),"c:\\app\\(.*\\)$","\\1","")
  execute "!python \"C:\\Program\ Files\\Google\\google_appengine\\appcfg.py\" update  ".s:nowDir
endfunction
cnorem <C-e> call GoogleUpload()<CR>

これが何がうれしいかというと、google app engineのソースを修正している際に
ノーマルモード状態から: 入力によりコマンドモードに移った後、 Ctrl+e入力で
現在のソースがあるディレクトリに対してappcfg.py upload を実行できるという
ものだ。
コマンドプロンプトを立ち上げておいてもいいのだが、appcfg.pyのフルパスを
入力するのが面倒だし、エディタから離れずにアップロードができるのは、
連続的にできるため、思考が途切れない。
普段の開発でもよく、svcコマンドやftpコマンドをこのようにキーマップに割り当てて
開発する。
コマンドモードに対してキーマップを割り振るのは比較的コマンドモードのキーは
割り当てられていないから、バッティングしないからである。

index.html

<html>
  <head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <title> 家計簿 </title>
 <script type="text/javascript" src="js/prototype-1.6.0.3.js"></script>
 <script type="text/javascript" src="js/common.js"></script>
 <script type="text/javascript" src="js/dateselect.js"></script>
 <script type="text/javascript" src="js/accountList.js"></script>
 <link href="css/account.css" rel="stylesheet" type="text/css">
 </head>
<body>
  <div>
    <input type="submit" value="項目編集" 
            onclick="
document.location.href='/category';"/>
    <input type="submit" value="一覧" 
           onclick="
document.location.href='/listaccount';"/>
  </div>
  <hr/>
  <div class="bold" id="totalAccount"></div>
  <form id="account" action="/" method="post">
    <div id="account_date">
    </div>
    <div>
      <span class="title" id="items_span"></span>
    </div>
    <div>
      <span class="title">費用</span>
      <input type="text" name="price" id="account_price" size="9"
                     
style="{ime-mode : disabled}"/>
    </div>
    <div><span class="title">内容</span>
      <input type="text" size="100" id="account_content"
               
name="content" style="vertical-align:top"/>
    </div>
    <input type="submit" value="送信" onclick="return dataSend()"/>
  </form>
  <div id="accountTitle"></div>
  <div id="accountTable"></div>
  <script type="text/javascript">
 function dataSend()
 {
 var a = new Ajax.Request( 
 "/addAccount",
 { 
 "method": "post",
 "parameters": Form.serialize("account"),
              onSuccess: function(request) { 
                $('account_content').value=""
                $('account_price').value=""
                setAccountData();            
 },
              onFailure: function(request) { 
 alert('読み込みに失敗しました');
 },
              onException: function (request) { 
 alert('読み込み中にエラーが発生しました');
 } 
 });
 return false;
 }
 function setAccountData()
 {
      setTotalAccount();
 var dateStr = $F('account_year')+"年"+$F('account_month')+"月"
                      
+$F('account_day')+"日";
 var tableStr = '<h3>'+dateStr+'の費用一覧 </h3>';
      $('accountTitle').html = tableStr;
      accountData.loadData();
 } 
 function setTotalAccount()
 {
 var a = new Ajax.Request( 
 "/gettotalaccount",
 { 
 "method": "get",
 "parameters": Form.serialize("account"),
              onSuccess: function(request) { 
                $('totalAccount').innerHTML=$F('account_year')+'年'+
         $F
('account_month')+'月の合計:'+
                  myFormatNumber(request.responseText) + '円';
 },
              onFailure: function(request) { 
 alert('読み込みに失敗しました');
 },
              onException: function (request) { 
 alert('読み込み中にエラーが発生しました');
 } 
 });
 }
 function setCategoriesData(categories)
 {
 if(categories.length == 0)
 {
 alert("初めてのご使用ですね。項目編集のボタンを押下して、" +
       "食費、交通費等の項目を設定してください。"
);
 return;
 }
      optStr = '項目<select name="category" id="category">';
 for (var i=0 ;i<categories.length;i++)
 {
        optStr += '<option value="'+categories[i].key+'" >'+
                                 categories
[i].name+'</option>';
 }
      optStr+='</select>';
      $("items_span").innerHTML = optStr;
 }
 function setCategories()
 {
 var a = new Ajax.Request( 
 "/getcategories",
 { 
 "method": "get",
              onSuccess: function(request) { 
                setCategoriesData(eval(request.responseText));
 },
              onFailure: function(request) { 
 alert('読み込みに失敗しました');
 },
              onException: function (request) { 
 alert('読み込み中にエラーが発生しました');
 } 
 });
 }
 var dateSelector = null;
 var accountData = null;
 document.observe('dom:loaded',function(){
    accountData = new AccountList('accountTable',"day","account");
    dateSelector = new DateSelector('account','account_date',
                                    setAccountData
);
    setCategories();
 });
 </script>
</body>
</html>

ではメインページのhtmlに移る。
昨日説明したとおり、テンプレートとしてindex.htmlを呼び出しているが、変数による
置き換えがひとつもない。
データはすべてajaxによってjson形式でサーバから取得してそのデータに基づいて
表示している。

jsはprototype.js以外は自作のjsファイルだ。
pythonを使う人は比較的jQueryを使う人が多いのだが、私はprototypeの方が
bind関数やquery stringのパース、htmlのエスケープ関数が最初から用意されていて
ありがたい。
document.observe('dom:loaded',function(){}はprototype.jsの常套句で、htmlのdom
が一通りパースされ、すべてのelementに対してイベントが登録できる状態(イメージの
読み込み等はまだ)になったときに呼び出されるイベントハンドラである。
accountData = new AccountList('accountTable',"day","account");
は AccountList(一覧を表示するdivのID,一覧の種類,jsonを取得する際に渡すデータの
元となるformのID)のオブジェクト呼び出しである。
一覧の種類は今のところ"day"と"list"があり、メインページで使用する"day"は当日
使った費用の一覧で、日付は自明のため出力せず、データを削除するためのボタン
は出力する。
一覧画面で出す"list"は日付はまたがるため出力し、削除ボタンは出力しないという
違いがある。
dateSelector = new DateSelector('account','account_date',setAccountData);は
DateSelector(jsonを取得する際に渡すデータの元となるformのID,日付選択のselectを
作成するdivのID,日付が変わった際に呼び出すコールバック関数)のオブジェクトを
作成している。
setCategories()はajaxで項目一覧(食費や交通費)のjsonを取得し、ID items_span
のdivの中に項目を選択できるselectを作成している。

各jsは明日以降説明する。

| | コメント (0) | トラックバック (0)

2008年11月12日 (水)

google app engineで家計簿作り

せっかくdjangoに慣れ親しんだから、google app engineを使わない手は
ないっしょということで家計簿ソフトを作成。

実際に使ってみたい人はhttp://lifeaccountnote.appspot.com/ にアクセス。
使い方は簡単で項目編集で食費、家賃など分類したい項目を作成した後
家計簿画面で項目、金額、内容を入力すれば、その日使ったデータの一覧
が下に表示される。また、一覧画面では何月何日~何月何日までの指定と項目
の指定(無指定も可)をすることで、条件に一致したデータの一覧と合計を出力する。

メイン画面のイメージ

Main

まずは設定ファイルであるapp.yamlから。


# アプリケーションのID

application: lifeaccountnote

# アプリケーションのバージョン
version: 1

# ランタイムの名称。現時点では「python」のみ
runtime: python

# アプリケーションが前提としているAPIのバージョン
api_version: 1

# URLと、その処理方法の定義
handlers:
- url: /js
  static_dir: js
- url: /css
  static_dir: css
- url: /.*
  script: lifeaccountnote.py

urlのところの
- url: /js
  static_dir: js
の意味はパスが/jsで始まったら、google app engineのアプリを起動せずに
jsディレクトリ配下のファイルを直接返しますよという意味。
url:/.*はそれ以外のパスはlifeaccountnote.pyというgoogle app engine
用のpythonプログラムを起動させますという意味。

lifeaccount.py (最初の画面のみ)
# vim: fileencoding=utf-8
#!-*- coding:utf-8 -*-
#日本語
import cgi
import logging
from django.utils import simplejson
import os
import wsgiref.handlers
import datetime
from google.appengine.api import users
from google.appengine.ext import db,webapp
from google.appengine.ext.webapp import template
from google.appengine.ext.webapp.util import login_required

class MainPage(webapp.RequestHandler):
  @login_required
  def get(self):
    path = os.path.join(os.path.dirname(__file__), 'index.html')
    self.response.out.write(template.render(path, {}))

  def post(self):
    user = users.get_current_user()
    _add_account(self,user)
    self.redirect("/")
def main():
  logging.getLogger().setLevel(logging.DEBUG)
  application = webapp.WSGIApplication(
                                       [('/', MainPage),
                                         ],
                                       debug=True)
  wsgiref.handlers.CGIHandler().run(application)
if __name__ == "__main__":
  main()

path = os.path.join(os.path.dirname(__file__), 'index.html')
は常套句。google app engineにアップロードしてもどこに配置されるか
判らないので__file__で今のファイルのフルパスを取得して、その
ファイルが置かれているディレクトリにテンプレートとして使用する
index.htmlのパスをpathにセットしている。
self.response.out.write(template.render(path, {}))
はtemplateにデータを書き込んだものをクライアントに返却しますよと
いう意味で、pathには先ほどのテンプレートのフルパスが入り、その後
テンプレートに渡すハッシュを渡すのだが、今回はAjaxでページを作成
するため、テンプレート自体をそのまま返却するため、空ハッシュを渡
している。だったらstaticのディレクトリに置けばいいのかも。

今日はこの辺で。

| | コメント (0) | トラックバック (0)