PythonとRubyの変数のスコープのまとめ

僕が普段使用するPythonRubyの変数のスコープについてまとめてみました。

Python

1. if文やfor文などの制御構造はスコープを作らない。
次のプログラムでは、if文の内側と外側のスコープは共通なので、if文内でaが上書きされてa=1が出力されます。

a = 0
if True:
  a = 1
print "a = %d" % a # a = 1

2. 関数定義とクラス定義では新しいスコープが作られる。

関数定義やクラス定義では新しいスコープが作られるので、関数定義の内側と外側で同名の変数が存在しても、両者は区別されます。
次のプログラムでは、関数foo内で変数aに代入を行っていますが、この場合fooの内側の変数と外側の変数は別物なので、外側の変数が上書きされることはありません。

a = 0

def foo():
  a = 1
  print "a = %d" % a # a = 1

foo()
print "a = %d" % a # a = 0

3. 内側のスコープから、外側のスコープの変数を参照することができる。

関数定義やクラス定義では新しいスコープが作成されますが、内側のスコープで同名の変数への代入を行わない限り、外側のスコープの変数を参照することが出来ます。

a = 0

class Foo(object):
  print "a = %d" % a # a = 0

def foo():
  print "a = %d" % a 

foo() # a = 0

Pythonでは関数内で関数を定義したり関数を変数に代入したりすることができるので、関数のローカル変数を参照するような関数を作成することが出来ます。
これをクロージャと呼びます。

def make_x_printer(x):
  def x_printer():
    print x # x はmake_x_printerのローカル変数だが、x_printerからも参照可能。
  return x_printer
a_printer = make_x_printer("a")
b_printer = make_x_printer("b")
a_printer()
b_printer()

4. 変数の作成は代入の時点ではなく、スコープの先頭で行われる。

スコープ内で変数への代入が行われると、内部的にはスコープの先頭で変数が作成されるようです。
次のプログラムでは関数foo内でaへの代入が行われているので、fooの先頭でaが作成されます。
そのため、外側のaはこの時点で参照できなくなっています。
さらにprint文が実行される時点ではまだaが未定義なので、UnboundLocalErrorが発生してしまいます。

a = 0

def foo():
  print "a = %d" % a
  a = 1

foo() # UnboundLocalError

Pythonの場合、この変数への代入が変数宣言を兼ねるという仕様のおかげで、現在のスコープと外側のスコープで変数名が衝突しても、外側のスコープの変数が誤って破壊される心配はありません。
ただし、クロージャで値を変更するようなプログラムを書く場合に、この仕様が邪魔になることがあります。
Pythonでは次のプログラムは正しく動作しません。

def make_counter():
  i = 0
  def counter():
    i+=1        
    return i
  return counter

c = make_counter()
print c()

i+=1という式はi=i+1と等価、すなわちiへの代入が存在するので関数counterのスコープでiという変数が新しく作成されます。
しかし、i=i+1の右辺が評価される時点ではiは初期化されていないので、UnboundLocalErrorが発生してしまいます。

Python2.xでは、次のプログラムのように、再代入を避けることによって、この問題を解決することができます。

def make_counter():
  i = [0]
  def counter():
    i[0] +=1
    return i[0]
  return counter

c = make_counter()
print c() # 1
print c() # 2

Python3.xではnonlocal文を使うことで、外側のスコープの変数への再代入を行うことができます。
nonlocal文を使えと先程の例をよりきれいに書くことが出来ます。

def make_counter():
  i = 0
  def counter():
    nonlocal i
    i+=1        
    return i
  return counter

c = make_counter()
print( c() ) 
print( c() )

Ruby

1. if文やwhile文などの制御構造はスコープを作らない。

Pythonと同様に、if文などの制御構文では新たなスコープは作成されません。

a = 0
if true
  a = 1
end
puts "a = #{a}" # a = 1

2. メソッド定義とクラス定義では新しいスコープが作られる。

Rubyの場合も、メソッド定義やクラス定義で新たなスコープが作成されます。
次のプログラムでは、fooの内側のaと外側のaは区別されるので、fooを実行しても外側のaが上書きされることはありません。

a = 0

def foo
  a = 1
  puts "a = #{a}" # a = 1
end

foo
puts "a = #{a}" # a = 0

3. メソッド定義やクラス定義の内側からは外側のスコープの変数を参照することが出来ない。

Rubyの場合、Pythonとは異なり、メソッド定義やクラス定義の中からは、外側のスコープの変数を参照することが出来ません。
次のプログラムの場合、トップレベルのaは(Pythonプログラマにとって)グローバル変数のようにも見えますが、Rubyの場合はこれはグローバル変数ではないので、メソッドfooの内部から変数aを参照することが出来ません。

a = 0

def foo
  puts "a = #{a}"
end

foo() # NameError

グローバル変数や定数の場合は、メソッド定義の内部からも外側のスコープの変数を参照することが出来ます。

$a = 0
A  = 1

def foo
  puts "$a = #{$a}"
  puts "A  = #{A}"
end

foo()

5. ブロックは新たなスコープを作る。ブロック内で定義された変数は、ブロックの中だけで有効。

ブロック内で作成された変数は、ブロックの中だけで有効です。

def foo
  yield
end

foo do
  a = 1
end
puts "a = #{a}" # NameError

6. ブロックの中からブロックの外で定義された変数にアクセスできる。さらに再代入も可能。

ブロックは新たなスコープを作りますが、ブロックの外側の変数も当然参照することが出来ます。
また、Pythonの場合と異なり、外側のスコープの同名の変数に再代入を行うことも可能です。

def foo
  yield
end

a = 0
foo do
  puts "a = #{a}" # a = 0
  a = 1
  puts "a = #{a}" # a = 1
end
puts "a = #{a}" # a = 1

ブロック内からブロック外の変数の再代入も行えるので、Python2.xに比べてクロージャを簡単につくることが出来ます。

def make_counter
  i = 0
  counter = lambda do
    i+=1
  end
  counter
end

c = make_counter
puts c.call
puts c.call

ブロックの外側と内側で意図しない変数名の衝突が起きても、変数の値が上書きされてしまうので、メソッドを極力短くして、変数のスコープを短く保つことが重要だと思います。


7. メソッド定義やクラス定義で外部の変数を参照したい場合、define_methodやClass.newを使う。

メソッド内で外側のスコープの変数(グローバル変数や定数を除く)を参照したい場合、通常のメソッド定義文やクラス定義文の代わりに、define_methodやClass.newを使います。
動的にクラスやメソッドを作成したい場合に使うテクニックです。

a = 0

Foo = Class.new do
  define_method(:foo) do
    puts "a = #{a}"
  end  
end

f = Foo.new
f.foo # a = 0

まとめ

PythonRubyはともに変数宣言を行わない動的言語ですが、こうして比べてみると変数のスコープはかなり異なることが分かります。
JavaScriptPerlLispなどは明示的な変数宣言を持つ言語なので、こうした言語との違いをまとめてみるのも面白いと思います。

radikoを録音して後で聞くためのプログラムを作ってみた

radikoを録音して、iPhoneなどで聞くためのプログラムを書いてみました。
といっても、rtmpdumpで録音、ffmpegエンコードし、gmailで送信するだけのプログラムです。
僕はこれを使って、朝のラジオ番組を仕事終わりの電車の中で聞いています。

ただし、プログラムを使うには、radikoが受信できてcronが使えるサーバーが必要です。
なので、かなり使えないプログラムであることは間違いありません。
もしこのプログラムを使う場合は、録画するチャンネル、録画時間、Gmailのアカウント情報などを適宜変更してください。

import os
import tempfile
import smtplib
from email.MIMEText import MIMEText
from email.MIMEMultipart import MIMEMultipart
from email.MIMEAudio import MIMEAudio
from email.Header import Header
from email.Utils import formatdate

GMailAccount = 'your_account@gmail.com'
GMailPasswd  = "your_password"

CHANNEL  = "ABC"
TIME     = 1800
RTMPDUMP = "/usr/local/bin/rtmpdump"
FFMPEG   = "/usr/bin/ffmpeg"
SUBJECT  = 'radiko'

def make_message(sender, to, subject, body, mp3file):
    encoding = 'utf-8'    
    msg = MIMEMultipart()
    msg['Subject'] = Header(subject, encoding)
    msg['From'] = sender
    msg['To']   = to
    msg['Date'] = formatdate()
    
    content = MIMEText(body.encode(encoding), 'plain', encoding)
    msg.attach(content)

    if mp3file:
      with open(mp3file) as io:
        attachment = MIMEAudio(io.read(), "mpeg", name=os.path.basename(mp3file))
      msg.attach(attachment)
    
    return msg

def send_mail(mp3file, sender=GMailAccount, to=GMailAccount, subject=SUBJECT, body=""):
  host = 'smtp.gmail.com'
  msg = make_message(sender, to, subject, body, mp3file)
  smtp = smtplib.SMTP(host, 587)
  try:
    smtp.ehlo()
    smtp.starttls()
    smtp.ehlo()
    smtp.login(GMailAccount, GMailPasswd)
    smtp.sendmail(sender, to, msg.as_string())
  finally:
    smtp.quit()

def main():
  flvfile = tempfile.NamedTemporaryFile(delete=False, suffix=".flv")
  mp3file = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
  flvfile.close()
  mp3file.close()

  rtmpdump_options = {
    "rtmp" : 'rtmpe://radiko.smartstream.ne.jp',
    "port" : "1935",
    "app" : '%s/_defInst_' % CHANNEL,
    "playpath" : 'simul-stream',
    "swfUrl" : 'http://radiko-dl.ssdl1.smartstream.ne.jp/radiko-dl/1.1/player/player_0.1.2.swf',
    "pageUrl" : 'http://radiko.jp/player/player.html#%s' % CHANNEL,
    "flashVer" : '"LNX 10,0,45,2"',
    "live" : "",
    "stop": str(TIME),
    "flv" : flvfile.name
    }
  ffmpeg_options = {
    "y" : "",
    "i" : flvfile.name,
    "ac" : "1",
    "ab" : "32",
    }

  rtmpdump_cmd = "%s %s" % (RTMPDUMP, " ".join(("--%s %s" % (k, v) for k, v in rtmpdump_options.iteritems())))  
  ffmpeg_cmd   = "%s %s %s" % (FFMPEG, " ".join(("-%s %s" % (k, v) for k, v in ffmpeg_options.iteritems())), mp3file.name)
  try:
    print rtmpdump_cmd
    os.system(rtmpdump_cmd)
    print ffmpeg_cmd
    os.system(ffmpeg_cmd)
    send_mail(mp3file=mp3file.name)
  finally:
    os.unlink(flvfile.name)
    os.unlink(mp3file.name)

if __name__ == '__main__':
  main()

GAEでDocutilsを使う方法

GAEで動くアプリケーションでDocutilsを使おうとしたところ、何故かエラーが発生してしまいました。エラーメッセージを読むと、GAEでは使用不可能なAPIを、Docutilsが内部で使用しているため、このようなエラーが発生してしまうということが分かりました。

この問題の解決方法は、既にこちらのページで紹介されています。その方法は、docutils/frontend.pyの以下の部分をコメントアウトするというものです。

if read_config_files and not self.defaults['_disable_config']:
    try:
        config_settings = self.get_standard_config_settings()
    except ValueError, error:
        self.error(error)
    self.set_defaults(**config_settings.__dict__)

しかし、この部分のソースをよく見ると、self.defaultという名前のディクショナリ(おそらく設定か何か)の_disable_configがTrueになっていれば、問題の部分を実行しないということが分かります。案の定、Djangoプロジェクトのsettings.pyに以下の設定を加えるだけで、この問題は解決することが出来ました。

RESTRUCTUREDTEXT_FILTER_SETTINGS = dict(_disable_config=True)

DocutilsのAPIを直接使う場合は、

from docutils.core import publish_parts

source = "HTML化したいreStructuredText形式のテキスト"
settings = dict(_disable_config=True)
parts = publish_parts(source=source, writer_name="html4css1", settings_overrides=settings)
html = parts["whole"]
# html = parts["fragment"]

といったところでしょうか。

ちなみに、Djangoではsettings.py中にRESTRUCTUREDTEXT_FILTER_SETTINGSという名前のディクショナリを作成することで、restructuredtextフィルタの挙動を制御することが出来ます。他の使い方としては、例えば、

RESTRUCTUREDTEXT_FILTER_SETTINGS = dict(initial_header_level=3)

としておけば、見出しで使うタグをh1ではなくh3にすることが出来ます。

DjangoでJavaScriptの国際化を行う方法

前回のエントリ「GAEでポケモンのデータベースサイトを作成しました - saito’s blog」で紹介したサイトでDjangoの国際化機能でJavaScriptの翻訳を行う際に,いくつかハマったことがあったので,ここにまとめておこうと思います.

Djangoのリファレンスによると、以下の手順によって、JavaScriptを翻訳することができます。

1. JavaScriptのソースファイル内の翻訳したい文字列をgettext()やngettext()で囲んでおく。
2. 次のコマンドでJavaScriptの翻訳カタログdjangojs.poを作成する。

django-admin.py makemessages -d djangojs -l ja

3. 作成したdjangojs.poを編集する。
4. 次のコマンドで翻訳カタログをコンパイルする。

django-admin.py compilemessages

5. URLとjavascript_catalogビューを結びつけるために、urls.pyに設定を追加する。

urlpatterns = patterns('',
    (r'^jsi18n/(?P<packages>\S+?)/$', 'django.views.i18n.javascript_catalog'),
)

6. htmlファイル内で翻訳したいJavaScriptファイルを読み込む前に、urls.pyで指定したURLをロードする。

<script type="text/javascript" src="/prefix/jsi18n/package_name/"></script>

僕がハマったのは、翻訳ファイルを置くディレクトリの場所です。
この問題にハマる以前は、プロジェクトのディレクトリ構成を以下のようにしていました。

project/ (プロジェクトのトップディレクトリ)
├── bw (アプリケーションのディレクトリ)
│   └── *.py
├── static (静的ファイルの置き場所)
│   └── js (JavaScriptファイルの置き場所)
├── locale (翻訳ファイルの置き場所)
└── *.py

このように翻訳ファイルの置き場所とアプリケーションのディレクトリを分けていると、翻訳文字列が読み込まれません。
その理由は、JavaScriptの翻訳カタログをできるだけ小さくするために、DjangoJavaScriptの翻訳をアプリケーション単位で生成するためです。

以下のように、翻訳したいJavaScriptの入ったディレクトリと翻訳ファイルの入ったディレクトリを、アプリケーションのディレクトリの下に配置することによって、翻訳文字列を正しく読み込むことができます。

project/
├── bw
│   ├── static
│   │   └── js
│   ├── locale
│   └── *.py
└── *.py

このディレクトリ構成かつurls.pyを上記のように設定した場合、JavaScriptの翻訳ファイルのURLは、/prefix/jsi18n/bw/になります。

また、僕が作ったサイトでは、日本語のページと英語のページを別のURLにするために、URLによって英語と日本語を切り替えていました。
Djangojavascript_catalogビューは、現在の言語に基づいて適切な翻訳ファイルを生成してくれるのですが、ブラウザがJavaScriptをキャッシュするため、異なる言語間でページを移動すると、前に使用していた言語が表示されてしまうという現象が起こりました。

翻訳ファイルのURLを言語ごとに分けることで、この問題を解決することができます。
まず、urls.pyを以下のように修正します。

url(r'^(?P<a_locale>(?:en)|(?:ja))/jsi18n/$', views.jsi18n_handler, name="bw.jsi18n")

そして、views.py内でjsi18n_handlerを以下のようにします。

import functools
from django.utils.translation import activate
from django.views.i18n import javascript_catalog

def select_lang(handler):
  @functools.wraps(handler)
  def wrapper(request, *args, **kwargs):
    if kwargs.get('a_locale', False) == 'ja':
      activate('ja')
    else:
      activate('en')
    return handler(request, *args, **kwargs)
  return wrapper

@select_lang
def jsi18n_handler(request, **kwargs):
  return javascript_catalog(request, packages="bw")

Djangoで言語を手動で切り替えるには、django.utils.translation.activateを使用します。
言語の切替は他のビューでも使う汎用的な処理なので、このようにデコレータを作って対処しています。

GAEでポケモンのデータベースサイトを作成しました

ここ最近、Google App EngineDjangoを使って、新作ポケモンのデータベースサイトを作っていました。デザインは全然ですが、一応それなりの機能が揃ってきたので、このブログで紹介したいと思います。
Top Page | Datebase & Calculator for Pokémon Black & White

データベース部分は、他の攻略サイトと比べると遥かに貧弱ですが、個体値計算機や努力値カウンター等のアプリケーションにはそれなりの工夫が施されているので、是非使ってみていただきたいと思います。

そもそも、このようなサイトを作った理由は、現在勉強中のDjangoGoogle App Engineを使って何かサイトを作ってみたいという理由ですが、どうせなら作るのならという理由で、Djangoの国際化フレームワークを使って英語版も作成してみました。今後はiPhoneAndroid用のページの作成やHTML5対応にも挑戦したいと思っています。

僕にとってGAEのデータストアの使用は今回初めてだったのですが、今回作ったようなほぼリードオンリーなサイトの場合、GAEのデータストアのメリットが全くなく、むしろデメリットばかりだということを、作った後で気づきました。
まず、RDBなら「HPがいくつ以上で攻撃力がいくつ以上」といったクエリを発行することができますが、GAEのデータストアでは不等式フィルタを使えるのは1つのプロパティまでに限られるので、そのようなクエリをGAEでは発行することが出来ません。
また、他のデータベースと性能を比較した訳ではないのですが、GAEのデータストアは速度が遅いように思います。
JavaScriptを使ったアプリでは、データストア内の全てのポケモンのデータをJSON化して送っているのですが、今のポケモンは600種類以上もいるので、データ数が600件以上となり、データの取得にかなり時間がかかってしまいました(GAEのダッシュボードで、このままだと課金内に収まらないと警告がでる程)。
幸い、appengine_helper_for_djangoを使えば、簡単にGAEのキャッシュ機能を使うことができます。プロジェクトのsettings.pyに、

CACHE_BACKEND = 'memcached://'

と1行追加し、キャッシュしたいview関数にdjango.views.decorators.cache.cache_pageデコレータを適用するだけでOKです。
cache_pageデコレータは?key=valueを含むような動的なURLに対してキャッシュを行わないようので、キャッシュしたいページは全てそのような動的なパラメータを含まない静的なURLにする必要があります。

VLCのエクステンションで地デジをリアルタイム視聴する

以前のエントリでpt1をGUIで操作するためのプログラムを紹介しました。
スナップショットはこんな感じです。

このプログラムは単に内部でrecpt1を実行しているだけなので、実際に地デジを視聴するにはvlcなどのメディアプレイヤーを別に起動しなくてはいけませんでした。もっとスマートな方法でできないか調べていたところ、vlcのエクステンション作成機能を使えばできそうだと分かったので、実際にやってみました。

vlcのエクステンションを使うにはバージョンの1.1以上が必要だそうで、ubuntu10.04で標準でインストールできるvlcのバージョンではエクステンションを使うことができません。なので、vlcのnightly buildを使うか、もしくはvlcをソースからコンパイルする必要があります。僕はソースからコンパイルする方法を試したので、その方法をここで紹介します。

vlcコンパイル

vlcコンパイルする方法は、こちらのページを参考にしました。

まずはじめに必要なパッケージをインストールし、競合するパッケージをアンインストールします。

sudo apt-get remove vlc vlc-data vlc-nox libvlc2 libvlccore2 mozilla-plugin-vlc vlc-plugin-jack vlc-plugin-pulse x264
sudo apt-get install checkinstall build-essential cmake libtool automake autoconf git-core ffmpeg libxcb-shm0-dev libxcb-xv0-dev libx11-xcb-dev libcdparanoia-dev libcdio-paranoia-dev libcdio-cdda-dev libqt4-dev qt4-dev-tools qt4-qmake nasm yasm libasm-dev lua5.1
sudo apt-get build-dep vlc

次にlibx264をコンパイルします。

wget ftp://ftp.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-20101002-2245.tar.bz2
tar jxf x264-snapshot-20101002-2245.tar.bz2
cd x264-snapshot-20101002-2245
./configure --enable-shared --prefix=/opt/local
make
sudo make install
sudo ldconfig

最後にvlcコンパイルします。vlcの最新の安定版のソースをダウンロードし、以下のコマンドを実行します。

export CPPFLAGS=-I/opt/local/include
export LDFLAGS=-L/opt/local/lib
tar jxf vlc-1.1.4.tar.bz2
cd vlc-1.1.4
./configure --prefix=/opt/local --enable-x11 --enable-xvideo --disable-gtk --enable-sdl --enable-avcodec --enable-avformat --enable-swscale --enable-mad --enable-libdvbpsi --enable-a52 --enable-libmpeg2 --enable-dvdnav --enable-faad --enable-vorbis --enable-ogg --enable-theora --enable-faac --enable-mkv --enable-freetype --enable-fribidi --enable-speex --enable-flac --enable-live555 --enable-caca --enable-skins --enable-skins2 --enable-alsa --disable-kde --enable-qt4 --enable-ncurses --enable-release --enable-realrtsp --enable-twolame --enable-real --enable-cddax --enable-mozilla --with-mozilla-pkg=xulrunner-plugin --enable-x264
make
sudo make install

ちなみに僕は自分でコンパイルするプログラムは/opt/localに入れているのでこのような設定になっています。プログラムのインストール先は適宜変更してください。

vlcからpt1を操作するLuaエクステンション

今回自作したvlcのエクステンションがこのプログラムです。

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

channel_name_to_number = {}
channel_name_to_number["NHK総合"]     = 22
channel_name_to_number["NHK教育"]     = 13
channel_name_to_number["サンテレビ"]  = 26
channel_name_to_number["MBS毎日放送"] = 16
channel_name_to_number["ABCテレビ"]   = 15
channel_name_to_number["関西テレビ"]  = 17
channel_name_to_number["読売テレビ"]  = 14
channel_name_to_number["テレビ大阪"]  = 18

function execute_with_stdout(cmd)
   local p = io.popen(cmd)
   local s = p:read("*a")
   p:close()
   return s
end

dlg      = nil
dropdown = nil
is_plyaing = false

function descriptor()
    return { title = "地デジ視聴";
	     capabilities = {} }
end

function activate()
   dlg      = nil
   dropdown = nil
   is_plyaing = false
   create_dialog()
end

function deactivate()
   stop_pt1()
   vlc.playlist.clear()
   vlc.deactivate()
end

function close()
   vlc.deactivate()
end

function create_dialog()
   dlg = vlc.dialog("地デジ視聴")

   local label = dlg:add_label("チャンネル : ", 1, 1, 1, 1)
   dropdown = dlg:add_dropdown(2, 1, 1, 1)

   for name, number in pairs(channel_name_to_number) do
      dropdown:add_value(name, number)
   end
   
   dlg:add_button("選択", click_select_button, 3, 1, 1, 1)
 end

function click_select_button()
   stop_pt1()
   local channel = dropdown:get_value()
   start_pt1(channel)
   if not is_playing then
      play_pt1()
      is_playing = true
   end
end

function start_pt1(channel)   
   local pt1_cmd = string.format("recpt1 --b25 --strip --udp --addr 127.0.0.1 --port 1234 %d - /dev/null &", channel)
   os.execute(pt1_cmd)
end

function stop_pt1()
   local pt1_pid = tonumber(execute_with_stdout("pidof -s recpt1"))
   if pt1_pid ~= nil then
      os.execute(string.format("kill -KILL %d", pt1_pid))
   end
end

function play_pt1()
   local path = "udp://@127.0.0.1:1234"
   local name = "recpt1"
   local mytable = { path = path; name = name }
   vlc.playlist.add({mytable})
end

このプログラムを拡張子.luaで~/.local/share/vlc/lua/extensions/の中に保存すると、エクステンションがvlcから使用可能になります。エクステンションを使用するには、vlcのメニューの「表示」から「地デジ視聴」を選択します。このようなダイアログが表示されるので、チャンネルを選んで選択ボタンを押すと、そのチャンネルを見ることができます。

関西以外の地域の場合はソースコードの一番上のchannel_name_to_number変数を適宜編集してください。この変数はチャンネルの名前と地デジの物理チャンネルのマップになっています。

ちなみにこのプログラムはpt1が搭載されているパソコンと地デジを視聴するパソコンが同一の場合を前提にしています。

Emacs LispとRubyとmozreplを使ってFirefoxを操作する

今回は、Emacs LispRubyとmozreplを使ってEmacsからFirefoxを操作する方法を紹介したいと思います。

mozreplとは

mozreplとはFirefoxのアドオンの一つで、Firefoxtelnetサーバーにしてしまうというものです。このアドオンを導入することで、ターミナルやプログラムからFirefoxを操作することが出来ます。

mozreplはこちらからインストールすることができます。mozreplをインストールしFirefoxを再起動させると、Firefoxの「ツール」メニューにMozReplというメニューが追加されます。そこでStartを実行すると、telnetコマンドを使ってFirefoxにアクセスできるようになります。

ターミナルで次のコマンドを打ってFirefoxにアクセスします。

rlwrap telnet localhost 4242

(4242はmozreplがlistenしているポート番号でmozreplメニューの「Change Port」で変更することが出来ます。また、rlwrapは無くても動きますが、rlwrapを使うことでカーソル移動やヒストリ機能が使えるようになるのでrlwrapを使うことをオススメします。Windowsの場合はTera Term等のプログラムでtelnetが使えたと思います。)すると、

repl>

というプロンプトがでてくるので、そこで

repl> content.location.href = 'http://www.google.co.jp'

と実行すると、Firefoxの現在のタブがGoogleのページに移ります。

mozreplのコマンドはjavascriptそのものなので、その場で関数を定義するなんてこともできるようです。

mozreplセッションを終了させるには、次のコマンドを実行します。

repl> repl.quit()

任意のURLを新しいタブで開くRubyプログラム

Emacsのプロセス関係の関数を使えば、EmacsからFirefoxを直接操作できるので、わざわざ別のプログラムを介する必要はないのですが、私自身がEmacsのそうした機能についてあまり詳しくないという理由から、Firefoxと通信を行う部分をRubyで書くことにしました。

require 'net/telnet'
def mozrepl_open
  $telnet = Net::Telnet.new("Host" => "localhost", "Port" => 4242, "Prompt" => /repl\> \z/n)
end
def mozrepl_cmd(str)
  $telnet.cmd(str)
  sleep(0.5)
end
def mozrepl_close
  $telnet.puts("repl.quit()")
  $telnet.close
end
mozrepl_open
mozrepl_cmd("gBrowser.selectedTab = gBrowser.addTab()")
if ARGV.length > 0
  mozrepl_cmd("content.location.href = '#{ARGV[0]}'")
else
  mozrepl_cmd("BrowserGoHome()")
end
mozrepl_close

このプログラムはmozreplを使ってFirefoxの新しいタブを作成し、そこで引数で指定したURLを開くというプログラムです。

Emacs Lisp

まず最初に、先程のRubyプログラムをEmacsから使うためのラッパー関数とURLをエンコードするための補助関数を作成します。

(defvar mozrepl-open-uri-path "~/.emacs.d/bin/mozrepl_open_uri.rb")

(defun mozrepl-open-uri (uri)
  (interactive "suri: ")
  (let ((cmd (format "ruby %s '%s'" mozrepl-open-uri-path uri)))
  (shell-command cmd)))

(defun uri-encode (str)
  (mapconcat
   (lambda (s)
     (mapconcat
      (lambda (x) (format "%%%x" x))
      (vconcat (encode-coding-string s 'utf-8))
      ""))
   (split-string str)
   "+"
   ))

(defun utf8-escape (str)
  (mapconcat
   (lambda (x) (format "%%%x" x))
   (vconcat (encode-coding-string str 'utf-8))
   ""))

変数mozrepl-open-uri-pathに、先程のRubyプログラムのパスを設定します。(URLをエンコードする関数の実装が怪しいです。)

この関数を使ってアプリケーションを作成していきます。

手始めにGoogle検索を実行するプログラムを書いてみます。

(defun mozrepl-google-search (keywords)
  (interactive "skeywords: ")
  (mozrepl-open-uri (format "http://www.google.co.jp/search?hl=ja&q=%s" (uri-encode keywords))))

(defun mozrepl-google-feeling-lucky (keywords)
  (interactive "skeywords: ")
  (mozrepl-open-uri (format "http://www.google.co.jp/search?hl=ja&btnI=&q=%s" (uri-encode keywords))))

(defun mozrepl-google-search-region (begin end)
  (interactive "r")
  (let (str)
    (setq str (buffer-substring-no-properties begin end))
    (mozrepl-google-search str)))

(defun mozrepl-google-feeling-lucky-region (begin end)
  (interactive "r")
  (let (str)
    (setq str (buffer-substring-no-properties begin end))
    (mozrepl-google-feeling-lucky str)))

(defun mozrepl-google-translate-region (begin end)
  (interactive "r")
  (let (str uri)
    (setq str (buffer-substring-no-properties begin end))
    (setq uri (format "http://translate.google.co.jp/translate_t?hl=ja&sl=en&tl=ja#%s%s"
		      (if (equal (find-charset-region begin end) '(ascii)) "en|ja|" "ja|en|")
		      (uri-encode str)))
    (mozrepl-open-uri uri)
    ))  

別のアプリケーションとして、言語のリファレンスのページに飛ぶプログラムを作ります。

ここで紹介するプログラムのアイデアは、調べたい言語のリファレンスのページを指定してGoogle検索を行うことで、見たいページが必ず検索のトップに現れるだろうという仮定に基づいています。
Google検索のサイト内検索機能と検索1位のページに直接飛ぶI'm Feeling Lucky検索を組み合わせています。

(defmacro define-mozrepl-x-search (name site)
  (let ((sym-i (intern (concat "mozrepl-" (symbol-name name) "-search")))
	(sym-g (intern (concat "mozrepl-" (symbol-name name) "-search-result")))
	(url-i (format "http://www.google.co.jp/search?hl=ja&btnI=&as_sitesearch=%s&q=%%s" site))
	(url-g (format "http://www.google.co.jp/search?hl=ja&as_sitesearch=%s&q=%%s" site)))
    `(progn
       (defun ,sym-i (keywords)
	 (interactive "skeywords: ")
	 (mozrepl-open-uri (format ,url-i (uri-encode keywords))))
       (defun ,sym-g (keywords)
	 (interactive "skeywords: ")
	 (mozrepl-open-uri (format ,url-g (uri-encode keywords))))
       )
    ))
(defmacro mozrepl-x-search-expand ()
  (let ((mozrepl-x-search-list
	 '((ruby        . "www.ruby-lang.org")
	   (python      . "docs.python.org"  )
	   (html        . "www.htmq.com"     )
	   (lisp        . "www.lispworks.com")
	   (gauche      . "practical-scheme.net/gauche/man")
	   (mathematica . "reference.wolfram.com/mathematica")
	   (allegro     . "www.franz.com/support/documentation/6.2/doc")
	   (mop         . "www.alu.org/mop")
	   (cocoa       . "developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Reference")
	   )))
    `(progn
       ,@(loop for elt in mozrepl-x-search-list
	       collect `(define-mozrepl-x-search ,(car elt) ,(cdr elt))))    
    ))
(mozrepl-x-search-expand)

define-mozrepl-x-searchマクロはmozrepl-x-searchとmozrepl-x-search-result(xの部分にnameで指定した名前が入る)という2つの関数を定義するマクロで、mozrepl-x-search-expandマクロはたくさんの言語に対応した関数を一気に定義するためのマクロです。mozrepl-x-search関数はI'm Feeling Lucky検索を使って検索1位のページに直接飛ぶ関数で、mozrepl-x-search-result関数はGoogleの検索結果のページを表示する関数です。