Hatena::ブログ(Diary)

つまみ食う このページをアンテナに追加 RSSフィード Twitter

2012-01-01

Pythonでmixiのプロフィール画像を変更する

あけましておめでとうございます。

早起きしてPython書き初めしたのでご報告です。


今年はmixiプロフィール画像を日ごとに変更したいと思った。mixiはプロフィール用画像を複数設定できて、その中から一枚をメインの画像にする。APIとか分からないので mechanize でやれば良いやって思ったのだけど、やってみたら少しハマったのでメモ。


とりあえず成果物

https://gist.github.com/1545546


使い方


    print "login"
    m = MixiProfileImage( EMAIL_ADDRESS, PASSWORD )
    
    print "delete current profile image"
    images = m.filter(level="1")
    for k, v in images.iteritems():
        m.delete(v)

    print "add new profile image"
    image = m.add( FILE_PATH , level="1", is_main="y")

他には 公開範囲、メイン画像どうかの 変更 と 絞り込み ができる。

mechanize なので mixi の仕様が変わったら使えなくなる。 


日替わりプロフィールに使える画像たち(参考)

http://www.mcdonalds.co.jp/menu/regular/index.html

http://www.akindo-sushiro.co.jp/menu/item.php

http://www.abysse.co.jp/world/flag/index.html

http://hanshintigers.jp/data/player/2012/index.html#pitcher

同じカテゴリーの画像だと日替わりアイコンにしても混乱させないはず。


仕組み

mechanizeで操作している。


画像の追加はファイルをPOSTで送信するだけなので簡単。

# self.client = mechanize.Browser()
self.client.select_form(nr=0)
self.client["level"] = [level]
    
self.client.add_file(open(path), "image/jpeg", path)
self.client.set_all_readonly(False)
self.client.submit()

画像の削除でハマった。

実際にブラウザで操作すると分かるのだけど、マウスでの操作が何段階かある。それらはJavaScriptで制御されていて最終的にajaxでリクエストを投げている(動きから判断した)。なので、mechanizeでその操作をそっくりなぞってみようとしたら、mechanizeはJavaScriptの動作は再現されない。そこで最終的に投げられるajaxのリクエストを調べて真似た。


以下やったこと

  • chrome developer tools を開く(Control+Alt+i)

使い方はこの記事が参考になる

Google ChromeJavaScriptデバッガの進化がすごい - 0xFF

http://d.hatena.ne.jp/os0x/20110422/1303468821


  • Scriptsタグの XHR Breakpoints の Any XHR のチェックを ON
  • 画像削除の一連の動作を普通にマウスでする
  • protorype-effects-なんとかファイルの途中でブレーク
  • ソースの下部にある{}をクリックすると、933行目で止まっている
this.transport.open(this.method.toUpperCase(), this.url, this.options.asynchronous);
if (this.options.asynchronous) {
    this.respondToReadyState.bind(this).defer(1)
}
this.transport.onreadystatechange = this.onStateChange.bind(this);
this.setRequestHeaders();
this.body = this.method == "post" ? (this.options.postBody || D) : null;
this.transport.send(this.body); // <- break
  • 上記ソース1行目の this.url に 最終行 this.body を POST で送信しているのが分かる
  • this.url には secret っていうキーみたいなのがある
  • とりあえず普通にHTMLソースを調べると secret っぽいキーがあったのでそれを利用する

RPC_URL = "/system/rpc.json?auth_type=postkey&secret=%s" % secret
params = '{"jsonrpc": "2.0", "method": "jp.mixi.profileimage.deleteEntry", "params": {"image_name": "%s"}, "id": 0}' % image_name

self.client.open(url, params)
  • できた!!!

2011-12-08

JavaScriptの継承できるクラスの書き方

インスタンス変数を参照したいときは普通に this 、自クラスや親クラスのメソッドを呼び出したいときは $this を使う。

特殊なことはしていないんだけど、変数名を工夫したら何か分かりやすくなった。


// 継承関数
var extend = function(Klass, SuperKlass) {
    var F = function() {};
    F.prototype = SuperKlass.prototype;
    Klass.prototype = new F();
    Klass.prototype.superclass = SuperKlass.prototype;
    return Klass.prototype;
};


var ClassA = (function() {
    var ClassA = function() {
        this.initialize.apply(this, arguments);
    }, $this = extend(ClassA, Object);

    $this.initialize = function() {
        this.value = "A";
    };
    $this.hoge = function() {
        console.log("ClassA.hoge!");
        $this.fuga.call(this); // fuga.call(this); でもOK
    };
    var fuga = $this.fuga = function() {
        console.log("ClassA.fuga! this.value=" + this.value);
    };
    return ClassA;
}());

var ClassB = (function() {
    var ClassB = function() {
        this.initialize.apply(this, arguments);
    }, $this = extend(ClassB, ClassA);
    
    $this.initialize = function() {
        this.value = "B";
    };
    
    $this.hoge = function() {
        console.log("ClassB.hoge!");
        $this.superclass.hoge.call(this);
    };
    
    $this.fuga = function() {
        console.log("ClassB.fuga! this.value=" + this.value);
        $this.superclass.fuga.call(this);
    };
    
    return ClassB;
}());


var ClassC = (function() {
    var ClassC = function() {
        this.initialize.apply(this, arguments);
    }, $this = extend(ClassC, ClassB);

    $this.initialize = function() {
        $this.superclass.initialize(this, arguments);
        this.value = "C";
    };

    $this.hoge = function() {
        console.log("ClassC.hoge!");
        $this.superclass.hoge.call(this);
    };

    return ClassC;
}());


var a = new ClassA();
var b = new ClassB();
var c = new ClassC();


c.hoge(); // ClassC.hoge -> ClassB.hoge -> ClassA.hoge -> ClassA.fuga と呼ばれる
console.log("----------------------------------------");
c.fuga(); // ClassB.fuga -> ClassA.fuga と呼ばれる


console.log("----------------------------------------");
console.log("* overwrite ClassB.fuga");
b.__proto__.fuga = function() {
    console.log("piyopiyo");
};
c.fuga(); // 上書きされた ClassB.fuga が呼ばれる

2011-12-02

JavaScriptでテンプレート文字列を作る

今つくっているプログラムで必要そうだったので書いた。

やりたいこと

"$0...$1の年収、$2..?"

みたいなテンプレートがあって、それに $0=うわっ, $1=私, $2=低すぎ を渡すと、「うわっ...私の年収、低すぎ..?」と変換したい。

案1: string.replace

template = "$0...$1の年収、$2..?";
$0 = "うわっ";
$1 = "私";
$2 = "低すぎ";

// incorrect
result = template.replace("$0", $0); 

// correct
result = template.replace(/\$0/g, $0); 
result = result.replace(/\$1/g, $1);
result = result.replace(/\$2/g, $2);

簡単にできる。JavaScript の replace は何故か1つしか置換してくれないので、最初の行のやり方は良くない。正規表現にグローバルオプションをつけて置換してあげれば良い。ただ、replaceする度に対象箇所を探しているので非効率な感じがする。テンプレートっていうのは置換したい場所は把握しているもんだ。毎回探すとかアホか。あと、言ってなかったけどバックスラッシュに続く $ はエスケープしたい。しかし、JavaScript正規表現 は何故か後読みしてくれないので、後の行のやり方も良くない。



案2: toString + join

JavaScriptオブジェクトには toString っていうメソッドがあって、文字列に変換したいときはこれが呼ばれる。そして配列には join っていうメソッドがあって、配列の中身を「文字列に変換して」くっつける。

つまり、こうできる。

$0 = {value:"ええっ"       , toString:function() {return this.value;}};
$1 = {value:"お前"         , toString:function() {return this.value;}};
$2 = {value:"そんなにあるの", toString:function() {return this.value;}};

template = [$0, "...", $1, "の年収、", $2, "..?"];

result = template.join("");

こっちのほうが早い。簡単に速度を計測したものを最後に載せておく。


コード

テンプレート文字列のクラスを作った。$0-$9まで使えて、エスケープもできる。

function TemplateString() {
    var str, defaultValue;
    
    if (arguments.length === 1) {
        str = arguments[0];
        defaultValue = [];
    } else if (arguments.length >= 2) {
        str = arguments[0];
        defaultValue = arguments[1];
    }
    
    this.vars = (function(n, defaultValue) {
        var list = [];
        var i, imax;
        for (i = 0; i < n; i++) {
            list[i] = {value:defaultValue[i]||"",
                       toString:function(){return this.value;}};
        }
        return list;
    }(10, defaultValue));
    
    this.set(str);
}

TemplateString.prototype.toString = function() {
    if (this.withT) {
        return this.value.join("");
    } else {
        return this.value;
    }
};

TemplateString.prototype.setvalue = function(index, value) {
    if (0 <= index && index < this.vars.length) {
        this.vars[index].value = value;
    }
};

TemplateString.prototype.set = function(str) {
    if (/[^\\]\$[0-9]/.test(" " + str)) {
        this.withT = true;
        this.value = this._make(str);
    } else {
        this.withT = false;
        this.value = str;
    }
};

TemplateString.prototype._make = function(str) {
    var list;
    var c, esc, dol, buffer;
    var i, imax;
    list = [];
    buffer = [];
    for (i = 0, imax = str.length; i < imax; i++) {
        c = str[i];
        if (c === "$" && !esc) {
            dol = true;
        } else if (c === "\\") {
            esc = true;
        } else {
            if (dol && "0" <= c && c <= "9") {
                if (buffer.length > 0) {
                    list.push(buffer.join(""));
                    buffer = [];
                }
                list.push(this.vars[c|0]);
            } else {
                buffer.push(c);
            }
            esc = false;
            dol = false;
        }
    }
    if (buffer.length > 0) list.push(buffer.join(""));
    return list;
};

速度比較

短い文字列 {うわっ}...{私}の年収、{低すぎ}..?

50000回2500000回
replace 110msec 4330msec
toSting+join 40msec 2330msec
TemplateString 50msec 2540msec

長い文字列 走れメロスの冒頭677文字

500000回
replace 4550msec
TemplateString1740msec
var limit = 50000;
(function(limit) {
    var template, lis, $0, $1, $2;
    var d, i, result;
    lis = ["うわっ", "私", "低すぎ"];
    d = +new Date();
    template = "$0...$1の年収、$2..?";
    for (i = 0; i < limit; i++) {
        $0 = lis[(i+0)%3];
        $1 = lis[(i+1)%3];
        $2 = lis[(i+2)%3];
        result = template.replace(/\$0/g, $0);
        result = result.replace(/\$1/g, $1);
        result = result.replace(/\$2/g, $2);
    }
    console.log("TIME", (+new Date() - d), result);
}(limit));

(function(limit) {
    var template, lis, $0, $1, $2;
    var d, i, result;
    lis = ["うわっ", "私", "低すぎ"];
    d = +new Date();
    $0 = {value:"", toString:function() {return this.value;}};
    $1 = {value:"", toString:function() {return this.value;}};
    $2 = {value:"", toString:function() {return this.value;}};
    template = [$0, "...", $1, "の年収、", $2, "..?"];
    for (i = 0; i < limit; i++) {
        $0.value = lis[(i+0)%3];
        $1.value = lis[(i+1)%3];
        $2.value = lis[(i+2)%3];
        result = template.join("");
    }
    console.log("TIME", (+new Date() - d), result);
}(limit));

(function(limit) {
    var template = "$0...$1の年収、$2..?";
    var lis;
    var d, i, tstr ,result;
    lis = ["うわっ", "私", "低すぎ"];
    d = +new Date();
    tstr = new TemplateString(template);
    for (i = 0; i < limit; i++) {
        tstr.setvalue(0, lis[(i+0)%3]);
        tstr.setvalue(1, lis[(i+1)%3]);
        tstr.setvalue(2, lis[(i+2)%3]);
        result = tstr.toString();
    }
    console.log("TIME", (+new Date() - d), result);
}(limit));

console.log("----------------------------------------");

(function() {
    var limit = 500000;
    var template = "$0は激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。$0には政治がわからぬ。$0は、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては、人一倍に敏感であった。きょう未明$0は村を出発し、野を越え山越え、十里はなれた此のシラクスの市にやって来た。$0には父も、母も無い。女房も無い。十六の、内気な$2と二人暮しだ。この$2は、村の或る律気な一牧人を、近々、花婿として迎える事になっていた。結婚式も間近かなのである。$0は、それゆえ、花嫁の衣裳やら祝宴の御馳走やらを買いに、はるばる市にやって来たのだ。先ず、その品々を買い集め、それから都の大路をぶらぶら歩いた。$0には竹馬の友があった。$1である。今は此のシラクスの市で、石工をしている。その友を、これから訪ねてみるつもりなのだ。久しく逢わなかったのだから、訪ねて行くのが楽しみである。歩いているうちに$0は、まちの様子を怪しく思った。ひっそりしている。もう既に日も落ちて、まちの暗いのは当りまえだが、けれども、なんだか、夜のせいばかりでは無く、市全体が、やけに寂しい。のんきな$0も、だんだん不安になって来た。路で逢った若い衆をつかまえて、何かあったのか、二年まえに此の市に来たときは、夜でも皆が歌をうたって、まちは賑やかであった筈だが、と質問した。若い衆は、首を振って答えなかった。しばらく歩いて老爺に逢い、こんどはもっと、語勢を強くして質問した。老爺は答えなかった。$0は両手で老爺のからだをゆすぶって質問を重ねた。老爺は、あたりをはばかる低声で、わずか答えた。";
    var $0 = "メロス";
    var $1 = "セリヌンティウス";
    var $2 = "妹";

    (function(limit, template, lis) {
        var d, i, result;
        d = +new Date();
        for (i = 0; i < limit; i++) {
            result = template.replace(/\$0/g, lis[(i+0)%3]);
            result = result.replace(/\$1/g, lis[(i+1)%3]);
            result = result.replace(/\$2/g, lis[(i+2)%3]);
        }
        console.log("TIME", (+new Date() - d), result.substr(0, 20));
    }(limit, template, [$0, $1, $2]));

    (function(limit, template, lis) {
        var d, i, tstr ,result;
        d = +new Date();
        tstr = new TemplateString(template);
        for (i = 0; i < limit; i++) {
            tstr.setvalue(0, lis[(i+0)%3]);
            tstr.setvalue(1, lis[(i+1)%3]);
            tstr.setvalue(2, lis[(i+2)%3]);
            result = tstr.toString();
        }
        console.log("TIME", (+new Date() - d), result.substr(0, 20));
    }(limit, template, [$0, $1, $2]));
}());

2011-11-23

BlobBuilder で 外部ファイルの要らない WebWorkers はつくれる

WebWorkersで別タブ選択中もきっちり動く無敵タイマーをつくる - つまみ食う

http://d.hatena.ne.jp/mohayonao/20111108/1320756534


先日書いたこのエントリーで、別タブを選択したときも精度が落ちないタイマーを作った。

でも、外部ファイルが必要だったのでライブラリ的なものに組み込むときにパスを自己解決できないという問題があった。

// 問題点:muteki-timer.js の設置場所が制限される
//        もしくはライブラリの外から muteki-timer.js の場所を教えてあげる必要がある
var url = "muteki-timer.js";
var timer = new Worker(url); 

今回は外部ファイルを使わない無敵タイマー。以下手順

  1. BlobBuilderっていうのを使ってコードを組み立てる。
  2. すごいと評判*1 の createObjectURL でURLを取得
  3. new Worker(url) する

ちなみに前回の方法は

  1. 外部ファイルを書く (muteki-timer.js)
  2. url = "./muteki-timer.js"
  3. new Worker(url) する

MutekiTimerクラス

上記の方法で無敵タイマークラスを作った。

無敵タイマーが使えないときは通常の setInterval を使う。コードはエントリの最後。


デモ

無敵タイマー と 通常の setInterval の比較ページ。

別タブを選択して、しばらく放置して戻ってくると差が分かる。

http://mohayonao.herokuapp.com/mutekitimer


サンプル

無限インベンション

http://mohayonao.herokuapp.com/invention


6文字リズムマシン

http://mohayonao.hatenablog.com/entry/2011/11/21/202019


どちらも音のなるウェブ楽器で、別タブを選択中も音が鳴り続ける。

(以前は別タブを選択すると音が激しく途切れていた)


メモ

  • Chrome, Firefox で使える
  • Chrome では ファイル(htmlダブルクリックして開いた状態)だと動かない
    • URL.createObjectURL が undefined を返す
  • Firefoxローカル(127.0.0.1 or localhost)だと動かない??
    • ちょっと前にそれで困った覚えがあるけど、今はそうでもない。バージョン依存?
  • たしかに createObjectURL はすごい。よく分からないけどもっと凄いことに使えそう。

コード

    function MutekiTimer() {
        this.initialize.apply(this, arguments);
    }
    MutekiTimer.prototype = {
        initialize: function() {
            var url = (function() {
                var BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder;
                var URL = window.URL || window.webkitURL;
                var MutekiTimerBlob;
                if (!BlobBuilder || !URL) return null;
                
                MutekiTimerBlob = new BlobBuilder();
                MutekiTimerBlob.append("var timerId = 0;");
                MutekiTimerBlob.append("this.onmessage = function(e) {");
                MutekiTimerBlob.append("  if (timerId !== 0) {");
                MutekiTimerBlob.append("    clearInterval(timerId);");
                MutekiTimerBlob.append("    timerId = 0;");
                MutekiTimerBlob.append("  }");
                MutekiTimerBlob.append("  if (e.data > 0) {");
                MutekiTimerBlob.append("    timerId = setInterval(function() {");
                MutekiTimerBlob.append("    postMessage(null);");
                MutekiTimerBlob.append("    }, e.data);");
                MutekiTimerBlob.append("  }");
                MutekiTimerBlob.append("};");
                return URL.createObjectURL(MutekiTimerBlob.getBlob());
            }());
            if (url) {
                this._timer = new Worker(url);
                this.isMuteki = true;
            } else {
                this._timer = null;
                this.isMuteki = false;
            }
            this._timerId = 0;
        },
        setInterval: function(func, interval) {
            if (this._timer) {
                this._timer.onmessage = function(e) {
                    func();
                };
                this._timer.postMessage(interval);
            } else {
                if (this._timerId !== 0) {
                    clearInterval(this._timerId);
                }
                this._timerId = setInterval(function() {
                    func();
                }, interval);
            }
        },
        clearInterval: function() {
            if (this._timer) {
                this._timer.postMessage(0);
            } else {
                if (this._timerId !== 0) {
                    clearInterval(this._timerId);
                }
                this._timerId = 0;
            }
        }
    };

2011-11-15

Instagramがエンジニアを見つけるために出題した独創的な課題をやってみた

面白そうなのでやってみた。


あなたは解ける? Instagramエンジニアを見つけるために出題した独創的な課題:Don't be lame

http://kenichinishimura.blogspot.com/2011/11/instagram.html


縦に分割された画像をゴチョゴチョやって元に戻す。


これを

f:id:mohayonao:20111115224823p:image


こうする

f:id:mohayonao:20111115224821p:image


以下、コードの中にコメントで書いた。

ぜんぜんエレガントじゃない。。


2011/11/16 追記

ぜんぜんエレガントじゃないどころか、解けていなかった。。

最後に画像のつながりを推測する段階で、いちばんつながっていない箇所が画像の端っこになるはずだけど、下のコードでは最後から2番目を端っこにしている(ズルしている)


2011/11/16 さらに追記

色をRGBからHSVに変換して、閾値をいじったら解けるようになった。

ただし、他の画像でも同じように出来るかは分からない。たぶん良い閾値を発見できればいけると思うけど。



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

import time
import math
import Image


def RGBtoHSV(r, g, b):
    """RGBをHSVに変換"""
    maxValue = max(r, max(g, b))
    minValue = min(r, min(g, b))
                   
    if maxValue == minValue:
        h = 0;
    elif maxValue == r:
        h = (60 * (g - b) / (maxValue - minValue) + 360) % 360;
    elif maxValue == g:
        h = (60 * (b - r) / (maxValue - minValue)) + 120;
    elif maxValue == b:
        h = (60 * (r - g) / (maxValue - minValue)) + 240;   
        
    if maxValue == 0:
        s = 0;
    else:
        s = (255 * ((maxValue - minValue) / maxValue));
        
    v = maxValue;

    return h, s, v;
	


def calcLinesDistance(d1, d2):
    """(行単位で)色の距離を求める"""
    calcDistance = lambda c1,c2: math.sqrt((c1[0]-c2[0])**2 + (c1[1]-c2[1])**2 + (c1[2]-c2[2])**2)
    return sum(calcDistance(d1[x], d2[x]) for x in xrange(len(d1))) / len(d1)


def guessSplitHeight(im, nlist=(2,4,6), thlist=(1.5,1.7)):
    """何ピクセルで分割されているか推測する"""
    
    width, height = im.size
    
    # 画像を行ごとのHSV値に変換
    data = tuple(RGBtoHSV(*c[:3]) for c in im.getdata())
    data = tuple(data[i*width:(i+1)*width] for i in xrange(height))
    
    # 各行ごとに次の行との差(距離)を計算する
    lis = tuple(calcLinesDistance(data[y-1], data[y]) for y in xrange(1, height))

    candidates = {}
    for n in nlist: # n は移動平均の数
        for th in thlist: # th は閾値
            prev = -1
            for i in xrange(n, len(lis)-n):
                # 周りに比べて距離が大きい??
                x = lis[i] / (sum(lis[i-n:n+i+1]) / (2 * n + 1))
                if x > th:
                    # 閾値を超えたら、前回の閾値を超えたときとの間隔をカウント
                    key = (i-prev)
                    if key not in candidates:
                        candidates[key] = 0
                    candidates[key] += 1
                    prev = i

    # いちばん良く出てくる間隔があやしい
    return sorted(candidates.items(), key=lambda a:-a[1])[0][0]



def guessConnect(im, n):
    """画像のつながりっぷりを推測する"""

    width, height = im.size
    h = height/n
    
    # 画像を分割サイズで切り抜く、
    # ついでに半分のサイズに(計算量を減らすのとある程度平均値で調査するため)
    cropper = lambda i: im.crop((0, i * h, width, (i+1)*h)).resize((width/2, h/2), Image.CUBIC)

    # 画像の端っこのデータだけ取る
    lis = []
    for i in xrange(n):
        data = tuple(RGBtoHSV(*c[:3]) for c in cropper(i).getdata())
        data = tuple(data[i*width/2:(i+1)*width/2] for i in xrange(h/2))
        lis.append(dict(L=data[0], R=data[-1]))
        
    # 分割したそれぞれの距離を求める (距離が小さい=自然にくっつきそう)
    distanceList = {}
    for i in xrange(n):
        for j in xrange(n):
            if i == j: continue
            d = calcLinesDistance(lis[i]["R"], lis[j]["L"])
            distanceList[(i, j)] = d

    # 距離が小さい順番につながりを確定させていく
    right, left = range(n), range(n)
    lis = []
    while len(left) > 0:
        mind = float("inf")
        pair = None
        for i in xrange(n):
            if i not in left: continue
            for j in xrange(n):
                if i == j: continue
                if j not in right: continue
                d = distanceList[(i, j)]
                if d < mind:
                    pair = (i, j, d)
                    mind = d
        left.remove(pair[0])
        right.remove(pair[1])
        lis.append(pair)
        
    # 順番にならべる
    map = dict(x[:2] for x in lis)
    head = key = lis[-1][1]
    result = [head]
    for i in xrange(n):
        result.append(map[key])
        key = map[key]
    return result


def main():
    begin = time.time()
    
    imgpath = "./instagram_shuttered.png"

    im1 = Image.open(imgpath)
    width, height = im1.size
    
    # 縦に捜査するの気持ち悪いので回転させる
    im2 = im1.rotate(-90)

    # 分割の間隔を推測する
    h = guessSplitHeight(im2.resize((height/4, width)))
    
    # つながりを推測する
    l = guessConnect(im2, width/h)

    # 張り合わせる
    im2 = Image.new(im1.mode, im1.size)
    for i1, i2 in enumerate(l):
        cpi = im1.crop((i2 * h, 0, (i2+1) * h, height))
        im2.paste(cpi, (i1 * h, 0, (i1+1) * h, height))
    im2.save("./result.png")
    
    print time.time() - begin, "sec"

if __name__ == "__main__":
    main()